Add multi-part story calculation

This commit is contained in:
Sijmen 2019-09-04 16:42:37 +02:00
parent 2311ae4919
commit e0f95d0d6b
1 changed files with 146 additions and 1 deletions

View File

@ -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