From f92de64d072009c8c4bf96b9eeb9fa245045662b Mon Sep 17 00:00:00 2001
From: Christian Heller <c.heller@plomlompom.de>
Date: Thu, 30 May 2024 08:12:33 +0200
Subject: [PATCH] Use same date ranging code for Day and Todo filtering.

---
 plomtask/dating.py | 24 ++++++++++++++++++++++++
 plomtask/days.py   | 36 +++---------------------------------
 plomtask/db.py     | 24 ++++++++++++++++++++++++
 plomtask/http.py   | 11 +++++------
 plomtask/todos.py  | 15 ++++++++++-----
 tests/days.py      |  3 ++-
 tests/todos.py     |  3 ++-
 7 files changed, 70 insertions(+), 46 deletions(-)
 create mode 100644 plomtask/dating.py

diff --git a/plomtask/dating.py b/plomtask/dating.py
new file mode 100644
index 0000000..e143fb6
--- /dev/null
+++ b/plomtask/dating.py
@@ -0,0 +1,24 @@
+"""Various utilities for handling dates."""
+from datetime import datetime
+from plomtask.exceptions import BadFormatException
+
+DATE_FORMAT = '%Y-%m-%d'
+MIN_RANGE_DATE = '2024-01-01'
+MAX_RANGE_DATE = '2030-12-31'
+
+
+def valid_date(date_str: str) -> str:
+    """Validate date against DATE_FORMAT or 'today', return in DATE_FORMAT."""
+    if date_str == 'today':
+        date_str = todays_date()
+    try:
+        dt = datetime.strptime(date_str, DATE_FORMAT)
+    except (ValueError, TypeError) as e:
+        msg = f'Given date of wrong format: {date_str}'
+        raise BadFormatException(msg) from e
+    return dt.strftime(DATE_FORMAT)
+
+
+def todays_date() -> str:
+    """Return current date in DATE_FORMAT."""
+    return datetime.now().strftime(DATE_FORMAT)
diff --git a/plomtask/days.py b/plomtask/days.py
index e3d56d7..d7083b4 100644
--- a/plomtask/days.py
+++ b/plomtask/days.py
@@ -1,30 +1,9 @@
 """Collecting Day and date-related items."""
 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'
-MAX_RANGE_DATE = '2030-12-31'
-
-
-def valid_date(date_str: str) -> str:
-    """Validate date against DATE_FORMAT or 'today', return in DATE_FORMAT."""
-    if date_str == 'today':
-        date_str = todays_date()
-    try:
-        dt = datetime.strptime(date_str, DATE_FORMAT)
-    except (ValueError, TypeError) as e:
-        msg = f'Given date of wrong format: {date_str}'
-        raise BadFormatException(msg) from e
-    return dt.strftime(DATE_FORMAT)
-
-
-def todays_date() -> str:
-    """Return current date in DATE_FORMAT."""
-    return datetime.now().strftime(DATE_FORMAT)
+from plomtask.dating import (DATE_FORMAT, valid_date)
 
 
 class Day(BaseModel[str]):
@@ -48,20 +27,11 @@ class Day(BaseModel[str]):
             fill_gaps: bool = False) -> list[Day]:
         """Return list of Days in database within (open) date_range interval.
 
-        If no range values provided, defaults them to MIN_RANGE_DATE and
-        MAX_RANGE_DATE. Also knows to properly interpret 'today' as value.
-
         On fill_gaps=True, will instantiate (without saving) Days of all dates
         within the date range that don't exist yet.
         """
-        min_date = '2024-01-01'
-        max_date = '2030-12-31'
-        start_date = valid_date(date_range[0] if date_range[0] else min_date)
-        end_date = valid_date(date_range[1] if date_range[1] else max_date)
-        days = []
-        sql = 'SELECT id FROM days WHERE id >= ? AND id <= ?'
-        for row in db_conn.exec(sql, (start_date, end_date)):
-            days += [cls.by_id(db_conn, row[0])]
+        ret = cls.by_date_range_with_limits(db_conn, date_range, 'id')
+        days, start_date, end_date = ret
         days.sort()
         if fill_gaps:
             if start_date not in [d.date for d in days]:
diff --git a/plomtask/db.py b/plomtask/db.py
index 548381e..4396b44 100644
--- a/plomtask/db.py
+++ b/plomtask/db.py
@@ -6,6 +6,7 @@ from difflib import Differ
 from sqlite3 import connect as sql_connect, Cursor, Row
 from typing import Any, Self, TypeVar, Generic
 from plomtask.exceptions import HandledException, NotFoundException
+from plomtask.dating import (MIN_RANGE_DATE, MAX_RANGE_DATE, valid_date)
 
 EXPECTED_DB_VERSION = 4
 MIGRATIONS_DIR = 'migrations'
@@ -354,6 +355,29 @@ class BaseModel(Generic[BaseModelId]):
                 items[item.id_] = item
         return list(items.values())
 
