From e150bee233a648950061b716dc1780581105ede6 Mon Sep 17 00:00:00 2001
From: Christian Heller <c.heller@plomlompom.de>
Date: Sat, 18 May 2024 08:04:10 +0200
Subject: [PATCH] Add Todo. and Process.calendarize to identify what Todos to
 show in calendar.

---
 migrations/3_add_Todo_and_Process_calendarize.sql |  2 ++
 migrations/{init_2.sql => init_3.sql}             |  4 +++-
 plomtask/days.py                                  |  7 +++++++
 plomtask/db.py                                    |  5 ++++-
 plomtask/http.py                                  |  4 ++++
 plomtask/processes.py                             |  7 ++++---
 plomtask/todos.py                                 | 14 ++++++++++----
 templates/calendar.html                           |  8 ++++++++
 templates/process.html                            |  5 +++++
 templates/todo.html                               |  5 +++++
 10 files changed, 52 insertions(+), 9 deletions(-)
 create mode 100644 migrations/3_add_Todo_and_Process_calendarize.sql
 rename migrations/{init_2.sql => init_3.sql} (96%)

diff --git a/migrations/3_add_Todo_and_Process_calendarize.sql b/migrations/3_add_Todo_and_Process_calendarize.sql
new file mode 100644
index 0000000..dcd65b2
--- /dev/null
+++ b/migrations/3_add_Todo_and_Process_calendarize.sql
@@ -0,0 +1,2 @@
+ALTER TABLE todos ADD COLUMN calendarize BOOLEAN NOT NULL DEFAULT FALSE;
+ALTER TABLE processes ADD COLUMN calendarize BOOLEAN NOT NULL DEFAULT FALSE;
diff --git a/migrations/init_2.sql b/migrations/init_3.sql
similarity index 96%
rename from migrations/init_2.sql
rename to migrations/init_3.sql
index 17e2db5..f261fd7 100644
--- a/migrations/init_2.sql
+++ b/migrations/init_3.sql
@@ -72,7 +72,8 @@ CREATE TABLE process_titles (
     FOREIGN KEY (parent) REFERENCES processes(id)
 );
 CREATE TABLE processes (
-    id INTEGER PRIMARY KEY
+    id INTEGER PRIMARY KEY,
+    calendarize BOOLEAN NOT NULL DEFAULT FALSE
 );
 CREATE TABLE todo_children (
     parent INTEGER NOT NULL,
@@ -109,6 +110,7 @@ CREATE TABLE todos (
     day TEXT NOT NULL,
     comment TEXT NOT NULL DEFAULT "",
     effort REAL,
+    calendarize BOOLEAN NOT NULL DEFAULT FALSE,
     FOREIGN KEY (process) REFERENCES processes(id),
     FOREIGN KEY (day) REFERENCES days(id)
 );
diff --git a/plomtask/days.py b/plomtask/days.py
index 5e97560..fe1ba44 100644
--- a/plomtask/days.py
+++ b/plomtask/days.py
@@ -3,6 +3,7 @@ from __future__ import annotations
 from datetime import datetime, timedelta
 from plomtask.exceptions import BadFormatException
 from plomtask.db import DatabaseConnection, BaseModel
+from plomtask.todos import Todo
 
 DATE_FORMAT = '%Y-%m-%d'
 MIN_RANGE_DATE = '2024-01-01'
@@ -36,6 +37,7 @@ class Day(BaseModel[str]):
         super().__init__(id_)
         self.datetime = datetime.strptime(self.date, DATE_FORMAT)
         self.comment = comment
+        self.calendarized_todos: list[Todo] = []
 
     def __lt__(self, other: Day) -> bool:
         return self.date < other.date
@@ -105,3 +107,8 @@ class Day(BaseModel[str]):
         """Return date succeeding date of this Day."""
         next_datetime = self.datetime + timedelta(days=1)
         return next_datetime.strftime(DATE_FORMAT)
+
+    def collect_calendarized_todos(self, db_conn: DatabaseConnection) -> None:
+        """Fill self.calendarized_todos."""
+        self.calendarized_todos = [t for t in Todo.by_date(db_conn, self.date)
+                                   if t.calendarize]
diff --git a/plomtask/db.py b/plomtask/db.py
index 0e7df15..b4dc3e9 100644
--- a/plomtask/db.py
+++ b/plomtask/db.py
@@ -7,7 +7,7 @@ from sqlite3 import connect as sql_connect, Cursor, Row
 from typing import Any, Self, TypeVar, Generic
 from plomtask.exceptions import HandledException, NotFoundException
 
-EXPECTED_DB_VERSION = 2
+EXPECTED_DB_VERSION = 3
 MIGRATIONS_DIR = 'migrations'
 FILENAME_DB_SCHEMA = f'init_{EXPECTED_DB_VERSION}.sql'
 PATH_DB_SCHEMA = f'{MIGRATIONS_DIR}/{FILENAME_DB_SCHEMA}'
@@ -131,6 +131,9 @@ class DatabaseFile:  # pylint: disable=too-few-public-methods
                         new_row += [f'    {segment}']
                 new_row[0] = new_row[0].lstrip()
                 new_row[-1] = new_row[-1].lstrip()
+                if new_row[-1] != ')' and new_row[-3][-1] != ',':
+                    new_row[-3] = new_row[-3] + ','
+                    new_row[-2:] = ['    ' + new_row[-1][:-1]] + [')']
                 new_rows += ['\n'.join(new_row)]
             return new_rows
 
diff --git a/plomtask/http.py b/plomtask/http.py
index d76ac05..080af8c 100644
--- a/plomtask/http.py
+++ b/plomtask/http.py
@@ -113,6 +113,8 @@ class TaskHandler(BaseHTTPRequestHandler):
         start = self.params.get_str('start')
         end = self.params.get_str('end')
         days = Day.all(self.conn, date_range=(start, end), fill_gaps=True)
+        for day in days:
+            day.collect_calendarized_todos(self.conn)
         return {'start': start, 'end': end, 'days': days}
 
     def do_GET_day(self) -> dict[str, object]:
@@ -302,6 +304,7 @@ class TaskHandler(BaseHTTPRequestHandler):
         todo.set_enables(self.conn, self.form_data.get_all_int('enables'))
         todo.set_disables(self.conn, self.form_data.get_all_int('disables'))
         todo.is_done = len(self.form_data.get_all_str('done')) > 0
+        todo.calendarize = len(self.form_data.get_all_str('calendarize')) > 0
         todo.comment = self.form_data.get_str('comment', ignore_strict=True)
         todo.save(self.conn)
         for condition in todo.enables:
@@ -325,6 +328,7 @@ class TaskHandler(BaseHTTPRequestHandler):
                                self.form_data.get_all_int('condition'))
         process.set_enables(self.conn, self.form_data.get_all_int('enables'))
         process.set_disables(self.conn, self.form_data.get_all_int('disables'))
