diff --git a/backend/cron.py b/backend/cron.py index 60c62a061a..a087178c65 100644 --- a/backend/cron.py +++ b/backend/cron.py @@ -1,6 +1,7 @@ import datetime from apscheduler.schedulers.asyncio import AsyncIOScheduler +from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.interval import IntervalTrigger from loguru import logger @@ -26,13 +27,128 @@ async def auto_unlock_tasks(): await Task.auto_unlock_tasks(project_id, conn) +async def update_all_project_stats(): + """ + Async function to update project statistics in the database. + """ + async with db_connection.database.connection() as conn: + logger.info("Started updating project stats.") + await conn.execute("UPDATE users SET projects_mapped = NULL;") + projects_query = "SELECT DISTINCT id FROM projects;" + projects = await conn.fetch_all(query=projects_query) + for project in projects: + project_id = project["id"] + logger.info(f"Processing project ID: {project_id}") + + # Update project statistics + await conn.execute( + """ + UPDATE projects + SET total_tasks = (SELECT COUNT(*) FROM tasks WHERE project_id = :project_id), + tasks_mapped = (SELECT COUNT(*) FROM tasks WHERE project_id = :project_id AND task_status = 2), + tasks_validated = (SELECT COUNT(*) FROM tasks WHERE project_id = :project_id AND task_status = 4), + tasks_bad_imagery = (SELECT COUNT(*) FROM tasks WHERE project_id = :project_id AND task_status = 6) + WHERE id = :project_id; + """, + {"project_id": project_id}, + ) + + # Update user stats + await conn.execute( + """ + UPDATE users + SET projects_mapped = array_append(projects_mapped, :project_id) + WHERE id IN ( + SELECT DISTINCT user_id + FROM task_history + WHERE action = 'STATE_CHANGE' AND project_id = :project_id + ); + """, + {"project_id": project_id}, + ) + + logger.info("Finished updating project stats.") + + +async def update_recent_updated_project_stats(): + """ + Async function to update project statistics for the recently updated projects in the database. + """ + async with db_connection.database.connection() as conn: + logger.info("Started updating recently updated projects' project stats.") + + # Calculate the cutoff date for the past week + one_week_ago = datetime.datetime.utcnow() - datetime.timedelta(days=7) + + # Fetch projects updated in the past week + projects_query = """ + SELECT DISTINCT id + FROM projects + WHERE last_updated > :one_week_ago; + """ + projects = await conn.fetch_all( + query=projects_query, values={"one_week_ago": one_week_ago} + ) + for project in projects: + project_id = project["id"] + logger.info(f"Processing project ID: {project_id}") + + # Update project statistics + await conn.execute( + """ + UPDATE projects + SET total_tasks = (SELECT COUNT(*) FROM tasks WHERE project_id = :project_id), + tasks_mapped = (SELECT COUNT(*) FROM tasks WHERE project_id = :project_id AND task_status = 2), + tasks_validated = (SELECT COUNT(*) FROM tasks WHERE project_id = :project_id AND task_status = 4), + tasks_bad_imagery = (SELECT COUNT(*) FROM tasks WHERE project_id = :project_id AND task_status = 6) + WHERE id = :project_id; + """, + {"project_id": project_id}, + ) + + # Update user stats + await conn.execute( + """ + UPDATE users + SET projects_mapped = + CASE + WHEN :project_id = ANY(projects_mapped) THEN projects_mapped + ELSE array_append(projects_mapped, :project_id) + END + WHERE id IN ( + SELECT DISTINCT user_id + FROM task_history + WHERE action = 'STATE_CHANGE' AND project_id = :project_id + ); + """, + {"project_id": project_id}, + ) + + logger.info("Finished updating project stats.") + + def setup_cron_jobs(): scheduler = AsyncIOScheduler() + scheduler.add_job( auto_unlock_tasks, IntervalTrigger(minutes=120), id="auto_unlock_tasks", replace_existing=True, ) + + scheduler.add_job( + update_all_project_stats, + CronTrigger(hour=0, minute=0), # Cron trigger for 12:00 AM + id="update_project_stats", + replace_existing=True, + ) + + scheduler.add_job( + update_recent_updated_project_stats, + CronTrigger(minute=0), # Cron trigger for every hour + id="update_recent_updated_project_stats", + replace_existing=True, + ) scheduler.start() logger.info("Scheduler initialized: auto_unlock_tasks runs every 2 hours.") diff --git a/backend/services/validator_service.py b/backend/services/validator_service.py index 21bb5faa4d..1165db3c85 100644 --- a/backend/services/validator_service.py +++ b/backend/services/validator_service.py @@ -105,7 +105,9 @@ async def lock_tasks_for_validation( "ProjectNotPublished- Validation not allowed because: Project not published" ) elif error_reason == ValidatingNotAllowed.USER_ALREADY_HAS_TASK_LOCKED: - user_tasks = Task.get_locked_tasks_for_user(validation_dto.user_id, db) + user_tasks = await Task.get_locked_tasks_for_user( + validation_dto.user_id, db + ) if set(user_tasks.locked_tasks) != set(validation_dto.task_ids): raise ValidatorServiceError( "UserAlreadyHasTaskLocked- User already has a task locked"