volunteer_groups.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. from uuid import UUID
  2. from fastapi import APIRouter, Depends, HTTPException
  3. from sqlalchemy import select
  4. from sqlalchemy.orm import Session
  5. from app.api import deps
  6. from app.api.utils import assert_project_exists, verify_id_list # adjust import path if different
  7. from app.models import OrgRole, Slot, Sms, User, Volunteer, VolunteerGroup
  8. from app.schemas.requests import (
  9. GroupMembershipRequest,
  10. GroupSmsRequest,
  11. VolunteerGroupCreateRequest,
  12. VolunteerGroupUpdateRequest,
  13. )
  14. from app.schemas.responses import SMSResponse, VolunteerGroupResponse, VolunteerResponse
  15. router = APIRouter(prefix="/project/{project_id}", tags=["volunteer_groups"])
  16. MANAGE_GROUPS = (OrgRole.ORG_ADMIN, OrgRole.RESPO_BENEVOLE)
  17. def _get_group_or_404(session: Session, project_id: UUID, group_id: UUID) -> VolunteerGroup:
  18. group = session.get(VolunteerGroup, group_id)
  19. if group is None or group.project_id != str(project_id):
  20. raise HTTPException(status_code=404, detail="Group not found")
  21. return group
  22. @router.get("/groups", response_model=list[VolunteerGroupResponse])
  23. async def list_project_groups(
  24. project_id: UUID,
  25. current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)),
  26. session: Session = Depends(deps.get_session),
  27. ):
  28. """List volunteer groups from project"""
  29. assert_project_exists(session, project_id)
  30. results = session.execute(select(VolunteerGroup).where(VolunteerGroup.project_id == project_id))
  31. return results.scalars().all()
  32. @router.post("/group", response_model=VolunteerGroupResponse)
  33. async def create_group(
  34. project_id: UUID,
  35. new_group: VolunteerGroupCreateRequest,
  36. current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)),
  37. session: Session = Depends(deps.get_session),
  38. ):
  39. """Create a new volunteer group in the project"""
  40. assert_project_exists(session, project_id)
  41. group = VolunteerGroup(project_id=project_id, **new_group.model_dump())
  42. session.add(group)
  43. session.commit()
  44. session.refresh(group)
  45. return group
  46. @router.get("/group/{group_id}", response_model=VolunteerGroupResponse)
  47. async def get_group(
  48. project_id: UUID,
  49. group_id: UUID,
  50. current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)),
  51. session: Session = Depends(deps.get_session),
  52. ):
  53. """Get a single volunteer group"""
  54. return _get_group_or_404(session, project_id, group_id)
  55. @router.patch("/group/{group_id}", response_model=VolunteerGroupResponse)
  56. async def update_group(
  57. project_id: UUID,
  58. group_id: UUID,
  59. edit_group: VolunteerGroupUpdateRequest,
  60. current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)),
  61. session: Session = Depends(deps.get_session),
  62. ):
  63. """Edit a volunteer group"""
  64. group = _get_group_or_404(session, project_id, group_id)
  65. group.name = edit_group.name
  66. group.color = edit_group.color
  67. session.commit()
  68. session.refresh(group)
  69. return group
  70. @router.delete("/group/{group_id}")
  71. async def delete_group(
  72. project_id: UUID,
  73. group_id: UUID,
  74. current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)),
  75. session: Session = Depends(deps.get_session),
  76. ):
  77. """Delete a volunteer group (does not delete its volunteers)"""
  78. group = _get_group_or_404(session, project_id, group_id)
  79. session.delete(group)
  80. session.commit()
  81. @router.post("/group/{group_id}/volunteers", response_model=VolunteerGroupResponse)
  82. async def add_volunteers_to_group(
  83. project_id: UUID,
  84. group_id: UUID,
  85. payload: GroupMembershipRequest,
  86. current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)),
  87. session: Session = Depends(deps.get_session),
  88. ):
  89. """Bulk-add volunteers to a group (idempotent -- already-members are skipped)"""
  90. group = _get_group_or_404(session, project_id, group_id)
  91. await verify_id_list(
  92. session, payload.volunteer_ids, project_id, Volunteer, "Invalid volunteer list"
  93. )
  94. existing_ids = {v.id for v in group.volunteers}
  95. for vid in payload.volunteer_ids:
  96. if str(vid) in existing_ids:
  97. continue
  98. volunteer = session.get(Volunteer, vid)
  99. group.volunteers.append(volunteer)
  100. session.commit()
  101. session.refresh(group)
  102. return group
  103. @router.delete("/group/{group_id}/volunteer/{volunteer_id}", response_model=VolunteerGroupResponse)
  104. async def remove_volunteer_from_group(
  105. project_id: UUID,
  106. group_id: UUID,
  107. volunteer_id: UUID,
  108. current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)),
  109. session: Session = Depends(deps.get_session),
  110. ):
  111. """Remove a single volunteer from a group"""
  112. group = _get_group_or_404(session, project_id, group_id)
  113. volunteer = session.get(Volunteer, volunteer_id)
  114. if volunteer is not None and volunteer in group.volunteers:
  115. group.volunteers.remove(volunteer)
  116. session.commit()
  117. session.refresh(group)
  118. return group
  119. @router.post("/group/{group_id}/add-to-slot/{slot_id}", response_model=list[VolunteerResponse])
  120. async def add_group_to_slot(
  121. project_id: UUID,
  122. group_id: UUID,
  123. slot_id: UUID,
  124. current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)),
  125. session: Session = Depends(deps.get_session),
  126. ):
  127. """Bulk-assigns every member of the group to the slot in one call.
  128. This is a one-time copy, not a live link: adding someone to the group
  129. later does NOT retroactively add them to slots already assigned."""
  130. group = _get_group_or_404(session, project_id, group_id)
  131. slot = session.get(Slot, slot_id)
  132. if slot is None or slot.project_id != str(project_id):
  133. raise HTTPException(status_code=404, detail="Slot not found in this project")
  134. existing_ids = {v.id for v in slot.volunteers}
  135. for volunteer in group.volunteers:
  136. if volunteer.id not in existing_ids:
  137. slot.volunteers.append(volunteer)
  138. session.commit()
  139. session.refresh(slot)
  140. return slot.volunteers
  141. @router.post("/group/{group_id}/send-sms", response_model=list[SMSResponse])
  142. async def send_sms_to_group(
  143. project_id: UUID,
  144. group_id: UUID,
  145. payload: GroupSmsRequest,
  146. current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)),
  147. session: Session = Depends(deps.get_session),
  148. ):
  149. """Sends an SMS immediately to every group member who has automatic_sms
  150. enabled and a phone number on file."""
  151. group = _get_group_or_404(session, project_id, group_id)
  152. sms_list = []
  153. for volunteer in group.volunteers:
  154. if not volunteer.automatic_sms or not volunteer.phone_number:
  155. continue
  156. content = payload.content.replace("{prenom}", volunteer.name).replace(
  157. "{nom}", volunteer.surname
  158. )
  159. sms = Sms(
  160. project_id=project_id,
  161. volunteer_id=volunteer.id,
  162. content=content,
  163. phone_number=volunteer.phone_number,
  164. )
  165. sms_list.append(sms)
  166. session.add_all(sms_list)
  167. session.commit()
  168. return sms_list