project.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. from datetime import timedelta, datetime, timezone
  2. from uuid import UUID
  3. from fastapi import APIRouter, Depends, HTTPException
  4. from sqlalchemy import delete, select
  5. from sqlalchemy.exc import IntegrityError
  6. from sqlalchemy.orm import Session
  7. from app.api import deps
  8. from app.models import (
  9. Project,
  10. Slot,
  11. SlotTemplate,
  12. SlotTag,
  13. Sms,
  14. User,
  15. Volunteer,
  16. )
  17. from app.schemas.requests import (
  18. ProjectImportGsheetRequest,
  19. ProjectRequest,
  20. ProjectSMSBatchRequest,
  21. )
  22. from app.schemas.responses import ProjectListResponse, ProjectResponse, SMSResponse
  23. from app.importData.gsheet import parseGsheet, extract_doc_uid, ParsingError
  24. router = APIRouter()
  25. @router.get("/projects", response_model=list[ProjectListResponse])
  26. async def list_project(
  27. current_user: User = Depends(deps.get_current_user),
  28. session: Session = Depends(deps.get_session),
  29. ):
  30. """Get project_list"""
  31. results = session.execute(select(Project))
  32. return results.scalars().all()
  33. @router.get("/public-projects", response_model=list[ProjectListResponse])
  34. async def list_public_project(
  35. session: Session = Depends(deps.get_session),
  36. ):
  37. """Get the list of public projects"""
  38. results = session.execute(select(Project).where(Project.is_public))
  39. return results.scalars().all()
  40. @router.post("/project", response_model=ProjectResponse)
  41. async def create_project(
  42. new_project: ProjectRequest,
  43. current_user: User = Depends(deps.get_current_user),
  44. session: Session = Depends(deps.get_session),
  45. ):
  46. """Create a new project"""
  47. project = Project(**new_project.model_dump())
  48. session.add(project)
  49. try:
  50. session.commit()
  51. except IntegrityError:
  52. raise HTTPException(400, "Project name already exist")
  53. session.refresh(project)
  54. return project
  55. @router.get("/public-project/{project_id}", response_model=ProjectResponse)
  56. async def get_public_project(
  57. project_id: UUID,
  58. session: Session = Depends(deps.get_session),
  59. ):
  60. """Get a project that is public"""
  61. result = session.get(Project, project_id)
  62. if (result is None) or not result.is_public:
  63. raise HTTPException(status_code=404, detail="Project not found")
  64. return result
  65. @router.get("/project/{project_id}", response_model=ProjectResponse)
  66. async def get_project(
  67. project_id: UUID,
  68. current_user: User = Depends(deps.get_current_user),
  69. session: Session = Depends(deps.get_session),
  70. ):
  71. """Get a project"""
  72. project = session.get(Project, project_id)
  73. if project is None:
  74. raise HTTPException(status_code=404, detail="Project not found")
  75. return project
  76. @router.post("/project/{project_id}", response_model=ProjectListResponse)
  77. async def update_project(
  78. project_id: UUID,
  79. edit_project: ProjectRequest,
  80. current_user: User = Depends(deps.get_current_user),
  81. session: Session = Depends(deps.get_session),
  82. ):
  83. """Edit project"""
  84. p = session.get(Project, project_id)
  85. if p is None:
  86. raise HTTPException(status_code=404, detail="Project not found")
  87. p.name = edit_project.name
  88. p.is_public = edit_project.is_public
  89. session.commit()
  90. return p
  91. @router.post("/project/{project_id}/import-gsheet", response_model=ProjectResponse)
  92. async def update_project_from_gsheet(
  93. project_id: UUID,
  94. gsheet: ProjectImportGsheetRequest,
  95. current_user: User = Depends(deps.get_current_user),
  96. session: Session = Depends(deps.get_session),
  97. ):
  98. """Edit project name"""
  99. p: Project = session.get(Project, project_id)
  100. if p is None:
  101. raise HTTPException(status_code=404, detail="Project not found")
  102. doc_id = extract_doc_uid(gsheet.sheet_url)
  103. if gsheet.erase_data:
  104. p.slots = []
  105. p.sms = []
  106. p.volunteers = []
  107. p.tags = []
  108. p.templates = []
  109. # Parse the gsheets
  110. try:
  111. data = parseGsheet(doc_id, gsheet.satursday_date)
  112. except ParsingError as exc:
  113. raise HTTPException(status_code=422, detail=str(exc))
  114. # Create the volunteer list
  115. volunteer_map: dict[str, Volunteer] = {}
  116. for _, row in data.contact.iterrows():
  117. volunteer = Volunteer(
  118. project_id=project_id,
  119. name=row["Prénom"],
  120. surname=row["Nom"],
  121. email=row["Mail"],
  122. phone_number=row["Tél"],
  123. automatic_sms=row["SMS"],
  124. )
  125. volunteer_map[row.key] = volunteer
  126. session.add(volunteer)
  127. # Create creneau templates
  128. template_map = {}
  129. tags_map = {tag.title: tag for tag in p.tags}
  130. for _, row in data.creneauData.iterrows():
  131. template = SlotTemplate(
  132. project_id=project_id,
  133. title=row.title,
  134. description=row.description,
  135. place=row.lieu,
  136. responsible_contact=row.responsable,
  137. )
  138. for s_tag in row.tags.split(","):
  139. if s_tag != "":
  140. if s_tag in tags_map:
  141. tag = tags_map[s_tag]
  142. else:
  143. tag = SlotTag(project_id=project_id, title=s_tag)
  144. session.add(tag)
  145. tags_map[s_tag] = tag
  146. template.tags.append(tag)
  147. template_map[template.title] = template
  148. session.add(template)
  149. df_planning = data.planning
  150. # group planning entry per same name and timing
  151. date_format = "%Y/%m/%d %H:%M"
  152. df_planning["key"] = (
  153. df_planning.nom.str.strip()
  154. + "_"
  155. + df_planning.start.dt.strftime(date_format)
  156. + "-"
  157. + df_planning.end.dt.strftime(date_format)
  158. )
  159. df_slots = df_planning.groupby("key")
  160. # Create slots
  161. for key in df_slots.groups.keys():
  162. group = df_slots.get_group(key)
  163. volunteers = group.benevole_nom.tolist()
  164. slot = Slot(
  165. project_id=project_id,
  166. title=group.nom.iloc[0],
  167. starting_time=group.start.iloc[0],
  168. ending_time=group.end.iloc[0],
  169. required_volunteers=len(volunteers),
  170. )
  171. # Add volunteer to slots
  172. for benevole_key in volunteers:
  173. if benevole_key in volunteer_map:
  174. slot.volunteers.append(volunteer_map[benevole_key])
  175. # add detail information if available
  176. template_id = group.template_id.iloc[0]
  177. if template_id in template_map:
  178. slot.template = template_map[template_id]
  179. session.add(slot)
  180. session.commit()
  181. session.refresh(p)
  182. return p
  183. @router.post("/project/{project_id}/create-all-sms", response_model=list[SMSResponse])
  184. async def create_sms_batch(
  185. project_id: UUID,
  186. sms_batch: ProjectSMSBatchRequest,
  187. current_user: User = Depends(deps.get_current_user),
  188. session: Session = Depends(deps.get_session),
  189. ):
  190. """Create SMS based on a template and the list of slots and volunteer associated to the project
  191. The placeholder that can be used in the template are
  192. - {titre} slot.title
  193. - {description} slot.description
  194. - {debut} slot.starting_time
  195. - {fin} slot.ending_ting
  196. - {respo} slot.responsible_contact
  197. - {prenom} volunteer.name
  198. - {nom} volunteer.surname
  199. """
  200. p = session.get(Project, project_id)
  201. if p is None:
  202. raise HTTPException(status_code=404, detail="Project not found")
  203. # Get all slots
  204. slots = session.execute(select(Slot).where(Slot.project_id == project_id))
  205. sms_list = []
  206. now = datetime.now(timezone.utc)
  207. for slot in slots.scalars():
  208. # Replace the slot placeholder by their value
  209. slot_content = (
  210. sms_batch.template.replace("{titre}", slot.title)
  211. .replace("{debut}", slot.starting_time.strftime("%Hh%M"))
  212. .replace("{fin}", slot.ending_time.strftime("%Hh%M"))
  213. )
  214. if slot.template is not None:
  215. slot_content = slot_content.replace("{description}", slot.template.description).replace(
  216. "{respo}", slot.template.responsible_contact
  217. )
  218. sending_time = slot.starting_time - timedelta(minutes=sms_batch.delta_t)
  219. # Skip SMS that should have been send before now
  220. if sending_time < now:
  221. continue
  222. for volunteer in slot.volunteers:
  223. if not volunteer.automatic_sms:
  224. continue
  225. # Create a new SMS customized for each user attache to the slot
  226. personalized_content = slot_content.replace("{prenom}", volunteer.name).replace(
  227. "{nom}", volunteer.surname
  228. )
  229. sms = Sms(
  230. project_id=project_id,
  231. volunteer_id=volunteer.id,
  232. content=personalized_content,
  233. phone_number=volunteer.phone_number,
  234. sending_time=sending_time,
  235. )
  236. sms_list.append(sms)
  237. session.add_all(sms_list)
  238. session.commit()
  239. return sms_list
  240. @router.delete("/project/{project_id}")
  241. async def delete_project(
  242. project_id: UUID,
  243. current_user: User = Depends(deps.get_current_user),
  244. session: Session = Depends(deps.get_session),
  245. ):
  246. """Delete project"""
  247. session.execute(delete(Project).where(Project.id == project_id))
  248. session.commit()