diff --git a/terka/adapters/orm.py b/terka/adapters/orm.py index 70927c1..5f35996 100644 --- a/terka/adapters/orm.py +++ b/terka/adapters/orm.py @@ -222,7 +222,9 @@ Column("end_date", Date, nullable=False), Column("status", Enum(sprint.SprintStatus)), Column("capacity", Integer, nullable=False), - Column("goal", String(225), nullable=True)) + Column("goal", String(225), nullable=True), + Column("started_at", DateTime, nullable=True), + Column("completed_at", DateTime, nullable=True)) sprint_tasks = Table( "sprint_tasks", @@ -232,6 +234,7 @@ Column("sprint", ForeignKey("sprints.id"), nullable=False), Column("story_points", Integer, nullable=False), Column("is_active_link", Boolean, nullable=False), + Column("unplanned", Boolean, nullable=False), ) time_tracker_entries = Table( diff --git a/terka/domain/entities/sprint.py b/terka/domain/entities/sprint.py index e9dba40..53e3e33 100644 --- a/terka/domain/entities/sprint.py +++ b/terka/domain/entities/sprint.py @@ -27,6 +27,8 @@ def __init__(self, status: str = "PLANNED", capacity: int = 40, goal: str | None = None, + started_at: datetime | None = None, + completed_at: datetime | None = None, **kwargs) -> None: if not start_date and not end_date: raise ValueError("Please add start and end date of the sprint") @@ -48,7 +50,8 @@ def __init__(self, self.status = self._validate_status(status) self.goal = goal self.capacity = capacity - self.is_completed = False + self.started_at = started_at + self.completed_at = completed_at def _validate_status(self, status): if status and status not in [s.name for s in SprintStatus]: @@ -64,17 +67,21 @@ def complete(self, tasks) -> None: if incompleted_tasks: logging.warning("[Sprint %d]: %d tasks haven't been completed", self.id, len(incompleted_tasks)) - self.is_completed = True + + @property + def is_completed(self) -> bool: + return self.status == SprintStatus.COMPLETED @property def overplanned(self) -> bool: return self.velocity > self.capacity @property - def remaining_capatity(self) -> float: + def remaining_capacity(self) -> float: if not self.overplanned: return self.capacity - self.velocity return 0 + @property def velocity(self) -> float: return round(sum([t.story_points for t in self.tasks]), 1) @@ -100,6 +107,14 @@ def total_time_spent(self): total_time_spent_sprint += sprint_task.tasks.total_time_spent return total_time_spent_sprint + @property + def unplanned_tasks(self) -> list[Task]: + tasks = [] + for entity_task in self.tasks: + if entity_task.unplanned: + tasks.append(entity_task.tasks) + return tasks + @property def open_tasks(self) -> list[Task]: tasks = [] @@ -120,7 +135,6 @@ def completed_tasks(self) -> list[Task]: tasks.append(task) return tasks - @property def pct_completed(self) -> float: if (total_tasks := len(self.tasks)) > 0: @@ -165,3 +179,4 @@ class SprintTask: story_points: int = 0 #TODO: add actual_time_spent: int = 0 is_active_link: bool = True + unplanned: bool = False diff --git a/terka/presentations/text_ui/ui.py b/terka/presentations/text_ui/ui.py index a179bc2..994f2b5 100644 --- a/terka/presentations/text_ui/ui.py +++ b/terka/presentations/text_ui/ui.py @@ -853,10 +853,21 @@ def compose(self) -> ComposeResult: key=lambda x: x[1], reverse=True): sorted_projects += f" * {name}: {Formatter.format_time_spent(value)} \n" + if started_at := self.entity.started_at: + started_at_string = started_at.strftime("%Y-%m-%d %H:%M") + else: + if self.entity.status.name != "PLANNED": + started_at_string = self.entity.start_date.strftime( + "%Y-%m-%d %H:%M") + else: + started_at_string = "Not started" + yield MarkdownViewer(f""" # Sprint details: * Period: {self.entity.start_date} - {self.entity.end_date} +* Started: {started_at_string} * Open tasks: {len(self.entity.open_tasks)} ({len(self.entity.tasks)}) +* Share of unplanned tasks: {round(len(self.entity.unplanned_tasks) / len(self.entity.tasks), 2) :.0%} * Pct Completed: {round(self.entity.pct_completed, 2) :.0%} * Velocity: {self.entity.velocity} ({self.entity.capacity}) * Time spend: {Formatter.format_time_spent(self.entity.total_time_spent)} diff --git a/terka/service_layer/handlers.py b/terka/service_layer/handlers.py index 8e3073b..f082351 100644 --- a/terka/service_layer/handlers.py +++ b/terka/service_layer/handlers.py @@ -91,7 +91,7 @@ def start(cmd: commands.StartSprint, raise exceptions.TerkaSprintCompleted( "Cannot start completed sprint") uow.tasks.update(entities.sprint.Sprint, cmd.id, - {"status": "ACTIVE"}) + {"status": "ACTIVE", "started": datetime.now()}) uow.commit() for sprint_task in existing_sprint.tasks: task = sprint_task.tasks @@ -378,8 +378,11 @@ def _add(cmd: commands.AddTask, bus: "messagebus.MessageBus", {"story_points": float(story_points)}) uow.commit() else: - if entity_name == "sprint" and story_points: - entity_dict["story_points"] = story_points + if entity_name == "sprint": + if story_points: + entity_dict["story_points"] = story_points + entity_dict["unplanned"] = (existing_entity.started_at < + datetime.now()) entity_task = entity_task_type(**entity_dict) uow.tasks.add(entity_task) uow.commit()