From 02162420b777938c53a001a5c50824265f51cf0c Mon Sep 17 00:00:00 2001 From: Anuj-Gupta4 Date: Thu, 12 Dec 2024 11:44:30 +0545 Subject: [PATCH] feat: auto unlock locked tasks after 3 days --- src/backend/app/db/models.py | 16 +++- src/backend/app/tasks/task_routes.py | 85 +++++++++++++++++++ .../migrations/011-add-date-to-entity.sql | 22 +++++ 3 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 src/backend/migrations/011-add-date-to-entity.sql diff --git a/src/backend/app/db/models.py b/src/backend/app/db/models.py index 043a9757b8..9ba84aff62 100644 --- a/src/backend/app/db/models.py +++ b/src/backend/app/db/models.py @@ -22,7 +22,7 @@ """ import json -from datetime import timedelta +from datetime import datetime, timedelta from io import BytesIO from re import sub from typing import TYPE_CHECKING, Annotated, Optional, Self @@ -1369,6 +1369,7 @@ class DbOdkEntities(BaseModel): status: EntityState project_id: int task_id: int + updated_at: Optional[AwareDatetime] = None @classmethod async def upsert( @@ -1394,7 +1395,7 @@ async def upsert( sql = """ INSERT INTO public.odk_entities - (entity_id, status, project_id, task_id) + (entity_id, status, project_id, task_id, updated_at) VALUES """ @@ -1407,20 +1408,27 @@ async def upsert( f"(%({entity_index}_entity_id)s, " f"%({entity_index}_status)s, " f"%({entity_index}_project_id)s, " - f"%({entity_index}_task_id)s)" + f"%({entity_index}_task_id)s, " + f"%({entity_index}_updated_at)s)" ) data[f"{entity_index}_entity_id"] = entity["id"] data[f"{entity_index}_status"] = EntityState(int(entity["status"])).name data[f"{entity_index}_project_id"] = project_id task_id = entity["task_id"] data[f"{entity_index}_task_id"] = int(task_id) if task_id else None + data[f"{entity_index}_updated_at"] = ( + datetime.fromisoformat(entity["updatedAt"].replace("Z", "+00:00")) + if entity["updatedAt"] + else None + ) sql += ( ", ".join(values) + """ ON CONFLICT (entity_id) DO UPDATE SET status = EXCLUDED.status, - task_id = EXCLUDED.task_id + task_id = EXCLUDED.task_id, + updated_at = EXCLUDED.updated_at RETURNING True; """ ) diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 3529b838e0..6f8ae8a1fc 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -115,3 +115,88 @@ async def get_task_event_history( ): """Get the detailed history for a task.""" return await DbTaskEvent.all(db, task_id=task_id, days=days, comments=comments) + + +@router.post("/unlock-tasks") +async def trigger_unlock_tasks(db: Annotated[Connection, Depends(db_conn)]): + """Endpoint to trigger unlock_old_locked_tasks manually.""" + await unlock_old_locked_tasks(db) + return {"message": "Old locked tasks unlocked successfully."} + + +async def unlock_old_locked_tasks(db): + """Unlock tasks locked for more than 3 days.""" + unlock_query = """ + BEGIN; + ALTER TABLE task_events DISABLE TRIGGER task_event_state_trigger; + + WITH svc_user AS ( + SELECT id AS svc_user_id, username AS svc_username + FROM users + WHERE username = 'svcfmtm' + ), + recent_events AS ( + SELECT DISTINCT ON (t.id, t.project_id) + t.id AS task_id, + t.project_id, + the.created_at AS last_event_time, + the.event AS last_event, + oe.status AS entity_status, + oe.updated_at + FROM tasks t + JOIN task_events the ON t.id = the.task_id AND t.project_id = the.project_id + LEFT JOIN ( + SELECT DISTINCT ON (task_id, project_id, entity_id) + entity_id, + status, + project_id, + task_id, + updated_at + FROM odk_entities oe1 + WHERE updated_at = ( + SELECT MAX(updated_at) + FROM odk_entities oe2 + WHERE oe1.task_id = oe2.task_id + AND oe1.project_id = oe2.project_id + ) + ) oe ON t.id = oe.task_id AND t.project_id = oe.project_id + ORDER BY t.id, t.project_id, the.created_at DESC + ), + filtered_events AS ( + SELECT * + FROM recent_events + WHERE last_event IN ('MAP', 'ASSIGN') + AND last_event_time < NOW() - INTERVAL '3 days' + AND ( + updated_at IS NULL + OR updated_at::timestamp < NOW() - INTERVAL '3 days' + ) + ) + INSERT INTO task_events ( + event_id, + task_id, + project_id, + event, + user_id, + state, + created_at, + username + ) + SELECT + gen_random_uuid(), + fe.task_id, + fe.project_id, + 'MAP'::taskevent, + svc.svc_user_id, + 'UNLOCKED_TO_MAP'::mappingstate, + NOW(), + svc.svc_username + FROM filtered_events fe + CROSS JOIN svc_user svc; + + ALTER TABLE task_events ENABLE TRIGGER task_event_state_trigger; + COMMIT; + """ + + async with db.cursor() as cur: + await cur.execute(unlock_query) diff --git a/src/backend/migrations/011-add-date-to-entity.sql b/src/backend/migrations/011-add-date-to-entity.sql new file mode 100644 index 0000000000..c16e4c87e6 --- /dev/null +++ b/src/backend/migrations/011-add-date-to-entity.sql @@ -0,0 +1,22 @@ +-- ## Migration to: +-- * track the last time an entity was updated + +-- Start a transaction +BEGIN; + +-- Add column 'updated_at' to 'odk_entities' table +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name='odk_entities' AND column_name='updated_at' + ) THEN + ALTER TABLE public.odk_entities + ADD COLUMN updated_at VARCHAR; + END IF; +END $$; + + +-- Commit the transaction +COMMIT;