From 982d712cbf12acde21ce448e0d1ed28468f1c90e Mon Sep 17 00:00:00 2001
From: Christian Heller <c.heller@plomlompom.de>
Date: Fri, 12 Apr 2024 22:17:25 +0200
Subject: [PATCH] Add GET /todo and Todo retrieval by ID.

---
 plomtask/http.py   | 23 ++++++++++++++++++++---
 plomtask/todos.py  |  7 +++++++
 scripts/init.sql   |  8 ++++++++
 templates/day.html | 13 ++++++++++++-
 tests/todos.py     | 44 ++++++++++++++++++++++++++++++++++++--------
 5 files changed, 83 insertions(+), 12 deletions(-)

diff --git a/plomtask/http.py b/plomtask/http.py
index 1743b90..91c3224 100644
--- a/plomtask/http.py
+++ b/plomtask/http.py
@@ -10,6 +10,7 @@ from plomtask.exceptions import HandledException, BadFormatException, \
         NotFoundException
 from plomtask.db import DatabaseConnection, DatabaseFile
 from plomtask.processes import Process
+from plomtask.todos import Todo
 
 TEMPLATES_DIR = 'templates'
 
@@ -110,7 +111,7 @@ class TaskHandler(BaseHTTPRequestHandler):
         """Handle any GET request."""
         try:
             conn, site, params = self._init_handling()
-            if site in {'calendar', 'day', 'process', 'processes'}:
+            if site in {'calendar', 'day', 'process', 'processes', 'todo'}:
                 html = getattr(self, f'do_GET_{site}')(conn, params)
             elif '' == site:
                 self._redirect('/day')
@@ -137,7 +138,18 @@ class TaskHandler(BaseHTTPRequestHandler):
         """Show single Day of ?date=."""
         date = params.get_str('date', todays_date())
         day = Day.by_date(conn, date, create=True)
-        return self.server.jinja.get_template('day.html').render(day=day)
+        todos = Todo.by_date(conn, date)
+        return self.server.jinja.get_template('day.html').render(
+                day=day, processes=Process.all(conn), todos=todos)
+
+    def do_GET_todo(self, conn: DatabaseConnection, params:
+                    ParamsParser) -> str:
+        """Show single Todo of ?id=."""
+        id_ = params.get_int_or_none('id')
+        if id_ is None:
+            raise NotFoundException('Todo of ID not found.')
+        todo = Todo.by_id(conn, id_)
+        return self.server.jinja.get_template('todo.html').render(todo=todo)
 
     def do_GET_process(self, conn: DatabaseConnection,
                        params: ParamsParser) -> str:
@@ -177,11 +189,16 @@ class TaskHandler(BaseHTTPRequestHandler):
 
     def do_POST_day(self, conn: DatabaseConnection, params: ParamsParser,
                     form_data: PostvarsParser) -> None:
-        """Update or insert Day of date and fields defined in postvars."""
+        """Update or insert Day of date and Todos mapped to it."""
         date = params.get_str('date')
         day = Day.by_date(conn, date, create=True)
         day.comment = form_data.get_str('comment')
         day.save(conn)
+        process_id = form_data.get_int_or_none('new_todo')
+        if process_id is not None:
+            process = Process.by_id(conn, process_id)
+            todo = Todo(None, process, False, day)
+            todo.save(conn)
 
     def do_POST_process(self, conn: DatabaseConnection, params: ParamsParser,
                         form_data: PostvarsParser) -> None:
diff --git a/plomtask/todos.py b/plomtask/todos.py
index 1b6b740..7150f0d 100644
--- a/plomtask/todos.py
+++ b/plomtask/todos.py
@@ -28,6 +28,13 @@ class Todo:
                    is_done=row[2],
                    day=Day.by_date(db_conn, row[3]))
 
+    @classmethod
+    def by_id(cls, db_conn: DatabaseConnection, id_: int) -> Todo:
+        """Get Todo of .id_=id_."""
+        for row in db_conn.exec('SELECT * FROM todos WHERE id = ?', (id_,)):
+            return cls.from_table_row(row, db_conn)
+        raise NotFoundException(f'Todo of ID not found: {id_}')
+
     @classmethod
     def by_date(cls, db_conn: DatabaseConnection, date: str) -> list[Todo]:
         """Collect all Todos for Day of date."""
