Add multi-part story calculation
This commit is contained in:
parent
2311ae4919
commit
e0f95d0d6b
|
@ -99,6 +99,7 @@ def __format_story(
|
||||||
for owner_id in owner_ids:
|
for owner_id in owner_ids:
|
||||||
state = story["current_state"]
|
state = story["current_state"]
|
||||||
totals[owner_id][state] += estimate / len(owner_ids)
|
totals[owner_id][state] += estimate / len(owner_ids)
|
||||||
|
estimate = f"{estimate}\033[0m"
|
||||||
|
|
||||||
if is_owner:
|
if is_owner:
|
||||||
code = f"\033[1;97m{code}"
|
code = f"\033[1;97m{code}"
|
||||||
|
@ -107,7 +108,7 @@ def __format_story(
|
||||||
owners = ", ".join(initials)
|
owners = ", ".join(initials)
|
||||||
|
|
||||||
state = _format_state(story["current_state"])
|
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(
|
def __print_stories(
|
||||||
|
@ -244,3 +245,147 @@ def stories(
|
||||||
stories_info(story)
|
stories_info(story)
|
||||||
else:
|
else:
|
||||||
_stories_current(project, scope, show_accepted)
|
_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
|
||||||
|
|
Loading…
Reference in New Issue