Skip to content

Instantly share code, notes, and snippets.

@ricejasonf
Created September 13, 2016 22:41
Show Gist options
  • Save ricejasonf/f94c6577934767df0a4d7bc0a8f54d10 to your computer and use it in GitHub Desktop.
Save ricejasonf/f94c6577934767df0a4d7bc0a8f54d10 to your computer and use it in GitHub Desktop.
Generate Markdown for Weekly Plan from PivotalTracker
{
"name": "pivotal-tools",
"version": "1.0.0",
"description": "",
"main": "weekly_plan.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Jason Rice",
"license": "ISC",
"dependencies": {
"bluebird": "^3.4.6",
"ramda": "^0.22.1"
}
}
const Bluebird = require('bluebird');
const process = require('process');
const https = require('https');
const R = require('ramda');
const util = require('util');
const api_token = process.env.PIVOTAL_API_TOKEN;
const project_id = process.env.PIVOTAL_API_PROJECT_ID;
const epic_label = 'concepts-notifications'
if (!project_id) throw "PIVOTAL_API_PROJECT_ID not set.";
const inspect = function(obj)
{
console.log(util.inspect(obj, false, null));
}
const tap_inspect = R.tap(inspect);
const daily_point_quota = 5;
const day_labels = [
'Tuesday',
'Wednesday',
'Thursday',
'Friday'
];
const requestGet = function(path)
{
return new Bluebird(function(resolve, reject)
{
https.request(
{
host: 'www.pivotaltracker.com',
//port: '443',
path: '/services/v5' + path,
method: 'GET',
headers: {
'X-TrackerToken': api_token
}
},
function(res)
{
let response_data = '';
res.on('error', (err) => reject(err));
if (res.statusCode > 500) return reject(res.statusMessage);
let finalize = res.statusCode == 200 ? resolve : reject;
res.setEncoding('utf8');
res.on('data', (chunk) => response_data += chunk);
res.on('end', () => resolve(JSON.parse(response_data)));
}).end();
});
}
Bluebird.all([
requestGet('/projects/' + project_id + '/stories'
+ '?date_format=millis'
+ '&fields=id%2Curl%2Cname%2Cowners%2Cestimate'
+ '&with_label=' + epic_label
+ '&with_state=started'
+ '&with_story_type=feature'
)
, requestGet('/projects/' + project_id + '/stories'
+ '?date_format=millis'
+ '&fields=id%2Curl%2Cname%2Cowners%2Cestimate'
+ '&with_label=' + epic_label
+ '&with_state=unstarted'
+ '&with_story_type=feature'
)
])
.spread((result1, result2) => renderWeeklyPlan(R.concat(result1, result2)))
.catch(fail)
;
/*
# This Week
## Tuesday
| Points | Assignee | Story |
|-------:|---------:|-------------------------------------------------------------------|
| 1 | Jason | [#129882877](https://www.pivotaltracker.com/story/show/129882877) |
| 1 | Jason | [#129882877](https://www.pivotaltracker.com/story/show/129882877) |
## Wednesday
## Thursday
## Friday
*/
function fail(error)
{
console.log(error);
process.exit(1);
}
function renderWeeklyPlan(result)
{
const daily_tasks = getDailyTasks(result);
const story_rows = R.map(R.map(renderStoryRow))(daily_tasks);
const renderDay_ = renderDay(daily_tasks);
const output = [
renderHeader()
, renderDay_(0)
, renderDay_(1)
, renderDay_(2)
, renderDay_(3)
];
process.stdout.write(R.join('')(output));
process.stdout.write('\n');
}
function renderHeader()
{
return '# Team Early Birds\n\n';
}
function renderDay(daily_tasks)
{
return function(i)
{
return R.compose(
R.join('')
, R.prepend(renderDayHeader(i))
, R.prepend(renderHeaderRow())
, R.map(renderStoryRow)
, R.nth(i)
)(daily_tasks)
};
}
function renderDayHeader(i)
{
return '\n\n## ' + day_labels[i] + '\n\n';
}
function renderHeaderRow()
{
return '| Points | Assignee | Id | Name |\n'
+ '|-------:|---------:|----|------|\n'
;
}
function renderStoryRow(story)
{
const components = [
story.estimate
, story.assignee
, `[#${story.id}](${story.url})`
, story.name
];
return '|' + components.join('|') + '|\n';
}
function getDailyTasks(stories)
{
const groups = groupStories(stories);
let daily_tasks = [];
for (let i = 0; i < day_labels.length; i++)
{
const tasks = R.compose(
R.unnest
, R.reject(R.isNil)
, R.map(R.nth(i))
)(groups);
if (R.isEmpty(tasks))
break;
daily_tasks.push(tasks);
}
return daily_tasks;
}
function selectStoriesForDay(groups)
{
return function(day)
{
return R.map(R.nth(day))(groups);
}
}
function groupStories(stories)
{
return R.compose(
R.map(R.dropLast(1))
, R.map(R.until(
R.compose(
lastIsEmpty
)
, splitAtQuota(daily_point_quota)
))
, R.map(R.of)
, groupByOwners
)(stories);
}
function groupByOwners(stories)
{
return R.compose(
R.map(R.last)
, R.toPairs
, R.groupBy(getOwnerInitials)
, R.map(setAssignee)
)(stories);
}
function splitAtQuota(quota)
{
return function(stories)
{
return R.compose(
R.concat(R.dropLast(1, stories))
, R.compose(
R.apply(R.splitAt)
, R.adjust(findIndexOfQuota(quota), 0)
, R.ap([mapSums, R.identity])
, R.of
)
, R.last
)(stories)
}
}
function findIndexOfQuota(quota)
{
return function(sums)
{
return R.compose(
R.clamp(0, 1000)
, R.ifElse(R.equals(-1), R.always(sums.length), R.identity)
, R.findIndex(R.lt(quota))
)(sums);
}
}
function mapSums(stories)
{
return R.compose(
R.addIndex(R.reduce)(
function(state, x, i, xs)
{
return R.append(R.sum(R.take(i+1, xs)), state);
},
[]
)
, R.map(R.compose(R.defaultTo(0), R.prop('estimate')))
)(stories);
}
function setAssignee(story)
{
return R.assoc('assignee', getOwnerInitials(story))(story);
}
function getOwnerInitials(story)
{
return R.compose(
R.propOr('ANON', 'initials')
, R.last
, R.prop('owners')
)(story);
}
function lastIsEmpty(list)
{
return R.compose(
R.isEmpty
, R.last
)(list);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment