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:
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue