55from datetime import datetime , timezone
66
77import orjson
8- from fastapi import APIRouter , Depends , status
8+ from fastapi import APIRouter , Depends , Query , status
99from redis .asyncio import Redis as AsyncRedis
1010
1111from app .dependencies .auth import manager
@@ -50,11 +50,15 @@ async def _release_lock(redis: AsyncRedis, lock_key: str) -> None:
5050
5151@router .get ("" )
5252async def get_todos (
53- user : User = Depends (manager ), redis : AsyncRedis = Depends (get_async_redis )
53+ include_archived : bool = Query (False ),
54+ user : User = Depends (manager ),
55+ redis : AsyncRedis = Depends (get_async_redis ),
5456):
55- """Get current user's todos."""
57+ """Get current user's todos. Excludes archived by default. """
5658 key = f"todos:{ user .id } "
5759 todos = await _read_todos (redis , key )
60+ if not include_archived :
61+ todos = [t for t in todos if not t .get ("archived" )]
5862 return APIResponse .ok (data = {"todos" : todos })
5963
6064
@@ -84,6 +88,8 @@ async def create_todo(
8488 "dueDate" : data .dueDate ,
8589 "priority" : data .priority or "medium" ,
8690 "category" : data .category ,
91+ "archived" : bool (data .archived ),
92+ "archivedAt" : data .archivedAt ,
8793 }
8894 todos .insert (0 , todo )
8995 await _write_todos (redis , key , todos )
@@ -128,6 +134,10 @@ async def patch_todo(
128134 t ["category" ] = data .category
129135 if data .completed is not None :
130136 t ["completed" ] = bool (data .completed )
137+ if data .archived is not None :
138+ t ["archived" ] = bool (data .archived )
139+ if data .archivedAt is not None :
140+ t ["archivedAt" ] = data .archivedAt
131141 todos [i ] = t
132142 updated = t
133143 break
@@ -171,6 +181,8 @@ async def replace_todo(
171181 "dueDate" : data .dueDate ,
172182 "priority" : data .priority or "medium" ,
173183 "category" : data .category ,
184+ "archived" : bool (data .archived ),
185+ "archivedAt" : data .archivedAt ,
174186 }
175187 todos [i ] = updated
176188 break
@@ -241,6 +253,8 @@ async def import_todos(
241253 "dueDate" : item .dueDate ,
242254 "priority" : item .priority or "medium" ,
243255 "category" : item .category ,
256+ "archived" : bool (item .archived ),
257+ "archivedAt" : item .archivedAt ,
244258 }
245259 )
246260 merged = new_items + existing
@@ -251,6 +265,112 @@ async def import_todos(
251265 return APIResponse .ok (message = "Todos imported" )
252266
253267
268+ @router .get ("/archived" )
269+ async def get_archived_todos (
270+ user : User = Depends (manager ), redis : AsyncRedis = Depends (get_async_redis )
271+ ):
272+ """Get all archived todos for current user."""
273+ key = f"todos:{ user .id } "
274+ todos = await _read_todos (redis , key )
275+ archived = [t for t in todos if t .get ("archived" )]
276+ return APIResponse .ok (data = {"todos" : archived })
277+
278+
279+ @router .post ("/{todo_id}/archive" )
280+ async def archive_todo (
281+ todo_id : str ,
282+ user : User = Depends (manager ),
283+ redis : AsyncRedis = Depends (get_async_redis ),
284+ ):
285+ """Archive a todo."""
286+ key = f"todos:{ user .id } "
287+ lock_key = f"todos:lock:{ user .id } "
288+ if not await _acquire_lock (redis , lock_key ):
289+ return APIResponse .error (
290+ message = "Server busy, please retry." , code = status .HTTP_423_LOCKED
291+ )
292+ updated = None
293+ try :
294+ todos = await _read_todos (redis , key )
295+ for i , t in enumerate (todos ):
296+ if t .get ("id" ) == todo_id :
297+ t ["archived" ] = True
298+ t ["archivedAt" ] = datetime .now (timezone .utc ).isoformat () # noqa: UP017
299+ todos [i ] = t
300+ updated = t
301+ break
302+ if updated is None :
303+ return APIResponse .error (
304+ message = "Todo not found" , code = status .HTTP_404_NOT_FOUND
305+ )
306+ await _write_todos (redis , key , todos )
307+ finally :
308+ await _release_lock (redis , lock_key )
309+
310+ return APIResponse .ok (data = {"todo" : updated }, message = "Todo archived" )
311+
312+
313+ @router .post ("/{todo_id}/unarchive" )
314+ async def unarchive_todo (
315+ todo_id : str ,
316+ user : User = Depends (manager ),
317+ redis : AsyncRedis = Depends (get_async_redis ),
318+ ):
319+ """Unarchive a todo."""
320+ key = f"todos:{ user .id } "
321+ lock_key = f"todos:lock:{ user .id } "
322+ if not await _acquire_lock (redis , lock_key ):
323+ return APIResponse .error (
324+ message = "Server busy, please retry." , code = status .HTTP_423_LOCKED
325+ )
326+ updated = None
327+ try :
328+ todos = await _read_todos (redis , key )
329+ for i , t in enumerate (todos ):
330+ if t .get ("id" ) == todo_id :
331+ t ["archived" ] = False
332+ t ["archivedAt" ] = None
333+ todos [i ] = t
334+ updated = t
335+ break
336+ if updated is None :
337+ return APIResponse .error (
338+ message = "Todo not found" , code = status .HTTP_404_NOT_FOUND
339+ )
340+ await _write_todos (redis , key , todos )
341+ finally :
342+ await _release_lock (redis , lock_key )
343+
344+ return APIResponse .ok (data = {"todo" : updated }, message = "Todo unarchived" )
345+
346+
347+ @router .post ("/archive-completed" )
348+ async def archive_completed (
349+ user : User = Depends (manager ), redis : AsyncRedis = Depends (get_async_redis )
350+ ):
351+ """Archive all completed (non-archived) todos."""
352+ key = f"todos:{ user .id } "
353+ lock_key = f"todos:lock:{ user .id } "
354+ if not await _acquire_lock (redis , lock_key ):
355+ return APIResponse .error (
356+ message = "Server busy, please retry." , code = status .HTTP_423_LOCKED
357+ )
358+ try :
359+ todos = await _read_todos (redis , key )
360+ now = datetime .now (timezone .utc ).isoformat () # noqa: UP017
361+ count = 0
362+ for t in todos :
363+ if t .get ("completed" ) and not t .get ("archived" ):
364+ t ["archived" ] = True
365+ t ["archivedAt" ] = now
366+ count += 1
367+ await _write_todos (redis , key , todos )
368+ finally :
369+ await _release_lock (redis , lock_key )
370+
371+ return APIResponse .ok (message = f"Archived { count } completed todos" )
372+
373+
254374@router .post ("/clear-completed" )
255375async def clear_completed (
256376 user : User = Depends (manager ), redis : AsyncRedis = Depends (get_async_redis )
0 commit comments