From ced3d81e59e2f73e544a5d2418377be2bb512882 Mon Sep 17 00:00:00 2001 From: Fillipe Goulart Date: Wed, 29 Oct 2025 20:17:01 -0300 Subject: [PATCH 1/5] Add convenience method to iterate over status indices --- src/tasks/models/task.py | 8 ++++++++ tests/tasks/models/test_task.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/tasks/models/task.py b/src/tasks/models/task.py index baa8e6e..cf4111c 100644 --- a/src/tasks/models/task.py +++ b/src/tasks/models/task.py @@ -1,6 +1,7 @@ from __future__ import annotations from datetime import datetime +from typing import Iterator import peewee as pw @@ -55,6 +56,13 @@ def category_name(self) -> str: else f"{self.NO_CATEGORY_STR}" ) + @staticmethod + def iter_status_indices() -> Iterator[int]: + """Convenient method to iterate over the statuses indices""" + return ( + status_index for status_index, _ in enumerate(settings.statuses) + ) + @staticmethod def group_by_status() -> dict[str, list[Task]]: """List all existing tasks by status and sorted by creation date""" diff --git a/tests/tasks/models/test_task.py b/tests/tasks/models/test_task.py index 2b8debf..a90d8a0 100644 --- a/tests/tasks/models/test_task.py +++ b/tests/tasks/models/test_task.py @@ -85,6 +85,14 @@ def test_category_name__nonexisting_category(tmp_db): assert task.category_name == Task.NO_CATEGORY_STR +def test_iter_status_indices(tmp_db): + expected_status_indices = [ + status_index for status_index, _ in enumerate(settings.statuses) + ] + + assert list(Task.iter_status_indices()) == expected_status_indices + + def test_group_by_status(tmp_db): """Tasks must be grouped by status and sorted by creation date""" cat1 = Category.create(name="cat1") From d22a739828c516cb7a0054696e31cdadcfbda506 Mon Sep 17 00:00:00 2001 From: Fillipe Goulart Date: Wed, 29 Oct 2025 20:18:37 -0300 Subject: [PATCH 2/5] bug! Fix the correct type for tasks by statuses --- src/tasks/models/task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks/models/task.py b/src/tasks/models/task.py index cf4111c..ae3d3e3 100644 --- a/src/tasks/models/task.py +++ b/src/tasks/models/task.py @@ -64,7 +64,7 @@ def iter_status_indices() -> Iterator[int]: ) @staticmethod - def group_by_status() -> dict[str, list[Task]]: + def group_by_status() -> dict[int, list[Task]]: """List all existing tasks by status and sorted by creation date""" tasks = ( Task.select() From d3c0fd07cab75f6c44ab32f952f4979e71429e52 Mon Sep 17 00:00:00 2001 From: Fillipe Goulart Date: Wed, 29 Oct 2025 20:20:02 -0300 Subject: [PATCH 3/5] Update view-all presenter for table The previous visualization always used the full terminal, and this is bad: - the new line after printing gets rid of the statuses names - in case the terminal is too short, it clips some tasks This commit replaces it with a table view, which, granted, is not as nice, but should solve the previous issues. --- src/tasks/presenters/view_all_presenter.py | 124 +++++++++++++++------ 1 file changed, 87 insertions(+), 37 deletions(-) diff --git a/src/tasks/presenters/view_all_presenter.py b/src/tasks/presenters/view_all_presenter.py index 6c208fd..4966e29 100644 --- a/src/tasks/presenters/view_all_presenter.py +++ b/src/tasks/presenters/view_all_presenter.py @@ -1,69 +1,119 @@ from __future__ import annotations -from typing import TYPE_CHECKING - -from rich.console import Console, Group -from rich.layout import Layout +from rich.console import Console from rich.markup import escape -from rich.panel import Panel from rich.rule import Rule +from rich.table import Table from config import settings - -if TYPE_CHECKING: - from src.tasks.models.task import Task +from src.tasks.models.task import Task class ViewAllPresenter: """A class to display a summary list of tasks""" NO_CATEGORY_STR = "" + EMPTY_STR = "" def __init__(self, console: Console | None = None) -> None: self._console = console or Console() - def present(self, tasks_by_status: dict[str, list[Task]]) -> None: - """Display a table-like view with all existing tasks + def present(self, tasks_by_status: dict[int, list[Task]]) -> None: + """Display a table view with all existing tasks The tasks are grouped by status, each one in a column. + The rows should be viewed as follows: + + Status 1 | Status 2 | Status 3 + ============================== + task 11 | task 12 | task 13 + ------- | | ------- + task 21 | | task 23 + ------- | | + task 31 | | + + Notice, for each status column, if a given task has an upcoming task, + they are separated by rules, otherwise we get an empty string. """ - layout = Layout(name="root") - tasks_presentation = [ - self._present_tasks_in_status(tasks_by_status[status_id]) - for status_id, _ in enumerate(settings.statuses) - ] - layout.split_row( - *( - Layout(Panel(tasks, title=status)) - for tasks, status in zip(tasks_presentation, settings.statuses) - ) - ) + table = Table(expand=True, title="Tasks") + for status in settings.statuses: + table.add_column(status, justify="left") + + tasks_rows = self._build_tasks_rows(tasks_by_status) + rules_rows = self._build_rules_rows(tasks_rows) + rows = self._build_all_rows(tasks_rows, rules_rows) + for row in rows: + table.add_row(*row) self._console.print("\n") - self._console.print(layout) + self._console.print(table) + + def _build_tasks_rows( + self, tasks_by_status: dict[int, list[Task]] + ) -> list[list[str]]: + """Build a table with the tasks in a presentation-like view + + This method groups only the rows with tasks. + If, for the i-th status, the j-th row has no task, the EMPTY_STR is + used instead. + """ + max_num_tasks_in_status = max( + len(tasks) for tasks in tasks_by_status.values() + ) - def _present_tasks_in_status(self, tasks: list[Task]) -> Group: - """Writes a list of tasks in a status column + def get_ith_task_or_empty(status_index: int, i: int) -> str: + tasks = tasks_by_status[status_index] + if i >= len(tasks): + return self.EMPTY_STR - The tasks should be presented like: + task = tasks[i] + return self._present_task(task) + + return [ + [ + get_ith_task_or_empty(status_index, i) + for status_index in Task.iter_status_indices() + ] # build each column of a row + for i in range(max_num_tasks_in_status) + ] - - --- - - --- - - ... + def _build_rules_rows( + self, task_rows: list[list[str]] + ) -> list[list[str | Rule]]: + """Build rules rows + + The rules separate one task from its subsequent one. In case a given + task has no next one, the EMPTY_STR is used instead. """ - if not tasks: - return Group("") - group_content = [ - el for task in tasks for el in (self._present_task(task), Rule()) + def get_ith_rule_or_empty( + task_row: list[str], status_index: int + ) -> Rule | str: + if task_row[status_index] == self.EMPTY_STR: + return self.EMPTY_STR + + return Rule() + + return [ + [ + get_ith_rule_or_empty(next_task_row, status_index) + for status_index in Task.iter_status_indices() + ] + for next_task_row in task_rows[1:] ] - group_content.pop() # remove last `Rule` - return Group(*group_content) + def _build_all_rows( + self, tasks_rows: list[list[str]], rules_rows: list[list[str | Rule]] + ) -> list[list[str | Rule]]: + """Combine the tasks and rules rows into a full table""" + rows = [] + for task_row, rule_row in zip(tasks_rows, rules_rows): + rows.append(task_row) + rows.append(rule_row) + + rows.append(tasks_rows[-1]) + return rows def _present_task(self, task: Task) -> str: """Display a single task From d4234ff57821fd6858719a6bf8e47c4127196b90 Mon Sep 17 00:00:00 2001 From: Fillipe Goulart Date: Fri, 31 Oct 2025 07:26:47 -0300 Subject: [PATCH 4/5] Improve table visualization - Make the columns of equal width with ratio=1 - Center only the table headers and keep the content left justified --- src/tasks/presenters/view_all_presenter.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/tasks/presenters/view_all_presenter.py b/src/tasks/presenters/view_all_presenter.py index 4966e29..2508e31 100644 --- a/src/tasks/presenters/view_all_presenter.py +++ b/src/tasks/presenters/view_all_presenter.py @@ -1,5 +1,6 @@ from __future__ import annotations +from rich.align import Align from rich.console import Console from rich.markup import escape from rich.rule import Rule @@ -38,7 +39,9 @@ def present(self, tasks_by_status: dict[int, list[Task]]) -> None: table = Table(expand=True, title="Tasks") for status in settings.statuses: - table.add_column(status, justify="left") + table.add_column( + Align(status, align="center"), justify="left", ratio=1 + ) tasks_rows = self._build_tasks_rows(tasks_by_status) rules_rows = self._build_rules_rows(tasks_rows) @@ -46,7 +49,6 @@ def present(self, tasks_by_status: dict[int, list[Task]]) -> None: for row in rows: table.add_row(*row) - self._console.print("\n") self._console.print(table) def _build_tasks_rows( From 0364f17b314ac0bd42807ad70aee0f4dc2a35d48 Mon Sep 17 00:00:00 2001 From: Fillipe Goulart Date: Fri, 31 Oct 2025 21:28:42 -0300 Subject: [PATCH 5/5] Change import to new Python versions --- src/tasks/models/task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks/models/task.py b/src/tasks/models/task.py index ae3d3e3..41a3b4a 100644 --- a/src/tasks/models/task.py +++ b/src/tasks/models/task.py @@ -1,7 +1,7 @@ from __future__ import annotations +from collections.abc import Iterator from datetime import datetime -from typing import Iterator import peewee as pw