+        process.calendarize = self.form_data.get_all_str('calendarize') != []
         process.save(self.conn)
         process.explicit_steps = []
         steps: list[tuple[int | None, int, int | None]] = []
diff --git a/plomtask/processes.py b/plomtask/processes.py
index 1778e4f..e136421 100644
--- a/plomtask/processes.py
+++ b/plomtask/processes.py
@@ -22,20 +22,21 @@ class ProcessStepsNode:
 
 class Process(BaseModel[int], ConditionsRelations):
     """Template for, and metadata for, Todos, and their arrangements."""
+    # pylint: disable=too-many-instance-attributes
     table_name = 'processes'
+    to_save = ['calendarize']
     to_save_versioned = ['title', 'description', 'effort']
     to_save_relations = [('process_conditions', 'process', 'conditions'),
                          ('process_enables', 'process', 'enables'),
                          ('process_disables', 'process', 'disables')]
 
-    # pylint: disable=too-many-instance-attributes
-
-    def __init__(self, id_: int | None) -> None:
+    def __init__(self, id_: int | None, calendarize: bool = False) -> None:
         super().__init__(id_)
         self.title = VersionedAttribute(self, 'process_titles', 'UNNAMED')
         self.description = VersionedAttribute(self, 'process_descriptions', '')
         self.effort = VersionedAttribute(self, 'process_efforts', 1.0)
         self.explicit_steps: list[ProcessStep] = []
+        self.calendarize = calendarize
         self.conditions: list[Condition] = []
         self.enables: list[Condition] = []
         self.disables: list[Condition] = []
