Skip to content

Commit b4252d7

Browse files
committed
feat: filter projects by status
1 parent 6d2b080 commit b4252d7

File tree

2 files changed

+86
-50
lines changed

2 files changed

+86
-50
lines changed

src/backend/app/projects/project_routes.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from shapely.ops import unary_union
2727
from app.projects import project_schemas, project_deps, project_logic, image_processing
2828
from app.db import database
29-
from app.models.enums import HTTPStatus, State
29+
from app.models.enums import HTTPStatus, State, ProjectCompletionStatus
3030
from app.s3 import add_file_to_bucket, s3_client
3131
from app.config import settings
3232
from app.users.user_deps import login_required
@@ -388,6 +388,9 @@ async def read_projects(
388388
filter_by_owner: Optional[bool] = Query(
389389
False, description="Filter projects by authenticated user (creator)"
390390
),
391+
status: Optional[ProjectCompletionStatus] = Query(
392+
None, description="Filter projects by status"
393+
),
391394
search: Optional[str] = Query(None, description="Search projects by name"),
392395
page: int = Query(1, ge=1, description="Page number"),
393396
results_per_page: int = Query(
@@ -400,7 +403,7 @@ async def read_projects(
400403
user_id = user_data.id if filter_by_owner else None
401404
skip = (page - 1) * results_per_page
402405
projects, total_count = await project_schemas.DbProject.all(
403-
db, user_id=user_id, search=search, skip=skip, limit=results_per_page
406+
db, user_id=user_id, search=search, status=status, skip=skip, limit=results_per_page
404407
)
405408
if not projects:
406409
return {

src/backend/app/projects/project_schemas.py

Lines changed: 81 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -382,80 +382,113 @@ async def one(db: Connection, project_id: uuid.UUID):
382382
project_record.task_count = len(task_records)
383383
return project_record
384384

385+
385386
async def all(
386387
db: Connection,
387388
user_id: Optional[str] = None,
388389
search: Optional[str] = None,
390+
status: Optional[ProjectCompletionStatus] = None,
389391
skip: int = 0,
390392
limit: int = 100,
391393
):
392394
"""
393395
Get all projects, count total tasks and task states (ongoing, completed, etc.).
394-
Optionally filter by the project creator (user) and search by project name.
396+
Optionally filter by the project creator (user), search by project name, and status.
395397
"""
396398
search_term = f"%{search}%" if search else "%"
397-
async with db.cursor(row_factory=dict_row) as cur:
398-
await cur.execute(
399-
"""
400-
SELECT
401-
p.id, p.slug, p.name, p.description, p.per_task_instructions, p.created_at, p.author_id,
402-
ST_AsGeoJSON(p.outline)::jsonb AS outline,
403-
p.requires_approval_from_manager_for_locking,
399+
status_value = status.value if status else None
404400

405-
-- Count total tasks for each project
406-
COUNT(t.id) AS total_task_count,
407-
408-
-- Count based on the latest state of tasks
409-
COUNT(CASE WHEN te.state IN ('LOCKED_FOR_MAPPING', 'REQUEST_FOR_MAPPING', 'IMAGE_UPLOADED', 'UNFLYABLE_TASK') THEN 1 END) AS ongoing_task_count,
410-
411-
-- Count based on the latest state of tasks
412-
COUNT(CASE WHEN te.state = 'IMAGE_PROCESSING_FINISHED' THEN 1 END) AS completed_task_count
413-
414-
FROM projects p
415-
LEFT JOIN tasks t ON t.project_id = p.id
416-
LEFT JOIN (
417-
-- Get the latest event per task
418-
SELECT DISTINCT ON (te.task_id)
419-
te.task_id,
420-
te.state,
421-
te.created_at
422-
FROM task_events te
423-
ORDER BY te.task_id, te.created_at DESC
424-
) AS te ON te.task_id = t.id
425-
426-
WHERE (p.author_id = COALESCE(%(user_id)s, p.author_id))
427-
AND p.name ILIKE %(search)s
428-
429-
-- Uncomment this if we want to restrict projects before local regulation accepts it
430-
-- AND (
431-
-- %(user_id)s IS NOT NULL
432-
-- OR p.requires_approval_from_regulator = 'f'
433-
-- OR p.regulator_approval_status = 'APPROVED'
434-
-- )
435-
436-
GROUP BY p.id
437-
ORDER BY p.created_at DESC
401+
async with db.cursor(row_factory=dict_row) as cur:
402+
query = """
403+
WITH project_stats AS (
404+
SELECT
405+
p.id,
406+
p.slug,
407+
p.name,
408+
p.description,
409+
p.per_task_instructions,
410+
p.created_at,
411+
p.author_id,
412+
ST_AsGeoJSON(p.outline)::jsonb AS outline,
413+
p.requires_approval_from_manager_for_locking,
414+
COUNT(t.id) AS total_task_count,
415+
COUNT(CASE WHEN te.state IN ('LOCKED_FOR_MAPPING', 'REQUEST_FOR_MAPPING', 'IMAGE_UPLOADED', 'UNFLYABLE_TASK') THEN 1 END) AS ongoing_task_count,
416+
COUNT(CASE WHEN te.state = 'IMAGE_PROCESSING_FINISHED' THEN 1 END) AS completed_task_count,
417+
CASE
418+
WHEN COUNT(CASE WHEN te.state = 'IMAGE_PROCESSING_FINISHED' THEN 1 END) = COUNT(t.id) THEN 'completed'
419+
WHEN COUNT(CASE WHEN te.state IN ('LOCKED_FOR_MAPPING', 'REQUEST_FOR_MAPPING', 'IMAGE_UPLOADED', 'UNFLYABLE_TASK') THEN 1 END) = 0
420+
AND COUNT(CASE WHEN te.state = 'IMAGE_PROCESSING_FINISHED' THEN 1 END) = 0 THEN 'not-started'
421+
ELSE 'ongoing'
422+
END AS calculated_status
423+
FROM projects p
424+
LEFT JOIN tasks t ON t.project_id = p.id
425+
LEFT JOIN (
426+
SELECT DISTINCT ON (te.task_id)
427+
te.task_id,
428+
te.state,
429+
te.created_at
430+
FROM task_events te
431+
ORDER BY te.task_id, te.created_at DESC
432+
) AS te ON te.task_id = t.id
433+
WHERE (p.author_id = COALESCE(%(user_id)s, p.author_id))
434+
AND p.name ILIKE %(search)s
435+
GROUP BY p.id, p.slug, p.name, p.description, p.per_task_instructions, p.created_at, p.author_id, p.outline, p.requires_approval_from_manager_for_locking
436+
)
437+
SELECT *
438+
FROM project_stats
439+
WHERE CAST(%(status)s AS text) IS NULL
440+
OR calculated_status = CAST(%(status)s AS text)
441+
ORDER BY created_at DESC
438442
OFFSET %(skip)s
439443
LIMIT %(limit)s
440-
""",
444+
"""
445+
446+
await cur.execute(
447+
query,
441448
{
442449
"skip": skip,
443450
"limit": limit,
444451
"user_id": user_id,
445452
"search": search_term,
453+
"status": status_value,
446454
},
447455
)
448456
db_projects = await cur.fetchall()
449457

450458
async with db.cursor() as cur:
459+
count_query = """
460+
WITH project_stats AS (
461+
SELECT
462+
p.id,
463+
CASE
464+
WHEN COUNT(CASE WHEN te.state = 'IMAGE_PROCESSING_FINISHED' THEN 1 END) = COUNT(t.id) THEN 'completed'
465+
WHEN COUNT(CASE WHEN te.state IN ('LOCKED_FOR_MAPPING', 'REQUEST_FOR_MAPPING', 'IMAGE_UPLOADED', 'UNFLYABLE_TASK') THEN 1 END) = 0
466+
AND COUNT(CASE WHEN te.state = 'IMAGE_PROCESSING_FINISHED' THEN 1 END) = 0 THEN 'not-started'
467+
ELSE 'ongoing'
468+
END AS calculated_status
469+
FROM projects p
470+
LEFT JOIN tasks t ON t.project_id = p.id
471+
LEFT JOIN (
472+
SELECT DISTINCT ON (te.task_id)
473+
te.task_id,
474+
te.state,
475+
te.created_at
476+
FROM task_events te
477+
ORDER BY te.task_id, te.created_at DESC
478+
) AS te ON te.task_id = t.id
479+
WHERE (p.author_id = COALESCE(%(user_id)s, p.author_id))
480+
AND p.name ILIKE %(search)s
481+
GROUP BY p.id
482+
)
483+
SELECT COUNT(*)
484+
FROM project_stats
485+
WHERE CAST(%(status)s AS text) IS NULL
486+
OR calculated_status = CAST(%(status)s AS text)
487+
"""
451488
await cur.execute(
452-
"""
453-
SELECT COUNT(*) FROM projects p
454-
WHERE (p.author_id = COALESCE(%(user_id)s, p.author_id))
455-
AND p.name ILIKE %(search)s""",
456-
{"user_id": user_id, "search": search_term},
489+
count_query,
490+
{"user_id": user_id, "search": search_term, "status": status_value},
457491
)
458-
459492
total_count = await cur.fetchone()
460493

461494
return db_projects, total_count[0]

0 commit comments

Comments
 (0)