From f16d9eec463dc99c5d71c5220f7771c49c7354b2 Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Wed, 6 Mar 2024 22:05:02 -0500 Subject: [PATCH] Introduce a time-sensitive block --- src/rules/team/rank.py | 53 ++++++++++++++++++++++++++++++++++++++++-- src/tests/conftest.py | 31 +++++++++++++++++++++++- src/tests/test_rank.py | 20 ++++++++++++++++ 3 files changed, 101 insertions(+), 3 deletions(-) diff --git a/src/rules/team/rank.py b/src/rules/team/rank.py index 6645374..d5e5719 100644 --- a/src/rules/team/rank.py +++ b/src/rules/team/rank.py @@ -34,6 +34,8 @@ improvements that have been ranked very high. """ +import datetime + import jira @@ -89,6 +91,9 @@ def __init__(self, parent): self.parent_issue = parent self.issues = [] + def yield_issues(self): + yield from self.issues + def parent_is_inprogress(self): if self.parent_issue is None: return False @@ -112,9 +117,39 @@ def claims(self, issue) -> bool: return self.parent_issue == parent_issue +class TimeSensitiveBlock(Block): + """A special-case block that gets ranked to the top""" + + def yield_issues(self): + if not self.issues: + return + duedate_field_id = self.issues[0].raw["Context"]["Field Ids"]["Due Date"] + duedate = lambda issue: getattr(issue.fields, duedate_field_id) + yield from sorted(self.issues, key=duedate) + + @property + def rank(self): + return float("inf") + + def parent_is_inprogress(self): + return False + + def claims(self, issue) -> bool: + return self._claims(issue) + + @staticmethod + def _claims(issue) -> bool: + duedate_field_id = issue.raw["Context"]["Field Ids"]["Due Date"] + critical_deadline = ( + datetime.datetime.today() + datetime.timedelta(days=30 * 6) + ).strftime("%Y-%m-%d") + duedate = getattr(issue.fields, duedate_field_id) + return duedate and duedate < critical_deadline + + class Blocks(list): def __init__(self, issues: list[jira.resources.Issue]) -> None: - self.blocks = [] + self.blocks = [TimeSensitiveBlock(None)] for issue in issues: self.add_issue(issue) @@ -142,7 +177,7 @@ def get_issues(self) -> list[jira.resources.Issue]: == block.issues[0].fields.project.key ): issues.append(block.parent_issue) - for issue in block.issues: + for issue in block.yield_issues(): issues.append(issue) return issues @@ -150,6 +185,7 @@ def sort(self): self._sort_by_project_rank() self._sort_by_status() self._deprioritize_orphan_blocks() + self._prioritize_timesensitive_block() def _sort_by_project_rank(self): """Rerank blocks based on the block's project rank. @@ -213,3 +249,16 @@ def _deprioritize_orphan_blocks(self): children.append(block) self.blocks = children + orphans + + def _prioritize_timesensitive_block(self): + """Issues that are time sensitive rise to the top of the list.""" + timesensitive = [] + other = [] + + for block in self.blocks: + if type(block) is TimeSensitiveBlock: + timesensitive.append(block) + else: + other.append(block) + + self.blocks = timesensitive + other diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 3413ec2..9614fab 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -1,13 +1,16 @@ +import datetime + import mock import pytest class MockIssue: - def __init__(self, idx, project, parent, rank): + def __init__(self, idx, project, parent, rank, duedate=None): raw = {} raw["Context"] = {} raw["Context"]["Field Ids"] = {} raw["Context"]["Field Ids"]["Rank"] = "rank" + raw["Context"]["Field Ids"]["Due Date"] = "duedate" raw["Context"]["Related Issues"] = {} raw["Context"]["Related Issues"]["Parent"] = parent @@ -17,6 +20,7 @@ def __init__(self, idx, project, parent, rank): self.fields = mock.MagicMock() self.fields.project.key = project self.fields.rank = rank + self.fields.duedate = duedate def __repr__(self): return f"<{type(self).__name__} {self.fields.project.key}-{self.idx}({self.fields.rank})>" @@ -36,3 +40,28 @@ def issues(): return dict( child0=child0, child1=child1, child2=child2, child3=child3, child4=child4 ) + + +@pytest.fixture +def issues_with_due_dates(): + project = "TESTPROJECT" + parent1 = MockIssue("parent1", project, None, 1) + child1 = MockIssue("child1", project, parent1, 2) + parent2 = MockIssue("parent2", project, None, 3) + child2 = MockIssue("child2", project, parent2, 4) + fmt = "%Y-%m-%d" + duedate = (datetime.datetime.today() + datetime.timedelta(days=30 * 7)).strftime( + fmt + ) + child3 = MockIssue("child3", project, parent2, 5, duedate=duedate) + duedate = (datetime.datetime.today() + datetime.timedelta(days=30 * 2)).strftime( + fmt + ) + child4 = MockIssue("child4", project, parent2, 6, duedate=duedate) + duedate = (datetime.datetime.today() + datetime.timedelta(days=30 * 1)).strftime( + fmt + ) + child5 = MockIssue("child5", project, parent2, 7, duedate=duedate) + return dict( + child1=child1, child2=child2, child3=child3, child4=child4, child5=child5 + ) diff --git a/src/tests/test_rank.py b/src/tests/test_rank.py index dd9eb5c..31f3f13 100644 --- a/src/tests/test_rank.py +++ b/src/tests/test_rank.py @@ -28,3 +28,23 @@ def test_rank_single_move(issues): assert new_ranking[5].key == "child2" assert new_ranking[6].key == "child0" assert new_ranking[7].key == "child4" + + +def test_rank_with_dates(issues_with_due_dates): + issues = issues_with_due_dates + issues["child3"].raw["Context"]["Related Issues"]["Parent"].fields.rank = -1 + blocks = rules.team.rank.Blocks(list(issues.values())) + old_ranking = blocks.get_issues() + blocks.sort() + new_ranking = blocks.get_issues() + import pprint + + pprint.pprint(new_ranking) + assert new_ranking != old_ranking + assert new_ranking[0].key == "child5" + assert new_ranking[1].key == "child4" + assert new_ranking[2].key == "parent2" + assert new_ranking[3].key == "child2" + assert new_ranking[4].key == "child3" + assert new_ranking[5].key == "parent1" + assert new_ranking[6].key == "child1"