cli-todo-jd 0.2.0__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 +174 -184
- cli_todo_jd/web/app.py +106 -0
- cli_todo_jd/web/templates/index.html +162 -0
- {cli_todo_jd-0.2.0.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 -113
- cli_todo_jd-0.2.0.dist-info/RECORD +0 -11
- cli_todo_jd-0.2.0.dist-info/entry_points.txt +0 -3
- {cli_todo_jd-0.2.0.dist-info → cli_todo_jd-0.3.0.dist-info}/WHEEL +0 -0
- {cli_todo_jd-0.2.0.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,15 +43,69 @@ 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
|
-
def list_todos(self) -> None:
|
|
47
|
+
def list_todos(self, *, show: str = "open") -> None:
|
|
48
|
+
"""List todos.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
show:
|
|
53
|
+
"open" (default), "done", or "all".
|
|
54
|
+
"""
|
|
55
|
+
show = (show or "open").lower()
|
|
56
|
+
if show not in {"open", "done", "all"}:
|
|
57
|
+
print("Error: show must be one of: open, done, all")
|
|
58
|
+
return
|
|
59
|
+
|
|
49
60
|
# Always read fresh so output reflects the DB
|
|
50
61
|
self._check_and_load_todos(self.file_path_to_db)
|
|
51
62
|
if not self.todos:
|
|
52
|
-
print("
|
|
63
|
+
print("Your todo list is empty! Start adding some with 'todo add <task>'")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
if show == "all":
|
|
67
|
+
self._table_print(title="Todos")
|
|
53
68
|
return
|
|
54
|
-
|
|
69
|
+
|
|
70
|
+
# Filter in-memory to keep this change minimal.
|
|
71
|
+
filtered_ids: list[int] = []
|
|
72
|
+
filtered_todos: list[str] = []
|
|
73
|
+
filtered_status: list[int] = []
|
|
74
|
+
for todo_id, todo, done in zip(
|
|
75
|
+
self.todo_ids, self.todos, self.status, strict=False
|
|
76
|
+
):
|
|
77
|
+
if show == "open" and not done:
|
|
78
|
+
filtered_ids.append(todo_id)
|
|
79
|
+
filtered_todos.append(todo)
|
|
80
|
+
filtered_status.append(done)
|
|
81
|
+
elif show == "done" and done:
|
|
82
|
+
filtered_ids.append(todo_id)
|
|
83
|
+
filtered_todos.append(todo)
|
|
84
|
+
filtered_status.append(done)
|
|
85
|
+
|
|
86
|
+
if not filtered_todos:
|
|
87
|
+
print(f"No todos found when filtering on {show}.")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
original_ids, original_todos, original_status = (
|
|
91
|
+
self.todo_ids,
|
|
92
|
+
self.todos,
|
|
93
|
+
self.status,
|
|
94
|
+
)
|
|
95
|
+
try:
|
|
96
|
+
self.todo_ids, self.todos, self.status = (
|
|
97
|
+
filtered_ids,
|
|
98
|
+
filtered_todos,
|
|
99
|
+
filtered_status,
|
|
100
|
+
)
|
|
101
|
+
title = "Open todos" if show == "open" else "Completed todos"
|
|
102
|
+
self._table_print(title=title)
|
|
103
|
+
finally:
|
|
104
|
+
self.todo_ids, self.todos, self.status = (
|
|
105
|
+
original_ids,
|
|
106
|
+
original_todos,
|
|
107
|
+
original_status,
|
|
108
|
+
)
|
|
55
109
|
|
|
56
110
|
def remove_todo(self, index: int) -> None:
|
|
57
111
|
# Maintain current UX: index refers to the displayed (1-based) ordering.
|
|
@@ -81,7 +135,6 @@ class TodoApp:
|
|
|
81
135
|
return
|
|
82
136
|
|
|
83
137
|
print(f'Removed todo: "{removed_item}"')
|
|
84
|
-
self._check_and_load_todos(self.file_path_to_db)
|
|
85
138
|
|
|
86
139
|
def clear_all(self) -> None:
|
|
87
140
|
try:
|
|
@@ -89,11 +142,16 @@ class TodoApp:
|
|
|
89
142
|
ensure_schema(conn)
|
|
90
143
|
with conn:
|
|
91
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';")
|
|
92
148
|
except sqlite3.Error as e:
|
|
93
149
|
print(f"Error: Failed to clear todos. ({e})")
|
|
94
150
|
return
|
|
95
151
|
|
|
152
|
+
self.todo_ids = []
|
|
96
153
|
self.todos = []
|
|
154
|
+
self.status = []
|
|
97
155
|
print("Cleared all todos.")
|
|
98
156
|
|
|
99
157
|
def _check_and_load_todos(self, file_path: Path) -> None:
|
|
@@ -113,12 +171,13 @@ class TodoApp:
|
|
|
113
171
|
"SELECT id, item, done, created_at, done_at FROM todos ORDER BY id"
|
|
114
172
|
).fetchall()
|
|
115
173
|
|
|
116
|
-
# In-memory
|
|
117
|
-
|
|
174
|
+
# In-memory lists are used by the interactive menu.
|
|
175
|
+
self.todo_ids = [int(row[0]) for row in rows]
|
|
118
176
|
self.todos = [row[1] for row in rows]
|
|
119
177
|
self.status = [row[2] for row in rows]
|
|
120
178
|
except sqlite3.Error as e:
|
|
121
179
|
print(f"Warning: Failed to load existing todos. Starting fresh. ({e})")
|
|
180
|
+
self.todo_ids = []
|
|
122
181
|
self.todos = []
|
|
123
182
|
self.status = []
|
|
124
183
|
|
|
@@ -134,11 +193,13 @@ class TodoApp:
|
|
|
134
193
|
for col in columns:
|
|
135
194
|
table.add_column(str(col))
|
|
136
195
|
|
|
137
|
-
for
|
|
196
|
+
for todo_id, todo, done in zip(
|
|
197
|
+
self.todo_ids, self.todos, self.status, strict=False
|
|
198
|
+
):
|
|
138
199
|
table.add_row(
|
|
139
|
-
|
|
200
|
+
str(todo_id),
|
|
140
201
|
str(todo),
|
|
141
|
-
"[green]✔[/green]" if
|
|
202
|
+
"[green]✔[/green]" if done else "[red]✖[/red]",
|
|
142
203
|
)
|
|
143
204
|
|
|
144
205
|
self._console.print(Padding(table, (2, 2)))
|
|
@@ -173,7 +234,6 @@ class TodoApp:
|
|
|
173
234
|
return
|
|
174
235
|
|
|
175
236
|
print(f'Marked todo as not done: "{item}"')
|
|
176
|
-
self._check_and_load_todos(self.file_path_to_db)
|
|
177
237
|
|
|
178
238
|
def mark_as_done(self, index: int) -> None:
|
|
179
239
|
self._check_and_load_todos(self.file_path_to_db)
|
|
@@ -205,7 +265,6 @@ class TodoApp:
|
|
|
205
265
|
return
|
|
206
266
|
|
|
207
267
|
print(f'Marked todo as done: "{item}"')
|
|
208
|
-
self._check_and_load_todos(self.file_path_to_db)
|
|
209
268
|
|
|
210
269
|
def update_done_data(self, index, done_value, done_at_value, todo_id):
|
|
211
270
|
text_done_value = "done" if done_value == 1 else "not done"
|
|
@@ -272,170 +331,101 @@ class TodoApp:
|
|
|
272
331
|
return
|
|
273
332
|
|
|
274
333
|
print(f'Edited todo: "{old_item}" to "{new_text}"')
|
|
275
|
-
self._check_and_load_todos(self.file_path_to_db)
|
|
276
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}"')
|
|
277
355
|
|
|
278
|
-
def
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
def mark_item_as_done(index: int, filepath: str):
|
|
355
|
-
app = create_list(file_path_to_db=filepath)
|
|
356
|
-
app.mark_as_done(index)
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
def mark_item_as_not_done(index: int, filepath: str):
|
|
360
|
-
app = create_list(file_path_to_db=filepath)
|
|
361
|
-
app.mark_as_not_done(index)
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
def cli_menu(filepath="./.todo_list.db"):
|
|
365
|
-
"""
|
|
366
|
-
Display the command-line interface menu for the todo list.
|
|
367
|
-
|
|
368
|
-
Parameters
|
|
369
|
-
----------
|
|
370
|
-
filepath : str, optional
|
|
371
|
-
The file path to the JSON file for storing todos, by default "./.todo_list.db"
|
|
372
|
-
"""
|
|
373
|
-
app = create_list(file_path_to_db=filepath)
|
|
374
|
-
while True:
|
|
375
|
-
action = questionary.select(
|
|
376
|
-
"What would you like to do?",
|
|
377
|
-
choices=[
|
|
378
|
-
"Add todo",
|
|
379
|
-
"List todos",
|
|
380
|
-
"Update todo status",
|
|
381
|
-
"Remove todo",
|
|
382
|
-
"Clear all todos",
|
|
383
|
-
"Exit",
|
|
384
|
-
],
|
|
385
|
-
).ask()
|
|
386
|
-
|
|
387
|
-
if action == "Add todo":
|
|
388
|
-
item = questionary.text("Enter the todo item:").ask()
|
|
389
|
-
app.add_todo(item)
|
|
390
|
-
elif action == "List todos":
|
|
391
|
-
app.list_todos()
|
|
392
|
-
elif action == "Update todo status":
|
|
393
|
-
if not app.todos:
|
|
394
|
-
print("No todos to update.")
|
|
395
|
-
continue
|
|
396
|
-
todo_choice = questionary.select(
|
|
397
|
-
"Select the todo to update:",
|
|
398
|
-
choices=["<Back>"] + app.todos,
|
|
399
|
-
).ask()
|
|
400
|
-
|
|
401
|
-
if todo_choice == "<Back>":
|
|
402
|
-
continue
|
|
403
|
-
|
|
404
|
-
todo_index = app.todos.index(todo_choice) + 1
|
|
405
|
-
status_choice = questionary.select(
|
|
406
|
-
"Mark as:",
|
|
407
|
-
choices=["Done", "Not Done", "<Back>"],
|
|
408
|
-
).ask()
|
|
409
|
-
|
|
410
|
-
if status_choice == "<Back>":
|
|
411
|
-
continue
|
|
412
|
-
elif status_choice == "Done":
|
|
413
|
-
app.mark_as_done(todo_index)
|
|
414
|
-
elif status_choice == "Not Done":
|
|
415
|
-
app.mark_as_not_done(todo_index)
|
|
416
|
-
app.list_todos()
|
|
417
|
-
elif action == "Remove todo":
|
|
418
|
-
if not app.todos:
|
|
419
|
-
print("No todos to remove.")
|
|
420
|
-
continue
|
|
421
|
-
todo_choice = questionary.select(
|
|
422
|
-
"Select the todo to remove:",
|
|
423
|
-
choices=["<Back>"] + app.todos,
|
|
424
|
-
).ask()
|
|
425
|
-
|
|
426
|
-
if todo_choice == "<Back>":
|
|
427
|
-
continue
|
|
428
|
-
|
|
429
|
-
todo_to_remove = app.todos.index(todo_choice) + 1
|
|
430
|
-
app.remove_todo(todo_to_remove)
|
|
431
|
-
|
|
432
|
-
elif action == "Clear all todos":
|
|
433
|
-
confirm = questionary.confirm(
|
|
434
|
-
"Are you sure you want to clear all todos?"
|
|
435
|
-
).ask()
|
|
436
|
-
if confirm:
|
|
437
|
-
app.clear_all()
|
|
438
|
-
elif action == "Exit":
|
|
439
|
-
break
|
|
440
|
-
else:
|
|
441
|
-
break
|
|
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
|
|
377
|
+
|
|
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)
|