Skip to content

Commit 739ef40

Browse files
authored
Merge pull request #1 from madpin/jules_wip_14270084468395557740
Jules wip 14270084468395557740
2 parents 6c9b1fa + 4eb8b79 commit 739ef40

28 files changed

+3784
-27
lines changed

backend/app/api/main.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
from fastapi import APIRouter
22

3-
from app.api.routes import items, login, private, users, utils
3+
from app.api.routes import items, login, private, users, utils, events, speeches # Added events, speeches
44
from app.core.config import settings
55

66
api_router = APIRouter()
7-
api_router.include_router(login.router)
8-
api_router.include_router(users.router)
9-
api_router.include_router(utils.router)
10-
api_router.include_router(items.router)
7+
api_router.include_router(login.router) # No prefix, tags=['login'] typically
8+
api_router.include_router(users.router, prefix="/users", tags=["users"])
9+
api_router.include_router(utils.router, prefix="/utils", tags=["utils"]) # Assuming utils has a prefix
10+
api_router.include_router(items.router, prefix="/items", tags=["items"])
11+
api_router.include_router(events.router, prefix="/events", tags=["events"]) # Added
12+
api_router.include_router(speeches.router, prefix="/speeches", tags=["speeches"]) # Added
1113

1214

1315
if settings.ENVIRONMENT == "local":
14-
api_router.include_router(private.router)
16+
# Assuming private router also has a prefix if it's for specific resources
17+
api_router.include_router(private.router, prefix="/private", tags=["private"])