diff --git a/plomtask/todos.py b/plomtask/todos.py
index 7cbe989..0fea234 100644
--- a/plomtask/todos.py
+++ b/plomtask/todos.py
@@ -23,7 +23,8 @@ class Todo(BaseModel[int], ConditionsRelations):
     """Individual actionable."""
     # pylint: disable=too-many-instance-attributes
     table_name = 'todos'
-    to_save = ['process_id', 'is_done', 'date', 'comment', 'effort']
+    to_save = ['process_id', 'is_done', 'date', 'comment', 'effort',
+               'calendarize']
     to_save_relations = [('todo_conditions', 'todo', 'conditions'),
                          ('todo_enables', 'todo', 'enables'),
                          ('todo_disables', 'todo', 'disables'),
@@ -31,9 +32,12 @@ class Todo(BaseModel[int], ConditionsRelations):
                          ('todo_children', 'child', 'parents')]
 
     # pylint: disable=too-many-arguments
-    def __init__(self, id_: int | None, process: Process,
-                 is_done: bool, date: str, comment: str = '',
-                 effort: None | float = None) -> None:
+    def __init__(self, id_: int | None,
+                 process: Process,
+                 is_done: bool,
+                 date: str, comment: str = '',
+                 effort: None | float = None,
+                 calendarize: bool = False) -> None:
         super().__init__(id_)
         if process.id_ is None:
             raise NotFoundException('Process of Todo without ID (not saved?)')
@@ -44,10 +48,12 @@ class Todo(BaseModel[int], ConditionsRelations):
         self.effort = effort
         self.children: list[Todo] = []
         self.parents: list[Todo] = []
+        self.calendarize = calendarize
         self.conditions: list[Condition] = []
         self.enables: list[Condition] = []
         self.disables: list[Condition] = []
         if not self.id_:
+            self.calendarize = self.process.calendarize
             self.conditions = self.process.conditions[:]
             self.enables = self.process.enables[:]
             self.disables = self.process.disables[:]
diff --git a/templates/calendar.html b/templates/calendar.html
index 220e9bb..7724203 100644
--- a/templates/calendar.html
+++ b/templates/calendar.html
@@ -49,6 +49,14 @@ to <input name="end" value="{{end}}" />
 <td>{{day.comment|e}}</td>
 </tr>
 
+{% for todo in day.calendarized_todos %}
+<tr>
+<td>[{% if todo.is_done %}X{% else %} {% endif %}]</td>
+<td><a href="todo?id={{todo.id_}}">{{todo.title.newest|e}}</td>
+<td>{{todo.comment|e}}</td>
+</tr>
+{% endfor %}
+
 {% endfor %}
 </table>
 {% endblock %}
diff --git a/templates/process.html b/templates/process.html
index d833c52..6dea493 100644
--- a/templates/process.html
+++ b/templates/process.html
@@ -55,6 +55,11 @@ add: <input name="new_step_to_{{step_id}}" list="candidates" autocomplete="off"
 <td><textarea name="description">{{process.description.newest|e}}</textarea><br />{% if process.id_ %} [<a href="process_descriptions?id={{process.id_}}">history</a>]{% endif %}</td>
 </tr>
 
+<tr>
+<th>calendarize</th>
+<td><input type="checkbox" name="calendarize" {% if process.calendarize %}checked {% endif %}</td>
+</tr>
+
 <tr>
 <th>conditions</th>
 <td>{{ macros.simple_checkbox_table("condition", process.conditions, "condition", "condition_candidates") }}</td>
diff --git a/templates/todo.html b/templates/todo.html
index 6817cb9..efaabdd 100644
--- a/templates/todo.html
+++ b/templates/todo.html
@@ -33,6 +33,11 @@
 <td><input name="comment" value="{{todo.comment|e}}"/></td>
 </tr>
 
+<tr>
+<th>calendarize</th>
+<td><input type="checkbox" name="calendarize" {% if todo.calendarize %}checked {% endif %}</td>
+</tr>
+
 <tr>
 <th>conditions</th>
 <td>{{ macros.simple_checkbox_table("condition", todo.conditions, "condition", "condition_candidates") }}</td>
-- 
2.30.2