diff --git a/terka/domain/entities/composite.py b/terka/domain/entities/composite.py index 45e8231..65d85bb 100644 --- a/terka/domain/entities/composite.py +++ b/terka/domain/entities/composite.py @@ -45,6 +45,7 @@ def backlog_tasks(self) -> list[Task]: if task.status.name == "BACKLOG": tasks.append(task) return tasks + @property def open_tasks(self) -> list[Task]: tasks = [] diff --git a/terka/domain/entities/project.py b/terka/domain/entities/project.py index 1984b29..37b42d5 100644 --- a/terka/domain/entities/project.py +++ b/terka/domain/entities/project.py @@ -4,6 +4,7 @@ from statistics import median from .entity import Entity +from .task import Task class ProjectStatus(Enum): DELETED = 0 @@ -110,6 +111,30 @@ def done(self): def deleted(self): return self._count_task_status("DELETED") + @property + def backlog_tasks(self) -> list[Task]: + tasks = [] + for task in self.tasks: + if task.status.name == "BACKLOG": + tasks.append(task) + return tasks + + @property + def open_tasks(self) -> list[Task]: + tasks = [] + for task in self.tasks: + if task.status.name not in ("DONE", "DELETED"): + tasks.append(task) + return tasks + + @property + def completed_tasks(self) -> list[Task]: + tasks = [] + for task in self.tasks: + if task.status.name in ("DONE", "DELETED"): + tasks.append(task) + return tasks + def daily_time_entries_hours( self, start_date: str | date | None = None, diff --git a/terka/domain/entities/sprint.py b/terka/domain/entities/sprint.py index 6406f7a..ab98002 100644 --- a/terka/domain/entities/sprint.py +++ b/terka/domain/entities/sprint.py @@ -89,11 +89,25 @@ def total_time_spent(self): return total_time_spent_sprint @property - def open_tasks(self): - return [ - task for task in self.tasks - if task.tasks.status.name not in ("DONE", "DELETED") - ] + def open_tasks(self) -> list[Task]: + tasks = [] + for entity_task in self.tasks: + task = entity_task.tasks + if task.status.name not in ("DONE", "DELETED"): + task.story_points = entity_task.story_points + tasks.append(task) + return tasks + + @property + def completed_tasks(self) -> list[Task]: + tasks = [] + for entity_task in self.tasks: + task = entity_task.tasks + if task.status.name in ("DONE", "DELETED"): + task.story_points = entity_task.story_points + tasks.append(task) + return tasks + @property def pct_completed(self) -> float: diff --git a/terka/domain/entities/task.py b/terka/domain/entities/task.py index a0408b3..682a5cc 100644 --- a/terka/domain/entities/task.py +++ b/terka/domain/entities/task.py @@ -7,6 +7,7 @@ from .entity import Entity + class TaskStatus(Enum): DELETED = 0 BACKLOG = 1 @@ -105,11 +106,24 @@ def daily_time_entries_hours( entries[day] += entry.time_spent_minutes / 60 return entries + @property + def project_name(self) -> str: + if project := self.project_: + return project.name + return "" + + @property + def assignee_name(self) -> str: + if assigned_to := self.assigned_to: + return assigned_to.name + return "" + @property def completion_date(self) -> datetime | None: for event in self.history: if event.new_value in ("DONE", "DELETED"): return event.date + return datetime.utcfromtimestamp(0) @property def is_stale(self): @@ -132,6 +146,27 @@ def is_completed(self): return True return False + @property + def collaborators_string(self) -> str: + if collaborators := self.collaborators: + collaborators_texts = sorted([ + collaborator.users.name for collaborator in list(collaborators) + if collaborator.users + ]) + collaborator_string = ",".join(collaborators_texts) + else: + collaborator_string = "" + return collaborator_string + + @property + def tags_string(self) -> str: + if tags := self.tags: + tags_text = ",".join( + [tag.base_tag.text for tag in list(tags)]) + else: + tags_text = "" + return tags_text + def __repr__(self): return f": {self.name}, {self.status.name}, {self.creation_date}" @@ -147,9 +182,8 @@ def __eq__(self, other) -> bool: if not isinstance(other, Task): return False if (self.name, self.description, self.project, self.status, - self.priority, self.due_date, - self.assignee) != (other.name, other.description, - other.project, other.status, other.priority, - other.due_date, other.assignee): + self.priority, self.due_date, self.assignee) != ( + other.name, other.description, other.project, other.status, + other.priority, other.due_date, other.assignee): return False return True diff --git a/terka/entrypoints/server.py b/terka/entrypoints/server.py index e4d94b5..2b1c938 100644 --- a/terka/entrypoints/server.py +++ b/terka/entrypoints/server.py @@ -408,11 +408,12 @@ def delete_user(user_id): return _build_response(result) -# tags +# tags @app.route("/api/v1/tags", methods=["GET"]) def list_tags(): return _build_response(views.tags(bus.uow)) + @app.route("/api/v1/tags/", methods=["GET"]) def get_tag(tag_id): return _build_response(views.tag(bus.uow, tag_id)) @@ -433,6 +434,7 @@ def delete_tag(): result = bus.handle(cmd) return _build_response(result) + def _build_response(msg="", status=200, mimetype="application/json"): """Helper method to build the response.""" msg = json.dumps(msg, indent=4, cls=EntityEncoder) diff --git a/terka/presentations/text_ui/ui.py b/terka/presentations/text_ui/ui.py index e92dc4c..0939f5f 100644 --- a/terka/presentations/text_ui/ui.py +++ b/terka/presentations/text_ui/ui.py @@ -58,7 +58,7 @@ def on_data_table_cell_selected(self, event: DataTable.CellSelected): self.query_one( components.Priority).value = task_obj.priority.name self.query_one( - components.Project).value = task_obj.project_.name + components.Project).value = task_obj.project_name self.query_one(components.Commentaries).values = [ (t.date.strftime("%Y-%m-%d %H:%M"), t.text) for t in task_obj.commentaries @@ -315,7 +315,7 @@ def compose(self) -> ComposeResult: yield Static(task_text, classes="header_overdue", id="header") else: yield Static(task_text, classes="header_simple", id="header") - yield Static(f"Project: [bold]{self.entity.project_.name}[/bold]", + yield Static(f"Project: [bold]{self.entity.project_name}[/bold]", classes="transp") yield Static(f"Status: [bold]{self.entity.status.name}[/bold]", classes="transp") @@ -469,46 +469,30 @@ def compose(self) -> ComposeResult: "created_at", "assignee", "tags", "collaborators", "time_spent"): table.add_column(column, key=column) - for task in sorted(self.entity.tasks, + for task in sorted(self.entity.backlog_tasks, key=lambda x: x.id, reverse=True): - if task.status.name == "BACKLOG": - if tags := task.tags: - tags_text = ",".join( - [tag.base_tag.text for tag in list(tags)]) - else: - tags_text = "" - if collaborators := task.collaborators: - collaborators_texts = sorted([ - collaborator.users.name - for collaborator in list(collaborators) - if collaborator.users - ]) - collaborator_string = ",".join(collaborators_texts) - else: - collaborator_string = "" - if task.is_overdue: - task_id = f"[red]{task.id}[/red]" - elif task.is_stale: - task_id = f"[yellow]{task.id}[/yellow]" - else: - task_id = str(task.id) - if len(commentaries := task.commentaries) > 0: - task_name = f"{task.name} [blue][{len(task.commentaries)}][/blue]" - else: - task_name = task.name - table.add_row(task_id, - task_name, - task.priority.name, - task.due_date, - task.creation_date.strftime("%Y-%m-%d"), - str(task.assigned_to.name) - if task.assigned_to else "", - tags_text, - collaborator_string, - Formatter.format_time_spent( - task.total_time_spent), - key=task.id) + if task.is_overdue: + task_id = f"[red]{task.id}[/red]" + elif task.is_stale: + task_id = f"[yellow]{task.id}[/yellow]" + else: + task_id = str(task.id) + if len(commentaries := task.commentaries) > 0: + task_name = f"{task.name} [blue][{len(task.commentaries)}][/blue]" + else: + task_name = task.name + table.add_row( + task_id, + task_name, + task.priority.name, + task.due_date, + task.creation_date.strftime("%Y-%m-%d"), + task.assignee_name, + task.tags_string, + task.collaborators_string, + Formatter.format_time_spent(task.total_time_spent), + key=task.id) yield table with TabPane("Open Tasks", id="tasks"): table = DataTable(id="project_open_tasks") @@ -516,46 +500,30 @@ def compose(self) -> ComposeResult: "assignee", "tags", "collaborators", "time_spent"): table.add_column(column, key=column) - for task in sorted(self.entity.tasks, + for task in sorted(self.entity.open_tasks, key=lambda x: x.status.name, reverse=True): - if task.status.name in ("TODO", "IN_PROGRESS", "REVIEW"): - if tags := task.tags: - tags_text = ",".join( - [tag.base_tag.text for tag in list(tags)]) - else: - tags_text = "" - if collaborators := task.collaborators: - collaborators_texts = sorted([ - collaborator.users.name - for collaborator in list(collaborators) - if collaborator.users - ]) - collaborator_string = ",".join(collaborators_texts) - else: - collaborator_string = "" - if task.is_overdue: - task_id = f"[red]{task.id}[/red]" - elif task.is_stale: - task_id = f"[yellow]{task.id}[/yellow]" - else: - task_id = str(task.id) - if len(commentaries := task.commentaries) > 0: - task_name = f"{task.name} [blue][{len(task.commentaries)}][/blue]" - else: - task_name = task.name - table.add_row(task_id, - task_name, - task.status.name, - task.priority.name, - task.due_date, - str(task.assigned_to.name) - if task.assigned_to else "", - tags_text, - collaborator_string, - Formatter.format_time_spent( - task.total_time_spent), - key=task.id) + if task.is_overdue: + task_id = f"[red]{task.id}[/red]" + elif task.is_stale: + task_id = f"[yellow]{task.id}[/yellow]" + else: + task_id = str(task.id) + if len(commentaries := task.commentaries) > 0: + task_name = f"{task.name} [blue][{len(task.commentaries)}][/blue]" + else: + task_name = task.name + table.add_row(task_id, + task_name, + task.status.name, + task.priority.name, + task.due_date, + task.assignee_name, + task.tags_string, + task.collaborators_string, + Formatter.format_time_spent( + task.total_time_spent), + key=task.id) yield table with TabPane("Completed Tasks", id="completed_tasks"): table = DataTable(id="project_completed_tasks_table") @@ -563,26 +531,9 @@ def compose(self) -> ComposeResult: "completion_date", "assignee", "tags", "collaborators", "time_spent"): table.add_column(column, key=column) - completed_tasks = [ - e for e in self.entity.tasks if e.completion_date - ] - for task in sorted(completed_tasks, + for task in sorted(self.entity.completed_tasks, key=lambda x: x.completion_date, reverse=True): - if tags := task.tags: - tags_text = ",".join( - [tag.base_tag.text for tag in list(tags)]) - else: - tags_text = "" - if collaborators := task.collaborators: - collaborators_texts = sorted([ - collaborator.users.name - for collaborator in list(collaborators) - if collaborator.users - ]) - collaborator_string = ",".join(collaborators_texts) - else: - collaborator_string = "" if len(commentaries := task.commentaries) > 0: task_name = f"{task.name} [blue][{len(task.commentaries)}][/blue]" else: @@ -593,9 +544,9 @@ def compose(self) -> ComposeResult: task.status.name, task.priority.name, task.completion_date.strftime("%Y-%m-%d"), - str(task.assigned_to.name) if task.assigned_to else "", - tags_text, - collaborator_string, + task.assignee_name, + task.tags_string, + task.collaborators_string, Formatter.format_time_spent(task.total_time_spent), key=task.id) yield table @@ -835,26 +786,9 @@ def compose(self) -> ComposeResult: "assignee", "tags", "collaborators", "time_spent"): table.add_column(column, key=column) - for sprint_task in sorted(self.entity.tasks, - key=lambda x: x.tasks.status.value, - reverse=True): - task = sprint_task.tasks - if tags := task.tags: - tags_text = ",".join([ - tag.base_tag.text for tag in list(tags) - if not tag.base_tag.text.startswith("sprint") - ]) - else: - tags_text = "" - if collaborators := task.collaborators: - collaborators_texts = sorted([ - collaborator.users.name - for collaborator in list(collaborators) - if collaborator.users - ]) - collaborator_string = ",".join(collaborators_texts) - else: - collaborator_string = "" + for task in sorted(self.entity.open_tasks, + key=lambda x: x.status.value, + reverse=True): if task.is_overdue: task_id = f"[red]{task.id}[/red]" elif task.is_stale: @@ -865,21 +799,19 @@ def compose(self) -> ComposeResult: task_name = f"{task.name} [blue][{len(task.commentaries)}][/blue]" else: task_name = task.name - if task.status.name not in ("DONE", "DELETED"): - table.add_row(task_id, - task_name, - task.status.name, - task.priority.name, - str(sprint_task.story_points), - task.project_.name, - task.due_date, - str(task.assigned_to.name) - if task.assigned_to else "", - tags_text, - collaborator_string, - Formatter.format_time_spent( - task.total_time_spent), - key=task.id) + table.add_row( + task_id, + task_name, + task.status.name, + task.priority.name, + str(task.story_points), + task.project_name, + task.due_date, + task.assignee_name, + task.tags_string, + task.collaborators_string, + Formatter.format_time_spent(task.total_time_spent), + key=task.id) yield table with TabPane("Completed Tasks", id="completed_tasks"): table = DataTable(id="sprint_completed_tasks_table") @@ -887,45 +819,26 @@ def compose(self) -> ComposeResult: "assignee", "tags", "collaborators", "story_points", "time_spent"): table.add_column(column, key=column) - for sprint_task in sorted(self.entity.tasks, - key=lambda x: x.id, - reverse=True): - task = sprint_task.tasks - if tags := task.tags: - tags_text = ",".join([ - tag.base_tag.text for tag in list(tags) - if not tag.base_tag.text.startswith("sprint") - ]) - else: - tags_text = "" - if collaborators := task.collaborators: - collaborators_texts = sorted([ - collaborator.users.name - for collaborator in list(collaborators) - if collaborator.users - ]) - collaborator_string = ",".join(collaborators_texts) - else: - collaborator_string = "" + for task in sorted(self.entity.completed_tasks, + key=lambda x: x.completion_date, + reverse=True): if len(commentaries := task.commentaries) > 0: task_name = f"{task.name} [blue][{len(task.commentaries)}][/blue]" else: task_name = task.name - if task.status.name in ("DONE", "DELETED"): - table.add_row( - str(task.id), - task_name, - task.project_.name, - task.completion_date.strftime("%Y-%m-%d") - if task.completion_date else "", - str(task.assigned_to.name) - if task.assigned_to else "", - tags_text, - collaborator_string, - Formatter.format_time_spent( - round(sprint_task.story_points * 60)), - Formatter.format_time_spent(task.total_time_spent), - key=task.id) + table.add_row( + str(task.id), + task_name, + task.project_name, + task.completion_date.strftime("%Y-%m-%d") + if task.completion_date else "", + task.assignee_name, + task.tags_string, + task.collaborators_string, + Formatter.format_time_spent( + round(task.story_points * 60)), + Formatter.format_time_spent(task.total_time_spent), + key=task.id) yield table with TabPane("Notes", id="notes"): table = DataTable(id="sprint_notes_table") @@ -962,7 +875,7 @@ def compose(self) -> ComposeResult: with TabPane("Overview", id="overview"): project_split = defaultdict(int) for task in self.get_tasks(all=True): - project = task.project_.name + project = task.project_name project_split[project] += task.total_time_spent collaborators = self.entity.collaborators sorted_collaborators = "" @@ -1029,7 +942,7 @@ def __init__(self, entity, bus) -> None: def on_mount(self) -> None: self.title = f"Epic: {self.entity.id}" - self.sub_title = f"Project: {self.entity.project_.name}" + self.sub_title = f"Project: {self.entity.project_name}" def compose(self) -> ComposeResult: yield Header() @@ -1045,20 +958,6 @@ def compose(self) -> ComposeResult: for task in sorted(self.entity.open_tasks, key=lambda x: x.status.name, reverse=True): - if tags := task.tags: - tags_text = ",".join( - [tag.base_tag.text for tag in list(tags)]) - else: - tags_text = "" - if collaborators := task.collaborators: - collaborators_texts = sorted([ - collaborator.users.name - for collaborator in list(collaborators) - if collaborator.users - ]) - collaborator_string = ",".join(collaborators_texts) - else: - collaborator_string = "" if task.is_overdue: task_id = f"[red]{task.id}[/red]" elif task.is_stale: @@ -1074,10 +973,9 @@ def compose(self) -> ComposeResult: task_name, task.status.name, task.priority.name, - task.due_date, - str(task.assigned_to.name) if task.assigned_to else "", - tags_text, - collaborator_string, + task.assignee_name, + task.tags_string, + task.collaborators_string, Formatter.format_time_spent(task.total_time_spent), key=task.id) yield table @@ -1090,20 +988,6 @@ def compose(self) -> ComposeResult: for task in sorted(self.entity.completed_tasks, key=lambda x: x.completion_date, reverse=True): - if tags := task.tags: - tags_text = ",".join( - [tag.base_tag.text for tag in list(tags)]) - else: - tags_text = "" - if collaborators := task.collaborators: - collaborators_texts = sorted([ - collaborator.users.name - for collaborator in list(collaborators) - if collaborator.users - ]) - collaborator_string = ",".join(collaborators_texts) - else: - collaborator_string = "" if len(commentaries := task.commentaries) > 0: task_name = f"{task.name} [blue][{len(task.commentaries)}][/blue]" else: @@ -1114,9 +998,9 @@ def compose(self) -> ComposeResult: task.status.name, task.priority.name, task.completion_date.strftime("%Y-%m-%d"), - str(task.assigned_to.name) if task.assigned_to else "", - tags_text, - collaborator_string, + task.assignee_name, + task.tags_string, + task.collaborators_string, Formatter.format_time_spent(task.total_time_spent), key=task.id) yield table