backend/app/api/routes/events.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import uuid
2+
from typing import Any, List
3+
4+
from fastapi import APIRouter, Depends, HTTPException, Body
5+
from sqlmodel import Session
6+
7+
from app import crud, models # Removed schemas
8+
from app.api import deps
9+
from app.services import speech_analysis_service
10+
11+
router = APIRouter()
12+
13+
14+
@router.post("/", response_model=models.CoordinationEventPublic, status_code=201)
15+
def create_event(
16+
*,
17+
db: Session = Depends(deps.get_db),
18+
event_in: models.CoordinationEventCreate,
19+
current_user: deps.CurrentUser,
20+
) -> models.CoordinationEvent:
21+
"""
22+
Create a new coordination event.
23+
The current user will be set as the creator and an initial participant.
24+
"""
25+
event = crud.create_event(session=db, event_in=event_in, creator_id=current_user.id)
26+
return event
27+
28+
29+
@router.get("/", response_model=List[models.CoordinationEventPublic])
30+
def list_user_events(
31+
*,
32+
db: Session = Depends(deps.get_db),
33+
current_user: deps.CurrentUser,
34+
) -> Any:
35+
"""
36+
List all events the current user is participating in.
37+
"""
38+
events = crud.get_user_events(session=db, user_id=current_user.id)
39+
return events
40+
41+
42+
@router.get("/{event_id}", response_model=models.CoordinationEventPublic)
43+
def get_event_details(
44+
*,
45+
db: Session = Depends(deps.get_db),
46+
event_id: uuid.UUID,
47+
current_user: deps.CurrentUser,
48+
) -> Any:
49+
"""
50+
Get details of a specific event. User must be a participant.
51+
"""
52+
event = crud.get_event(session=db, event_id=event_id)
53+
if not event:
54+
raise HTTPException(status_code=404, detail="Event not found")
55+
56+
# Check if current user is a participant
57+
is_participant = any(p.user_id == current_user.id for p in event.participants)
58+
if not is_participant and event.creator_id != current_user.id: # Creator also has access
59+
raise HTTPException(status_code=403, detail="Not enough permissions")
60+
return event
61+
62+
63+
@router.post("/{event_id}/participants", response_model=models.EventParticipantPublic)
64+
def add_event_participant(
65+
*,
66+
db: Session = Depends(deps.get_db),
67+
event_id: uuid.UUID,
68+
user_id_to_add: uuid.UUID = Body(..., embed=True),
69+
role: str = Body("participant", embed=True),
70+
current_user: deps.CurrentUser,
71+
) -> Any:
72+
"""
73+
Add a user to an event. Only the event creator can add participants.
74+
"""
75+
event = crud.get_event(session=db, event_id=event_id)
76+
if not event:
77+
raise HTTPException(status_code=404, detail="Event not found")
78+
if event.creator_id != current_user.id:
79+
raise HTTPException(status_code=403, detail="Only the event creator can add participants")
80+
81+
# Check if user to add exists (optional, DB will catch it if not)
82+
user_to_add = db.get(models.User, user_id_to_add)
83+
if not user_to_add:
84+
raise HTTPException(status_code=404, detail="User to add not found")
85+
86+
participant = crud.add_event_participant(
87+
session=db, event_id=event_id, user_id=user_id_to_add, role=role
88+
)
89+
if not participant:
90+
raise HTTPException(status_code=400, detail="Participant already in event or other error")
91+
return participant
92+
93+
94+
@router.delete("/{event_id}/participants/{user_id_to_remove}", response_model=models.Message)
95+
def remove_event_participant(
96+
*,
97+
db: Session = Depends(deps.get_db),
98+
event_id: uuid.UUID,
99+
user_id_to_remove: uuid.UUID,
100+
current_user: deps.CurrentUser,
101+
) -> Any:
102+
"""
103+
Remove a participant from an event.
104+
Allowed if:
105+
- Current user is the event creator.
106+
- Current user is removing themselves.
107+
"""
108+
event = crud.get_event(session=db, event_id=event_id)
109+
if not event:
110+
raise HTTPException(status_code=404, detail="Event not found")
111+
112+
is_creator = event.creator_id == current_user.id
113+
is_self_removal = user_id_to_remove == current_user.id
114+
115+
if not (is_creator or is_self_removal):
116+
raise HTTPException(status_code=403, detail="Not enough permissions to remove participant")
117+
118+
# Prevent creator from being removed by themselves if they are the last participant (or handle elsewhere)
119+
if is_self_removal and is_creator and len(event.participants) == 1:
120+
raise HTTPException(status_code=400, detail="Creator cannot remove themselves if they are the last participant. Delete the event instead.")
121+
122+
123+
removed_participant = crud.remove_event_participant(
124+
session=db, event_id=event_id, user_id=user_id_to_remove
125+
)
126+
if not removed_participant:
127+
raise HTTPException(status_code=404, detail="Participant not found in this event")
128+
return models.Message(message="Participant removed successfully")
129+
130+
131+
@router.get("/{event_id}/participants", response_model=List[models.UserPublic])
132+
def list_event_participants(
133+
*,
134+
db: Session = Depends(deps.get_db),
135+
event_id: uuid.UUID,
136+
current_user: deps.CurrentUser,
137+
) -> Any:
138+
"""
139+
List participants of an event. User must be a participant of the event.
140+
"""
141+
event = crud.get_event(session=db, event_id=event_id)
142+
if not event:
143+
raise HTTPException(status_code=404, detail="Event not found")
144+
145+
is_participant = any(p.user_id == current_user.id for p in event.participants)
146+
if not is_participant and event.creator_id != current_user.id:
147+
raise HTTPException(status_code=403, detail="User must be a participant to view other participants")
148+
149+
participants = crud.get_event_participants(session=db, event_id=event_id)
150+
return participants
151+
152+
153+
@router.get("/{event_id}/speech-analysis", response_model=List[models.PersonalizedNudgePublic])
154+
def get_event_speech_analysis(
155+
*,
156+
db: Session = Depends(deps.get_db),
157+
event_id: uuid.UUID,
158+
current_user: deps.CurrentUser,
159+
) -> Any:
160+
"""
161+
Perform analysis on speeches within an event and return personalized nudges
162+
for the current user. User must be a participant of the event.
163+
"""
164+
event = crud.get_event(session=db, event_id=event_id)
165+
if not event:
166+
raise HTTPException(status_code=404, detail="Event not found")
167+
168+
# Check if current user is a participant or creator
169+
is_participant = any(p.user_id == current_user.id for p in event.participants)
170+
if not is_participant and event.creator_id != current_user.id:
171+
raise HTTPException(status_code=403, detail="User must be a participant or creator to access speech analysis.")
172+
173+
all_event_nudges = speech_analysis_service.analyse_event_speeches(db=db, event_id=event_id)
174+
175+
# Filter nudges for the current user
176+
user_nudges = [
177+
models.PersonalizedNudgePublic(
178+
nudge_type=n.nudge_type,
179+
message=n.message,
180+
severity=n.severity
181+
)
182+
for n in all_event_nudges if n.user_id == current_user.id
183+
]
184+
185+
return user_nudges

