pivotalcli/commands/stories.py

183 lines
6.0 KiB
Python
Raw Normal View History

2018-04-03 10:56:07 +00:00
import argparse
2018-07-21 12:36:02 +00:00
import click
2018-04-03 10:56:07 +00:00
from collections import defaultdict
from datetime import datetime, timedelta
2018-07-21 12:36:02 +00:00
from typing import Any, DefaultDict, Dict, List, Sequence, Tuple, Optional
2018-04-03 10:56:07 +00:00
2018-07-22 09:45:15 +00:00
import base32_crockford as base32
2018-04-03 10:56:07 +00:00
import tabulate
import api.projects
import api.stories
from config import Config
from util import require_login
from . import COLOR_HEADER, _format_state, _get_persons
2018-07-22 09:45:15 +00:00
from ._stories_info import stories_info
2018-07-21 12:36:02 +00:00
from .cli import cli
2018-04-03 10:56:07 +00:00
STATES = 'unstarted', 'planned', 'started', 'finished', 'delivered', 'accepted'
Persons = Dict[int, Dict[str, Any]]
Totals = DefaultDict[int, Dict[str, int]]
2018-07-22 09:45:15 +00:00
def __get_row(item: Tuple[int, Dict[str, int]], persons: Persons,
show_accepted: bool) -> Sequence:
2018-04-03 10:56:07 +00:00
owner_id, points = item
name = persons[owner_id]['name']
estimates = [points[state] for state in STATES]
progress = '['
progress += f'\033[90m' + 'P' * round(points['planned'])
progress += f'\033[38;5;226m' + 'S' * round(points['started'])
progress += f'\033[94m' + 'F' * round(points['finished'])
progress += f'\033[38;5;208m' + 'D' * round(points['delivered'])
2018-07-22 09:45:15 +00:00
if show_accepted:
progress += f'\033[92m' + 'A' * round(points['accepted'])
2018-04-03 10:56:07 +00:00
progress += '\033[0m]'
2018-07-22 09:45:15 +00:00
return name, (*estimates), sum(estimates), progress
2018-04-03 10:56:07 +00:00
def __format_story(story: Dict[str, Any], persons: Persons, totals: Totals) \
-> Tuple[str, str, str, str, str, int]:
2018-07-22 09:45:15 +00:00
code = base32.encode(story['id'])
2018-04-03 10:56:07 +00:00
is_owner = False
initials = []
owner_ids = story.get('owner_ids', [])
for owner_id in owner_ids:
value = persons[owner_id]['initials']
if value == Config['user']['initials']:
is_owner = True
value = f'\033[96m{value}\033[97m'
initials.append(value)
type_ = story['story_type']
2018-04-03 10:56:07 +00:00
estimate = story.get('estimate')
if estimate:
for owner_id in owner_ids:
state = story['current_state']
totals[owner_id][state] += estimate / len(owner_ids)
if is_owner:
code = f'\033[1;97m{code}'
estimate = f'\033[97m{estimate}\033[0m' if estimate else None
owners = ', '.join(initials)
state = _format_state(story['current_state'])
return code, type_, story['name'], owners, state, estimate
2018-04-03 10:56:07 +00:00
2018-07-22 09:45:15 +00:00
def __print_stories(stories_: List[Dict[str, Any]], persons: Persons,
totals: Totals, show_accepted: bool) -> None:
table = (__format_story(story, persons, totals) for story in stories_
if show_accepted or story['current_state'] != 'accepted')
2018-04-03 10:56:07 +00:00
headers = 'Code', 'Type', 'Story name', 'Owners', 'State', 'Pts'
print(tabulate.tabulate(table, headers=headers), end='\n\n')
2018-07-22 09:45:15 +00:00
def __print_totals(totals: Totals, persons: Persons, show_accepted: bool) \
-> None:
2018-04-03 10:56:07 +00:00
COLOR_HEADER.print('Point totals:', end='\n\n')
state_headers = [_format_state(state) for state in STATES]
headers = ('Owner', *state_headers, 'Total', 'Progress')
2018-07-22 09:45:15 +00:00
data = sorted((__get_row(item, persons, show_accepted)
for item in totals.items()),
key=lambda row: row[-2], reverse=True)
2018-04-03 10:56:07 +00:00
print(tabulate.tabulate(data, headers), end='\n\n')
2018-07-22 09:45:15 +00:00
def __print_burndown(token: str, iteration: Dict[str, Any],
show_accepted: bool) -> None:
2018-04-03 10:56:07 +00:00
COLOR_HEADER.print('Burndown:', end='\n\n')
start = datetime.strptime(iteration['start'], '%Y-%m-%dT%H:%M:%SZ')
history = api.projects.get_history_days(
token, iteration['project_id'], start - timedelta(days=1))
accepted_points = history['data'][0][1]
last_counts: List[int] = []
2018-04-03 10:56:07 +00:00
for date, *counts in history['data'][1:]:
if len(counts) > 0:
# Update the last_counts variable if an update is available.
last_counts = counts
elif len(last_counts) > 0:
# If no counts are available, use those of the previous day.
counts = last_counts
else:
# If there are no last_counts either, just skip the day
continue
2018-04-03 10:56:07 +00:00
progress = ''
2018-07-22 09:45:15 +00:00
if show_accepted:
2018-04-03 10:56:07 +00:00
progress += '\033[92m' + 'A' * round(counts[0] - accepted_points)
progress += '\033[38;5;208m' + 'D' * round(counts[1])
progress += '\033[94m' + 'F' * round(counts[2])
progress += '\033[38;5;226m' + 'S' * round(counts[3])
progress += '\033[90m' + 'P' * round(counts[5])
progress += '\033[0m'
print(f'{date}: {progress}')
print()
2018-07-21 12:36:02 +00:00
def _stories_current(project: str, scope: str, show_accepted: bool) -> None:
2018-04-03 10:56:07 +00:00
try:
2018-07-21 12:36:02 +00:00
project_id = int(Config['project_aliases'][project])
2018-04-03 10:56:07 +00:00
except KeyError:
2018-07-22 09:45:15 +00:00
project_id = base32.decode(project)
2018-04-03 10:56:07 +00:00
token = Config['user']['api_token']
iterations = api.projects.get_iterations(
2018-07-21 12:36:02 +00:00
token, project_id, scope=scope)
2018-04-03 10:56:07 +00:00
if not iterations:
print('No current iteration.')
return
2018-07-21 12:36:02 +00:00
iteration = iterations[0]
2018-04-03 10:56:07 +00:00
persons = _get_persons(token, project_id=project_id)
totals: DefaultDict[int, Dict[str, int]] = \
defaultdict(lambda: dict((state, 0) for state in STATES))
2018-07-22 09:45:15 +00:00
__print_stories(iteration['stories'], persons, totals, show_accepted)
__print_totals(totals, persons, show_accepted)
__print_burndown(token, iteration, show_accepted)
2018-04-03 10:56:07 +00:00
2018-07-21 12:36:02 +00:00
def _set_story_state(story: str, state: str) -> None:
token = Config['user']['api_token']
2018-07-22 09:45:15 +00:00
story_id = base32.decode(story)
2018-07-21 12:36:02 +00:00
api.stories.put_story(token, story_id, current_state=state)
@cli.command('stories')
@click.argument('project')
@click.argument('story', required=False)
@click.option('--scope', default='current')
@click.option('--show-accepted/--hide-accepted', default=True)
@click.option('--set-state', type=click.Choice([
'started', 'finished', 'delivered', 'rejected', 'accepted']))
2018-04-03 10:56:07 +00:00
@require_login
2018-07-21 12:36:02 +00:00
def stories(project: str, story: Optional[str], scope: str,
show_accepted: bool, set_state: str) -> None:
if story is not None:
if set_state is not None:
_set_story_state(story, set_state)
else:
2018-07-22 09:45:15 +00:00
stories_info(story)
2018-04-03 10:56:07 +00:00
else:
2018-07-21 12:36:02 +00:00
_stories_current(project, scope, show_accepted)