Skip to content

Commit

Permalink
Add verification that sprint has enough capacity
Browse files Browse the repository at this point in the history
  • Loading branch information
AndreyMarkinPPC committed Feb 17, 2024
1 parent e643ddf commit a352092
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 31 deletions.
42 changes: 26 additions & 16 deletions terka/domain/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,48 @@
from datetime import datetime
from dataclasses import dataclass, asdict

from terka import exceptions


@dataclass
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()
if k in cls.__match_args__ and v is not None
}
[
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down
11 changes: 8 additions & 3 deletions terka/domain/entities/sprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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')
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions terka/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,7 @@ class TerkaRefreshException(TerkaException):

class TerkaSprintOutOfCapacity(TerkaException):
...


class TerkaSprintInvalidCapacity(TerkaException):
...
16 changes: 8 additions & 8 deletions terka/service_layer/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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'
Expand All @@ -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:
Expand Down
19 changes: 15 additions & 4 deletions tests/test_handers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -187,16 +187,27 @@ 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,
story_points=2)
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)
Expand Down

0 comments on commit a352092

Please sign in to comment.