--- /dev/null
+ALTER TABLE todos ADD COLUMN new_day_id INTEGER;
+UPDATE todos SET new_day_id = (
+ SELECT days.days_since_millennium
+ FROM days
+ WHERE days.id = todos.day);
+
+CREATE TABLE days_new (
+ id INTEGER PRIMARY KEY,
+ comment TEXT NOT NULL
+);
+INSERT INTO days_new SELECT
+ days_since_millennium,
+ comment
+FROM days;
+DROP TABLE days;
+ALTER TABLE days_new RENAME TO days;
+
+CREATE TABLE todos_new (
+ id INTEGER PRIMARY KEY,
+ process INTEGER NOT NULL,
+ is_done BOOLEAN NOT NULL,
+ day INTEGER 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)
+);
+INSERT INTO todos_new SELECT
+ id,
+ process,
+ is_done,
+ new_day_id,
+ comment,
+ effort,
+ calendarize
+FROM todos;
+DROP TABLE todos;
+ALTER TABLE todos_new RENAME TO todos;
+++ /dev/null
-CREATE TABLE condition_descriptions (
- parent INTEGER NOT NULL,
- timestamp TEXT NOT NULL,
- description TEXT NOT NULL,
- PRIMARY KEY (parent, timestamp),
- FOREIGN KEY (parent) REFERENCES conditions(id)
-);
-CREATE TABLE condition_titles (
- parent INTEGER NOT NULL,
- timestamp TEXT NOT NULL,
- title TEXT NOT NULL,
- PRIMARY KEY (parent, timestamp),
- FOREIGN KEY (parent) REFERENCES conditions(id)
-);
-CREATE TABLE conditions (
- id INTEGER PRIMARY KEY,
- is_active BOOLEAN NOT NULL
-);
-CREATE TABLE days (
- id TEXT PRIMARY KEY,
- comment TEXT NOT NULL,
- days_since_millennium INTEGER NOT NULL DEFAULT 0
-);
-CREATE TABLE process_blockers (
- process INTEGER NOT NULL,
- condition INTEGER NOT NULL,
- PRIMARY KEY (process, condition),
- FOREIGN KEY (process) REFERENCES processes(id),
- FOREIGN KEY (condition) REFERENCES conditions(id)
-);
-CREATE TABLE process_conditions (
- process INTEGER NOT NULL,
- condition INTEGER NOT NULL,
- PRIMARY KEY (process, condition),
- FOREIGN KEY (process) REFERENCES processes(id),
- FOREIGN KEY (condition) REFERENCES conditions(id)
-);
-CREATE TABLE process_descriptions (
- parent INTEGER NOT NULL,
- timestamp TEXT NOT NULL,
- description TEXT NOT NULL,
- PRIMARY KEY (parent, timestamp),
- FOREIGN KEY (parent) REFERENCES processes(id)
-);
-CREATE TABLE process_disables (
- process INTEGER NOT NULL,
- condition INTEGER NOT NULL,
- PRIMARY KEY(process, condition),
- FOREIGN KEY (process) REFERENCES processes(id),
- FOREIGN KEY (condition) REFERENCES conditions(id)
-);
-CREATE TABLE process_efforts (
- parent INTEGER NOT NULL,
- timestamp TEXT NOT NULL,
- effort REAL NOT NULL,
- PRIMARY KEY (parent, timestamp),
- FOREIGN KEY (parent) REFERENCES processes(id)
-);
-CREATE TABLE process_enables (
- process INTEGER NOT NULL,
- condition INTEGER NOT NULL,
- PRIMARY KEY(process, condition),
- FOREIGN KEY (process) REFERENCES processes(id),
- FOREIGN KEY (condition) REFERENCES conditions(id)
-);
-CREATE TABLE process_step_suppressions (
- process INTEGER NOT NULL,
- process_step INTEGER NOT NULL,
- PRIMARY KEY (process, process_step),
- FOREIGN KEY (process) REFERENCES processes(id),
- FOREIGN KEY (process_step) REFERENCES process_steps(id)
-);
-CREATE TABLE process_steps (
- id INTEGER PRIMARY KEY,
- owner INTEGER NOT NULL,
- step_process INTEGER NOT NULL,
- parent_step INTEGER,
- FOREIGN KEY (owner) REFERENCES processes(id),
- FOREIGN KEY (step_process) REFERENCES processes(id),
- FOREIGN KEY (parent_step) REFERENCES process_steps(step_id)
-);
-CREATE TABLE process_titles (
- parent INTEGER NOT NULL,
- timestamp TEXT NOT NULL,
- title TEXT NOT NULL,
- PRIMARY KEY (parent, timestamp),
- FOREIGN KEY (parent) REFERENCES processes(id)
-);
-CREATE TABLE processes (
- id INTEGER PRIMARY KEY,
- calendarize BOOLEAN NOT NULL DEFAULT FALSE
-);
-CREATE TABLE todo_blockers (
- todo INTEGER NOT NULL,
- condition INTEGER NOT NULL,
- PRIMARY KEY (todo, condition),
- FOREIGN KEY (todo) REFERENCES todos(id),
- FOREIGN KEY (condition) REFERENCES conditions(id)
-);
-CREATE TABLE todo_children (
- parent INTEGER NOT NULL,
- child INTEGER NOT NULL,
- PRIMARY KEY (parent, child),
- FOREIGN KEY (parent) REFERENCES todos(id),
- FOREIGN KEY (child) REFERENCES todos(id)
-);
-CREATE TABLE todo_conditions (
- todo INTEGER NOT NULL,
- condition INTEGER NOT NULL,
- PRIMARY KEY(todo, condition),
- FOREIGN KEY (todo) REFERENCES todos(id),
- FOREIGN KEY (condition) REFERENCES conditions(id)
-);
-CREATE TABLE todo_disables (
- todo INTEGER NOT NULL,
- condition INTEGER NOT NULL,
- PRIMARY KEY(todo, condition),
- FOREIGN KEY (todo) REFERENCES todos(id),
- FOREIGN KEY (condition) REFERENCES conditions(id)
-);
-CREATE TABLE todo_enables (
- todo INTEGER NOT NULL,
- condition INTEGER NOT NULL,
- PRIMARY KEY(todo, condition),
- FOREIGN KEY (todo) REFERENCES todos(id),
- FOREIGN KEY (condition) REFERENCES conditions(id)
-);
-CREATE TABLE todos (
- id INTEGER PRIMARY KEY,
- process INTEGER NOT NULL,
- is_done BOOLEAN NOT NULL,
- 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)
-);
--- /dev/null
+CREATE TABLE "days" (
+ id INTEGER PRIMARY KEY,
+ comment TEXT NOT NULL
+);
+CREATE TABLE "todos" (
+ id INTEGER PRIMARY KEY,
+ process INTEGER NOT NULL,
+ is_done BOOLEAN NOT NULL,
+ day INTEGER 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)
+);
+CREATE TABLE condition_descriptions (
+ parent INTEGER NOT NULL,
+ timestamp TEXT NOT NULL,
+ description TEXT NOT NULL,
+ PRIMARY KEY (parent, timestamp),
+ FOREIGN KEY (parent) REFERENCES conditions(id)
+);
+CREATE TABLE condition_titles (
+ parent INTEGER NOT NULL,
+ timestamp TEXT NOT NULL,
+ title TEXT NOT NULL,
+ PRIMARY KEY (parent, timestamp),
+ FOREIGN KEY (parent) REFERENCES conditions(id)
+);
+CREATE TABLE conditions (
+ id INTEGER PRIMARY KEY,
+ is_active BOOLEAN NOT NULL
+);
+CREATE TABLE process_blockers (
+ process INTEGER NOT NULL,
+ condition INTEGER NOT NULL,
+ PRIMARY KEY (process, condition),
+ FOREIGN KEY (process) REFERENCES processes(id),
+ FOREIGN KEY (condition) REFERENCES conditions(id)
+);
+CREATE TABLE process_conditions (
+ process INTEGER NOT NULL,
+ condition INTEGER NOT NULL,
+ PRIMARY KEY (process, condition),
+ FOREIGN KEY (process) REFERENCES processes(id),
+ FOREIGN KEY (condition) REFERENCES conditions(id)
+);
+CREATE TABLE process_descriptions (
+ parent INTEGER NOT NULL,
+ timestamp TEXT NOT NULL,
+ description TEXT NOT NULL,
+ PRIMARY KEY (parent, timestamp),
+ FOREIGN KEY (parent) REFERENCES processes(id)
+);
+CREATE TABLE process_disables (
+ process INTEGER NOT NULL,
+ condition INTEGER NOT NULL,
+ PRIMARY KEY(process, condition),
+ FOREIGN KEY (process) REFERENCES processes(id),
+ FOREIGN KEY (condition) REFERENCES conditions(id)
+);
+CREATE TABLE process_efforts (
+ parent INTEGER NOT NULL,
+ timestamp TEXT NOT NULL,
+ effort REAL NOT NULL,
+ PRIMARY KEY (parent, timestamp),
+ FOREIGN KEY (parent) REFERENCES processes(id)
+);
+CREATE TABLE process_enables (
+ process INTEGER NOT NULL,
+ condition INTEGER NOT NULL,
+ PRIMARY KEY(process, condition),
+ FOREIGN KEY (process) REFERENCES processes(id),
+ FOREIGN KEY (condition) REFERENCES conditions(id)
+);
+CREATE TABLE process_step_suppressions (
+ process INTEGER NOT NULL,
+ process_step INTEGER NOT NULL,
+ PRIMARY KEY (process, process_step),
+ FOREIGN KEY (process) REFERENCES processes(id),
+ FOREIGN KEY (process_step) REFERENCES process_steps(id)
+);
+CREATE TABLE process_steps (
+ id INTEGER PRIMARY KEY,
+ owner INTEGER NOT NULL,
+ step_process INTEGER NOT NULL,
+ parent_step INTEGER,
+ FOREIGN KEY (owner) REFERENCES processes(id),
+ FOREIGN KEY (step_process) REFERENCES processes(id),
+ FOREIGN KEY (parent_step) REFERENCES process_steps(step_id)
+);
+CREATE TABLE process_titles (
+ parent INTEGER NOT NULL,
+ timestamp TEXT NOT NULL,
+ title TEXT NOT NULL,
+ PRIMARY KEY (parent, timestamp),
+ FOREIGN KEY (parent) REFERENCES processes(id)
+);
+CREATE TABLE processes (
+ id INTEGER PRIMARY KEY,
+ calendarize BOOLEAN NOT NULL DEFAULT FALSE
+);
+CREATE TABLE todo_blockers (
+ todo INTEGER NOT NULL,
+ condition INTEGER NOT NULL,
+ PRIMARY KEY (todo, condition),
+ FOREIGN KEY (todo) REFERENCES todos(id),
+ FOREIGN KEY (condition) REFERENCES conditions(id)
+);
+CREATE TABLE todo_children (
+ parent INTEGER NOT NULL,
+ child INTEGER NOT NULL,
+ PRIMARY KEY (parent, child),
+ FOREIGN KEY (parent) REFERENCES todos(id),
+ FOREIGN KEY (child) REFERENCES todos(id)
+);
+CREATE TABLE todo_conditions (
+ todo INTEGER NOT NULL,
+ condition INTEGER NOT NULL,
+ PRIMARY KEY(todo, condition),
+ FOREIGN KEY (todo) REFERENCES todos(id),
+ FOREIGN KEY (condition) REFERENCES conditions(id)
+);
+CREATE TABLE todo_disables (
+ todo INTEGER NOT NULL,
+ condition INTEGER NOT NULL,
+ PRIMARY KEY(todo, condition),
+ FOREIGN KEY (todo) REFERENCES todos(id),
+ FOREIGN KEY (condition) REFERENCES conditions(id)
+);
+CREATE TABLE todo_enables (
+ todo INTEGER NOT NULL,
+ condition INTEGER NOT NULL,
+ PRIMARY KEY(todo, condition),
+ FOREIGN KEY (todo) REFERENCES todos(id),
+ FOREIGN KEY (condition) REFERENCES conditions(id)
+);
"""Various utilities for handling dates."""
-from datetime import date as datetime_date, timedelta
+from datetime import date as dt_date, timedelta
from plomtask.exceptions import BadFormatException
-def valid_date(date_str: str) -> str:
- """Validate against ISO format/relative terms; return in ISO format."""
+def dt_date_from_str(date_str: str) -> dt_date:
+ """Validate against ISO format, colloq. terms; return as datetime.date."""
if date_str == 'today':
date_str = date_in_n_days(0)
elif date_str == 'yesterday':
elif date_str == 'tomorrow':
date_str = date_in_n_days(1)
try:
- date = datetime_date.fromisoformat(date_str)
+ date = dt_date.fromisoformat(date_str)
except (ValueError, TypeError) as e:
msg = f'Given date of wrong format: {date_str}'
raise BadFormatException(msg) from e
- return date.isoformat()
+ return date
+
+
+def days_n_from_dt_date(date: dt_date) -> int:
+ """Return number of days from Jan 1st 2000 to datetime.date."""
+ return (date - dt_date(2000, 1, 1)).days
+
+
+def dt_date_from_days_n(days_n: int) -> dt_date:
+ """Return datetime.date for days_n after Jan 1st 2000."""
+ return dt_date(2000, 1, 1) + timedelta(days=days_n)
def date_in_n_days(n: int) -> str:
"""Return in ISO format date from today + n days."""
- date = datetime_date.today() + timedelta(days=n)
+ date = dt_date.today() + timedelta(days=n)
return date.isoformat()
from __future__ import annotations
from typing import Any, Self
from sqlite3 import Row
-from datetime import date as datetime_date, timedelta
+from datetime import date as dt_date, timedelta
from plomtask.db import DatabaseConnection, BaseModel, BaseModelId
from plomtask.todos import Todo
-from plomtask.dating import valid_date
+from plomtask.dating import dt_date_from_days_n, days_n_from_dt_date
class Day(BaseModel):
"""Individual days defined by their dates."""
table_name = 'days'
- to_save_simples = ['comment', 'days_since_millennium']
+ to_save_simples = ['comment']
add_to_dict = ['todos']
can_create_by_id = True
- def __init__(self,
- date: str,
- comment: str = '',
- days_since_millennium: int = -1
- ) -> None:
- id_ = valid_date(date)
+ def __init__(self, id_: int, comment: str = '') -> None:
super().__init__(id_)
- self.date = datetime_date.fromisoformat(self.date_str)
self.comment = comment
self.todos: list[Todo] = []
- self.days_since_millennium = days_since_millennium
-
- def __lt__(self, other: Self) -> bool:
- return self.date_str < other.date_str
@classmethod
def from_table_row(cls, db_conn: DatabaseConnection, row: Row | list[Any]
) -> Self:
"""Make from DB row, with linked Todos."""
day = super().from_table_row(db_conn, row)
- assert isinstance(day.id_, str)
- day.todos = Todo.by_date(db_conn, day.id_)
+ day.todos = Todo.by_date(db_conn, day.date)
return day
@classmethod
def by_id(cls, db_conn: DatabaseConnection, id_: BaseModelId) -> Self:
- """Extend BaseModel.by_id
-
- Checks Todo.days_to_update if we need to a retrieved Day's .todos,
- and also ensures we're looking for proper dates and not strings like
- "yesterday" by enforcing the valid_date translation.
- """
- assert isinstance(id_, str)
- possibly_translated_date = valid_date(id_)
- day = super().by_id(db_conn, possibly_translated_date)
+ """Checks Todo.days_to_update if we need to a retrieved Day's .todos"""
+ day = super().by_id(db_conn, id_)
+ assert isinstance(day.id_, int)
if day.id_ in Todo.days_to_update:
- assert isinstance(day.id_, str)
Todo.days_to_update.remove(day.id_)
- day.todos = Todo.by_date(db_conn, day.id_)
+ day.todos = Todo.by_date(db_conn, day.date)
return day
@classmethod
- def with_filled_gaps(cls, days: list[Self], start_date: str, end_date: str
- ) -> list[Self]:
- """In days, fill with (un-stored) Days gaps between start/end_date."""
- days = days[:]
- start_date, end_date = valid_date(start_date), valid_date(end_date)
- if start_date > end_date:
+ def with_filled_gaps(
+ cls, conn: DatabaseConnection, dt_start: dt_date, dt_end: dt_date
+ ) -> list[Self]:
+ """Show days >= start_date, <= end_date, fill gaps with un-storeds."""
+ if dt_start > dt_end:
return []
- days = [d for d in days
- if d.date_str >= start_date and d.date_str <= end_date]
- days.sort()
- if start_date not in [d.date_str for d in days]:
- days[:] = [cls(start_date)] + days
- if end_date not in [d.date_str for d in days]:
- days += [cls(end_date)]
- if len(days) > 1:
- gapless_days = []
- for i, day in enumerate(days):
- gapless_days += [day]
- if i < len(days) - 1:
- while day.next_date != days[i+1].date_str:
- day = cls(day.next_date)
- gapless_days += [day]
- days[:] = gapless_days
- return days
+ start_n_days = days_n_from_dt_date(dt_start)
+ end_n_days = days_n_from_dt_date(dt_end)
+ ranged_days = [d for d in cls.all(conn)
+ if isinstance(d.id_, int)
+ and d.id_ >= start_n_days and d.id_ <= end_n_days]
+ ranged_days.sort()
+ if (not ranged_days) or (isinstance(ranged_days[0].id_, int)
+ and start_n_days < ranged_days[0].id_):
+ ranged_days.insert(0, cls(start_n_days))
+ assert isinstance(ranged_days[-1].id_, int)
+ if end_n_days > ranged_days[-1].id_:
+ ranged_days.append(cls(end_n_days))
+ if len(ranged_days) > 1:
+ degapped_ranged_days = []
+ for i, day in enumerate(ranged_days):
+ degapped_ranged_days += [day]
+ if i < len(ranged_days) - 1:
+ next_one = ranged_days[i+1]
+ assert isinstance(day.id_, int)
+ assert isinstance(next_one.id_, int)
+ while day.id_ + 1 != next_one.id_:
+ assert isinstance(day.id_, int)
+ day = cls(day.id_ + 1)
+ degapped_ranged_days += [day]
+ return degapped_ranged_days
+ return ranged_days
+
+ @property
+ def _dt_date(self) -> dt_date:
+ """Return chronological location as datetime.date."""
+ assert isinstance(self.id_, int)
+ return dt_date_from_days_n(self.id_)
@property
- def date_str(self) -> str:
- """Return self.id_ under the assumption it's a date string."""
- assert isinstance(self.id_, str)
- return self.id_
+ def date(self) -> str:
+ """Return chronological location as ISO format date."""
+ return self._dt_date.isoformat()
@property
def first_of_month(self) -> bool:
"""Return if self is first day of a month."""
- assert isinstance(self.id_, str)
- return self.id_[-2:] == '01'
+ return self.date[-2:] == '01'
@property
def month_name(self) -> str:
- """Return what month self is part of."""
- return self.date.strftime('%B')
+ """Return name of month self is part of."""
+ return self._dt_date.strftime('%B')
@property
def weekday(self) -> str:
- """Return what weekday matches self."""
- return self.date.strftime('%A')
+ """Return weekday name matching self."""
+ return self._dt_date.strftime('%A')
@property
def prev_date(self) -> str:
- """Return date preceding date of self."""
- prev_date = self.date - timedelta(days=1)
- return prev_date.isoformat()
+ """Return ISO-formatted date preceding date of self."""
+ return (self._dt_date - timedelta(days=1)).isoformat()
@property
def next_date(self) -> str:
- """Return date succeeding date of this Day."""
- next_date = self.date + timedelta(days=1)
- return next_date.isoformat()
+ """Return ISO-formatted date succeeding date of this Day."""
+ return (self._dt_date + timedelta(days=1)).isoformat()
@property
def calendarized_todos(self) -> list[Todo]:
"""Database management."""
from __future__ import annotations
+from datetime import date as dt_date
from os import listdir
from os.path import basename, isfile
from difflib import Differ
from typing import Any, Self, Callable
from plomtask.exceptions import (HandledException, NotFoundException,
BadFormatException)
-from plomtask.dating import valid_date
-EXPECTED_DB_VERSION = 6
+EXPECTED_DB_VERSION = 7
MIGRATIONS_DIR = 'migrations'
FILENAME_DB_SCHEMA = f'init_{EXPECTED_DB_VERSION}.sql'
PATH_DB_SCHEMA = f'{MIGRATIONS_DIR}/{FILENAME_DB_SCHEMA}'
def _mig_6_calc_days_since_millennium(conn: SqlConnection) -> None:
- # pylint: disable=import-outside-toplevel
- from datetime import date as dt_date
rows = conn.execute('SELECT * FROM days').fetchall()
for row in [list(r) for r in rows]:
row[-1] = (dt_date.fromisoformat(row[0]) - dt_date(2000, 1, 1)).days
items[item.id_] = item
return sorted(list(items.values()))
- @classmethod
- def by_date_range_with_limits(cls,
- db_conn: DatabaseConnection,
- date_range: tuple[str, str],
- date_col: str = 'day'
- ) -> tuple[list[Self], str, str]:
- """Return list of items in DB within (closed) date_range interval.
-
- If no range values provided, defaults them to 'yesterday' and
- 'tomorrow'. Knows to properly interpret these and 'today' as value.
- """
- start_str = date_range[0] if date_range[0] else 'yesterday'
- end_str = date_range[1] if date_range[1] else 'tomorrow'
- start_date = valid_date(start_str)
- end_date = valid_date(end_str)
- 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),
- build_q_marks=False):
- items += [cls.by_id(db_conn, row[0])]
- return items, start_date, end_date
-
@classmethod
def matching(cls, db_conn: DatabaseConnection, pattern: str) -> list[Self]:
"""Return all objects whose .to_search match pattern."""
from json import dumps as json_dumps
from os.path import split as path_split
from jinja2 import Environment as JinjaEnv, FileSystemLoader as JinjaFSLoader
-from plomtask.dating import date_in_n_days
+from plomtask.dating import (
+ days_n_from_dt_date, dt_date_from_str, date_in_n_days)
from plomtask.days import Day
from plomtask.exceptions import (HandledException, BadFormatException,
NotFoundException)
"""
start = self._params.get_str_or_fail('start', '')
end = self._params.get_str_or_fail('end', '')
- end = end if end != '' else date_in_n_days(366)
- #
- days, start, end = Day.by_date_range_with_limits(self._conn,
- (start, end), 'id')
- days = Day.with_filled_gaps(days, start, end)
+ dt_start = dt_date_from_str(start if start else date_in_n_days(-1))
+ dt_end = dt_date_from_str(end if end else date_in_n_days(366))
+ days = Day.with_filled_gaps(self._conn, dt_start, dt_end)
today = date_in_n_days(0)
- return {'start': start, 'end': end, 'days': days, 'today': today}
+ return {'start': dt_start.isoformat(), 'end': dt_end.isoformat(),
+ 'today': today, 'days': days}
def do_GET_calendar(self) -> dict[str, object]:
"""Show Days from ?start= to ?end= – normal view."""
date = self._params.get_str('date', date_in_n_days(0))
make_type = self._params.get_str_or_fail('make_type', 'full')
#
- day = Day.by_id_or_create(self._conn, date)
+ assert isinstance(date, str)
+ day = Day.by_id_or_create(self._conn,
+ days_n_from_dt_date(dt_date_from_str(date)))
conditions_present = []
enablers_for = {}
disablers_for = {}
for _ in [id_ for id_ in done_todos if id_ not in old_todos]:
raise BadFormatException('"done" field refers to unknown Todo')
#
- day = Day.by_id_or_create(self._conn, date)
+ day_id = days_n_from_dt_date(dt_date_from_str(date))
+ day = Day.by_id_or_create(self._conn, day_id)
day.comment = day_comment
day.save(self._conn)
new_todos = []
for process_id in sorted(new_todos_by_process):
process = Process.by_id(self._conn, process_id)
- todo = Todo(None, process, False, date)
+ todo = Todo(None, process, False, day_id)
todo.save(self._conn)
new_todos += [todo]
if 'full' == make_type:
for process_id, parent_id in make_data:
parent = Todo.by_id(self._conn, parent_id)
process = Process.by_id(self._conn, process_id)
- made = Todo(None, process, False, todo.date)
+ made = Todo(None, process, False, todo.day_id)
made.save(self._conn)
if 'full' == approach:
made.ensure_children(self._conn)
"""Actionables."""
from __future__ import annotations
+from datetime import date as dt_date
from typing import Any, Self, Set
from sqlite3 import Row
from plomtask.misc import DictableNode
from plomtask.conditions import Condition, ConditionsRelations
from plomtask.exceptions import (NotFoundException, BadFormatException,
HandledException)
-from plomtask.dating import valid_date
+from plomtask.dating import (
+ days_n_from_dt_date, dt_date_from_str, dt_date_from_days_n)
class TodoNode(DictableNode):
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-public-methods
table_name = 'todos'
- to_save_simples = ['process_id', 'is_done', 'date', 'comment', 'effort',
+ to_save_simples = ['process_id', 'is_done', 'day_id', 'comment', 'effort',
'calendarize']
to_save_relations = [('todo_conditions', 'todo', 'conditions', 0),
('todo_blockers', 'todo', 'blockers', 0),
('todo_children', 'parent', 'children', 0),
('todo_children', 'child', 'parents', 1)]
to_search = ['comment']
- days_to_update: Set[str] = set()
+ days_to_update: Set[int] = set()
children: list[Todo]
parents: list[Todo]
sorters = {'doneness': lambda t: t.is_done,
'title': lambda t: t.title_then,
'comment': lambda t: t.comment,
- 'date': lambda t: t.date}
+ 'date': lambda t: t.day_id}
# pylint: disable=too-many-arguments
def __init__(self, id_: int | None,
process: Process,
is_done: bool,
- date: str,
+ day_id: int,
comment: str = '',
effort: None | float = None,
calendarize: bool = False
raise NotFoundException('Process of Todo without ID (not saved?)')
self.process = process
self._is_done = is_done
- self.date = valid_date(date)
+ self.day_id = day_id
self.comment = comment
self.effort = effort
self.children = []
self.enables = self.process.enables[:]
self.disables = self.process.disables[:]
+ @property
+ def date(self) -> str:
+ """Return ISO formatted date matching .day_id."""
+ return dt_date_from_days_n(self.day_id).isoformat()
+
@classmethod
- def by_date_range(cls, db_conn: DatabaseConnection,
- date_range: tuple[str, str] = ('', '')) -> list[Self]:
- """Collect Todos of Days within date_range."""
- todos, _, _ = cls.by_date_range_with_limits(db_conn, date_range)
- return todos
+ def by_date_range_with_limits(cls,
+ db_conn: DatabaseConnection,
+ date_range: tuple[str, str],
+ ) -> tuple[list[Self], str, str]:
+ """Return Todos within (closed) date_range interval.
+
+ If no range values provided, defaults them to 'yesterday' and
+ 'tomorrow'. Knows to properly interpret these and 'today' as value.
+ """
+ dt_date_limits: list[dt_date] = []
+ for i in range(2):
+ dt_date_limits += [
+ dt_date_from_str(date_range[i] if date_range[i]
+ else ('yesterday', 'tomorrow')[i])]
+ items: list[Self] = []
+ for row in db_conn.exec(
+ f'SELECT id FROM {cls.table_name} WHERE day >= ? AND day <= ?',
+ tuple(days_n_from_dt_date(d) for d in dt_date_limits),
+ build_q_marks=False):
+ items += [cls.by_id(db_conn, row[0])]
+ return (items,
+ dt_date_limits[0].isoformat(), dt_date_limits[1].isoformat())
def ensure_children(self, db_conn: DatabaseConnection) -> None:
"""Ensure Todo children (create or adopt) demanded by Process chain."""
break
if not satisfier:
satisfier = self.__class__(None, step_node.process, False,
- parent.date)
+ parent.day_id)
satisfier.save(db_conn)
sub_step_nodes = sorted(
step_node.steps,
@classmethod
def by_date(cls, db_conn: DatabaseConnection, date: str) -> list[Self]:
"""Collect all Todos for Day of date."""
- return cls.by_date_range(db_conn, (date, date))
+ return cls.by_date_range_with_limits(db_conn, (date, date))[0]
@property
def is_doable(self) -> bool:
@property
def title_then(self) -> str:
- """Shortcut to .process.title.at(self.date)"""
+ """Shortcut to .process.title.at(self.date)."""
title_then = self.process.title.at(self.date)
assert isinstance(title_then, str)
return title_then
self.remove(db_conn)
return
if self.id_ is None:
- self.__class__.days_to_update.add(self.date)
+ self.__class__.days_to_update.add(self.day_id)
super().save(db_conn)
for condition in self.enables + self.disables + self.conditions:
condition.save(db_conn)
"""Remove from DB, including relations."""
if not self.is_deletable:
raise HandledException('Cannot remove non-deletable Todo.')
- self.__class__.days_to_update.add(self.date)
+ self.__class__.days_to_update.add(self.day_id)
children_to_remove = self.children[:]
parents_to_remove = self.parents[:]
for child in children_to_remove:
"""Test Days module."""
-from datetime import date as datetime_date, datetime, timedelta
+from datetime import date as dt_date, datetime, timedelta
from typing import Any
from tests.utils import (TestCaseSansDB, TestCaseWithDB, TestCaseWithServer,
- Expected)
+ Expected, date_and_day_id, dt_date_from_day_id)
from plomtask.dating import date_in_n_days as tested_date_in_n_days
from plomtask.days import Day
# Simply the ISO format for dates as used in plomtask.dating, but for testing
-# purposes we state our expectations here independently from that
+# purposes we state our expectations here independently and explicitly
TESTING_DATE_FORMAT = '%Y-%m-%d'
at plomtask.dating.date_in_n_days, but want to state our expectations
explicitly to rule out importing issues from the original.
"""
- date = datetime_date.today() + timedelta(days=n)
+ date = dt_date.today() + timedelta(days=n)
return date.strftime(TESTING_DATE_FORMAT)
+def _days_n_for_date(date: str) -> int:
+ return (dt_date.fromisoformat(date) - dt_date(2000, 1, 1)).days
+
+
class TestsSansDB(TestCaseSansDB):
"""Days module tests not requiring DB setup."""
checked_class = Day
- legal_ids = ['2024-01-01', '2024-02-29']
- illegal_ids = ['foo', '2023-02-29', '2024-02-30', '2024-02-01 23:00:00']
def test_date_in_n_days(self) -> None:
"""Test dating.date_in_n_days"""
def test_Day_date_weekday_neighbor_dates(self) -> None:
"""Test Day's date parsing and neighbourhood resolution."""
- self.assertEqual(datetime_date(2024, 5, 1), Day('2024-05-01').date)
- self.assertEqual('Sunday', Day('2024-03-17').weekday)
- self.assertEqual('March', Day('2024-03-17').month_name)
- self.assertEqual('2023-12-31', Day('2024-01-01').prev_date)
- self.assertEqual('2023-03-01', Day('2023-02-28').next_date)
+ self.assertEqual(dt_date(2000, 1, 2).isoformat(), Day(1).date)
+ self.assertEqual(dt_date(2001, 1, 2).isoformat(), Day(367).date)
+ self.assertEqual('Sunday', Day(1).weekday)
+ self.assertEqual('March', Day(75).month_name)
+ self.assertEqual('2000-12-31', Day(366).prev_date)
+ self.assertEqual('2001-03-01', Day(424).next_date)
class TestsWithDB(TestCaseWithDB):
"""Tests requiring DB, but not server setup."""
checked_class = Day
- default_ids = ('2024-01-01', '2024-01-02', '2024-01-03')
-
- def test_Day_by_date_range_with_limits(self) -> None:
- """Test .by_date_range_with_limits."""
- self.check_by_date_range_with_limits('id', set_id_field=False)
def test_Day_with_filled_gaps(self) -> None:
"""Test .with_filled_gaps."""
+ day_ids = [n + 1 for n in range(9)]
+ dt_dates = [dt_date_from_day_id(id_) for id_ in day_ids]
def expect_within_full_range_as_commented(
range_indexes: tuple[int, int],
indexes_to_provide: list[int]
) -> None:
start_i, end_i = range_indexes
- days_provided = []
- days_expected = days_sans_comment[:]
- for i in indexes_to_provide:
- day_with_comment = days_with_comment[i]
- days_provided += [day_with_comment]
- days_expected[i] = day_with_comment
+ days_expected = [Day(n) for n in day_ids]
+ to_remove = []
+ for idx in indexes_to_provide:
+ days_expected[idx] = Day(day_ids[idx], '#')
+ days_expected[idx].save(self.db_conn)
+ to_remove += [days_expected[idx]]
days_expected = days_expected[start_i:end_i+1]
- start, end = dates[start_i], dates[end_i]
- days_result = self.checked_class.with_filled_gaps(days_provided,
- start, end)
+ days_result = Day.with_filled_gaps(
+ self.db_conn, dt_dates[start_i], dt_dates[end_i])
self.assertEqual(days_result, days_expected)
+ for day in to_remove:
+ day.remove(self.db_conn)
- # for provided Days we use those from days_with_comment, to identify
- # them against same-dated mere filler Days by their lack of comment
- # (identity with Day at the respective position in days_sans_comment)
- dates = [f'2024-02-0{n+1}' for n in range(9)]
- days_with_comment = [Day(date, comment=date[-1:]) for date in dates]
- days_sans_comment = [Day(date, comment='') for date in dates]
# check provided Days recognizable in (full-range) interval
expect_within_full_range_as_commented((0, 8), [0, 4, 8])
# check limited range, but limiting Days provided
# check single-element selection creating only filler beyond provided
expect_within_full_range_as_commented((1, 1), [2, 4, 6])
# check (un-saved) filler Days don't show up in cache or DB
- day = Day(dates[3])
+ day = Day(day_ids[3])
day.save(self.db_conn)
- self.checked_class.with_filled_gaps([day], dates[0], dates[-1])
+ Day.with_filled_gaps(self.db_conn, dt_dates[0], dt_dates[-1])
self.check_identity_with_cache_and_db([day])
def __init__(self, start: int, end: int, *args: Any, **kwargs: Any
) -> None:
- self._fields = {'start': _testing_date_in_n_days(start),
- 'end': _testing_date_in_n_days(end),
- 'today': _testing_date_in_n_days(0)}
- self._fields['days'] = [_testing_date_in_n_days(i)
- for i in range(start, end+1)]
+ today_dt = dt_date.today()
+ today_iso = today_dt.isoformat()
+ self._fields = {
+ 'start': (today_dt + timedelta(days=start)).isoformat(),
+ 'end': (today_dt + timedelta(days=end)).isoformat(),
+ 'today': today_iso}
+ self._fields['days'] = [
+ _days_n_for_date(today_iso) + i for i in range(start, end+1)]
super().__init__(*args, **kwargs)
- for date in self._fields['days']:
- self.lib_set('Day', [self.day_as_dict(date)])
+ for day_id in self._fields['days']:
+ self.lib_set('Day', [self.day_as_dict(day_id)])
class ExpectedGetDay(Expected):
_default_dict = {'make_type': 'full'}
_on_empty_make_temp = ('Day', 'day_as_dict')
- def __init__(self, date: str, *args: Any, **kwargs: Any) -> None:
- self._fields = {'day': date}
+ def __init__(self, day_id: int, *args: Any, **kwargs: Any) -> None:
+ self._fields = {'day': day_id}
super().__init__(*args, **kwargs)
def recalc(self) -> None:
super().recalc()
todos = [t for t in self.lib_all('Todo')
- if t['date'] == self._fields['day']]
+ if t['day_id'] == self._fields['day']]
self.lib_get('Day', self._fields['day'])['todos'] = self.as_ids(todos)
self._fields['top_nodes'] = [
{'children': [], 'seen': 0, 'todo': todo['id']}
self.check_get_defaults('/day', '2024-01-01', 'date')
self.check_get('/day?date=2024-02-30', 400)
# check undefined day
- exp = ExpectedGetDay(_testing_date_in_n_days(0))
+ today_iso = dt_date.today().isoformat()
+ exp = ExpectedGetDay(_days_n_for_date(today_iso))
self.check_json_get('/day', exp)
# check defined day with make_type parameter
- date = '2024-01-01'
- exp = ExpectedGetDay(date)
+ date, day_id = date_and_day_id(1)
+ exp = ExpectedGetDay(day_id)
exp.set('make_type', 'bar')
self.check_json_get(f'/day?date={date}&make_type=bar', exp)
# check parsing of 'yesterday', 'today', 'tomorrow'
for name, dist in [('yesterday', -1), ('today', 0), ('tomorrow', +1)]:
- date = _testing_date_in_n_days(dist)
- exp = ExpectedGetDay(date)
+ exp = ExpectedGetDay(_days_n_for_date(today_iso) + dist)
self.check_json_get(f'/day?date={name}', exp)
def test_fail_POST_day(self) -> None:
post_url = f'/day?date={name}'
redir_url = f'{post_url}&make_type={post["make_type"]}'
self.check_post(post, post_url, 302, redir_url)
- exp = ExpectedGetDay(date)
- exp.set_day_from_post(date, post)
+ day_id = _days_n_for_date(date)
+ exp = ExpectedGetDay(day_id)
+ exp.set_day_from_post(day_id, post)
self.check_json_get(post_url, exp)
def test_GET_day_with_processes_and_todos(self) -> None:
"""Test GET /day displaying Processes and Todos (no trees)."""
- date = '2024-01-01'
- exp = ExpectedGetDay(date)
+ date, day_id = date_and_day_id(1)
+ exp = ExpectedGetDay(day_id)
# check Processes get displayed in ['processes'] and ['_library'],
# even without any Todos referencing them
proc_posts = [{'title': 'foo', 'description': 'oof', 'effort': 1.1},
def test_POST_day_todo_make_types(self) -> None:
"""Test behavior of POST /todo on 'make_type'='full' and 'empty'."""
- date = '2024-01-01'
- exp = ExpectedGetDay(date)
+ date, day_id = date_and_day_id(1)
+ exp = ExpectedGetDay(day_id)
# create two Processes, with second one step of first one
self.post_exp_process([exp], {}, 2)
self.post_exp_process([exp], {'new_top_step': 2}, 1)
def test_POST_day_new_todo_order_commutative(self) -> None:
"""Check that order of 'new_todo' values in POST /day don't matter."""
- date = '2024-01-01'
- exp = ExpectedGetDay(date)
+ date, day_id = date_and_day_id(1)
+ exp = ExpectedGetDay(day_id)
self.post_exp_process([exp], {}, 2)
self.post_exp_process([exp], {'new_top_step': 2}, 1)
exp.lib_set('ProcessStep', [
exp.lib_get('Todo', 1)['children'] = [2]
self.check_json_get(f'/day?date={date}', exp)
# … and then in the other, expecting same node tree / relations
- exp.lib_del('Day', date)
- date = '2024-01-02'
- exp.set('day', date)
+ exp.lib_del('Day', day_id)
+ date, day_id = date_and_day_id(2)
+ exp.set('day', day_id)
day_post = {'make_type': 'full', 'new_todo': [2, 1]}
- self.post_exp_day([exp], day_post, date)
+ self.post_exp_day([exp], day_post, day_id)
exp.lib_del('Todo', 1)
exp.lib_del('Todo', 2)
top_nodes[0]['todo'] = 3 # was: 1
def test_POST_day_todo_deletion_by_negative_effort(self) -> None:
"""Test POST /day removal of Todos by setting negative effort."""
- date = '2024-01-01'
- exp = ExpectedGetDay(date)
+ date, day_id = date_and_day_id(1)
+ exp = ExpectedGetDay(day_id)
self.post_exp_process([exp], {}, 1)
self.post_exp_day([exp], {'new_todo': [1]})
# check cannot remove Todo if commented
def test_GET_day_with_conditions(self) -> None:
"""Test GET /day displaying Conditions and their relations."""
- date = '2024-01-01'
- exp = ExpectedGetDay(date)
+ date, day_id = date_and_day_id(1)
+ exp = ExpectedGetDay(day_id)
# check non-referenced Conditions not shown
cond_posts = [{'is_active': 0, 'title': 'A', 'description': 'a'},
{'is_active': 1, 'title': 'B', 'description': 'b'}]
date = _testing_date_in_n_days(-2)
end_date = _testing_date_in_n_days(+5)
exp = ExpectedGetCalendar(-5, +5)
- self.post_exp_day([exp], {'day_comment': 'foo'}, date)
+ self.post_exp_day([exp],
+ {'day_comment': 'foo'}, _days_n_for_date(date))
url = f'/calendar?start={start_date}&end={end_date}'
self.check_json_get(url, exp)
self.post_exp_process([exp], p, 1)
self.check_json_get('/process?id=1', exp)
# check n_todos field
- self.post_exp_day([], {'new_todo': ['1']}, '2024-01-01')
- self.post_exp_day([], {'new_todo': ['1']}, '2024-01-02')
+ self.post_exp_day([], {'new_todo': ['1']}, 1)
+ self.post_exp_day([], {'new_todo': ['1']}, 2)
exp.set('n_todos', 2)
self.check_json_get('/process?id=1', exp)
# check cannot delete if Todos to Process
"""Test Todos module."""
from typing import Any
+from datetime import date as dt_date, timedelta
from tests.utils import (TestCaseSansDB, TestCaseWithDB, TestCaseWithServer,
- Expected)
+ Expected, date_and_day_id)
from plomtask.todos import Todo
from plomtask.processes import Process
from plomtask.exceptions import BadFormatException, HandledException
Todo requiring a _saved_ Process wouldn't run without a DB.
"""
checked_class = Todo
- default_init_kwargs = {'process': None, 'is_done': False,
- 'date': '2024-01-01'}
+ default_init_kwargs = {'process': None, 'is_done': False, 'day_id': 1}
def setUp(self) -> None:
super().setUp()
def test_Todo_by_date(self) -> None:
"""Test findability of Todos by date."""
- date1, date2 = '2024-01-01', '2024-01-02'
- t1 = Todo(None, self.proc, False, date1)
+ date_1, day_id_1 = date_and_day_id(1)
+ date_2, _ = date_and_day_id(2)
+ t1 = Todo(None, self.proc, False, day_id_1)
t1.save(self.db_conn)
- t2 = Todo(None, self.proc, False, date1)
+ t2 = Todo(None, self.proc, False, day_id_1)
t2.save(self.db_conn)
- self.assertEqual(Todo.by_date(self.db_conn, date1), [t1, t2])
- self.assertEqual(Todo.by_date(self.db_conn, date2), [])
+ self.assertEqual(Todo.by_date(self.db_conn, date_1), [t1, t2])
+ self.assertEqual(Todo.by_date(self.db_conn, date_2), [])
with self.assertRaises(BadFormatException):
self.assertEqual(Todo.by_date(self.db_conn, 'foo'), [])
def test_Todo_by_date_range_with_limits(self) -> None:
"""Test .by_date_range_with_limits."""
- self.check_by_date_range_with_limits('day')
+ # pylint: disable=too-many-locals
+ f = Todo.by_date_range_with_limits
+ # check illegal ranges
+ legal_range = ('yesterday', 'tomorrow')
+ for i in [0, 1]:
+ for bad_date in ['foo', '2024-02-30', '2024-01-01 12:00:00']:
+ date_range_l = list(legal_range[:])
+ date_range_l[i] = bad_date
+ with self.assertRaises(HandledException):
+ f(self.db_conn, (date_range_l[0], date_range_l[1]))
+ # check empty, translation of 'yesterday' and 'tomorrow'
+ items, start, end = f(self.db_conn, legal_range)
+ self.assertEqual(items, [])
+ dt_today = dt_date.today()
+ dt_yesterday = dt_today + timedelta(days=-1)
+ dt_tomorrow = dt_today + timedelta(days=+1)
+ self.assertEqual(start, dt_yesterday.isoformat())
+ self.assertEqual(end, dt_tomorrow.isoformat())
+ # prepare dated items for non-empty results
+ kwargs = self.default_init_kwargs.copy()
+ todos = []
+ dates_and_day_ids = [date_and_day_id(i) for i in range(5)]
+ for day_id in [t[1] for t in dates_and_day_ids[1:-1]]:
+ kwargs['day_id'] = day_id
+ todos += [Todo(None, **kwargs)]
+ # check ranges still empty before saving
+ date_range = (dates_and_day_ids[1][0], dates_and_day_ids[-2][0])
+ self.assertEqual(f(self.db_conn, date_range)[0], [])
+ # check all objs displayed within interval
+ for todo in todos:
+ todo.save(self.db_conn)
+ self.assertEqual(f(self.db_conn, date_range)[0], todos)
+ # check that only displayed what exists within interval
+ date_range = (dates_and_day_ids[1][0], dates_and_day_ids[-3][0])
+ expected = [todos[0], todos[1]]
+ self.assertEqual(f(self.db_conn, date_range)[0], expected)
+ date_range = (dates_and_day_ids[-2][0], dates_and_day_ids[-1][0])
+ expected = [todos[2]]
+ self.assertEqual(f(self.db_conn, date_range)[0], expected)
+ # check that inverted interval displays nothing
+ date_range = (dates_and_day_ids[-1][0], dates_and_day_ids[0][0])
+ self.assertEqual(f(self.db_conn, date_range)[0], [])
+ # check that "today" is interpreted, and single-element interval
+ kwargs['day_id'] = (dt_today - dt_date(2000, 1, 1)).days
+ todo_today = Todo(None, **kwargs)
+ todo_today.save(self.db_conn)
+ date_range = ('today', 'today')
+ items, start, end = f(self.db_conn, date_range)
+ self.assertEqual(start, dt_today.isoformat())
+ self.assertEqual(start, end)
+ self.assertEqual(items, [todo_today])
def test_Todo_children(self) -> None:
"""Test Todo.children relations."""
- date1 = '2024-01-01'
- todo_1 = Todo(None, self.proc, False, date1)
- todo_2 = Todo(None, self.proc, False, date1)
+ todo_1 = Todo(None, self.proc, False, 1)
+ todo_2 = Todo(None, self.proc, False, 1)
todo_2.save(self.db_conn)
# check un-saved Todo cannot parent
with self.assertRaises(HandledException):
todo_1.add_child(todo_2)
todo_1.save(self.db_conn)
- todo_3 = Todo(None, self.proc, False, date1)
+ todo_3 = Todo(None, self.proc, False, 1)
# check un-saved Todo cannot be parented
with self.assertRaises(HandledException):
todo_1.add_child(todo_3)
"""Shared test utilities."""
# pylint: disable=too-many-lines
from __future__ import annotations
+from datetime import datetime, date as dt_date, timedelta
from unittest import TestCase
from typing import Mapping, Any, Callable
from threading import Thread
from http.client import HTTPConnection
-from datetime import date as datetime_date, datetime, timedelta
from time import sleep
from json import loads as json_loads, dumps as json_dumps
from urllib.parse import urlencode
from plomtask.exceptions import NotFoundException, HandledException
-VERSIONED_VALS: dict[str,
- list[str] | list[float]] = {'str': ['A', 'B'],
- 'float': [0.3, 1.1]}
-VALID_TRUES = {True, 'True', 'true', '1', 'on'}
+_VERSIONED_VALS: dict[str,
+ list[str] | list[float]] = {'str': ['A', 'B'],
+ 'float': [0.3, 1.1]}
+_VALID_TRUES = {True, 'True', 'true', '1', 'on'}
+
+
+def dt_date_from_day_id(day_id: int) -> dt_date:
+ """Return datetime.date of adding day_id days to 2000-01-01."""
+ return dt_date(2000, 1, 1) + timedelta(days=day_id)
+
+
+def date_and_day_id(day_id: int) -> tuple[str, int]:
+ """Interpet day_id as n of days since millennium, return (date, day_id)."""
+ return dt_date_from_day_id(day_id).isoformat(), day_id
class TestCaseAugmented(TestCase):
default = self.checked_class.versioned_defaults[attr_name]
owner = self.checked_class(None, **self.default_init_kwargs)
attr = getattr(owner, attr_name)
- to_set = VERSIONED_VALS[attr.value_type_name]
+ to_set = _VERSIONED_VALS[attr.value_type_name]
f(self, owner, attr_name, attr, default, to_set)
return wrapper
hashes_db_found = [hash(x) for x in db_found]
self.assertEqual(sorted(hashes_content), sorted(hashes_db_found))
- def check_by_date_range_with_limits(self,
- date_col: str,
- set_id_field: bool = True
- ) -> None:
- """Test .by_date_range_with_limits."""
- # pylint: disable=too-many-locals
- f = self.checked_class.by_date_range_with_limits
- # check illegal ranges
- legal_range = ('yesterday', 'tomorrow')
- for i in [0, 1]:
- for bad_date in ['foo', '2024-02-30', '2024-01-01 12:00:00']:
- date_range = list(legal_range[:])
- date_range[i] = bad_date
- with self.assertRaises(HandledException):
- f(self.db_conn, date_range, date_col)
- # check empty, translation of 'yesterday' and 'tomorrow'
- items, start, end = f(self.db_conn, legal_range, date_col)
- self.assertEqual(items, [])
- yesterday = datetime_date.today() + timedelta(days=-1)
- tomorrow = datetime_date.today() + timedelta(days=+1)
- self.assertEqual(start, yesterday.isoformat())
- self.assertEqual(end, tomorrow.isoformat())
- # prepare dated items for non-empty results
- kwargs_with_date = self.default_init_kwargs.copy()
- if set_id_field:
- kwargs_with_date['id_'] = None
- objs = []
- dates = ['2024-01-01', '2024-01-02', '2024-01-04']
- for date in ['2024-01-01', '2024-01-02', '2024-01-04']:
- kwargs_with_date['date'] = date
- obj = self.checked_class(**kwargs_with_date)
- objs += [obj]
- # check ranges still empty before saving
- date_range = [dates[0], dates[-1]]
- self.assertEqual(f(self.db_conn, date_range, date_col)[0], [])
- # check all objs displayed within closed interval
- for obj in objs:
- obj.save(self.db_conn)
- self.assertEqual(f(self.db_conn, date_range, date_col)[0], objs)
- # check that only displayed what exists within interval
- date_range = ['2023-12-20', '2024-01-03']
- expected = [objs[0], objs[1]]
- self.assertEqual(f(self.db_conn, date_range, date_col)[0], expected)
- date_range = ['2024-01-03', '2024-01-30']
- expected = [objs[2]]
- self.assertEqual(f(self.db_conn, date_range, date_col)[0], expected)
- # check that inverted interval displays nothing
- date_range = [dates[-1], dates[0]]
- self.assertEqual(f(self.db_conn, date_range, date_col)[0], [])
- # check that "today" is interpreted, and single-element interval
- today_date = datetime_date.today().isoformat()
- kwargs_with_date['date'] = today_date
- obj_today = self.checked_class(**kwargs_with_date)
- obj_today.save(self.db_conn)
- date_range = ['today', 'today']
- items, start, end = f(self.db_conn, date_range, date_col)
- self.assertEqual(start, today_date)
- self.assertEqual(start, end)
- self.assertEqual(items, [obj_today])
-
@TestCaseAugmented._run_if_with_db_but_not_server
@TestCaseAugmented._run_on_versioned_attributes
def test_saving_versioned_attributes(self,
return [item['id'] for item in items]
@staticmethod
- def day_as_dict(date: str, comment: str = '') -> dict[str, object]:
+ def day_as_dict(id_: int, comment: str = '') -> dict[str, object]:
"""Return JSON of Day to expect."""
- return {'id': date, 'comment': comment, 'todos': [],
- 'days_since_millennium': -1}
+ return {'id': id_, 'comment': comment, 'todos': []}
- def set_day_from_post(self, date: str, d: dict[str, Any]) -> None:
- """Set Day of date in library based on POST dict d."""
- day = self.day_as_dict(date)
+ def set_day_from_post(self, id_: int, d: dict[str, Any]) -> None:
+ """Set Day of id_ in library based on POST dict d."""
+ day = self.day_as_dict(id_)
for k, v in d.items():
if 'day_comment' == k:
day['comment'] = v
if next_id <= todo['id']:
next_id = todo['id'] + 1
for proc_id in sorted([id_ for id_ in v if id_]):
- todo = self.todo_as_dict(next_id, proc_id, date)
+ todo = self.todo_as_dict(next_id, proc_id, id_)
self.lib_set('Todo', [todo])
next_id += 1
elif 'done' == k:
cond = self.lib_get('Condition', id_)
if cond:
cond['is_active'] = 'is_active' in d and\
- d['is_active'] in VALID_TRUES
+ d['is_active'] in _VALID_TRUES
for category in ['title', 'description']:
history = cond['_versioned'][category]
if len(history) > 0:
@staticmethod
def todo_as_dict(id_: int = 1,
process_id: int = 1,
- date: str = '2024-01-01',
+ day_id: int = 1,
conditions: None | list[int] = None,
disables: None | list[int] = None,
blockers: None | list[int] = None,
"""Return JSON of Todo to expect."""
# pylint: disable=too-many-arguments
d = {'id': id_,
- 'date': date,
+ 'day_id': day_id,
'process_id': process_id,
'is_done': is_done,
'calendarize': calendarize,
new_children = v if isinstance(v, list) else [v]
corrected_kwargs['children'] += new_children
continue
- if k in {'is_done', 'calendarize'} and v in VALID_TRUES:
+ if k in {'is_done', 'calendarize'} and v in _VALID_TRUES:
v = True
corrected_kwargs[k] = v
todo = self.lib_get('Todo', id_)
if k in ignore\
or k.startswith('step_') or k.startswith('new_step_to'):
continue
- if k in {'calendarize'} and v in VALID_TRUES:
+ if k in {'calendarize'} and v in _VALID_TRUES:
v = True
elif k in {'suppressed_steps', 'explicit_steps', 'conditions',
'disables', 'enables', 'blockers'}:
def post_exp_day(self,
exps: list[Expected],
payload: dict[str, Any],
- date: str = '2024-01-01'
+ day_id: int = 1
) -> None:
"""POST /day, appropriately update Expecteds."""
if 'make_type' not in payload:
payload['make_type'] = 'empty'
if 'day_comment' not in payload:
payload['day_comment'] = ''
+ date = dt_date_from_day_id(day_id).isoformat()
target = f'/day?date={date}'
redir_to = f'{target}&make_type={payload["make_type"]}'
self.check_post(payload, target, 302, redir_to)
for exp in exps:
- exp.set_day_from_post(date, payload)
+ exp.set_day_from_post(day_id, payload)
def post_exp_process(self,
exps: list[Expected],