backend/app/api/routes/speeches.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import uuid
2+
from typing import Any, List
3+
4+
from fastapi import APIRouter, Depends, HTTPException
5+
from sqlmodel import Session
6+
7+
from app import crud, models # Removed schemas
8+
from app.api import deps
9+
10+
router = APIRouter()
11+
12+
# Helper function to check if user is participant or creator of the event associated with a speech
13+
def check_event_access_for_speech(db: Session, speech_id: uuid.UUID, user: models.User) -> models.SecretSpeech:
14+
speech = crud.get_speech(session=db, speech_id=speech_id)
15+
if not speech:
16+
raise HTTPException(status_code=404, detail="Speech not found")
17+
18+
event = crud.get_event(session=db, event_id=speech.event_id)
19+
if not event:
20+
raise HTTPException(status_code=404, detail="Associated event not found") # Should not happen if DB is consistent
21+
22+
is_participant = any(p.user_id == user.id for p in event.participants)
23+
if not is_participant and event.creator_id != user.id:
24+
raise HTTPException(status_code=403, detail="User does not have access to the event of this speech")
25+
return speech
26+
27+
# Helper function to check if user is participant or creator of an event
28+
def check_event_membership(db: Session, event_id: uuid.UUID, user: models.User) -> models.CoordinationEvent:
29+
event = crud.get_event(session=db, event_id=event_id)
30+
if not event:
31+
raise HTTPException(status_code=404, detail="Event not found")
32+
33+
is_participant = any(p.user_id == user.id for p in event.participants)
34+
if not is_participant and event.creator_id != user.id:
35+
raise HTTPException(status_code=403, detail="User must be a participant or creator of the event")
36+
return event
37+
38+
39+
@router.post("/", response_model=models.SecretSpeechPublic, status_code=201)
40+
def create_speech(
41+
*,
42+
db: Session = Depends(deps.get_db),
43+
speech_in: models.SecretSpeechWithInitialVersionCreate, # Use the new combined schema
44+
current_user: deps.CurrentUser,
45+
) -> Any:
46+
"""
47+
Create a new secret speech. The current user will be set as the creator.
48+
An initial version of the speech is created with the provided draft.
49+
User must be a participant of the specified event.
50+
"""
51+
# Check if user has access to the event
52+
event = check_event_membership(db=db, event_id=speech_in.event_id, user=current_user)
53+
if not event: # Should be handled by check_event_membership raising HTTPException
54+
raise HTTPException(status_code=404, detail="Event not found or user not participant.")
55+
56+
# The SecretSpeechCreate model is currently empty, so we pass an instance.
57+
# The actual speech metadata (event_id, creator_id) are passed directly to crud.create_speech
58+
db_speech = crud.create_speech(
59+
session=db,
60+
speech_in=models.SecretSpeechCreate(), # Pass empty base model if no direct fields
61+
event_id=speech_in.event_id,
62+
creator_id=current_user.id,
63+
initial_draft=speech_in.initial_speech_draft,
64+
initial_tone=speech_in.initial_speech_tone,
65+
initial_duration=speech_in.initial_estimated_duration_minutes,
66+
)
67+
return db_speech
68+
69+
70+
@router.get("/event/{event_id}", response_model=List[models.SecretSpeechPublic])
71+
def list_event_speeches(
72+
*,
73+
db: Session = Depends(deps.get_db),
74+
event_id: uuid.UUID,
75+
current_user: deps.CurrentUser,
76+
) -> Any:
77+
"""
78+
Get all speeches for a given event. User must be a participant of the event.
79+
"""
80+
check_event_membership(db=db, event_id=event_id, user=current_user)
81+
speeches = crud.get_event_speeches(session=db, event_id=event_id)
82+
return speeches
83+
84+
85+
@router.get("/{speech_id}", response_model=models.SecretSpeechPublic) # Consider a more detailed model for owner
86+
def get_speech_details(
87+
*,
88+
db: Session = Depends(deps.get_db),
89+
speech_id: uuid.UUID,
90+
current_user: deps.CurrentUser,
91+
) -> Any:
92+
"""
93+
Get a specific speech. User must have access to the event of this speech.
94+
If the user is the creator of the speech, they might get more details
95+
(e.g. draft of the current version - this needs handling in response shaping).
96+
"""
97+
speech = check_event_access_for_speech(db=db, speech_id=speech_id, user=current_user)
98+
# Basic SecretSpeechPublic doesn't include version details.
99+
# If we want to embed current version, we'd fetch it and combine.
100+
# For now, returning speech metadata. API consumer can fetch versions separately.
101+
return speech
102+
103+
104+
@router.post("/{speech_id}/versions", response_model=models.SecretSpeechVersionPublic, status_code=201)
105+
def create_speech_version(
106+
*,
107+
db: Session = Depends(deps.get_db),
108+
speech_id: uuid.UUID,
109+
version_in: models.SecretSpeechVersionCreate,
110+
current_user: deps.CurrentUser,
111+
) -> Any:
112+
"""
113+
Create a new version for a secret speech.
114+
User must be the creator of the speech or a participant in the event (adjust as needed).
115+
"""
116+
speech = crud.get_speech(session=db, speech_id=speech_id)
117+
if not speech:
118+
raise HTTPException(status_code=404, detail="Speech not found")
119+
120+
# Permission: only speech creator can add versions
121+
if speech.creator_id != current_user.id:
122+
# Or, check event participation if that's the rule:
123+
# check_event_access_for_speech(db=db, speech_id=speech_id, user=current_user)
124+
raise HTTPException(status_code=403, detail="Only the speech creator can add new versions.")
125+
126+
new_version = crud.create_speech_version(
127+
session=db, version_in=version_in, speech_id=speech_id, creator_id=current_user.id
128+
)
129+
return new_version
130+
131+
132+
@router.get("/{speech_id}/versions", response_model=List[models.SecretSpeechVersionPublic])
133+
def list_speech_versions(
134+
*,
135+
db: Session = Depends(deps.get_db),
136+
speech_id: uuid.UUID,
137+
current_user: deps.CurrentUser,
138+
) -> Any:
139+
"""
140+
List all versions of a speech.
141+
If current user is speech creator, they see full details (including draft).
142+
Otherwise, they see the public version (no draft).
143+
"""
144+
speech = check_event_access_for_speech(db=db, speech_id=speech_id, user=current_user)
145+
versions = crud.get_speech_versions(session=db, speech_id=speech_id)
146+
147+
public_versions = []
148+
for v in versions:
149+
if speech.creator_id == current_user.id or v.creator_id == current_user.id: # Speech creator or version creator sees draft
150+
public_versions.append(models.SecretSpeechVersionDetailPublic.model_validate(v))
151+
else:
152+
public_versions.append(models.SecretSpeechVersionPublic.model_validate(v))
153+
return public_versions
154+
155+
156+
@router.put("/{speech_id}/versions/{version_id}/set-current", response_model=models.SecretSpeechPublic)
157+
def set_current_speech_version(
158+
*,
159+
db: Session = Depends(deps.get_db),
160+
speech_id: uuid.UUID,
161+
version_id: uuid.UUID,
162+
current_user: deps.CurrentUser,
163+
) -> Any:
164+
"""
165+
Set a specific version of a speech as the current one.
166+
User must be the creator of the speech.
167+
"""
168+
speech = crud.get_speech(session=db, speech_id=speech_id)
169+
if not speech:
170+
raise HTTPException(status_code=404, detail="Speech not found")
171+
if speech.creator_id != current_user.id:
172+
raise HTTPException(status_code=403, detail="Only the speech creator can set the current version.")
173+
174+
updated_speech = crud.set_current_speech_version(
175+
session=db, speech_id=speech_id, version_id=version_id
176+
)
177+
if not updated_speech:
178+
raise HTTPException(status_code=404, detail="Version not found or does not belong to this speech.")
179+
return updated_speech

0 commit comments

Comments
 (0)