tps_reporter 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/HISTORY.md +4 -0
- data/README.md +86 -0
- data/Rakefile +14 -0
- data/bin/tps +145 -0
- data/data/index.haml +182 -0
- data/data/sample.yml +18 -0
- data/lib/tps/cli_reporter.rb +76 -0
- data/lib/tps/task.rb +166 -0
- data/lib/tps/task_list.rb +17 -0
- data/lib/tps/version.rb +5 -0
- data/lib/tps.rb +34 -0
- data/test/hello.yml +42 -0
- data/test/test_helper.rb +13 -0
- data/test/tps_test.rb +71 -0
- data/tps_reporter.gemspec +16 -0
- metadata +83 -0
data/HISTORY.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# Task progress sheet reporter
|
2
|
+
|
3
|
+

|
4
|
+
|
5
|
+
We often need to make regular reports of things done for our projects at work. I
|
6
|
+
hate doing these by hand. This tool lets us build these reports from YAML files.
|
7
|
+
|
8
|
+
Get started
|
9
|
+
-----------
|
10
|
+
|
11
|
+
Install TPS (Ruby):
|
12
|
+
|
13
|
+
$ gem install tps_reporter
|
14
|
+
|
15
|
+
...then generate a sample file. (or create `tasks.yml` based on [this][s]
|
16
|
+
sample.)
|
17
|
+
|
18
|
+
$ tps sample
|
19
|
+
|
20
|
+
Edit it, then generate the report:
|
21
|
+
|
22
|
+
$ tps open
|
23
|
+
|
24
|
+
[s]: https://github.com/rstacruz/tps_reporter/blob/master/data/sample.yml
|
25
|
+
|
26
|
+
Format
|
27
|
+
------
|
28
|
+
|
29
|
+
The tasks file, usually `tasks.yml`, is in YAML format.
|
30
|
+
|
31
|
+
Tasks are always keys (ie, they all end in `:`). They can be nested as far
|
32
|
+
as you like.
|
33
|
+
|
34
|
+
``` yaml
|
35
|
+
Edit users:
|
36
|
+
Register and signup:
|
37
|
+
Login and logout:
|
38
|
+
```
|
39
|
+
|
40
|
+
To define task metadata for *leaf* tasks, add it as an array inside the task:
|
41
|
+
|
42
|
+
``` yaml
|
43
|
+
Manage employees: [done]
|
44
|
+
```
|
45
|
+
|
46
|
+
Or for *branch* tasks, add it under the `_` task:
|
47
|
+
|
48
|
+
``` yaml
|
49
|
+
Manage employees:
|
50
|
+
_: [done]
|
51
|
+
Creating employees:
|
52
|
+
Editing employees:
|
53
|
+
```
|
54
|
+
|
55
|
+
The metadata is just a simple YAML array that you can conveniently define using
|
56
|
+
`[tag1, tag2, etc]`. Allowed metadata are:
|
57
|
+
|
58
|
+
- `done`
|
59
|
+
- `in progress`
|
60
|
+
- `pt/2839478` *(Pivotal tracker ID. Links to a Pivotal tracker story.)*
|
61
|
+
- `0pt` *(points; influences percentage. needs to end in __pt__ or __pts__.)*
|
62
|
+
- `10%` *(task progress. implies __in progress__.)*
|
63
|
+
|
64
|
+
Example:
|
65
|
+
|
66
|
+
``` yaml
|
67
|
+
Creating employees: [40%]
|
68
|
+
Editing employees: [done, 2pts]
|
69
|
+
```
|
70
|
+
|
71
|
+
Exporting to PDF or image
|
72
|
+
-------------------------
|
73
|
+
|
74
|
+
If you're on a Mac, install [Paparazzi](http://derailer.org/paparazzi)
|
75
|
+
and use the `tps paparazzi` command. This will open the report in Paparazzi
|
76
|
+
where you can save or copy it as an image, or PDF.
|
77
|
+
|
78
|
+
Command line
|
79
|
+
------------
|
80
|
+
|
81
|
+
There's also a command line reporter that you can access via `tps print`. It
|
82
|
+
looks like this:
|
83
|
+
|
84
|
+
![Comamnd line reporter][cli]
|
85
|
+
|
86
|
+
[cli]: https://img.skitch.com/20120204-ccb2guerhrjmj3rht3e4ies4ur.png
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
desc "Invokes the test suite in multiple RVM environments"
|
2
|
+
task :'test!' do
|
3
|
+
# Override this by adding RVM_TEST_ENVS=".." in .rvmrc
|
4
|
+
envs = ENV['RVM_TEST_ENVS'] || '1.9.2@sinatra,1.8.7@sinatra'
|
5
|
+
puts "* Testing in the following RVM environments: #{envs.gsub(',', ', ')}"
|
6
|
+
system "rvm #{envs} rake test" or abort
|
7
|
+
end
|
8
|
+
|
9
|
+
desc "Runs tests"
|
10
|
+
task :test do
|
11
|
+
Dir['test/*_test.rb'].each { |f| load f }
|
12
|
+
end
|
13
|
+
|
14
|
+
task :default => :test
|
data/bin/tps
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:.unshift File.expand_path('../../lib', __FILE__)
|
3
|
+
require 'tps'
|
4
|
+
|
5
|
+
module Params
|
6
|
+
def extract(what) i = index(what) and slice!(i, 2)[1] end;
|
7
|
+
def first_is(what) shift if first == what; end
|
8
|
+
end
|
9
|
+
|
10
|
+
ARGV.extend Params
|
11
|
+
|
12
|
+
module TPS::Command
|
13
|
+
extend self
|
14
|
+
|
15
|
+
def help
|
16
|
+
puts "Usage: tps <command> [-f filename] [-o output]"
|
17
|
+
puts ""
|
18
|
+
puts "Commands:"
|
19
|
+
puts " html Builds HTML"
|
20
|
+
puts " open Builds HTML and opens it in the browser"
|
21
|
+
puts " paparazzi Builds HTML and opens it in Paparazzi (Mac)"
|
22
|
+
puts " print Prints the report to the console."
|
23
|
+
puts ""
|
24
|
+
puts "Options (optional):"
|
25
|
+
puts " -f FILE Specifies the input file. Defaults to tasks.yml."
|
26
|
+
puts " -o/--output OUTPUT Specifies the output HTML file."
|
27
|
+
puts ""
|
28
|
+
end
|
29
|
+
|
30
|
+
def html
|
31
|
+
t = get_tasks
|
32
|
+
path = output { |file| file.write t.to_html }
|
33
|
+
info "Wrote to '#{path}'."
|
34
|
+
|
35
|
+
path
|
36
|
+
end
|
37
|
+
|
38
|
+
def sample
|
39
|
+
require 'fileutils'
|
40
|
+
fn = tasks_filename
|
41
|
+
|
42
|
+
if File.exists?(fn)
|
43
|
+
err "Error: #{fn} already exists."
|
44
|
+
exit 130
|
45
|
+
end
|
46
|
+
|
47
|
+
FileUtils.cp TPS.root('data', 'sample.yml'), fn
|
48
|
+
info "Created '#{fn}'."
|
49
|
+
info "Edit it, then use `tps html` to generate HTML from it."
|
50
|
+
end
|
51
|
+
|
52
|
+
def open
|
53
|
+
fn = html
|
54
|
+
open_file fn
|
55
|
+
end
|
56
|
+
|
57
|
+
def paparazzi
|
58
|
+
fn = html
|
59
|
+
open_file "paparazzi:(minwidth=1,minheight=1)#{fn}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def print
|
63
|
+
t = get_tasks
|
64
|
+
reporter = TPS::CliReporter.new(t)
|
65
|
+
reporter.print
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
def err(str)
|
70
|
+
$stdout << "#{str}\n"
|
71
|
+
end
|
72
|
+
|
73
|
+
def info(str)
|
74
|
+
puts str
|
75
|
+
end
|
76
|
+
|
77
|
+
def tasks_filename
|
78
|
+
ARGV.extract('-f') ||
|
79
|
+
ENV['TPS_FILE'] ||
|
80
|
+
Dir['./{Tasksfile,tasks.yml}'].first ||
|
81
|
+
"tasks.yml"
|
82
|
+
end
|
83
|
+
|
84
|
+
def output(&blk)
|
85
|
+
fn = ARGV.extract('--output') || ARGV.extract('-o') || get_temp_filename
|
86
|
+
|
87
|
+
File.open(fn, 'w', &blk)
|
88
|
+
fn
|
89
|
+
end
|
90
|
+
|
91
|
+
def get_tasks
|
92
|
+
fn = tasks_filename
|
93
|
+
if !File.exists?(fn)
|
94
|
+
err "No tasks file found."
|
95
|
+
err "Create a sample using `tsp sample`."
|
96
|
+
exit 256
|
97
|
+
end
|
98
|
+
|
99
|
+
begin
|
100
|
+
TPS::TaskList.new yaml: fn
|
101
|
+
rescue => e
|
102
|
+
err "Parse error: #{e.message}"
|
103
|
+
exit 256
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def opener
|
108
|
+
program = %w[open xdg-open start].detect { |cmd| `which #{cmd}` }
|
109
|
+
unless program
|
110
|
+
err "No opener found."
|
111
|
+
exit 256
|
112
|
+
end
|
113
|
+
|
114
|
+
program
|
115
|
+
end
|
116
|
+
|
117
|
+
def open_file(file)
|
118
|
+
require 'shellwords'
|
119
|
+
system "#{opener} #{file.shellescape}"
|
120
|
+
end
|
121
|
+
|
122
|
+
def get_temp_filename
|
123
|
+
require 'tmpdir'
|
124
|
+
File.join Dir.tmpdir, "tasks-#{'%x' % [rand * 2**48]}.html"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
if ARGV.first_is('html')
|
129
|
+
TPS::Command.html
|
130
|
+
|
131
|
+
elsif ARGV.first_is('open')
|
132
|
+
TPS::Command.open
|
133
|
+
|
134
|
+
elsif ARGV.first_is('sample')
|
135
|
+
TPS::Command.sample
|
136
|
+
|
137
|
+
elsif ARGV.first_is('print')
|
138
|
+
TPS::Command.print
|
139
|
+
|
140
|
+
elsif ARGV.first_is('paparazzi')
|
141
|
+
TPS::Command.paparazzi
|
142
|
+
|
143
|
+
else
|
144
|
+
TPS::Command.help
|
145
|
+
end
|
data/data/index.haml
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
!!! 5
|
2
|
+
%html
|
3
|
+
%head
|
4
|
+
%script{src: "http://cdnjs.cloudflare.com/ajax/libs/jquery/1.7/jquery.min.js"}
|
5
|
+
|
6
|
+
:javascript
|
7
|
+
$("td").live('click', function() {
|
8
|
+
$(this).closest('tr').toggleClass('highlight');
|
9
|
+
});
|
10
|
+
|
11
|
+
%style
|
12
|
+
:plain
|
13
|
+
body {
|
14
|
+
padding: 0;
|
15
|
+
margin: 0; }
|
16
|
+
|
17
|
+
body, td {
|
18
|
+
font-family: pt sans, sans-serif;
|
19
|
+
color: #333;
|
20
|
+
line-height: 13.5pt;
|
21
|
+
font-size: 9pt; }
|
22
|
+
|
23
|
+
table {
|
24
|
+
border: solid 2px #aaa;
|
25
|
+
padding: 2px;
|
26
|
+
|
27
|
+
background: #fafafa;
|
28
|
+
margin: 0;
|
29
|
+
width: 600px;
|
30
|
+
border-collapse: collapse; }
|
31
|
+
|
32
|
+
table td, table th {
|
33
|
+
padding: 5px 10px;
|
34
|
+
border-top: solid 1px #eee; }
|
35
|
+
|
36
|
+
/* Columns */
|
37
|
+
tr>.task { width: 50%; text-align: left; }
|
38
|
+
tr>.points { width: 9%; }
|
39
|
+
tr>.progress { width: 18%; }
|
40
|
+
tr>.owner { display: none; }
|
41
|
+
|
42
|
+
/* Indentation */
|
43
|
+
.level-1 .task { padding-left: 25px; }
|
44
|
+
.level-2 .task { padding-left: 50px; }
|
45
|
+
.level-3 .task { padding-left: 75px; }
|
46
|
+
.level-4 .task { padding-left: 100px; }
|
47
|
+
|
48
|
+
/* Overrides for parents */
|
49
|
+
.milestone td.task,
|
50
|
+
.feature td.task {
|
51
|
+
font-weight: bold;
|
52
|
+
font-size: 1.1em; }
|
53
|
+
|
54
|
+
tr.milestone td,
|
55
|
+
tr.feature td {
|
56
|
+
border-top: solid 1px #888; }
|
57
|
+
|
58
|
+
tr .progress .bar {
|
59
|
+
display: none; }
|
60
|
+
|
61
|
+
tr.milestone .progress .bar,
|
62
|
+
tr.feature .progress .bar {
|
63
|
+
display: block; }
|
64
|
+
|
65
|
+
tr td.points>* {
|
66
|
+
display: none; }
|
67
|
+
|
68
|
+
tr.milestone td.points>*,
|
69
|
+
tr.feature td.points>* {
|
70
|
+
display: inline; }
|
71
|
+
|
72
|
+
/* Header */
|
73
|
+
thead {
|
74
|
+
display: none; }
|
75
|
+
|
76
|
+
th {
|
77
|
+
color: #888; }
|
78
|
+
|
79
|
+
/* Zebra */
|
80
|
+
tr td,
|
81
|
+
tr {
|
82
|
+
background: #ffffff; }
|
83
|
+
|
84
|
+
tr:nth-child(odd) td,
|
85
|
+
tr:nth-child(odd) {
|
86
|
+
background: #fafafa; }
|
87
|
+
|
88
|
+
tr.highlight,
|
89
|
+
tr.highlight td {
|
90
|
+
background: #fafae0; }
|
91
|
+
|
92
|
+
/* Status box */
|
93
|
+
span.status {
|
94
|
+
display: inline-block;
|
95
|
+
width: 12px;
|
96
|
+
height: 12px;
|
97
|
+
margin: 0 5px 0 0;
|
98
|
+
|
99
|
+
border-radius: 2px;
|
100
|
+
|
101
|
+
position: relative;
|
102
|
+
top: 2px;
|
103
|
+
background: #ddd; }
|
104
|
+
|
105
|
+
span.status.in_progress {
|
106
|
+
background: #ea3; }
|
107
|
+
|
108
|
+
span.status.done {
|
109
|
+
background: #393; }
|
110
|
+
|
111
|
+
/* Progress */
|
112
|
+
.progress .number {
|
113
|
+
display: none; }
|
114
|
+
|
115
|
+
.progress .bar {
|
116
|
+
height: 10px;
|
117
|
+
border-radius: 5px;
|
118
|
+
background: #ddd; }
|
119
|
+
|
120
|
+
.progress .bar span {
|
121
|
+
display: block;
|
122
|
+
height: 10px;
|
123
|
+
border-radius: 5px;
|
124
|
+
background: #888; }
|
125
|
+
|
126
|
+
a.meta {
|
127
|
+
font-size: 0.9em;
|
128
|
+
text-decoration: none;
|
129
|
+
|
130
|
+
background: #ddd;
|
131
|
+
padding: 1px 3px;
|
132
|
+
border-radius: 2px;
|
133
|
+
border-bottom: solid 1px #ccc;
|
134
|
+
|
135
|
+
margin: 0 5px;
|
136
|
+
color: #777; }
|
137
|
+
|
138
|
+
/* Progress */
|
139
|
+
td.points {
|
140
|
+
text-align: left;
|
141
|
+
font-size: 0.9em; }
|
142
|
+
|
143
|
+
td.points .points.done {
|
144
|
+
font-weight: bold; }
|
145
|
+
|
146
|
+
td.points .of,
|
147
|
+
td.points .points.total {
|
148
|
+
color: #888; }
|
149
|
+
|
150
|
+
%body
|
151
|
+
|
152
|
+
%table
|
153
|
+
%thead
|
154
|
+
%tr
|
155
|
+
%th.task Task
|
156
|
+
%th.owner Owner
|
157
|
+
%th.progress Progress
|
158
|
+
%th.points Points
|
159
|
+
|
160
|
+
- list.walk do |task, recurse|
|
161
|
+
|
162
|
+
%tr{class: "level-#{task.level} #{task.tasks? ? 'parent' : 'leaf'} #{'root' if task.root?} #{'feature' if task.feature?} #{'milestone' if task.milestone?}"}
|
163
|
+
%td.task
|
164
|
+
%span.status{class: "#{task.status}"}
|
165
|
+
= task
|
166
|
+
- if task.pivotal_id
|
167
|
+
%a.meta{href: task.pivotal_url}= "Pivotal: #{task.pivotal_id}"
|
168
|
+
|
169
|
+
%td.owner
|
170
|
+
= task.owner
|
171
|
+
|
172
|
+
%td.progress
|
173
|
+
.bar
|
174
|
+
%span{style: "width: #{(task.percent*95+5).to_i}%"}
|
175
|
+
%span.number= "#{(task.percent*100).to_i}%"
|
176
|
+
|
177
|
+
%td.points
|
178
|
+
%span.points.done= task.points_done.round(1).to_s.gsub(/\.0+$/,'')
|
179
|
+
%span.of of
|
180
|
+
%span.points.total= task.points.round(1).to_s.gsub(/\.0+$/,'')
|
181
|
+
|
182
|
+
- recurse.call if recurse
|
data/data/sample.yml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Version 1:
|
2
|
+
|
3
|
+
User signup:
|
4
|
+
Register for an account:
|
5
|
+
Log in: [done]
|
6
|
+
Forget password:
|
7
|
+
|
8
|
+
Manage users:
|
9
|
+
_: [in progress]
|
10
|
+
Create users: [in progress]
|
11
|
+
Delete users:
|
12
|
+
User profile page:
|
13
|
+
|
14
|
+
Blog:
|
15
|
+
Creating new posts: [done]
|
16
|
+
Comments: [done]
|
17
|
+
Moderating comments: [done]
|
18
|
+
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module TPS
|
2
|
+
class CliReporter
|
3
|
+
attr_reader :task
|
4
|
+
|
5
|
+
def initialize(task)
|
6
|
+
@task = task
|
7
|
+
end
|
8
|
+
|
9
|
+
def print
|
10
|
+
puts report
|
11
|
+
end
|
12
|
+
|
13
|
+
def report
|
14
|
+
re = ""
|
15
|
+
task.walk do |t, recurse|
|
16
|
+
re += CliReporter.new(t).report_task
|
17
|
+
recurse.call if recurse
|
18
|
+
end
|
19
|
+
re
|
20
|
+
end
|
21
|
+
|
22
|
+
def report_task
|
23
|
+
indent = ' ' * (4 * task.level)
|
24
|
+
|
25
|
+
# Columns
|
26
|
+
c1 = "%s %s %s" % [ indent, status, task.name ]
|
27
|
+
c2 = if task.feature? || task.milestone?
|
28
|
+
progress
|
29
|
+
else
|
30
|
+
' '*12
|
31
|
+
end
|
32
|
+
|
33
|
+
pref = c("-"*80, 30)+"\n" if task.feature?
|
34
|
+
|
35
|
+
# Put together
|
36
|
+
"#{pref}" + "%-95s%s\n" % [ c1, c2 ]
|
37
|
+
end
|
38
|
+
|
39
|
+
def color
|
40
|
+
if task.done?
|
41
|
+
32
|
42
|
+
elsif task.in_progress?
|
43
|
+
34
|
44
|
+
else
|
45
|
+
30
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def status
|
50
|
+
l = c("(", 30)
|
51
|
+
r = c(")", 30)
|
52
|
+
if task.done?
|
53
|
+
l + c('##', color) + r
|
54
|
+
elsif task.in_progress?
|
55
|
+
l + c('--', color) + r
|
56
|
+
else
|
57
|
+
l + c(' ', color) + r
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def progress
|
62
|
+
max = 12
|
63
|
+
len = (task.percent * max).to_i
|
64
|
+
|
65
|
+
prog = ("%-#{max}s" % ["="*len])
|
66
|
+
prog = c("|"*len, color) + c("."*(max-len), 30)
|
67
|
+
|
68
|
+
prog
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
def c(str, c=nil)
|
73
|
+
c ? "\033[#{c}m#{str}\033[0m" : str
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/lib/tps/task.rb
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'tilt'
|
3
|
+
|
4
|
+
module TPS
|
5
|
+
class Task
|
6
|
+
attr_reader :name # "Create metrics"
|
7
|
+
attr_reader :tasks # Array of Tasks
|
8
|
+
attr_reader :owner
|
9
|
+
attr_reader :pivotal_id
|
10
|
+
attr_reader :parent
|
11
|
+
|
12
|
+
def initialize(parent, name, data=nil)
|
13
|
+
@name = name
|
14
|
+
@tasks = Array.new
|
15
|
+
@parent = parent
|
16
|
+
|
17
|
+
if data.is_a?(Array)
|
18
|
+
tags = data
|
19
|
+
tasks = nil
|
20
|
+
elsif data.is_a?(Hash) && data['_']
|
21
|
+
tags = data.delete('_')
|
22
|
+
tasks = data
|
23
|
+
else
|
24
|
+
tags = Array.new
|
25
|
+
tasks = data
|
26
|
+
end
|
27
|
+
|
28
|
+
# Parse tags.
|
29
|
+
tags.each do |t|
|
30
|
+
# [done]
|
31
|
+
if ['done', 'ok'].include?(t)
|
32
|
+
@status = :done
|
33
|
+
elsif ['in progress', '...'].include?(t)
|
34
|
+
@status = :in_progress
|
35
|
+
# [@rstacruz]
|
36
|
+
elsif t =~ /^@/
|
37
|
+
@owner = t[1..-1]
|
38
|
+
# [pt/28394] -- Pivotal tracker
|
39
|
+
elsif t =~ /^pt\/(.*)$/i
|
40
|
+
@pivotal_id = $1.strip
|
41
|
+
# [50%] -- percentage
|
42
|
+
elsif t =~ /^([\d\.]+)%$/
|
43
|
+
@status = :in_progress
|
44
|
+
@percent = $1.strip.to_f / 100
|
45
|
+
# [0pt] -- points
|
46
|
+
elsif t =~ /^([\d\.]+)pts?/i
|
47
|
+
@points = $1.strip.to_f
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
@tasks = tasks.map { |task, data| Task.new self, task, data } if tasks
|
52
|
+
|
53
|
+
n = @name.to_s.downcase
|
54
|
+
@milestone = root? && (n.include?('milestone') || n.include?('version'))
|
55
|
+
end
|
56
|
+
|
57
|
+
def status
|
58
|
+
# If no status is given, infer the status based on tasks.
|
59
|
+
if !@status && tasks?
|
60
|
+
if all_tasks_done?
|
61
|
+
return :done
|
62
|
+
elsif has_started_tasks?
|
63
|
+
return :in_progress
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
@status or :unstarted
|
68
|
+
end
|
69
|
+
|
70
|
+
def points
|
71
|
+
if @points
|
72
|
+
@points
|
73
|
+
elsif tasks?
|
74
|
+
tasks.inject(0.0) { |pts, task| pts + task.points }
|
75
|
+
else
|
76
|
+
1.0
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def points_done
|
81
|
+
points * percent
|
82
|
+
end
|
83
|
+
|
84
|
+
def all_tasks_done?
|
85
|
+
tasks? and !tasks.any? { |t| ! t.done? }
|
86
|
+
end
|
87
|
+
|
88
|
+
def has_started_tasks?
|
89
|
+
tasks? and tasks.any? { |t| t.in_progress? or t.done? }
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_s
|
93
|
+
name
|
94
|
+
end
|
95
|
+
|
96
|
+
def done?
|
97
|
+
status == :done
|
98
|
+
end
|
99
|
+
|
100
|
+
def in_progress?
|
101
|
+
status == :in_progress
|
102
|
+
end
|
103
|
+
|
104
|
+
def unstarted?
|
105
|
+
status == :unstarted
|
106
|
+
end
|
107
|
+
|
108
|
+
def tasks?
|
109
|
+
tasks.any?
|
110
|
+
end
|
111
|
+
|
112
|
+
def pivotal_url
|
113
|
+
"https://www.pivotaltracker.com/story/show/#{pivotal_id}" if pivotal_id
|
114
|
+
end
|
115
|
+
|
116
|
+
def percent
|
117
|
+
if done?
|
118
|
+
1.0
|
119
|
+
elsif @percent
|
120
|
+
@percent
|
121
|
+
elsif tasks?
|
122
|
+
total = tasks.inject(0.0) { |pts, task| pts + task.points }
|
123
|
+
tasks.inject(0) { |i, task| i + task.points_done } / total
|
124
|
+
else
|
125
|
+
in_progress? ? 0.5 : 0
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def level
|
130
|
+
parent ? parent.level + 1 : 0
|
131
|
+
end
|
132
|
+
|
133
|
+
def root?
|
134
|
+
! parent
|
135
|
+
end
|
136
|
+
|
137
|
+
def feature?
|
138
|
+
root? or parent.milestone?
|
139
|
+
end
|
140
|
+
|
141
|
+
def milestone?
|
142
|
+
!! @milestone
|
143
|
+
end
|
144
|
+
|
145
|
+
# - list.walk do |task, recurse|
|
146
|
+
# %ul
|
147
|
+
# %li
|
148
|
+
# = task
|
149
|
+
# - recurse.call if recurse
|
150
|
+
def walk(&blk)
|
151
|
+
tasks.each do |task|
|
152
|
+
yield task, lambda {
|
153
|
+
task.walk { |t, recurse| blk.call t, recurse }
|
154
|
+
}
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def to_html(template=nil)
|
159
|
+
require 'tilt'
|
160
|
+
template ||= TPS.root('data', 'index.haml')
|
161
|
+
|
162
|
+
tpl = Tilt.new(template)
|
163
|
+
tpl.evaluate({}, list: self)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module TPS
|
2
|
+
class TaskList < Task
|
3
|
+
def initialize(options)
|
4
|
+
super nil, nil
|
5
|
+
|
6
|
+
data = if options[:yaml]
|
7
|
+
YAML::load_file options[:yaml]
|
8
|
+
elsif options[:data]
|
9
|
+
options[:data]
|
10
|
+
else
|
11
|
+
options
|
12
|
+
end
|
13
|
+
|
14
|
+
@tasks = data.map { |task, data| Task.new nil, task, data }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/tps/version.rb
ADDED
data/lib/tps.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'tilt'
|
3
|
+
|
4
|
+
# Common usage:
|
5
|
+
#
|
6
|
+
# list = TPS::TaskList.new data: yaml_data
|
7
|
+
# list = TPS::TaskList.new yaml: filename
|
8
|
+
#
|
9
|
+
# # Returns an array of tasks.
|
10
|
+
# task.tasks
|
11
|
+
#
|
12
|
+
# # Metadata:
|
13
|
+
# task.name
|
14
|
+
# task.owner
|
15
|
+
# task.status
|
16
|
+
#
|
17
|
+
# task.done?
|
18
|
+
# task.in_progress?
|
19
|
+
#
|
20
|
+
# list.to_html
|
21
|
+
#
|
22
|
+
module TPS
|
23
|
+
ROOT = File.expand_path('../../', __FILE__)
|
24
|
+
|
25
|
+
autoload :Task, 'tps/task'
|
26
|
+
autoload :TaskList, 'tps/task_list'
|
27
|
+
autoload :CliReporter, 'tps/cli_reporter'
|
28
|
+
|
29
|
+
require 'tps/version'
|
30
|
+
|
31
|
+
def self.root(*a)
|
32
|
+
File.join ROOT, *a
|
33
|
+
end
|
34
|
+
end
|
data/test/hello.yml
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
Milestone 1:
|
2
|
+
First task: [25%] #0
|
3
|
+
|
4
|
+
User login: #1
|
5
|
+
Login:
|
6
|
+
Signup:
|
7
|
+
|
8
|
+
Overridden percent: #2
|
9
|
+
_: [50%]
|
10
|
+
one: [done]
|
11
|
+
two: [done]
|
12
|
+
three: [done]
|
13
|
+
|
14
|
+
Explicit points: #3
|
15
|
+
_: [15pt]
|
16
|
+
one: [done]
|
17
|
+
two: [done]
|
18
|
+
three: [done]
|
19
|
+
four:
|
20
|
+
|
21
|
+
Compound points: #4
|
22
|
+
one:
|
23
|
+
two:
|
24
|
+
three:
|
25
|
+
sub1: [3pts, done]
|
26
|
+
sub2:
|
27
|
+
|
28
|
+
Point rescaling: #5
|
29
|
+
# Has 6 sub points, but will be rescaled to 8.
|
30
|
+
# That is, the done "3" point task is actually worth 4 points.
|
31
|
+
one:
|
32
|
+
_: [8pts]
|
33
|
+
sub1: [3pts, done]
|
34
|
+
sub2: [1pt]
|
35
|
+
sub3: [1pt]
|
36
|
+
sub4: [1pt]
|
37
|
+
|
38
|
+
In progress: #6
|
39
|
+
one:
|
40
|
+
two: [in progress]
|
41
|
+
|
42
|
+
Milestone 2:
|
data/test/test_helper.rb
ADDED
data/test/tps_test.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
class MyTest < UnitTest
|
4
|
+
setup do
|
5
|
+
@list = TPS::TaskList.new yaml: f('hello.yml')
|
6
|
+
@milestone = @list.tasks.first
|
7
|
+
end
|
8
|
+
|
9
|
+
test "Has tasks" do
|
10
|
+
assert @list.tasks?
|
11
|
+
assert @list.tasks.size == 2
|
12
|
+
assert @list.tasks.first.tasks.size >= 2
|
13
|
+
end
|
14
|
+
|
15
|
+
test "Explicit percent" do
|
16
|
+
task = @milestone.tasks[0]
|
17
|
+
assert task.in_progress?
|
18
|
+
assert task.status == :in_progress
|
19
|
+
assert task.percent == 0.25
|
20
|
+
end
|
21
|
+
|
22
|
+
test "Overriding percent" do
|
23
|
+
task = @milestone.tasks[2]
|
24
|
+
assert task.name == "Overridden percent"
|
25
|
+
assert task.in_progress?
|
26
|
+
assert task.status == :in_progress
|
27
|
+
assert task.percent == 0.5
|
28
|
+
end
|
29
|
+
|
30
|
+
test "Points" do
|
31
|
+
task = @milestone.tasks[1]
|
32
|
+
assert task.points == 2.0
|
33
|
+
end
|
34
|
+
|
35
|
+
test "Explicit points" do
|
36
|
+
task = @milestone.tasks[3]
|
37
|
+
assert task.points == 15
|
38
|
+
assert task.percent == 0.75
|
39
|
+
assert task.points_done == 11.25
|
40
|
+
end
|
41
|
+
|
42
|
+
test "Compound points" do
|
43
|
+
task = @milestone.tasks[4]
|
44
|
+
assert task.points == 6
|
45
|
+
assert task.percent == 0.50
|
46
|
+
end
|
47
|
+
|
48
|
+
test "Point rescaling" do
|
49
|
+
task = @milestone.tasks[5]
|
50
|
+
assert task.points == 8
|
51
|
+
assert task.points_done == 4.0
|
52
|
+
assert task.percent == 0.5
|
53
|
+
end
|
54
|
+
|
55
|
+
test "In progress" do
|
56
|
+
task = @milestone.tasks[6]
|
57
|
+
assert_equal 2, task.tasks.size
|
58
|
+
assert task.tasks[1].in_progress?
|
59
|
+
assert_equal 0.5, task.tasks[1].percent
|
60
|
+
assert_equal 0.5, task.tasks[1].points_done
|
61
|
+
assert_equal 0.25, task.percent
|
62
|
+
end
|
63
|
+
|
64
|
+
test "Milestone" do
|
65
|
+
assert @milestone.milestone?
|
66
|
+
end
|
67
|
+
|
68
|
+
test "HTML works" do
|
69
|
+
assert @list.to_html
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require './lib/tps/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "tps_reporter"
|
5
|
+
s.version = TPS.version
|
6
|
+
s.summary = %{Task progress sheet reporter.}
|
7
|
+
s.description = %Q{A YAML-powered, simple command-line task report builder.}
|
8
|
+
s.authors = ["Rico Sta. Cruz"]
|
9
|
+
s.email = ["rico@sinefunc.com"]
|
10
|
+
s.homepage = "http://github.com/rstacruz/tps_reporter"
|
11
|
+
s.files = `git ls-files`.strip.split("\n")
|
12
|
+
s.executables = Dir["bin/*"].map { |f| File.basename(f) }
|
13
|
+
|
14
|
+
s.add_dependency "tilt"
|
15
|
+
s.add_development_dependency "contest"
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tps_reporter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Rico Sta. Cruz
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-02-04 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: tilt
|
16
|
+
requirement: &2152658640 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2152658640
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: contest
|
27
|
+
requirement: &2152658200 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2152658200
|
36
|
+
description: A YAML-powered, simple command-line task report builder.
|
37
|
+
email:
|
38
|
+
- rico@sinefunc.com
|
39
|
+
executables:
|
40
|
+
- tps
|
41
|
+
extensions: []
|
42
|
+
extra_rdoc_files: []
|
43
|
+
files:
|
44
|
+
- HISTORY.md
|
45
|
+
- README.md
|
46
|
+
- Rakefile
|
47
|
+
- bin/tps
|
48
|
+
- data/index.haml
|
49
|
+
- data/sample.yml
|
50
|
+
- lib/tps.rb
|
51
|
+
- lib/tps/cli_reporter.rb
|
52
|
+
- lib/tps/task.rb
|
53
|
+
- lib/tps/task_list.rb
|
54
|
+
- lib/tps/version.rb
|
55
|
+
- test/hello.yml
|
56
|
+
- test/test_helper.rb
|
57
|
+
- test/tps_test.rb
|
58
|
+
- tps_reporter.gemspec
|
59
|
+
homepage: http://github.com/rstacruz/tps_reporter
|
60
|
+
licenses: []
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options: []
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ! '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
requirements: []
|
78
|
+
rubyforge_project:
|
79
|
+
rubygems_version: 1.8.10
|
80
|
+
signing_key:
|
81
|
+
specification_version: 3
|
82
|
+
summary: Task progress sheet reporter.
|
83
|
+
test_files: []
|