Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/tasks/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from collections.abc import Iterator
from datetime import datetime

import peewee as pw
Expand Down Expand Up @@ -56,7 +57,14 @@ def category_name(self) -> str:
)

@staticmethod
def group_by_status() -> dict[str, list[Task]]:
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[int, list[Task]]:
"""List all existing tasks by status and sorted by creation date"""
tasks = (
Task.select()
Expand Down
124 changes: 88 additions & 36 deletions src/tasks/presenters/view_all_presenter.py
Original file line number Diff line number Diff line change
@@ -1,69 +1,121 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from rich.console import Console, Group
from rich.layout import Layout
from rich.align import Align
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 = "<No category>"
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(
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)
rows = self._build_all_rows(tasks_rows, rules_rows)
for row in rows:
table.add_row(*row)

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()
)

self._console.print("\n")
self._console.print(layout)
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

task = tasks[i]
return self._present_task(task)

def _present_tasks_in_status(self, tasks: list[Task]) -> Group:
"""Writes a list of tasks in a status column
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)
]

The tasks should be presented like:
def _build_rules_rows(
self, task_rows: list[list[str]]
) -> list[list[str | Rule]]:
"""Build rules rows

<task1_display>
---
<task2_display>
---
<task3_display>
...
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])

Copilot AI Nov 1, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code will fail with an IndexError when tasks_rows is empty. Add a guard condition to handle the case when there are no tasks, or ensure tasks_rows has at least one element before accessing tasks_rows[-1].

Suggested change
rows.append(tasks_rows[-1])
if tasks_rows:
rows.append(tasks_rows[-1])

Copilot uses AI. Check for mistakes.
return rows

def _present_task(self, task: Task) -> str:
"""Display a single task
Expand Down
8 changes: 8 additions & 0 deletions tests/tasks/models/test_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down