Skip to content

Commit

Permalink
Add TerkaProject view
Browse files Browse the repository at this point in the history
* Add ability for interactively explore project when calling `terka show
project` command.
* Simplify the codebase:
  * Add `is_stale`, `is_overdue` and `completion_date` properties to
    Task (and remove their calculation from `printer`)
  * Add `total_time_spent` property to project
  * Make `TerkaTask` view neater, remove not working `Comment` section
  * Add `formatter` module to extract formatting operations
  • Loading branch information
AndreyMarkinPPC committed Nov 6, 2023
1 parent 2993ead commit 6f2620b
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 94 deletions.
6 changes: 5 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@

setup(
name="terka",
<<<<<<< HEAD
version="1.18.2",
version="1.18.1",
=======
version="1.19.0",
>>>>>>> 1c1d2ee (Add TerkaProject view)
description="CLI utility for creating and managing tasks in a terminal",
long_description=README,
long_description_content_type="text/markdown",
Expand All @@ -22,7 +26,7 @@
"sqlalchemy==1.4.0",
"pyaml",
"rich",
"textual==0.5.0",
"textual==0.41.0"
"plotext==5.2.8",
"pandas",
"matplotlib",
Expand Down
21 changes: 21 additions & 0 deletions terka/domain/project.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from collections import defaultdict
from enum import Enum


Expand Down Expand Up @@ -32,5 +33,25 @@ def _validate_status(self, status):
else:
return status

@property
def total_time_spent(self):
total_time_spent_project = 0
for task in self.tasks:
total_time_spent_project += task.total_time_spent
return total_time_spent_project

@property
def task_collaborators(self):
collaborators = defaultdict(int)
for task in self.tasks:
if task_collaborators := task.collaborators:
for collaborator in task.collaborators:
name = collaborator.users.name or "me"
collaborators[name] += task.total_time_spent
else:
collaborators["me"] += task.total_time_spent
return collaborators


def __str__(self):
return f"<Project {self.id}>: {self.name} {self.tasks}"
23 changes: 22 additions & 1 deletion terka/domain/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from enum import Enum
from datetime import datetime
from datetime import date, datetime, timedelta


class TaskStatus(Enum):
Expand Down Expand Up @@ -73,5 +73,26 @@ def total_time_spent(self):
return sum([t.time_spent_minutes for t in self.time_spent])
return 0

@property
def completion_date(self) -> datetime | None:
for event in self.history:
if event.new_value in ("DONE", "DELETED"):
return event.date

@property
def is_stale(self):
if self.history and self.status.name in ("TODO", "IN_PROGRESS",
"REVIEW"):
if max([event.date for event in self.history
]) < (datetime.today() - timedelta(days=5)):
return True
return False

@property
def is_overdue(self):
if self.due_date and self.due_date <= date.today():
return True
return False

def __repr__(self):
return f"<Task {self.id}>: {self.name}, {self.status.name}, {self.creation_date}"
21 changes: 21 additions & 0 deletions terka/service_layer/formatter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class Formatter:

@staticmethod
def calculate_time_spent(entity) -> str:
time_spent_sum = sum(
[entry.time_spent_minutes for entry in entity.time_spent])
return Formatter.format_time_spent(time_spent_sum)

@staticmethod
def format_time_spent(time_spent: int) -> str:
time_spent_hours = time_spent // 60
time_spent_minutes = time_spent % 60
if time_spent_hours and time_spent_minutes:
time_spent = f"{time_spent_hours}H:{time_spent_minutes}M"
elif time_spent_hours:
time_spent = f"{time_spent_hours}H:00M"
elif time_spent_minutes:
time_spent = f"00H:{time_spent_minutes}M"
else:
time_spent = ""
return time_spent
111 changes: 55 additions & 56 deletions terka/service_layer/printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
from rich.table import Table
from statistics import mean, median

from terka.service_layer import services, views
from terka.service_layer.ui import TerkaTask
from terka.service_layer import services, views, formatter
from terka.service_layer.ui import TerkaTask, TerkaProject


@dataclass
Expand Down Expand Up @@ -341,7 +341,7 @@ def print_sprint(self, entities, repo, print_options, kwargs=None):
for entry in task.time_spent
])

time_spent = self._format_time_spent(time_spent_sum)
time_spent = formatter.Formatter.format_time_spent(time_spent_sum)
if (total_tasks := len(tasks)) > 0:
pct_completed = round(
(len(tasks) - len(open_tasks)) / len(tasks) * 100)
Expand Down Expand Up @@ -424,8 +424,9 @@ def print_project(self, entities, print_options, kwargs=None):
non_active_projects = Table(box=rich.box.SQUARE_DOUBLE_HEAD,
expand=print_options.expand_table)
default_columns = ("id", "name", "description", "status", "open_tasks",
"overdue", "stale", "backlog", "todo", "in_progress",
"review", "done", "median_task_age")
"overdue", "stale", "backlog", "todo",
"in_progress", "review", "done", "median_task_age",
"time_spent")

