volunteer_groups.py 6.9 KB

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