volunteer_groups.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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, 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
  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(
  36. "/group",
  37. response_model=VolunteerGroupResponse,
  38. openapi_extra={"sse_event": "volunteer_group_created"},
  39. )
  40. async def create_group(
  41. project_id: UUID,
  42. new_group: VolunteerGroupCreateRequest,
  43. current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)),
  44. session: Session = Depends(deps.get_session),
  45. ):
  46. """Create a new volunteer group in the project"""
  47. assert_project_exists_or_404(session, project_id)
  48. group = VolunteerGroup(project_id=project_id, **new_group.model_dump())
  49. session.add(group)
  50. session.commit()
  51. session.refresh(group)
  52. return group
  53. @router.get(
  54. "/group/{group_id}",
  55. response_model=VolunteerGroupResponse,
  56. )
  57. async def get_group(
  58. project_id: UUID,
  59. group_id: UUID,
  60. current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)),
  61. session: Session = Depends(deps.get_session),
  62. ):
  63. """Get a single volunteer group"""
  64. return _get_group_or_404(session, project_id, group_id)
  65. @router.post(
  66. "/group/{group_id}",
  67. response_model=VolunteerGroupResponse,
  68. openapi_extra={"sse_event": "volunteer_group_updated"},
  69. )
  70. async def update_group(
  71. project_id: UUID,
  72. group_id: UUID,
  73. edit_group: VolunteerGroupUpdateRequest,
  74. current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)),
  75. session: Session = Depends(deps.get_session),
  76. ):
  77. """Edit a volunteer group"""
  78. group = _get_group_or_404(session, project_id, group_id)
  79. group.name = edit_group.name
  80. group.color = edit_group.color
  81. session.commit()
  82. session.refresh(group)
  83. return group
  84. @router.delete(
  85. "/group/{group_id}",
  86. openapi_extra={"sse_event": "volunteer_group_deleted"},
  87. )
  88. async def delete_group(
  89. project_id: UUID,
  90. group_id: UUID,
  91. current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)),
  92. session: Session = Depends(deps.get_session),
  93. ):
  94. """Delete a volunteer group (does not delete its volunteers)"""
  95. group = _get_group_or_404(session, project_id, group_id)
  96. session.delete(group)
  97. session.commit()
  98. @router.post(
  99. "/group/{group_id}/volunteers",
  100. response_model=VolunteerGroupResponse,
  101. openapi_extra={"sse_event": "volunteer_group_updated"},
  102. )
  103. async def add_volunteers_to_group(
  104. project_id: UUID,
  105. group_id: UUID,
  106. payload: GroupMembershipRequest,
  107. current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)),
  108. session: Session = Depends(deps.get_session),
  109. ):
  110. """Bulk-add volunteers to a group (idempotent -- already-members are skipped)"""
  111. group = _get_group_or_404(session, project_id, group_id)
  112. await verify_id_list(
  113. session, payload.volunteer_ids, project_id, Volunteer, "Invalid volunteer list"
  114. )
  115. existing_ids = {v.id for v in group.volunteers}
  116. for vid in payload.volunteer_ids:
  117. if str(vid) in existing_ids:
  118. continue
  119. volunteer = session.get(Volunteer, vid)
  120. group.volunteers.append(volunteer)
  121. session.commit()
  122. session.refresh(group)
  123. return group
  124. @router.delete(
  125. "/group/{group_id}/volunteer/{volunteer_id}",
  126. response_model=VolunteerGroupResponse,
  127. openapi_extra={"sse_event": "volunteer_group_updated"},
  128. )
  129. async def remove_volunteer_from_group(
  130. project_id: UUID,
  131. group_id: UUID,
  132. volunteer_id: UUID,
  133. current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)),
  134. session: Session = Depends(deps.get_session),
  135. ):
  136. """Remove a single volunteer from a group"""
  137. group = _get_group_or_404(session, project_id, group_id)
  138. volunteer = session.get(Volunteer, volunteer_id)
  139. if volunteer is not None and volunteer in group.volunteers:
  140. group.volunteers.remove(volunteer)
  141. session.commit()
  142. session.refresh(group)
  143. return group
  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