From e0f95d0d6b6ce1acd8acd3121e5108d7dc30099c Mon Sep 17 00:00:00 2001 From: Sijmen Schoon Date: Wed, 4 Sep 2019 16:42:37 +0200 Subject: [PATCH] Add multi-part story calculation --- commands/stories.py | 147 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 146 insertions(+), 1 deletion(-) diff --git a/commands/stories.py b/commands/stories.py index 81ea805..912ccf3 100644 --- a/commands/stories.py +++ b/commands/stories.py @@ -99,6 +99,7 @@ def __format_story( for owner_id in owner_ids: state = story["current_state"] totals[owner_id][state] += estimate / len(owner_ids) + estimate = f"{estimate}\033[0m" if is_owner: code = f"\033[1;97m{code}" @@ -107,7 +108,7 @@ def __format_story( owners = ", ".join(initials) state = _format_state(story["current_state"]) - return code, type_, story["name"], owners, state, estimate, "\033[0m" + return code, type_, story["name"], owners, state, estimate def __print_stories( @@ -244,3 +245,147 @@ def stories( stories_info(story) else: _stories_current(project, scope, show_accepted) + + +def __calculate_stories(all_owners, hours, point_scale, allow_split=False): + owners = all_owners.copy() + + stories = [] + original_hours = hours + while owners: + best, done = None, False + for n in range(min(len(owners), 3), 0, -1): + if done: + break + + for points in point_scale: + error = hours * n - points + if error in (point_scale if allow_split else (0,)): + done = True + best = points, n + break + + if best is None: + if not allow_split: + return __calculate_stories(all_owners, hours, point_scale, True) + else: + return None + + points, n = best + last_points = points / n + + story_owners = owners[:n] + del owners[:n] + + stories.append((story_owners, points)) + if error != 0: + stories.append((story_owners, error)) + + return stories + + +@cli.command("meeting") +@click.argument("project", type=click.STRING) +@require_login +def meeting(project: str): + try: + project_id = int(Config["project_aliases"][project]) + except KeyError: + project_id = base32.decode(project) + token = Config["user"]["api_token"] + + members = api.projects.get_memberships(token, project_id) + members = {m["person"]["name"]: m["person"]["id"] for m in members} + + labels = api.projects.get_labels(token, project_id) + labels = {l["name"]: l["id"] for l in labels} + + project_info = api.projects.get_project(token, project_id) + point_scale = [ + 1 if p == "0" else int(float(p) * 2) + for p in project_info["point_scale"].split(",") + ][::-1] + [0] + + answers = inquirer.prompt( + [ + inquirer.Text("name", message="Meeting name"), + inquirer.Text( + "hours", + message="Meeting length (hours, multiple of 0.5)", + validate=lambda _, x: re.match("^[0-9]+(\.[05])?$", x), + ), + inquirer.Checkbox( + "owners", + message="Attendees (select with space, confirm with enter)", + choices=list(members.keys()), + ), + ] + ) + if not answers: + return + + name = answers["name"] + hours = round(float(answers["hours"]) * 2) + owners = answers["owners"] + + total_hours = hours * len(owners) + print( + f"{len(owners)} attendees on a {hours / 2} hour meeting, for a total " + f"of {total_hours / 2} points." + ) + + stories = __calculate_stories(owners, hours, point_scale) + if stories is None: + print("Could not find a solution") + return + + print("\nGoing to create:") + for story_owners, points in stories: + print( + f" - A {points / 2} point story for {', '.join(story_owners)} " + f"({points / 2 / len(story_owners)} each)" + ) + print() + + answers = inquirer.prompt( + [ + inquirer.List("state", message="State of the story", choices=STATES), + inquirer.Checkbox( + "labels", + message="Labels to add (select with space, confirm with enter)", + choices=list(labels.keys()), + ), + inquirer.List( + "requester", message="Story requester", choices=list(members.keys()) + ), + ] + ) + if not answers: + return + + print("Enter the story description, and press Ctrl+D to confirm.") + description = sys.stdin.read() + print() + + primary_story = None + for i, (story_owners, points) in enumerate(tqdm(stories, "Creating stories...")): + s_name = f"{name} ({i + 1}/{len(stories)})" + s_requester = members[answers["requester"]] + s_label_ids = [labels[name] for name in answers["labels"]] + s_owner_ids = [members[name] for name in story_owners] + s_description = description.strip() + + story = api.stories.post( + token, + project_id, + name=s_name, + requested_by_id=s_requester, + label_ids=s_label_ids, + owner_ids=s_owner_ids, + description=s_description, + estimate=points // 2, + ) + + s_id = int(story["id"]) + if primary_story is None: + primary_story = s_id