+    @classmethod
+    def by_date_range_with_limits(cls: type[BaseModelInstance],
+                                  db_conn: DatabaseConnection,
+                                  date_range: tuple[str, str] = ('', ''),
+                                  date_col: str = 'day'
+                                  ) -> tuple[list[BaseModelInstance], str,
+                                             str]:
+        """Return list of Days in database within (open) date_range interval.
+
+        If no range values provided, defaults them to MIN_RANGE_DATE and
+        MAX_RANGE_DATE. Also knows to properly interpret 'today' as value.
+        """
+        min_date = MIN_RANGE_DATE
+        max_date = MAX_RANGE_DATE
+        start_date = valid_date(date_range[0] if date_range[0] else min_date)
+        end_date = valid_date(date_range[1] if date_range[1] else max_date)
+        items = []
+        sql = f'SELECT id FROM {cls.table_name} '
+        sql += f'WHERE {date_col} >= ? AND {date_col} <= ?'
+        for row in db_conn.exec(sql, (start_date, end_date)):
+            items += [cls.by_id(db_conn, row[0])]
+        return items, start_date, end_date
+
     @classmethod
     def matching(cls: type[BaseModelInstance], db_conn: DatabaseConnection,
                  pattern: str) -> list[BaseModelInstance]:
diff --git a/plomtask/http.py b/plomtask/http.py
index b81083b..a2e8fa6 100644
--- a/plomtask/http.py
+++ b/plomtask/http.py
@@ -5,7 +5,8 @@ from http.server import HTTPServer
 from urllib.parse import urlparse, parse_qs
 from os.path import split as path_split
 from jinja2 import Environment as JinjaEnv, FileSystemLoader as JinjaFSLoader
-from plomtask.days import Day, todays_date
+from plomtask.dating import todays_date
+from plomtask.days import Day
 from plomtask.exceptions import HandledException, BadFormatException, \
         NotFoundException
 from plomtask.db import DatabaseConnection, DatabaseFile
@@ -160,11 +161,9 @@ class TaskHandler(BaseHTTPRequestHandler):
         process_id = self.params.get_int_or_none('process_id')
         comment_pattern = self.params.get_str('comment_pattern')
         todos = []
-        for t in Todo.matching(self.conn, comment_pattern):
-            # pylint: disable=too-many-boolean-expressions
-            if (start and t.date < start)\
-                    or (end and t.date > end)\
-                    or (process_id and t.process.id_ != process_id):
+        for t in Todo.by_date_range(self.conn, (start, end)):
+            if (process_id and t.process.id_ != process_id)\
+                    or (comment_pattern not in t.comment):
                 continue
             todos += [t]
         if sort_by == 'doneness':
diff --git a/plomtask/todos.py b/plomtask/todos.py
index ffef677..46a353d 100644
--- a/plomtask/todos.py
+++ b/plomtask/todos.py
@@ -9,6 +9,7 @@ from plomtask.versioned_attributes import VersionedAttribute
 from plomtask.conditions import Condition, ConditionsRelations
 from plomtask.exceptions import (NotFoundException, BadFormatException,
                                  HandledException)
+from plomtask.dating import valid_date
 
 
 @dataclass
@@ -46,7 +47,7 @@ class Todo(BaseModel[int], ConditionsRelations):
             raise NotFoundException('Process of Todo without ID (not saved?)')
         self.process = process
         self._is_done = is_done
-        self.date = date
+        self.date = valid_date(date)
         self.comment = comment
         self.effort = effort
         self.children: list[Todo] = []
@@ -59,6 +60,13 @@ class Todo(BaseModel[int], ConditionsRelations):
             self.enables = self.process.enables[:]
             self.disables = self.process.disables[:]
 
+    @classmethod
+    def by_date_range(cls, db_conn: DatabaseConnection,
+                      date_range: tuple[str, str] = ('', '')) -> list[Todo]:
+        """Collect Todos of Days within date_range."""
+        todos, _, _ = cls.by_date_range_with_limits(db_conn, date_range)
+        return todos
+
     @classmethod
     def create_with_children(cls, db_conn: DatabaseConnection, date: str,
                              process_ids: list[int]) -> list[Todo]:
@@ -117,10 +125,7 @@ class Todo(BaseModel[int], ConditionsRelations):
     @classmethod
     def by_date(cls, db_conn: DatabaseConnection, date: str) -> list[Todo]:
         """Collect all Todos for Day of date."""
-        todos = []
-        for id_ in db_conn.column_where('todos', 'id', 'day', date):
-            todos += [cls.by_id(db_conn, id_)]
-        return todos
+        return cls.by_date_range(db_conn, (date, date))
 
     @property
     def is_doable(self) -> bool:
diff --git a/tests/days.py b/tests/days.py
index 0556164..c1e1343 100644
--- a/tests/days.py
+++ b/tests/days.py
@@ -2,7 +2,8 @@
 from unittest import TestCase
 from datetime import datetime
 from tests.utils import TestCaseWithDB, TestCaseWithServer
-from plomtask.days import Day, todays_date
+from plomtask.dating import todays_date
+from plomtask.days import Day
 from plomtask.exceptions import BadFormatException
 
 
diff --git a/tests/todos.py b/tests/todos.py
index 4ba5a1c..86986b6 100644
--- a/tests/todos.py
+++ b/tests/todos.py
@@ -63,7 +63,8 @@ class TestsWithDB(TestCaseWithDB):
         t2.save(self.db_conn)
         self.assertEqual(Todo.by_date(self.db_conn, self.date1), [t1, t2])
         self.assertEqual(Todo.by_date(self.db_conn, self.date2), [])
-        self.assertEqual(Todo.by_date(self.db_conn, 'foo'), [])
+        with self.assertRaises(BadFormatException):
+            self.assertEqual(Todo.by_date(self.db_conn, 'foo'), [])
 
     def test_Todo_on_conditions(self) -> None:
         """Test effect of Todos on Conditions."""
-- 
2.30.2