diff --git a/scripts/init.sql b/scripts/init.sql
index 1245030..6dca372 100644
--- a/scripts/init.sql
+++ b/scripts/init.sql
@@ -35,3 +35,11 @@ CREATE TABLE process_titles (
 CREATE TABLE processes (
     id INTEGER PRIMARY KEY
 );
+CREATE TABLE todos (
+    id INTEGER PRIMARY KEY,
+    process_id INTEGER NOT NULL,
+    is_done BOOLEAN NOT NULL,
+    day TEXT NOT NULL,
+    FOREIGN KEY (process_id) REFERENCES processes(id),
+    FOREIGN KEY (day) REFERENCES days(date)
+);
diff --git a/templates/day.html b/templates/day.html
index 7a34e58..44fd90d 100644
--- a/templates/day.html
+++ b/templates/day.html
@@ -7,7 +7,18 @@
 </p>
 <form action="day?date={{day.date}}" method="POST">
 comment: <input name="comment" value="{{day.comment|e}}" />
-<input type="submit" value="OK" />
+<input type="submit" value="OK" /><br />
+add todo: <input name="new_todo" list="processes" autocomplete="off" />
+<datalist id="processes">
+{% for process in processes %}
+<option value="{{process.id_}}">{{process.title.newest|e}}</option>
+{% endfor %}
+</datalist>
 </form>
+<ul>
+{% for todo in todos %}
+<li><a href="todo?id={{todo.id_}}">{{todo.process.title.newest|e}}</a>
+{% endfor %}
+</ul>
 {% endblock %}
 
diff --git a/tests/todos.py b/tests/todos.py
index 93b34d1..8bcd181 100644
--- a/tests/todos.py
+++ b/tests/todos.py
@@ -9,20 +9,36 @@ from plomtask.exceptions import NotFoundException
 class TestsWithDB(TestCaseWithDB):
     """Tests not requiring DB setup."""
 
-    def test_Todo_by_date(self) -> None:
+    def test_Todo_by_id(self) -> None:
         """Test creation and findability of Todos."""
+        day = Day('2024-01-01')
+        process = Process(None)
+        todo = Todo(None, process, False, day)
+        with self.assertRaises(NotFoundException):
+            todo.save(self.db_conn)
+        process.save_without_steps(self.db_conn)
+        todo.save(self.db_conn)
+        with self.assertRaises(NotFoundException):
+            _ = Todo.by_id(self.db_conn, 1)
+        day.save(self.db_conn)
+        self.assertEqual(Todo.by_id(self.db_conn, 1), todo)
+        with self.assertRaises(NotFoundException):
+            self.assertEqual(Todo.by_id(self.db_conn, 0), todo)
+        with self.assertRaises(NotFoundException):
+            self.assertEqual(Todo.by_id(self.db_conn, 2), todo)
+
+    def test_Todo_by_date(self) -> None:
+        """Test findability of Todos by date."""
         day1 = Day('2024-01-01')
         day2 = Day('2024-01-02')
-        process1 = Process(None)
-        todo1 = Todo(None, process1, False, day1)
-        with self.assertRaises(NotFoundException):
-            todo1.save(self.db_conn)
-        process1.save_without_steps(self.db_conn)
+        process = Process(None)
+        process.save_without_steps(self.db_conn)
+        todo1 = Todo(None, process, False, day1)
         todo1.save(self.db_conn)
-        todo2 = Todo(None, process1, False, day1)
+        todo2 = Todo(None, process, False, day1)
         todo2.save(self.db_conn)
         with self.assertRaises(NotFoundException):
-            Todo.by_date(self.db_conn, day1.date),
+            _ = Todo.by_date(self.db_conn, day1.date)
         day1.save(self.db_conn)
         day2.save(self.db_conn)
         self.assertEqual(Todo.by_date(self.db_conn, day1.date), [todo1, todo2])
@@ -58,3 +74,15 @@ class TestsWithServer(TestCaseWithServer):
         self.assertEqual(todo1.id_, 2)
         self.assertEqual(todo1.process.id_, process2.id_)
         self.assertEqual(todo1.is_done, False)
+
+    def test_do_GET_todo(self) -> None:
+        """Test GET /todo response codes."""
+        form_data = {'title': '', 'description': '', 'effort': 1}
+        self.check_post(form_data, '/process?id=', 302, '/')
+        form_data = {'comment': '', 'new_todo': 1}
+        self.check_post(form_data, '/day?date=2024-01-01', 302, '/')
+        self.check_get('/todo', 404)
+        self.check_get('/todo?id=', 404)
+        self.check_get('/todo?id=foo', 400)
+        self.check_get('/todo?id=0', 404)
+        self.check_get('/todo?id=1', 200)
-- 
2.30.2