cli-todo-jd 0.2.1__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cli_todo_jd/cli/cli_entry.py +243 -0
- cli_todo_jd/cli/cli_menu.py +129 -0
- cli_todo_jd/helpers.py +109 -0
- cli_todo_jd/main.py +140 -189
- cli_todo_jd/web/app.py +106 -0
- cli_todo_jd/web/templates/index.html +162 -0
- {cli_todo_jd-0.2.1.dist-info → cli_todo_jd-0.3.0.dist-info}/METADATA +16 -9
- cli_todo_jd-0.3.0.dist-info/RECORD +15 -0
- cli_todo_jd-0.3.0.dist-info/entry_points.txt +4 -0
- cli_todo_jd/cli_entry.py +0 -148
- cli_todo_jd-0.2.1.dist-info/RECORD +0 -11
- cli_todo_jd-0.2.1.dist-info/entry_points.txt +0 -3
- {cli_todo_jd-0.2.1.dist-info → cli_todo_jd-0.3.0.dist-info}/WHEEL +0 -0
- {cli_todo_jd-0.2.1.dist-info → cli_todo_jd-0.3.0.dist-info}/top_level.txt +0 -0
cli_todo_jd/main.py
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
-
import questionary
|
|
3
2
|
from rich.console import Console
|
|
4
3
|
from rich.table import Table
|
|
5
4
|
from rich.padding import Padding
|
|
6
5
|
import sqlite3
|
|
7
|
-
|
|
8
6
|
from cli_todo_jd.storage.schema import ensure_schema
|
|
9
7
|
from cli_todo_jd.storage.migrate import migrate_from_json
|
|
10
8
|
|
|
@@ -14,17 +12,19 @@ def main():
|
|
|
14
12
|
|
|
15
13
|
|
|
16
14
|
class TodoApp:
|
|
17
|
-
"""
|
|
18
|
-
A simple command-line todo application.
|
|
19
|
-
"""
|
|
15
|
+
"""A simple command-line todo application."""
|
|
20
16
|
|
|
21
17
|
def __init__(self, file_path_to_db="./.todo_list.db"):
|
|
22
|
-
self.
|
|
23
|
-
self.
|
|
18
|
+
self.todo_ids: list[int] = []
|
|
19
|
+
self.todos: list[str] = []
|
|
20
|
+
self.status: list[int] = []
|
|
24
21
|
self.file_path_to_db = Path(file_path_to_db)
|
|
25
22
|
self._check_and_load_todos(self.file_path_to_db)
|
|
26
23
|
self._console = Console()
|
|
27
24
|
|
|
25
|
+
def reload_todos(self) -> None:
|
|
26
|
+
self._check_and_load_todos(self.file_path_to_db)
|
|
27
|
+
|
|
28
28
|
def add_todo(self, item: str) -> None:
|
|
29
29
|
item = (item or "").strip()
|
|
30
30
|
if not item:
|
|
@@ -43,7 +43,6 @@ class TodoApp:
|
|
|
43
43
|
return
|
|
44
44
|
|
|
45
45
|
print(f'Added todo: "{item}"')
|
|
46
|
-
self._check_and_load_todos(self.file_path_to_db)
|
|
47
46
|
|
|
48
47
|
def list_todos(self, *, show: str = "open") -> None:
|
|
49
48
|
"""List todos.
|
|
@@ -61,35 +60,52 @@ class TodoApp:
|
|
|
61
60
|
# Always read fresh so output reflects the DB
|
|
62
61
|
self._check_and_load_todos(self.file_path_to_db)
|
|
63
62
|
if not self.todos:
|
|
64
|
-
print("
|
|
63
|
+
print("Your todo list is empty! Start adding some with 'todo add <task>'")
|
|
65
64
|
return
|
|
66
65
|
|
|
67
66
|
if show == "all":
|
|
68
67
|
self._table_print(title="Todos")
|
|
69
68
|
return
|
|
70
69
|
|
|
71
|
-
# Filter in-memory to keep this change minimal.
|
|
70
|
+
# Filter in-memory to keep this change minimal.
|
|
71
|
+
filtered_ids: list[int] = []
|
|
72
72
|
filtered_todos: list[str] = []
|
|
73
73
|
filtered_status: list[int] = []
|
|
74
|
-
for todo, done in zip(
|
|
74
|
+
for todo_id, todo, done in zip(
|
|
75
|
+
self.todo_ids, self.todos, self.status, strict=False
|
|
76
|
+
):
|
|
75
77
|
if show == "open" and not done:
|
|
78
|
+
filtered_ids.append(todo_id)
|
|
76
79
|
filtered_todos.append(todo)
|
|
77
80
|
filtered_status.append(done)
|
|
78
81
|
elif show == "done" and done:
|
|
82
|
+
filtered_ids.append(todo_id)
|
|
79
83
|
filtered_todos.append(todo)
|
|
80
84
|
filtered_status.append(done)
|
|
81
85
|
|
|
82
86
|
if not filtered_todos:
|
|
83
|
-
print("No todos found.")
|
|
87
|
+
print(f"No todos found when filtering on {show}.")
|
|
84
88
|
return
|
|
85
89
|
|
|
86
|
-
original_todos, original_status =
|
|
90
|
+
original_ids, original_todos, original_status = (
|
|
91
|
+
self.todo_ids,
|
|
92
|
+
self.todos,
|
|
93
|
+
self.status,
|
|
94
|
+
)
|
|
87
95
|
try:
|
|
88
|
-
self.todos, self.status =
|
|
96
|
+
self.todo_ids, self.todos, self.status = (
|
|
97
|
+
filtered_ids,
|
|
98
|
+
filtered_todos,
|
|
99
|
+
filtered_status,
|
|
100
|
+
)
|
|
89
101
|
title = "Open todos" if show == "open" else "Completed todos"
|
|
90
102
|
self._table_print(title=title)
|
|
91
103
|
finally:
|
|
92
|
-
self.todos, self.status =
|
|
104
|
+
self.todo_ids, self.todos, self.status = (
|
|
105
|
+
original_ids,
|
|
106
|
+
original_todos,
|
|
107
|
+
original_status,
|
|
108
|
+
)
|
|
93
109
|
|
|
94
110
|
def remove_todo(self, index: int) -> None:
|
|
95
111
|
# Maintain current UX: index refers to the displayed (1-based) ordering.
|
|
@@ -119,7 +135,6 @@ class TodoApp:
|
|
|
119
135
|
return
|
|
120
136
|
|
|
121
137
|
print(f'Removed todo: "{removed_item}"')
|
|
122
|
-
self._check_and_load_todos(self.file_path_to_db)
|
|
123
138
|
|
|
124
139
|
def clear_all(self) -> None:
|
|
125
140
|
try:
|
|
@@ -127,11 +142,16 @@ class TodoApp:
|
|
|
127
142
|
ensure_schema(conn)
|
|
128
143
|
with conn:
|
|
129
144
|
conn.execute("DELETE FROM todos;")
|
|
145
|
+
# Reset AUTOINCREMENT counter so ids start from 1 again.
|
|
146
|
+
# This is SQLite-specific and only applies to tables created with AUTOINCREMENT.
|
|
147
|
+
conn.execute("DELETE FROM sqlite_sequence WHERE name = 'todos';")
|
|
130
148
|
except sqlite3.Error as e:
|
|
131
149
|
print(f"Error: Failed to clear todos. ({e})")
|
|
132
150
|
return
|
|
133
151
|
|
|
152
|
+
self.todo_ids = []
|
|
134
153
|
self.todos = []
|
|
154
|
+
self.status = []
|
|
135
155
|
print("Cleared all todos.")
|
|
136
156
|
|
|
137
157
|
def _check_and_load_todos(self, file_path: Path) -> None:
|
|
@@ -151,12 +171,13 @@ class TodoApp:
|
|
|
151
171
|
"SELECT id, item, done, created_at, done_at FROM todos ORDER BY id"
|
|
152
172
|
).fetchall()
|
|
153
173
|
|
|
154
|
-
# In-memory
|
|
155
|
-
|
|
174
|
+
# In-memory lists are used by the interactive menu.
|
|
175
|
+
self.todo_ids = [int(row[0]) for row in rows]
|
|
156
176
|
self.todos = [row[1] for row in rows]
|
|
157
177
|
self.status = [row[2] for row in rows]
|
|
158
178
|
except sqlite3.Error as e:
|
|
159
179
|
print(f"Warning: Failed to load existing todos. Starting fresh. ({e})")
|
|
180
|
+
self.todo_ids = []
|
|
160
181
|
self.todos = []
|
|
161
182
|
self.status = []
|
|
162
183
|
|
|
@@ -172,11 +193,13 @@ class TodoApp:
|
|
|
172
193
|
for col in columns:
|
|
173
194
|
table.add_column(str(col))
|
|
174
195
|
|
|
175
|
-
for
|
|
196
|
+
for todo_id, todo, done in zip(
|
|
197
|
+
self.todo_ids, self.todos, self.status, strict=False
|
|
198
|
+
):
|
|
176
199
|
table.add_row(
|
|
177
|
-
|
|
200
|
+
str(todo_id),
|
|
178
201
|
str(todo),
|
|
179
|
-
"[green]✔[/green]" if
|
|
202
|
+
"[green]✔[/green]" if done else "[red]✖[/red]",
|
|
180
203
|
)
|
|
181
204
|
|
|
182
205
|
self._console.print(Padding(table, (2, 2)))
|
|
@@ -211,7 +234,6 @@ class TodoApp:
|
|
|
211
234
|
return
|
|
212
235
|
|
|
213
236
|
print(f'Marked todo as not done: "{item}"')
|
|
214
|
-
self._check_and_load_todos(self.file_path_to_db)
|
|
215
237
|
|
|
216
238
|
def mark_as_done(self, index: int) -> None:
|
|
217
239
|
self._check_and_load_todos(self.file_path_to_db)
|
|
@@ -243,7 +265,6 @@ class TodoApp:
|
|
|
243
265
|
return
|
|
244
266
|
|
|
245
267
|
print(f'Marked todo as done: "{item}"')
|
|
246
|
-
self._check_and_load_todos(self.file_path_to_db)
|
|
247
268
|
|
|
248
269
|
def update_done_data(self, index, done_value, done_at_value, todo_id):
|
|
249
270
|
text_done_value = "done" if done_value == 1 else "not done"
|
|
@@ -310,171 +331,101 @@ class TodoApp:
|
|
|
310
331
|
return
|
|
311
332
|
|
|
312
333
|
print(f'Edited todo: "{old_item}" to "{new_text}"')
|
|
313
|
-
self._check_and_load_todos(self.file_path_to_db)
|
|
314
334
|
|
|
335
|
+
def remove_by_id(self, todo_id: int) -> None:
|
|
336
|
+
try:
|
|
337
|
+
with sqlite3.connect(self.file_path_to_db) as conn:
|
|
338
|
+
ensure_schema(conn)
|
|
339
|
+
row = conn.execute(
|
|
340
|
+
"SELECT id, item FROM todos WHERE id = ?;",
|
|
341
|
+
(todo_id,),
|
|
342
|
+
).fetchone()
|
|
343
|
+
if row is None:
|
|
344
|
+
print("Error: Invalid todo id.")
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
_, removed_item = row
|
|
348
|
+
with conn:
|
|
349
|
+
conn.execute("DELETE FROM todos WHERE id = ?;", (todo_id,))
|
|
350
|
+
except sqlite3.Error as e:
|
|
351
|
+
print(f"Error: Failed to remove todo. ({e})")
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
print(f'Removed todo: "{removed_item}"')
|
|
355
|
+
|
|
356
|
+
def mark_done_by_id(self, todo_id: int) -> None:
|
|
357
|
+
try:
|
|
358
|
+
with sqlite3.connect(self.file_path_to_db) as conn:
|
|
359
|
+
ensure_schema(conn)
|
|
360
|
+
row = conn.execute(
|
|
361
|
+
"SELECT id, item FROM todos WHERE id = ?;",
|
|
362
|
+
(todo_id,),
|
|
363
|
+
).fetchone()
|
|
364
|
+
if row is None:
|
|
365
|
+
print("Error: Invalid todo id.")
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
_, item = row
|
|
369
|
+
with conn:
|
|
370
|
+
conn.execute(
|
|
371
|
+
"UPDATE todos SET done = 1, done_at = datetime('now') WHERE id = ?;",
|
|
372
|
+
(todo_id,),
|
|
373
|
+
)
|
|
374
|
+
except sqlite3.Error as e:
|
|
375
|
+
print(f"Error: Failed to mark todo as done. ({e})")
|
|
376
|
+
return
|
|
315
377
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
index : int
|
|
371
|
-
The index of the todo item to remove.
|
|
372
|
-
filepath : str
|
|
373
|
-
The file path to the JSON file for storing todos.
|
|
374
|
-
"""
|
|
375
|
-
app = create_list(file_path_to_db=filepath)
|
|
376
|
-
app.remove_todo(index)
|
|
377
|
-
app.list_todos()
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
def clear_list_of_items(filepath: str):
|
|
381
|
-
"""
|
|
382
|
-
Clear all items from the todo list.
|
|
383
|
-
|
|
384
|
-
Parameters
|
|
385
|
-
----------
|
|
386
|
-
filepath : str
|
|
387
|
-
The file path to the JSON file for storing todos.
|
|
388
|
-
"""
|
|
389
|
-
app = create_list(file_path_to_db=filepath)
|
|
390
|
-
app.clear_all()
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
def mark_item_as_done(index: int, filepath: str):
|
|
394
|
-
app = create_list(file_path_to_db=filepath)
|
|
395
|
-
app.mark_as_done(index)
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
def mark_item_as_not_done(index: int, filepath: str):
|
|
399
|
-
app = create_list(file_path_to_db=filepath)
|
|
400
|
-
app.mark_as_not_done(index)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
def cli_menu(filepath="./.todo_list.db"):
|
|
404
|
-
"""
|
|
405
|
-
Display the command-line interface menu for the todo list.
|
|
406
|
-
|
|
407
|
-
Parameters
|
|
408
|
-
----------
|
|
409
|
-
filepath : str, optional
|
|
410
|
-
The file path to the JSON file for storing todos, by default "./.todo_list.db"
|
|
411
|
-
"""
|
|
412
|
-
app = create_list(file_path_to_db=filepath)
|
|
413
|
-
while True:
|
|
414
|
-
action = questionary.select(
|
|
415
|
-
"What would you like to do?",
|
|
416
|
-
choices=[
|
|
417
|
-
"Add todo",
|
|
418
|
-
"List todos",
|
|
419
|
-
"Update todo status",
|
|
420
|
-
"Remove todo",
|
|
421
|
-
"Clear all todos",
|
|
422
|
-
"Exit",
|
|
423
|
-
],
|
|
424
|
-
).ask()
|
|
425
|
-
|
|
426
|
-
if action == "Add todo":
|
|
427
|
-
item = questionary.text("Enter the todo item:").ask()
|
|
428
|
-
app.add_todo(item)
|
|
429
|
-
elif action == "List todos":
|
|
430
|
-
app.list_todos(show="all")
|
|
431
|
-
elif action == "Update todo status":
|
|
432
|
-
if not app.todos:
|
|
433
|
-
print("No todos to update.")
|
|
434
|
-
continue
|
|
435
|
-
todo_choice = questionary.select(
|
|
436
|
-
"Select the todo to update:",
|
|
437
|
-
choices=["<Back>"] + app.todos,
|
|
438
|
-
).ask()
|
|
439
|
-
|
|
440
|
-
if todo_choice == "<Back>" or todo_choice is None:
|
|
441
|
-
continue
|
|
442
|
-
|
|
443
|
-
todo_index = app.todos.index(todo_choice) + 1
|
|
444
|
-
status_choice = questionary.select(
|
|
445
|
-
"Mark as:",
|
|
446
|
-
choices=["Done", "Not Done", "<Back>"],
|
|
447
|
-
).ask()
|
|
448
|
-
|
|
449
|
-
if status_choice == "<Back>" or status_choice is None:
|
|
450
|
-
continue
|
|
451
|
-
elif status_choice == "Done":
|
|
452
|
-
app.mark_as_done(todo_index)
|
|
453
|
-
elif status_choice == "Not Done":
|
|
454
|
-
app.mark_as_not_done(todo_index)
|
|
455
|
-
app.list_todos(show="all")
|
|
456
|
-
elif action == "Remove todo":
|
|
457
|
-
if not app.todos:
|
|
458
|
-
print("No todos to remove.")
|
|
459
|
-
continue
|
|
460
|
-
todo_choice = questionary.select(
|
|
461
|
-
"Select the todo to remove:",
|
|
462
|
-
choices=["<Back>"] + app.todos,
|
|
463
|
-
).ask()
|
|
464
|
-
|
|
465
|
-
if todo_choice == "<Back>" or todo_choice is None:
|
|
466
|
-
continue
|
|
467
|
-
|
|
468
|
-
todo_to_remove = app.todos.index(todo_choice) + 1
|
|
469
|
-
app.remove_todo(todo_to_remove)
|
|
470
|
-
|
|
471
|
-
elif action == "Clear all todos":
|
|
472
|
-
confirm = questionary.confirm(
|
|
473
|
-
"Are you sure you want to clear all todos?"
|
|
474
|
-
).ask()
|
|
475
|
-
if confirm:
|
|
476
|
-
app.clear_all()
|
|
477
|
-
elif action == "Exit":
|
|
478
|
-
break
|
|
479
|
-
else:
|
|
480
|
-
break
|
|
378
|
+
print(f'Marked todo as done: "{item}"')
|
|
379
|
+
|
|
380
|
+
def mark_not_done_by_id(self, todo_id: int) -> None:
|
|
381
|
+
try:
|
|
382
|
+
with sqlite3.connect(self.file_path_to_db) as conn:
|
|
383
|
+
ensure_schema(conn)
|
|
384
|
+
row = conn.execute(
|
|
385
|
+
"SELECT id, item FROM todos WHERE id = ?;",
|
|
386
|
+
(todo_id,),
|
|
387
|
+
).fetchone()
|
|
388
|
+
if row is None:
|
|
389
|
+
print("Error: Invalid todo id.")
|
|
390
|
+
return
|
|
391
|
+
|
|
392
|
+
_, item = row
|
|
393
|
+
with conn:
|
|
394
|
+
conn.execute(
|
|
395
|
+
"UPDATE todos SET done = 0, done_at = NULL WHERE id = ?;",
|
|
396
|
+
(todo_id,),
|
|
397
|
+
)
|
|
398
|
+
except sqlite3.Error as e:
|
|
399
|
+
print(f"Error: Failed to mark todo as not done. ({e})")
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
print(f'Marked todo as not done: "{item}"')
|
|
403
|
+
|
|
404
|
+
def edit_by_id(self, todo_id: int, new_text: str) -> None:
|
|
405
|
+
new_text = (new_text or "").strip()
|
|
406
|
+
if not new_text:
|
|
407
|
+
print("Error: Todo item cannot be empty.")
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
with sqlite3.connect(self.file_path_to_db) as conn:
|
|
412
|
+
ensure_schema(conn)
|
|
413
|
+
row = conn.execute(
|
|
414
|
+
"SELECT id, item FROM todos WHERE id = ?;",
|
|
415
|
+
(todo_id,),
|
|
416
|
+
).fetchone()
|
|
417
|
+
if row is None:
|
|
418
|
+
print("Error: Invalid todo id.")
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
_, old_item = row
|
|
422
|
+
with conn:
|
|
423
|
+
conn.execute(
|
|
424
|
+
"UPDATE todos SET item = ? WHERE id = ?;",
|
|
425
|
+
(new_text, todo_id),
|
|
426
|
+
)
|
|
427
|
+
except sqlite3.Error as e:
|
|
428
|
+
print(f"Error: Failed to edit todo. ({e})")
|
|
429
|
+
return
|
|
430
|
+
|
|
431
|
+
print(f'Edited todo: "{old_item}" to "{new_text}"')
|
cli_todo_jd/web/app.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from flask import Flask, redirect, render_template, request, url_for
|
|
7
|
+
|
|
8
|
+
from cli_todo_jd.storage.schema import ensure_schema
|
|
9
|
+
from cli_todo_jd.storage.migrate import migrate_from_json
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_app(db_path: Path) -> Flask:
|
|
13
|
+
app = Flask(__name__)
|
|
14
|
+
app.config["TODO_DB_PATH"] = str(db_path)
|
|
15
|
+
|
|
16
|
+
def _connect() -> sqlite3.Connection:
|
|
17
|
+
conn = sqlite3.connect(db_path)
|
|
18
|
+
conn.row_factory = sqlite3.Row
|
|
19
|
+
ensure_schema(conn)
|
|
20
|
+
return conn
|
|
21
|
+
|
|
22
|
+
# Optional one-time JSON migration (mirrors CLI behavior)
|
|
23
|
+
json_path = db_path.with_suffix(".json")
|
|
24
|
+
if json_path.exists() and db_path.suffix == ".db":
|
|
25
|
+
migrate_from_json(json_path=json_path, db_path=db_path, backup=True)
|
|
26
|
+
|
|
27
|
+
@app.get("/")
|
|
28
|
+
def index():
|
|
29
|
+
show = request.args.get("show", "open")
|
|
30
|
+
if show not in {"open", "done", "all"}:
|
|
31
|
+
show = "open"
|
|
32
|
+
|
|
33
|
+
with _connect() as conn:
|
|
34
|
+
if show == "open":
|
|
35
|
+
todos = conn.execute(
|
|
36
|
+
"SELECT id, item, done, created_at, done_at FROM todos WHERE done = ? ORDER BY id DESC",
|
|
37
|
+
(0,),
|
|
38
|
+
).fetchall()
|
|
39
|
+
elif show == "done":
|
|
40
|
+
todos = conn.execute(
|
|
41
|
+
"SELECT id, item, done, created_at, done_at FROM todos WHERE done = ? ORDER BY id DESC",
|
|
42
|
+
(1,),
|
|
43
|
+
).fetchall()
|
|
44
|
+
else:
|
|
45
|
+
todos = conn.execute(
|
|
46
|
+
"SELECT id, item, done, created_at, done_at FROM todos ORDER BY id DESC"
|
|
47
|
+
).fetchall()
|
|
48
|
+
|
|
49
|
+
return render_template("index.html", todos=todos, show=show)
|
|
50
|
+
|
|
51
|
+
@app.post("/add")
|
|
52
|
+
def add():
|
|
53
|
+
item = (request.form.get("item") or "").strip()
|
|
54
|
+
if item:
|
|
55
|
+
with _connect() as conn:
|
|
56
|
+
with conn:
|
|
57
|
+
conn.execute("INSERT INTO todos(item, done) VALUES (?, 0)", (item,))
|
|
58
|
+
return redirect(url_for("index"))
|
|
59
|
+
|
|
60
|
+
@app.post("/toggle/<int:todo_id>")
|
|
61
|
+
def toggle(todo_id: int):
|
|
62
|
+
with _connect() as conn:
|
|
63
|
+
row = conn.execute(
|
|
64
|
+
"SELECT done FROM todos WHERE id = ?", (todo_id,)
|
|
65
|
+
).fetchone()
|
|
66
|
+
if row is not None:
|
|
67
|
+
new_done = 0 if row["done"] else 1
|
|
68
|
+
with conn:
|
|
69
|
+
if new_done:
|
|
70
|
+
conn.execute(
|
|
71
|
+
"UPDATE todos SET done = 1, done_at = datetime('now') WHERE id = ?",
|
|
72
|
+
(todo_id,),
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
conn.execute(
|
|
76
|
+
"UPDATE todos SET done = 0, done_at = NULL WHERE id = ?",
|
|
77
|
+
(todo_id,),
|
|
78
|
+
)
|
|
79
|
+
return redirect(url_for("index"))
|
|
80
|
+
|
|
81
|
+
@app.post("/delete/<int:todo_id>")
|
|
82
|
+
def delete(todo_id: int):
|
|
83
|
+
with _connect() as conn:
|
|
84
|
+
with conn:
|
|
85
|
+
conn.execute("DELETE FROM todos WHERE id = ?", (todo_id,))
|
|
86
|
+
return redirect(url_for("index"))
|
|
87
|
+
|
|
88
|
+
@app.post("/clear")
|
|
89
|
+
def clear():
|
|
90
|
+
if request.form.get("confirm") == "yes":
|
|
91
|
+
with _connect() as conn:
|
|
92
|
+
with conn:
|
|
93
|
+
conn.execute("DELETE FROM todos")
|
|
94
|
+
return redirect(url_for("index"))
|
|
95
|
+
|
|
96
|
+
return app
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def run_web(
|
|
100
|
+
db_path: Path, host: str = "127.0.0.1", port: int = 8000, debug: bool = False
|
|
101
|
+
) -> None:
|
|
102
|
+
db_path = Path(db_path)
|
|
103
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
|
|
105
|
+
app = create_app(db_path)
|
|
106
|
+
app.run(host=host, port=port, debug=debug)
|