if print_options.columns:
printable_columns = print_options.columns.split(",")
Expand All @@ -448,14 +449,8 @@ def print_project(self, entities, print_options, kwargs=None):
]
stale_tasks = []
for task in entity.tasks:
if (event_history :=
task.history) and task.status.name in (
"TODO", "IN_PROGRESS", "REVIEW"):
for event in event_history:
if max([
event.date for event in event_history
]) < (datetime.today() - timedelta(days=5)):
stale_tasks.append(task)
if task.is_stale:
stale_tasks.append(task)
stale_tasks = list(set(stale_tasks))
median_task_age = round(
median([(datetime.now() - task.creation_date).days
Expand All @@ -473,19 +468,34 @@ def print_project(self, entities, print_options, kwargs=None):
else:
entity_id = f"[green]{entity.id}[/green]"
printable_row = {
"id": f"{entity.id}",
"name": str(entity.name),
"description": entity.description,
"status": entity.status.name,
"open_tasks": str(open_tasks),
"overdue": str(len(overdue_tasks)),
"stale": str(len(stale_tasks)),
"backlog": str(backlog),
"todo": str(todo),
"in_progress": str(in_progress),
"review": str(review),
"done": str(done),
"median_task_age": str(median_task_age),
"id":
f"{entity.id}",
"name":
str(entity.name),
"description":
entity.description,
"status":
entity.status.name,
"open_tasks":
str(open_tasks),
"overdue":
str(len(overdue_tasks)),
"stale":
str(len(stale_tasks)),
"backlog":
str(backlog),
"todo":
str(todo),
"in_progress":
str(in_progress),
"review":
str(review),
"done":
str(done),
"median_task_age":
str(median_task_age),
"time_spent":
formatter.Formatter.format_time_spent(entity.total_time_spent),
}
printable_elements = [
value for key, value in printable_row.items()
Expand Down Expand Up @@ -543,6 +553,18 @@ def print_project(self, entities, print_options, kwargs=None):
if "time" in viz:
time_entries = views.time_spent(self.repo.session, tasks)
self._print_time_utilization(time_entries)
if len(entities) == 1:
collaborators = defaultdict(int)
for task in entities[0].tasks:
if task_collaborators := task.collaborators:
for collaborator in task.collaborators:
name = collaborator.users.name
collaborators[name] = +task.total_time_spent
else:
collaborators["me"] = +task.total_time_spent

app = TerkaProject(entity=entities[0])
app.run()

def print_task(self,
entities,
Expand Down Expand Up @@ -658,24 +680,6 @@ def _sort_open_tasks(self, entities):
def _count_task_status(self, tasks, status: str) -> int:
return len([task for task in tasks if task.status.name == status])

def _calculate_time_spent(self, entity) -> str:
time_spent_sum = sum(
[entry.time_spent_minutes for entry in entity.time_spent])
return self._format_time_spent(time_spent_sum)

def _format_time_spent(self, time_spent: int) -> str:
time_spent_hours = time_spent // 60
time_spent_minutes = time_spent % 60
if time_spent_hours and time_spent_minutes:
time_spent = f"{time_spent_hours}H:{time_spent_minutes}M"
elif time_spent_hours:
time_spent = f"{time_spent_hours}H:00M"
elif time_spent_minutes:
time_spent = f"00H:{time_spent_minutes}M"
else:
time_spent = ""
return time_spent

def _get_filtered_entities(self, entities, kwargs):
if kwargs:
filtering_attributes = set(list(kwargs))
Expand Down Expand Up @@ -796,7 +800,8 @@ def sorting_fn(x):
project = None
priority = entity.priority.name if hasattr(entity.priority,
"name") else "UNKNOWN"
time_spent = self._calculate_time_spent(entity)
time_spent = formatter.Formatter.format_time_spent(
entity.total_time_spent)
if entity.status.name in ("DELETED", "DONE") and all_tasks:
completed_tasks.append(entity)
if story_points:
Expand All @@ -814,16 +819,10 @@ def sorting_fn(x):
completed_date = max(completed_events).strftime("%Y-%m-%d")
if not all_tasks:
entity_id = str(entity.id)
elif entity.due_date and entity.due_date <= date.today():
elif entity.is_overdue:
entity_id = f"[red]{entity.id}[/red]"
elif event_history and entity.status.name in ("TODO",
"IN_PROGRESS",
"REVIEW"):
if max([event.date for event in event_history
]) < (datetime.today() - timedelta(days=5)):
entity_id = f"[yellow]{entity.id}[/yellow]"
else:
entity_id = str(entity.id)
elif entity.is_stale:
entity_id = f"[yellow]{entity.id}[/yellow]"
else:
entity_id = str(entity.id)

Expand All @@ -833,7 +832,7 @@ def sorting_fn(x):
"description": entity.description,
"story_points": str(story_point),
"status": entity.status.name,
"priority": priority,
"priority": priority,
"project": project,
"due_date": str(entity.due_date or completed_date),
"tags": tag_string,
Expand All @@ -842,7 +841,7 @@ def sorting_fn(x):
}
printable_elements = [
value for key, value in printable_row.items()
if key in default_columns
if key in default_columns
]
if story_points:
table.add_row(*printable_elements)
Expand Down
Loading

0 comments on commit 6f2620b

Please sign in to comment.