From a35209233fef45787d730e04caeacc52186bf8da Mon Sep 17 00:00:00 2001 From: Andrei Markin Date: Sat, 17 Feb 2024 08:52:15 +0400 Subject: [PATCH] Add verification that sprint has enough capacity --- terka/domain/commands.py | 42 ++++++++++++++++++++------------- terka/domain/entities/sprint.py | 11 ++++++--- terka/exceptions.py | 4 ++++ terka/service_layer/handlers.py | 16 ++++++------- tests/test_handers.py | 19 +++++++++++---- 5 files changed, 61 insertions(+), 31 deletions(-) diff --git a/terka/domain/commands.py b/terka/domain/commands.py index 47c7729..6ad7fd7 100644 --- a/terka/domain/commands.py +++ b/terka/domain/commands.py @@ -2,6 +2,8 @@ from datetime import datetime from dataclasses import dataclass, asdict +from terka import exceptions + @dataclass class Command: @@ -9,12 +11,12 @@ class Command: def get_only_set_attributes(self) -> dict: set_attributes = {} for key, value in asdict(self).items(): - if key == "due_date" or value: - set_attributes[key] = value if value != "Remove" else None + if key == 'due_date' or value: + set_attributes[key] = value if value != 'Remove' else None return set_attributes @classmethod - def from_kwargs(cls, **kwargs: dict) -> Type["Command"]: + def from_kwargs(cls, **kwargs: dict) -> Type['Command']: attributes = { k: v for k, v in kwargs.items() @@ -22,26 +24,26 @@ def from_kwargs(cls, **kwargs: dict) -> Type["Command"]: } [ Command.format_date(attributes, d) - for d in ("due_date", "start_date", "end_date") + for d in ('due_date', 'start_date', 'end_date') ] return cls(**attributes) def __bool__(self) -> bool: - return all(f for f in self.__dataclass_fields__ if f != "id") + return all(f for f in self.__dataclass_fields__ if f != 'id') def inject(self, config: dict) -> None: self.__dict__.update(config) - self.__dict__["created_by"] = self.__dict__.pop("user") + self.__dict__['created_by'] = self.__dict__.pop('user') @staticmethod def format_date(attributes: dict, date_attribute: str): if date_attribute_value := attributes.get(date_attribute): if not isinstance(date_attribute_value, datetime): - if date_attribute_value == "Remove": - attributes[date_attribute] = "Remove" + if date_attribute_value == 'Remove': + attributes[date_attribute] = 'Remove' else: attributes[date_attribute] = datetime.strptime( - date_attribute_value, "%Y-%m-%d") + date_attribute_value, '%Y-%m-%d') # Base Commands @@ -95,7 +97,7 @@ class Update(Command): def __bool__(self) -> bool: cmd_dict = dict(self.__dict__) - _ = cmd_dict.pop("id") + _ = cmd_dict.pop('id') if any(cmd_dict.values()): return True return False @@ -160,8 +162,8 @@ class CreateTask(Command): project: str | None = None assignee: str | None = None due_date: str | None = None - status: str = "BACKLOG" - priority: str = "NORMAL" + status: str = 'BACKLOG' + priority: str = 'NORMAL' sync: bool = True created_by: str | None = None @@ -192,7 +194,7 @@ class UpdateTask(Command): def __bool__(self) -> bool: cmd_dict = dict(self.__dict__) - _ = cmd_dict.pop("id") + _ = cmd_dict.pop('id') if any(cmd_dict.values()): return True return False @@ -260,7 +262,7 @@ class CreateProject(Command): name: str | None = None description: str | None = None workspace: int = 1 - status: str = "ACTIVE" + status: str = 'ACTIVE' @dataclass @@ -273,7 +275,7 @@ class UpdateProject(Command): def __bool__(self) -> bool: cmd_dict = dict(self.__dict__) - _ = cmd_dict.pop("id") + _ = cmd_dict.pop('id') if any(cmd_dict.values()): return True return False @@ -370,14 +372,22 @@ class UpdateSprint(Command): status: str | None = None start_date: str | None = None end_date: str | None = None + capacity: int | None = None def __bool__(self) -> bool: cmd_dict = dict(self.__dict__) - _ = cmd_dict.pop("id") + _ = cmd_dict.pop('id') if any(cmd_dict.values()): return True return False + def __post_init__(self) -> None: + if self.capacity and self.capacity <= 0: + raise exceptions.TerkaSprintInvalidCapacity( + f'Invalid capacity {self.capacity}! ' + 'Sprint capacity cannot 0 or less' + ) + @dataclass class CompleteSprint(Complete): diff --git a/terka/domain/entities/sprint.py b/terka/domain/entities/sprint.py index 87ca5d3..11219c6 100644 --- a/terka/domain/entities/sprint.py +++ b/terka/domain/entities/sprint.py @@ -6,8 +6,9 @@ from datetime import datetime, date from dataclasses import dataclass -from .entity import Entity -from .task import Task +from terka import exceptions +from terka.domain.entities.entity import Entity +from terka.domain.entities.task import Task logger = logging.getLogger(__name__) @@ -29,7 +30,7 @@ def __init__(self, goal: str | None = None, started_at: datetime | None = None, **kwargs) -> None: - if not start_date and not end_date: + if not start_date or not end_date: raise ValueError('Please add start and end date of the sprint') if start_date.date() < datetime.today().date(): raise ValueError(f'start date cannot be less than today') @@ -48,6 +49,10 @@ def __init__(self, raise ValueError(f'Sprint end date cannot be less than today') self.status = self._validate_status(status) self.goal = goal + if capacity < 0: + raise exceptions.TerkaSprintInvalidCapacity( + f'Invalid capacity {capacity}! Sprint capacity cannot 0 or less' + ) self.capacity = capacity self.started_at = started_at self.completed_at = None diff --git a/terka/exceptions.py b/terka/exceptions.py index 714a518..a0ced56 100644 --- a/terka/exceptions.py +++ b/terka/exceptions.py @@ -40,3 +40,7 @@ class TerkaRefreshException(TerkaException): class TerkaSprintOutOfCapacity(TerkaException): ... + + +class TerkaSprintInvalidCapacity(TerkaException): + ... diff --git a/terka/service_layer/handlers.py b/terka/service_layer/handlers.py index 0c339e8..782c15a 100644 --- a/terka/service_layer/handlers.py +++ b/terka/service_layer/handlers.py @@ -389,8 +389,8 @@ def _add(cmd: commands.AddTask, bus: 'messagebus.MessageBus', if story_points: entity_dict['story_points'] = story_points if started_at := existing_entity.started_at: - entity_dict['unplanned'] = (started_at < - datetime.now()) + entity_dict['unplanned'] = (started_at + < datetime.now()) entity_task = entity_task_type(**entity_dict) uow.tasks.add(entity_task) uow.commit() @@ -403,8 +403,8 @@ def _add(cmd: commands.AddTask, bus: 'messagebus.MessageBus', if existing_entity.overplanned: raise exceptions.TerkaSprintOutOfCapacity( f'Sprint {entity_id} is overplanned') - if (entity_task.story_points > - existing_entity.remaining_capacity): + if (entity_task.story_points + > existing_entity.remaining_capacity): raise exceptions.TerkaSprintOutOfCapacity( f'Sprint {entity_id} will overplanned ' f'when task with {entity_task.story_points} is added' @@ -415,10 +415,10 @@ def _add(cmd: commands.AddTask, bus: 'messagebus.MessageBus', if existing_task.status.name == 'BACKLOG': task_params.update({'status': 'TODO'}) if (not existing_task.due_date - or existing_task.due_date > - existing_entity.end_date - or existing_task.due_date < - existing_entity.start_date): + or existing_task.due_date + > existing_entity.end_date + or existing_task.due_date + < existing_entity.start_date): task_params.update( {'due_date': existing_entity.end_date}) if task_params: diff --git a/tests/test_handers.py b/tests/test_handers.py index 0f0e9f5..7418d81 100644 --- a/tests/test_handers.py +++ b/tests/test_handers.py @@ -9,7 +9,7 @@ class TestTask: def test_create_simple_task(self, bus): cmd = commands.CreateTask(name='test') - bus.handle(cmd) + new_task = bus.handle(cmd) new_task = bus.uow.tasks.get_by_id(entities.task.Task, 1) expected_task = entities.task.Task( name='test', @@ -187,9 +187,8 @@ def test_adding_task_to_sprint(self, bus, new_sprint, sprint_with_tasks): for task in sprint_tasks: assert task.story_points == 0 - def test_cannot_add_task_to_sprint(self, bus, - new_sprint_with_limited_capacity, - tasks): + def test_cannot_add_task_to_sprint_with_limited_capacity( + self, bus, new_sprint_with_limited_capacity, tasks): task_1, task_2 = tasks add_task_1 = commands.AddTask(id=task_1, sprint=new_sprint_with_limited_capacity, @@ -197,6 +196,18 @@ def test_cannot_add_task_to_sprint(self, bus, with pytest.raises(exceptions.TerkaSprintOutOfCapacity): bus.handle(add_task_1) + def test_updating_sprint_capacity(self, bus, new_sprint): + cmd = commands.UpdateSprint(id=new_sprint, capacity=4) + bus.handle(cmd) + sprint = bus.uow.tasks.get_by_id(entities.sprint.Sprint, new_sprint) + assert sprint.capacity == 4 + + @pytest.mark.parametrize('capacity', [-2, -1]) + def test_cannot_update_sprint_capacity_to_zero_or_below( + self, bus, new_sprint, capacity): + with pytest.raises(exceptions.TerkaSprintInvalidCapacity): + cmd = commands.UpdateSprint(id=new_sprint, capacity=capacity) + def test_starting_sprint_changes_status_due_date(self, bus, new_sprint, sprint_with_tasks): cmd = commands.StartSprint(new_sprint)