From 314380eec6c1f83f77ba165f0577caa661233976 Mon Sep 17 00:00:00 2001 From: Koncept Kit <63216427+konceptkit@users.noreply.github.com> Date: Tue, 6 Jan 2026 01:03:01 +0700 Subject: [PATCH] Add missing endpoints, fix batch updates, and implement RSVP status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## New Endpoints - **GET /admin/events/{event_id}**: Get single event details (admin) - Allows viewing unpublished events - Returns full event with RSVP count ## Enhanced Endpoints - **PUT /admin/events/{event_id}/attendance**: Accept batch updates - Add BatchAttendanceUpdate model for array of updates - Support both single and bulk attendance marking - Return count of updated records - **GET /events**: Include user RSVP status in response - Query current user's RSVP for each event - Enable calendar color coding by status - **GET /events/{event_id}**: Include user RSVP status - Query current user's RSVP for event details - Maintain consistency with list endpoint ## Bug Fixes - **GET /members/event-activity**: Fix timezone comparison - Add timezone-aware conversion for event.end_at - Resolve "can't compare offset-naive and offset-aware" error 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- server.py | 131 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 93 insertions(+), 38 deletions(-) diff --git a/server.py b/server.py index b170483..beef198 100644 --- a/server.py +++ b/server.py @@ -364,6 +364,9 @@ class AttendanceUpdate(BaseModel): user_id: str attended: bool +class BatchAttendanceUpdate(BaseModel): + updates: list[AttendanceUpdate] + class UpdateUserStatusRequest(BaseModel): status: str @@ -1499,7 +1502,14 @@ async def get_events( EventRSVP.rsvp_status == RSVPStatus.yes ).count() - # No user_rsvp_status in public endpoint + # Get current user's RSVP status for this event + user_rsvp = db.query(EventRSVP).filter( + EventRSVP.event_id == event.id, + EventRSVP.user_id == current_user.id + ).first() + + user_rsvp_status = user_rsvp.rsvp_status.value if user_rsvp else None + result.append(EventResponse( id=str(event.id), title=event.title, @@ -1512,7 +1522,7 @@ async def get_events( created_by=str(event.created_by), created_at=event.created_at, rsvp_count=rsvp_count, - user_rsvp_status=None + user_rsvp_status=user_rsvp_status )) return result @@ -1532,9 +1542,14 @@ async def get_event( EventRSVP.rsvp_status == RSVPStatus.yes ).count() - # No user_rsvp_status in public endpoint - user_rsvp = None - + # Get current user's RSVP status for this event + user_rsvp = db.query(EventRSVP).filter( + EventRSVP.event_id == event_id, + EventRSVP.user_id == current_user.id + ).first() + + user_rsvp_status = user_rsvp.rsvp_status.value if user_rsvp else None + return EventResponse( id=str(event.id), title=event.title, @@ -1547,7 +1562,7 @@ async def get_event( created_by=str(event.created_by), created_at=event.created_at, rsvp_count=rsvp_count, - user_rsvp_status=user_rsvp + user_rsvp_status=user_rsvp_status ) @api_router.post("/events/{event_id}/rsvp") @@ -1618,7 +1633,9 @@ async def get_my_event_activity( } # Separate upcoming vs past events - if event.end_at > now: + # Ensure timezone-aware comparison + event_end_at = event.end_at.replace(tzinfo=timezone.utc) if event.end_at.tzinfo is None else event.end_at + if event_end_at > now: upcoming_events.append(event_data) else: past_events.append(event_data) @@ -3793,9 +3810,40 @@ async def update_event( db.commit() db.refresh(event) - + return {"message": "Event updated successfully"} +@api_router.get("/admin/events/{event_id}", response_model=EventResponse) +async def get_admin_event( + event_id: str, + current_user: User = Depends(require_permission("events.view")), + db: Session = Depends(get_db) +): + """Get single event details (admin) - allows viewing unpublished events""" + event = db.query(Event).filter(Event.id == event_id).first() + if not event: + raise HTTPException(status_code=404, detail="Event not found") + + rsvp_count = db.query(EventRSVP).filter( + EventRSVP.event_id == event.id, + EventRSVP.rsvp_status == RSVPStatus.yes + ).count() + + return EventResponse( + id=str(event.id), + title=event.title, + description=event.description, + start_at=event.start_at, + end_at=event.end_at, + location=event.location, + capacity=event.capacity, + published=event.published, + created_by=str(event.created_by), + created_at=event.created_at, + rsvp_count=rsvp_count, + user_rsvp_status=None + ) + @api_router.get("/admin/events/{event_id}/rsvps") async def get_event_rsvps( event_id: str, @@ -3826,46 +3874,53 @@ async def get_event_rsvps( @api_router.put("/admin/events/{event_id}/attendance") async def mark_attendance( event_id: str, - request: AttendanceUpdate, + request: BatchAttendanceUpdate, current_user: User = Depends(require_permission("events.attendance")), db: Session = Depends(get_db) ): + """Mark attendance for one or more users (supports batch updates)""" event = db.query(Event).filter(Event.id == event_id).first() if not event: raise HTTPException(status_code=404, detail="Event not found") - - rsvp = db.query(EventRSVP).filter( - EventRSVP.event_id == event_id, - EventRSVP.user_id == request.user_id - ).first() - # Auto-create RSVP if it doesn't exist (for retroactive attendance marking) - if not rsvp: - rsvp = EventRSVP( - event_id=event_id, - user_id=request.user_id, - rsvp_status=RSVPStatus.yes, # Default to 'yes' for attended events - attended=False, - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc) - ) - db.add(rsvp) - db.flush() # Get the ID without committing + updated_count = 0 + + # Process each update in the batch + for update in request.updates: + rsvp = db.query(EventRSVP).filter( + EventRSVP.event_id == event_id, + EventRSVP.user_id == update.user_id + ).first() + + # Auto-create RSVP if it doesn't exist (for retroactive attendance marking) + if not rsvp: + rsvp = EventRSVP( + event_id=event_id, + user_id=update.user_id, + rsvp_status=RSVPStatus.yes, # Default to 'yes' for attended events + attended=False, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc) + ) + db.add(rsvp) + db.flush() # Get the ID without committing + + rsvp.attended = update.attended + rsvp.attended_at = datetime.now(timezone.utc) if update.attended else None + rsvp.updated_at = datetime.now(timezone.utc) + + # If user attended and they were pending validation, update their status + if update.attended: + user = db.query(User).filter(User.id == update.user_id).first() + if user and user.status == UserStatus.pending_validation: + user.status = UserStatus.pre_validated + user.updated_at = datetime.now(timezone.utc) + + updated_count += 1 - rsvp.attended = request.attended - rsvp.attended_at = datetime.now(timezone.utc) if request.attended else None - rsvp.updated_at = datetime.now(timezone.utc) - - # If user attended and they were pending validation, update their status - if request.attended: - user = db.query(User).filter(User.id == request.user_id).first() - if user and user.status == UserStatus.pending_validation: - user.status = UserStatus.pre_validated - user.updated_at = datetime.now(timezone.utc) - db.commit() - return {"message": "Attendance marked successfully"} + return {"message": f"Attendance marked successfully for {updated_count} {'person' if updated_count == 1 else 'people'}"} @api_router.get("/admin/events") async def get_admin_events( -- 2.39.5