tps_reporter 0.0.2 → 0.2.0

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/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ *.json
2
+ *.cson
data/GUIDE.md ADDED
@@ -0,0 +1,54 @@
1
+ Guide
2
+ =====
3
+
4
+ Advanced usage guide!
5
+
6
+ Sprint support
7
+ --------------
8
+
9
+ First, make a sprints block, preferrably at the top of your `tasks.yml` file.
10
+ List down the sprints.
11
+
12
+ Then simply add the sprint ID to your tasks.
13
+
14
+ # tasks.yml
15
+ Sprints:
16
+ s1: Sprint 1 (Nov 1-15)
17
+ s2: Sprint 2 (Nov 16-30)
18
+ s3: Sprint 3 (Dec 1-15)
19
+
20
+ Beta release:
21
+ Account:
22
+ Login: [s1]
23
+ Logout: [s1, done]
24
+ Signup: [s2]
25
+
26
+ It's also recursive--you can put sprints in your parent tasks under `_`:
27
+
28
+ Beta release:
29
+ Blog:
30
+ _: [s3]
31
+ Create posts:
32
+ Read posts:
33
+ Delete posts:
34
+
35
+ Trello card linking support
36
+ ---------------------------
37
+
38
+ Simply link the card shortcut under each item using `tr/XXXX`, where *XXXX* is
39
+ the short URL ID for the Trello card. You can see the short ID in Trello by
40
+ clicking "more..." inside the card popup.
41
+
42
+ Beta release:
43
+ Blog: [tr/Xh3pAGp1]
44
+ Account management:
45
+
46
+ You can also link the card numbers, but you have to define the main Trello board
47
+ URL. Simply add `Trello URL: ____` to the top of the file.
48
+
49
+
50
+ Trello URL: https://trello.com/board/trello-resources/4f84a60f0cbdcb7e7d40e099
51
+
52
+ Beta release:
53
+ Blog: [tr/42]
54
+ Account management: [tr/12, done]
data/HISTORY.md CHANGED
@@ -1,3 +1,27 @@
1
+ v0.2.0 - Nov 24, 2012
2
+ ---------------------
3
+
4
+ ### New features:
5
+
6
+ * Added support for sprints! See `GUIDE.md` for info.
7
+ * Trello card linking support! See `GUIDE.md` again.
8
+ * Redesigned HTML output.
9
+ * Added "Export mode" in the HTML output.
10
+
11
+ ### Internals:
12
+
13
+ * Implement @task['name'] lookup.
14
+ * Implement Task#id.
15
+ * Sprints: add a sprint model.
16
+ * Task: implement #filter, #filter_by, #contains_sprint?
17
+
18
+ v0.0.3 - Nov 21, 2012
19
+ ---------------------
20
+
21
+ * Added support for linking tasks to Trello cards.
22
+ * Fixed error when invoking 'tps' without arguments.
23
+ * Fixed typo in message when invoked without a tasks.yml file.
24
+
1
25
  v0.0.2 - Feb 04, 2012
2
26
  ---------------------
3
27
 
data/README.md CHANGED
@@ -3,7 +3,8 @@
3
3
  ![TPS report](https://img.skitch.com/20120203-nr24dn9u7euchmqa516718unpe.png)
4
4
 
5
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.
6
+ hate doing these by hand. This tool lets us build these reports from YAML files
7
+ [such as this][s].
7
8
 
8
9
  Get started
9
10
  -----------
@@ -12,8 +13,8 @@ Install TPS (Ruby):
12
13
 
13
14
  $ gem install tps_reporter
14
15
 
15
- ...then generate a sample file. (or create `tasks.yml` based on [this][s]
16
- sample.)
16
+ ...then generate a sample file. (or create `tasks.yml` based on [this sample
17
+ file.][s])
17
18
 
18
19
  $ tps sample
19
20
 
@@ -58,6 +59,7 @@ The metadata is just a simple YAML array that you can conveniently define using
58
59
  - `done`
59
60
  - `in progress`
60
61
  - `pt/2839478` *(Pivotal tracker ID. Links to a Pivotal tracker story.)*
62
+ - `tr/LabxGP3` *(Trello card short name. Links to a Trello card.)*
61
63
  - `0pt` *(points; influences percentage. needs to end in __pt__ or __pts__.)*
62
64
  - `10%` *(task progress. implies __in progress__.)*
63
65
 
data/bin/tps CHANGED
@@ -3,8 +3,17 @@ $:.unshift File.expand_path('../../lib', __FILE__)
3
3
  require 'tps'
4
4
 
5
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
6
+ def extract(what)
7
+ i = index(what) and slice!(i, 2)[1]
8
+ end
9
+
10
+ def first_is(what)
11
+ shift if first == what
12
+ end
13
+
14
+ def first_matches(what)
15
+ shift if first.downcase == what.downcase[0...first.size]
16
+ end
8
17
  end
9
18
 
10
19
  ARGV.extend Params
@@ -17,9 +26,12 @@ module TPS::Command
17
26
  puts ""
18
27
  puts "Commands:"
19
28
  puts " html Builds HTML"
29
+ puts " html --stdout Builds HTML in stdout"
20
30
  puts " open Builds HTML and opens it in the browser"
21
31
  puts " paparazzi Builds HTML and opens it in Paparazzi (Mac)"
22
32
  puts " print Prints the report to the console."
33
+ puts " import FILE FORMAT Import from a file of a given format."
34
+ puts " (Supported formats: trello)"
23
35
  puts ""
24
36
  puts "Options (optional):"
25
37
  puts " -f FILE Specifies the input file. Defaults to tasks.yml."
@@ -29,8 +41,14 @@ module TPS::Command
29
41
 
30
42
  def html
31
43
  t = get_tasks
32
- path = output { |file| file.write t.to_html }
33
- info "Wrote to '#{path}'."
44
+ stdout = ARGV.delete('--stdout')
45
+
46
+ if stdout
47
+ puts t.to_html
48
+ else
49
+ path = output { |file| file.write t.to_html }
50
+ info "Wrote to '#{path}'."
51
+ end
34
52
 
35
53
  path
36
54
  end
@@ -65,6 +83,13 @@ module TPS::Command
65
83
  reporter.print
66
84
  end
67
85
 
86
+ def import(file, format=nil)
87
+ require 'tps/importer'
88
+ import = TPS::Importer.create(file, format)
89
+ File.open('tasks.yml', 'w') { |f| f.write import.to_yaml }
90
+ puts "Wrote to tasks.yml."
91
+ end
92
+
68
93
  private
69
94
  def err(str)
70
95
  $stdout << "#{str}\n"
@@ -92,13 +117,13 @@ private
92
117
  fn = tasks_filename
93
118
  if !File.exists?(fn)
94
119
  err "No tasks file found."
95
- err "Create a sample using `tsp sample`."
120
+ err "Create a sample using `tps sample`."
96
121
  exit 256
97
122
  end
98
123
 
99
124
  begin
100
125
  TPS::TaskList.new yaml: fn
101
- rescue => e
126
+ rescue Psych::SyntaxError => e
102
127
  err "Parse error: #{e.message}"
103
128
  exit 256
104
129
  end
@@ -125,19 +150,25 @@ private
125
150
  end
126
151
  end
127
152
 
128
- if ARGV.first_is('html')
153
+ if ARGV.empty?
154
+ TPS::Command.help
155
+
156
+ elsif ARGV.first_matches('html')
129
157
  TPS::Command.html
130
158
 
131
- elsif ARGV.first_is('open')
159
+ elsif ARGV.first_matches('open')
132
160
  TPS::Command.open
133
161
 
134
- elsif ARGV.first_is('sample')
162
+ elsif ARGV.first_matches('sample')
135
163
  TPS::Command.sample
136
164
 
137
- elsif ARGV.first_is('print')
165
+ elsif ARGV.first_matches('print')
138
166
  TPS::Command.print
139
167
 
140
- elsif ARGV.first_is('paparazzi')
168
+ elsif ARGV.first_matches('import')
169
+ TPS::Command.import *ARGV
170
+
171
+ elsif ARGV.first_matches('paparazzi')
141
172
  TPS::Command.paparazzi
142
173
 
143
174
  else
data/data/index.haml CHANGED
@@ -4,13 +4,45 @@
4
4
  %script{src: "http://cdnjs.cloudflare.com/ajax/libs/jquery/1.7/jquery.min.js"}
5
5
 
6
6
  :javascript
7
- $("td").live('click', function() {
7
+ $("td").live('dblclick', function() {
8
8
  $(this).closest('tr').toggleClass('highlight');
9
9
  });
10
10
 
11
+ // Folding
12
+ $(".task .status").live('click', function(e) {
13
+ e.preventDefault();
14
+ e.stopPropagation();
15
+ var id = $(this).closest('tr').attr('id');
16
+ $(".in_"+id).toggle();
17
+ });
18
+
19
+ // Filter by sprints
20
+ $('.sprint-nav a').live('click', function(e) {
21
+ e.preventDefault();
22
+ e.stopPropagation();
23
+
24
+ var sprint = $(this).attr('data-sprint');
25
+
26
+ $(".tasks tr")
27
+ .hide()
28
+ .filter('.sprint-'+sprint+', .has_sprint-'+sprint).show();
29
+ });
30
+
31
+ $('[href="#all"]').live('click', function(e) {
32
+ e.preventDefault();
33
+ $('.tasks tr').show();
34
+ });
35
+
36
+ $('[href="#print"]').live('click', function(e) {
37
+ e.preventDefault();
38
+ $('body').addClass('print');
39
+ });
40
+
41
+
11
42
  %style
12
43
  :plain
13
44
  body {
45
+ background: #eee;
14
46
  padding: 0;
15
47
  margin: 0; }
16
48
 
@@ -20,13 +52,23 @@
20
52
  line-height: 13.5pt;
21
53
  font-size: 9pt; }
22
54
 
55
+ h2.table-header {
56
+ padding: 0 0 20px 0;
57
+ margin: 20px auto;
58
+ text-align: center;
59
+ font-family: helvetica neue, sans-serif;
60
+ font-size: 28pt;
61
+ font-weight: 100;
62
+ color: #888;
63
+ text-shadow: 0 1px 0 rgba(250, 250, 250, 0.7); }
64
+
23
65
  table {
24
- border: solid 2px #aaa;
66
+ border: solid 10px #fff;
67
+ box-shadow: 0 1px 10px rgba(0, 0, 0, 0.15);
25
68
  padding: 2px;
26
69
 
27
70
  background: #fafafa;
28
- margin: 0;
29
- width: 600px;
71
+ margin: 0 auto 20px auto;
30
72
  border-collapse: collapse; }
31
73
 
32
74
  table td, table th {
@@ -34,9 +76,10 @@
34
76
  border-top: solid 1px #eee; }
35
77
 
36
78
  /* Columns */
37
- tr>.task { width: 50%; text-align: left; }
38
- tr>.points { width: 9%; }
39
- tr>.progress { width: 18%; }
79
+ tr>.task { width: auto; text-align: left; padding-right: 50px; }
80
+ tr>.points { width: 70px; }
81
+ tr>.progress { width: 100px; }
82
+ tr>.sprints { padding-right: 50px; }
40
83
  tr>.owner { display: none; }
41
84
 
42
85
  /* Indentation */
@@ -46,28 +89,30 @@
46
89
  .level-4 .task { padding-left: 100px; }
47
90
 
48
91
  /* Overrides for parents */
49
- .milestone td.task,
50
- .feature td.task {
92
+ tr.milestone td.task,
93
+ tr.feature td.task {
51
94
  font-weight: bold;
52
95
  font-size: 1.1em; }
53
96
 
54
97
  tr.milestone td,
55
98
  tr.feature td {
56
- border-top: solid 1px #888; }
99
+ border-top: solid 1px #ccc; }
57
100
 
58
- tr .progress .bar {
101
+ tr.subtask .progress .bar {
59
102
  display: none; }
60
103
 
61
- tr.milestone .progress .bar,
62
- tr.feature .progress .bar {
63
- display: block; }
64
-
65
- tr td.points>* {
104
+ tr.subtask td.points>* {
66
105
  display: none; }
67
106
 
68
- tr.milestone td.points>*,
69
- tr.feature td.points>* {
70
- display: inline; }
107
+ /* Milestone */
108
+ tr.milestone td {
109
+ line-height: 18pt; }
110
+
111
+ tr.milestone:not(:first-child) td {
112
+ border-top: solid 3px #777; }
113
+
114
+ tr.milestone td.task {
115
+ font-size: 1.2em; }
71
116
 
72
117
  /* Header */
73
118
  thead {
@@ -90,23 +135,24 @@
90
135
  background: #fafae0; }
91
136
 
92
137
  /* Status box */
93
- span.status {
138
+ .status {
94
139
  display: inline-block;
95
140
  width: 12px;
96
141
  height: 12px;
97
142
  margin: 0 5px 0 0;
98
143
 
99
- border-radius: 2px;
144
+ border-radius: 6px;
145
+ box-shadow: inset 0 0 0 1px rgba(0,0,0,0.1), inset 1px 1px 1px rgba(0, 0, 0, 0.1);
100
146
 
101
147
  position: relative;
102
148
  top: 2px;
103
149
  background: #ddd; }
104
150
 
105
- span.status.in_progress {
106
- background: #ea3; }
151
+ .parent .status {
152
+ cursor: pointer; }
107
153
 
108
- span.status.done {
109
- background: #393; }
154
+ .parent .status:hover {
155
+ box-shadow: inset 0 0 0 2px rgba(0, 0, 0, 0.1); }
110
156
 
111
157
  /* Progress */
112
158
  .progress .number {
@@ -115,25 +161,43 @@
115
161
  .progress .bar {
116
162
  height: 10px;
117
163
  border-radius: 5px;
118
- background: #ddd; }
164
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.05), inset 1px 1px 2px rgba(0, 0, 0, 0.1);
165
+ background: #eee; }
119
166
 
120
167
  .progress .bar span {
168
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.05), inset -1px -1px 7px rgba(0, 0, 0, 0.1);
169
+ min-width: 10px;
121
170
  display: block;
122
171
  height: 10px;
123
172
  border-radius: 5px;
124
173
  background: #888; }
125
174
 
126
- a.meta {
127
- font-size: 0.9em;
175
+ .meta {
176
+ white-space: nowrap;
177
+ font-size: 8pt;
178
+ font-weight: normal;
128
179
  text-decoration: none;
129
180
 
130
- background: #ddd;
131
- padding: 1px 3px;
181
+ background: #eee;
182
+ padding: 1px 5px;
132
183
  border-radius: 2px;
133
- border-bottom: solid 1px #ccc;
184
+ border-bottom: solid 1px #ddd;
185
+ border-right: solid 1px #ddd;
186
+
187
+ margin: 0 10px;
188
+ color: #999; }
189
+
190
+ a.meta:hover {
191
+ background: #ddd; }
134
192
 
135
- margin: 0 5px;
136
- color: #777; }
193
+ a.meta:active {
194
+ background: #bbb; }
195
+
196
+ span.meta {
197
+ background: #ddd;
198
+ border-bottom: solid 1px #ccc;
199
+ border-right: solid 1px #ccc;
200
+ color: #666; }
137
201
 
138
202
  /* Progress */
139
203
  td.points {
@@ -147,31 +211,244 @@
147
211
  td.points .points.total {
148
212
  color: #888; }
149
213
 
150
- %body
214
+ /* Sprints */
215
+ .sprint-display {
216
+ background: #eee;
217
+ border-radius: 6px;
218
+ height: 12px;
219
+ position: relative; }
220
+
221
+ .sprint-display {
222
+ overflow: hidden; }
223
+
224
+ .sprint-progress {
225
+ display: block;
226
+ float: left;
227
+
228
+ background-color: #eee;
229
+ background: -webkit-linear-gradient(left, #eee, #ddd);
230
+ height: 2px;
231
+ border-top-left-radius: 1px;
232
+ border-bottom-left-radius: 1px;
233
+ margin-top: 8px; }
234
+
235
+ .sprint-marker {
236
+ display: block;
237
+ float: left;
238
+
239
+ background: #ccc;
240
+
241
+ width: 6px;
242
+ height: 6px;
243
+ border-radius: 3px;
244
+ float: left;
151
245
 
152
- %table
246
+ margin-top: 6px; }
247
+
248
+ .sprint-label {
249
+ display: block;
250
+ float: left;
251
+
252
+ font-size: 7pt;
253
+ color: #888;
254
+ padding-left: 5px; }
255
+
256
+ .status-bg-in_progress {
257
+ background: #ea3; }
258
+
259
+ .status-bg-done {
260
+ background: #393; }
261
+
262
+ /* Navigation */
263
+ body {
264
+ padding: 80px 0 50px 0; }
265
+
266
+ .nav {
267
+ position: fixed;
268
+ top: 0;
269
+ left: 0;
270
+ right: 0;
271
+ z-index: 5;
272
+
273
+ padding: 0 20px;
274
+ background: rgba(220, 220, 220, 0.95);
275
+ box-shadow: inset 0 -5px 3px -3px rgba(0, 0, 0, 0.06);
276
+
277
+ font-size: 8pt;
278
+ font-family: sans-serif; }
279
+
280
+ .nav:after {
281
+ content: '';
282
+ display: table;
283
+ clear: both; }
284
+
285
+ .nav ul, .nav li, .nav h4 {
286
+ margin: 0;
287
+ padding: 0;
288
+ list-style-type: none; }
289
+
290
+ .nav ul, .nav li, .nav h4, .nav a, .nav .segment {
291
+ float: left; }
292
+
293
+ .nav h4 {
294
+ padding: 1px;
295
+ color: #888;
296
+ text-shadow: 0 1px 0 rgba(250, 250, 250, 0.4); }
297
+
298
+ .nav ul {
299
+ margin-left: 10px; }
300
+
301
+ .nav .segment {
302
+ margin: 0;
303
+ padding: 10px 20px 10px 10px;
304
+ border-left: solid 1px #ccc;
305
+ box-shadow: inset 1px 0 0 #e0e0e0; }
306
+
307
+ .nav .segment:first-child {
308
+ box-shadow: none;
309
+ border-left: 0; }
310
+
311
+ .nav h4,
312
+ .nav a {
313
+ height: 24px;
314
+ line-height: 24px; }
315
+
316
+ .nav a {
317
+ color: #888;
318
+ text-shadow: 0 1px 0 rgba(250, 250, 250, 0.4);
319
+ text-decoration: none;
320
+
321
+ display: block;
322
+ padding: 0 10px;
323
+ border: solid 1px rgba(0, 0, 0, 0.1);
324
+ border-radius: 3px; }
325
+
326
+ .nav ul a {
327
+ border-radius: 0;
328
+ border-left-width: 0; }
329
+
330
+ .nav a:hover {
331
+ background: rgba(250, 250, 250, 0.2); }
332
+
333
+ .nav ul li:first-child a {
334
+ border-top-left-radius: 3px;
335
+ border-bottom-left-radius: 3px;
336
+ border-left-width: 1px; }
337
+
338
+ .nav ul li:last-child a {
339
+ border-top-right-radius: 3px;
340
+ border-bottom-right-radius: 3px; }
341
+
342
+ /* Print mode */
343
+ body.print {
344
+ padding: 0;
345
+ background: transparent; }
346
+
347
+ body.print .table-header,
348
+ body.print .nav {
349
+ display: none; }
350
+
351
+ body.print table {
352
+ box-shadow: none;
353
+ margin-left: 0;
354
+ margin-right: 0;
355
+ border: 0; }
356
+
357
+ /* Sprint table */
358
+ table.sprints td {
359
+ padding: 10px; }
360
+
361
+ table.sprints td.progress {
362
+ width: 200px; }
363
+
364
+ table.sprints td.progress .bar {
365
+ background: #ccc; }
366
+
367
+ %body
368
+ .nav
369
+ .segment
370
+ - if list.sprints?
371
+ %h4 Filter by:
372
+ %ul.sprint-nav
373
+ - list.sprints.values.each do |sprint|
374
+ %li
375
+ %a{href: "#sprint-#{sprint.slug}", data: { sprint: sprint.slug }}= sprint.id
376
+
377
+ %ul
378
+ %li
379
+ %a{href: '#all'} All
380
+
381
+ .segment
382
+ %ul
383
+ %li
384
+ %a{href: '#print'} Export mode
385
+
386
+
387
+ - if list.sprints?
388
+ %h2.table-header Schedule
389
+
390
+ %table.sprints
391
+ - max_points = list.sprints.values.map(&:points).max
392
+ - list.sprints.values.each do |sprint|
393
+ %tr{data: { sprint: sprint.slug }}
394
+ %td.name= sprint.name
395
+ %td.progress
396
+ .bar{style: "width: #{sprint.points*100/max_points}%"}
397
+ - if sprint.percent > 0
398
+ %span{style: "width: #{(sprint.percent*100).to_i}%"}
399
+ %span.number= "#{(sprint.percent*100).to_i}%"
400
+ %td.points
401
+ %span.points.done= sprint.points_done.round(1).to_s.gsub(/\.0+$/,'')
402
+ %span.of of
403
+ %span.points.total= sprint.points.round(1).to_s.gsub(/\.0+$/,'')
404
+ %br
405
+
406
+ %h2.table-header Tasks
407
+
408
+ %table.tasks
153
409
  %thead
154
410
  %tr
155
411
  %th.task Task
156
412
  %th.owner Owner
413
+ - if list.sprints?
414
+ %th.sprints Schedule
157
415
  %th.progress Progress
158
416
  %th.points Points
159
417
 
160
418
  - list.walk do |task, recurse|
161
419
 
162
- %tr{class: "level-#{task.level} #{task.tasks? ? 'parent' : 'leaf'} #{'root' if task.root?} #{'feature' if task.feature?} #{'milestone' if task.milestone?}"}
420
+ %tr{id: "task_#{task.id}", class: "#{task.css_class}"}
163
421
  %td.task
164
- %span.status{class: "#{task.status}"}
422
+ %span.status{class: "status-bg-#{task.status}"}
165
423
  = task
166
424
  - if task.pivotal_id
167
- %a.meta{href: task.pivotal_url}= "Pivotal: #{task.pivotal_id}"
425
+ %a.meta{href: task.pivotal_url, target: '_blank'}= "PT ##{task.pivotal_id}"
426
+ - if task.trello_id
427
+ %a.meta{href: task.trello_url, target: '_blank'}= "Trello"
428
+
429
+ - if task.tags.any?
430
+ - task.tags.each do |tag|
431
+ %span.meta= tag
432
+
433
+ - if task.leaf? && task.points != 1
434
+ %span.meta= ("%.2f" % task.points).gsub(/\.?0+$/,'') + "pts"
168
435
 
169
436
  %td.owner
170
437
  = task.owner
171
438
 
439
+ - if list.sprints?
440
+ %td.sprints
441
+ - if task.sprint?
442
+ %span.sprint-display
443
+ %span.sprint-progress{style: "width: #{45*task.sprint.index+6}px"}
444
+ %span.sprint-marker{class: "status-bg-#{task.status}"}
445
+ %span.sprint-label
446
+ = task.sprint.id
447
+
172
448
  %td.progress
173
449
  .bar
174
- %span{style: "width: #{(task.percent*95+5).to_i}%"}
450
+ - if task.percent > 0
451
+ %span{style: "width: #{(task.percent*100).to_i}%"}
175
452
  %span.number= "#{(task.percent*100).to_i}%"
176
453
 
177
454
  %td.points
@@ -180,3 +457,4 @@
180
457
  %span.points.total= task.points.round(1).to_s.gsub(/\.0+$/,'')
181
458
 
182
459
  - recurse.call if recurse
460
+
@@ -25,15 +25,23 @@ module TPS
25
25
  # Columns
26
26
  c1 = "%s %s %s" % [ indent, status, task.name ]
27
27
  c2 = if task.feature? || task.milestone?
28
- progress
28
+ "%6s %s" % [ points, progress ]
29
29
  else
30
- ' '*12
30
+ ' '*19
31
31
  end
32
32
 
33
33
  pref = c("-"*80, 30)+"\n" if task.feature?
34
34
 
35
35
  # Put together
36
- "#{pref}" + "%-95s%s\n" % [ c1, c2 ]
36
+ "#{pref}" + "%-88s%s\n" % [ c1, c2 ]
37
+ end
38
+
39
+ def points
40
+ [
41
+ "%3i" % [ task.points_done ],
42
+ c("/", 30),
43
+ c("%-2i" % [ task.points ], 32)
44
+ ].join ''
37
45
  end
38
46
 
39
47
  def color
@@ -0,0 +1,67 @@
1
+ require 'json'
2
+ require 'yaml'
3
+
4
+ module TPS
5
+ module Importer
6
+ def self.create(file, format=nil)
7
+ TPS::Importer::Trello.new file
8
+ end
9
+
10
+ class Base
11
+ def initialize(file)
12
+ @file = file
13
+ @tree = Hash.new
14
+ end
15
+
16
+ def to_yaml
17
+ YAML::dump @tree
18
+ end
19
+ end
20
+
21
+ class Trello < Base
22
+ def initialize(file)
23
+ super
24
+ @data = JSON.parse(File.read(@file))
25
+ @tree = Hash.new
26
+ work!
27
+ end
28
+
29
+ def milestone
30
+ name = @data['name'] + ' milestone'
31
+ @tree[name] ||= Hash.new
32
+ end
33
+
34
+ def archived
35
+ name = @data['name'] + ' archived milestone'
36
+ @tree[name] ||= Hash.new
37
+ end
38
+
39
+ def work!
40
+ milestone
41
+ archived
42
+
43
+ @lists ||= Hash.new
44
+ @data['lists'].each do |list|
45
+
46
+ parent = list['closed'] ? archived : milestone
47
+ parent[list['name']] ||= Hash.new
48
+
49
+ @lists[list['id']] = parent[list['name']]
50
+ end
51
+
52
+ @data['cards'].each do |card|
53
+ parent = @lists[card['idList']]
54
+
55
+ labels = card['labels'].map { |l| l['name'].downcase }
56
+
57
+ value = Array.new
58
+ value << 'done' if labels.include?("done")
59
+ value << 'in progress' if labels.include?("in progress")
60
+
61
+ parent[card['name']] = value.empty? ? nil : value
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+
data/lib/tps/sprint.rb ADDED
@@ -0,0 +1,48 @@
1
+ module TPS
2
+ class Sprint
3
+ attr_accessor :id
4
+ attr_accessor :name
5
+ attr_accessor :list
6
+
7
+ def initialize(id, name, list)
8
+ @id = id
9
+ @name = name
10
+ @list = list
11
+ end
12
+
13
+ # Returns the numeric index of the sprint.
14
+ #
15
+ # s1.index #=> 0
16
+ # s2.index #=> 1
17
+ #
18
+ def index
19
+ list.sprints.keys.index id
20
+ end
21
+
22
+ def points
23
+ sublist ? sublist.points : 0.0
24
+ end
25
+
26
+ def points_done
27
+ sublist ? sublist.points_done : 0.0
28
+ end
29
+
30
+ def percent
31
+ sublist ? sublist.percent : 0.0
32
+ end
33
+
34
+ def sublist
35
+ @sublist ||= list.filter_by_sprint(self)
36
+ end
37
+
38
+ def slug
39
+ slugify id
40
+ end
41
+
42
+ private
43
+
44
+ def slugify(str)
45
+ str.scan(/[A-Za-z0-9]+/).join('_').downcase
46
+ end
47
+ end
48
+ end
data/lib/tps/task.rb CHANGED
@@ -7,12 +7,18 @@ module TPS
7
7
  attr_reader :tasks # Array of Tasks
8
8
  attr_reader :owner
9
9
  attr_reader :pivotal_id
10
+ attr_reader :trello_id
10
11
  attr_reader :parent
12
+ attr_reader :tags
13
+ attr_reader :list # the root TaskList
14
+ attr_reader :id
11
15
 
12
- def initialize(parent, name, data=nil)
16
+ def initialize(parent, name, data=nil, list)
13
17
  @name = name
14
18
  @tasks = Array.new
19
+ @tags = Array.new
15
20
  @parent = parent
21
+ @list = list
16
22
 
17
23
  if data.is_a?(Array)
18
24
  tags = data
@@ -38,6 +44,9 @@ module TPS
38
44
  # [pt/28394] -- Pivotal tracker
39
45
  elsif t =~ /^pt\/(.*)$/i
40
46
  @pivotal_id = $1.strip
47
+ # [tl/28394] -- Trello
48
+ elsif t =~ /^tr\/(.*)$/i
49
+ @trello_id = $1.strip
41
50
  # [50%] -- percentage
42
51
  elsif t =~ /^([\d\.]+)%$/
43
52
  @status = :in_progress
@@ -45,13 +54,20 @@ module TPS
45
54
  # [0pt] -- points
46
55
  elsif t =~ /^([\d\.]+)pts?/i
47
56
  @points = $1.strip.to_f
57
+ # [-all] -- tags
58
+ elsif %w[- #].include?(t[0])
59
+ @tags.push t[1..-1]
60
+ # Sprint name
61
+ elsif list && list.sprints[t]
62
+ @sprint = list.sprints[t]
48
63
  end
49
64
  end
50
65
 
51
- @tasks = tasks.map { |task, data| Task.new self, task, data } if tasks
66
+ @tasks = tasks.map { |task, data| Task.new self, task, data, self.list } if tasks
52
67
 
53
- n = @name.to_s.downcase
54
- @milestone = root? && (n.include?('milestone') || n.include?('version'))
68
+ @milestone = root? && is_milestone?(@name)
69
+
70
+ @id = list.get_id if list
55
71
  end
56
72
 
57
73
  def status
@@ -109,10 +125,109 @@ module TPS
109
125
  tasks.any?
110
126
  end
111
127
 
128
+ def sprint?
129
+ !! sprint
130
+ end
131
+
132
+ def sprint
133
+ @sprint || (parent && parent.sprint)
134
+ end
135
+
136
+ def contains_sprint?(sprint)
137
+ contains? { |t| t.sprint == sprint }
138
+ end
139
+
140
+ # Filters a task list by tasks that match a given block, preserving its
141
+ # ancestors even if they don't match.
142
+ #
143
+ # @list.contains? { |t| t.done? }
144
+ # @list.contains? { |t| t.sprint == sprint }
145
+ #
146
+ def contains?(&blk)
147
+ blk.call(self) || tasks.detect { |t| t.contains?(&blk) }
148
+ end
149
+
150
+ def ancestor?(&blk)
151
+ blk.call(self) || parent && parent.ancestor?(&blk)
152
+ end
153
+
154
+ # Filters a task tree to tasks of a given criteria, preserving its
155
+ # ancestors even if they don't match.
156
+ #
157
+ # @list.filter { |t| t.done? }
158
+ #
159
+ def filter(&blk)
160
+ filter_by { |t| t.contains?(&blk) || t.ancestor?(&blk) }
161
+ end
162
+
163
+ # Returns a list of a task's ancestors, excluding the list.
164
+ def breadcrumbs(include_self=true)
165
+ arr = []
166
+ arr += [self] if include_self
167
+ arr = parent.breadcrumbs + arr if parent
168
+ arr
169
+ end
170
+
171
+ def css_class
172
+ [
173
+ "level-#{level}",
174
+ "status-#{status}",
175
+ (tasks? ? 'parent' : 'leaf'),
176
+ ('root' if root?),
177
+ ('feature' if feature?),
178
+ ('milestone' if milestone?),
179
+ ('subtask' if subtask?),
180
+ ("sprint-#{sprint.slug}" if sprint?),
181
+ sprint_css_classes,
182
+ breadcrumbs(false).map { |t| "in_task_#{t.id}" }
183
+ ].flatten.compact.join(' ')
184
+ end
185
+
186
+ def sprint_css_classes
187
+ list.sprints.values.map { |sprint|
188
+ "has_sprint-#{sprint.slug}" if contains_sprint?(sprint)
189
+ }.compact
190
+ end
191
+
192
+ def subtask?
193
+ !feature? && !milestone?
194
+ end
195
+
196
+ def filter_by_sprint(sprint)
197
+ filter { |t| t.sprint == sprint }
198
+ end
199
+
200
+ # Works like #filter, but only preserves ancestors if they match.
201
+ def filter_by(&blk)
202
+ return nil unless blk.call(self)
203
+ task = self.dup
204
+ task.instance_variable_set :@tasks, tasks.map { |t| t.filter_by(&blk) }.compact
205
+ task
206
+ end
207
+
112
208
  def pivotal_url
113
209
  "https://www.pivotaltracker.com/story/show/#{pivotal_id}" if pivotal_id
114
210
  end
115
211
 
212
+ # Looks up a task.
213
+ #
214
+ # @list['Login']
215
+ #
216
+ def [](name)
217
+ tasks.detect { |t| t.name == name }
218
+ end
219
+
220
+ def trello_url
221
+ if trello_id
222
+ id = trello_id.match(/([A-Za-z0-9]+)$/) && $1.strip
223
+ if list.trello_board_url && id.match(/^[0-9]+/)
224
+ "#{list.trello_board_url}/#{id}"
225
+ else
226
+ "https://trello.com/c/#{id}"
227
+ end
228
+ end
229
+ end
230
+
116
231
  def percent
117
232
  if done?
118
233
  1.0
@@ -142,6 +257,10 @@ module TPS
142
257
  !! @milestone
143
258
  end
144
259
 
260
+ def leaf?
261
+ tasks.empty?
262
+ end
263
+
145
264
  # - list.walk do |task, recurse|
146
265
  # %ul
147
266
  # %li
@@ -162,5 +281,14 @@ module TPS
162
281
  tpl = Tilt.new(template)
163
282
  tpl.evaluate({}, list: self)
164
283
  end
284
+
285
+ private
286
+
287
+ def is_milestone?(str)
288
+ str = str.to_s.downcase
289
+ str.match(/^milestone|milestone$/i) ||
290
+ str.match(/^release|release$/i) ||
291
+ str.match(/^version|version$/i)
292
+ end
165
293
  end
166
294
  end
data/lib/tps/task_list.rb CHANGED
@@ -1,7 +1,12 @@
1
1
  module TPS
2
+ # The root node.
2
3
  class TaskList < Task
4
+ attr_accessor :tasks
5
+ attr_accessor :sprints
6
+ attr_accessor :trello_board_url
7
+
3
8
  def initialize(options)
4
- super nil, nil
9
+ super nil, nil, nil, nil
5
10
 
6
11
  data = if options[:yaml]
7
12
  YAML::load_file options[:yaml]
@@ -11,7 +16,22 @@ module TPS
11
16
  options
12
17
  end
13
18
 
14
- @tasks = data.map { |task, data| Task.new nil, task, data }
19
+ sprint_data = data.delete('Sprints') || {}
20
+ @sprints = Hash[*sprint_data.map { |id, name| [id, Sprint.new(id, name, self)] }.flatten]
21
+
22
+ @trello_board_url = data.delete('Trello URL')
23
+
24
+ @tasks = data.map { |task, data| Task.new nil, task, data, self }
25
+ end
26
+
27
+ def sprints?
28
+ sprints.any?
29
+ end
30
+
31
+ # Returns a fresh ID. (internal)
32
+ def get_id
33
+ @task_count ||= 0
34
+ @task_count += 1
15
35
  end
16
36
  end
17
37
  end
data/lib/tps/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module TPS
2
2
  def self.version
3
- "0.0.2"
3
+ "0.2.0"
4
4
  end
5
5
  end
data/lib/tps.rb CHANGED
@@ -25,6 +25,7 @@ module TPS
25
25
  autoload :Task, 'tps/task'
26
26
  autoload :TaskList, 'tps/task_list'
27
27
  autoload :CliReporter, 'tps/cli_reporter'
28
+ autoload :Sprint, 'tps/sprint'
28
29
 
29
30
  require 'tps/version'
30
31
 
data/test/hello.yml CHANGED
@@ -39,4 +39,11 @@ Milestone 1:
39
39
  one:
40
40
  two: [in progress]
41
41
 
42
+ Progress override: #7
43
+ # It's supposed to be 50% done because of it's subtasks, but
44
+ # Defining our own percentage overrides that.
45
+ _: [20%]
46
+ one: [done]
47
+ two:
48
+
42
49
  Milestone 2:
@@ -0,0 +1,64 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ class SprintTest < UnitTest
4
+ setup do
5
+ @list = TPS::TaskList.new yaml: f('sprints.yml')
6
+ @milestone = @list.tasks.first
7
+ @s1 = @list.sprints['s1']
8
+ @s2 = @list.sprints['s2']
9
+ end
10
+
11
+ test "Sprints" do
12
+ assert @list.sprints.size == 2
13
+ assert @s1.name == 'Sprint one'
14
+ assert @s2.name == 'Sprint two'
15
+ end
16
+
17
+ test "Sprint model attributes" do
18
+ sprint = @list.sprints['s1']
19
+ assert sprint.name == 'Sprint one'
20
+ assert sprint.list == @list
21
+ end
22
+
23
+ test "Tasks should be assigned to sprints" do
24
+ assert @list['Version 1']['Account']['Login'].sprint == @s1
25
+ end
26
+
27
+ test "Task#contains_sprint?" do
28
+ assert @list.contains_sprint?(@s1)
29
+ assert @list.contains_sprint?(@s2)
30
+ end
31
+
32
+ test "Task#contains_sprint? part 2" do
33
+ task = @list['Version 1']['Account']['Login']
34
+ assert task.contains_sprint?(@s1)
35
+ assert ! task.contains_sprint?(@s2)
36
+ end
37
+
38
+ test "Task#filter_by_sprint" do
39
+ list = @list.filter_by_sprint(@s1)
40
+ assert ! list['Version 1']['Account']['Login'].nil?
41
+ assert list['Version 1']['Account']['Signup'].nil?
42
+ end
43
+
44
+ test "Sub-tasks of a sprint task" do
45
+ list = @list.filter_by_sprint(@s1)
46
+ task = list['Version 1']['Comments']['Creating']
47
+ assert ! task.nil?
48
+ end
49
+
50
+ test "Sprint#index" do
51
+ assert @list.sprints['s1'].index == 0
52
+ assert @list.sprints['s2'].index == 1
53
+ end
54
+
55
+ test "Sprint#points" do
56
+ assert @s1.points == 5
57
+ end
58
+
59
+ test "Sprint#points_done" do
60
+ assert @s1.points_done == 3
61
+ end
62
+ end
63
+
64
+
data/test/sprints.yml ADDED
@@ -0,0 +1,18 @@
1
+ Sprints:
2
+ s1: Sprint one
3
+ s2: Sprint two
4
+
5
+ Version 1:
6
+ Account:
7
+ Login: [s1]
8
+ Signup: [s2]
9
+
10
+ Comments:
11
+ _: [s1]
12
+ Creating: [done]
13
+ Deleting:
14
+
15
+ Posts:
16
+ _: [s1, done]
17
+ Creating:
18
+ Deleting:
data/test/tps_test.rb CHANGED
@@ -61,6 +61,12 @@ class MyTest < UnitTest
61
61
  assert_equal 0.25, task.percent
62
62
  end
63
63
 
64
+ test "Progress override" do
65
+ task = @milestone.tasks[7]
66
+ assert_equal 0.2, task.percent
67
+ assert_equal 0.4, task.points_done
68
+ end
69
+
64
70
  test "Milestone" do
65
71
  assert @milestone.milestone?
66
72
  end
@@ -68,4 +74,25 @@ class MyTest < UnitTest
68
74
  test "HTML works" do
69
75
  assert @list.to_html
70
76
  end
77
+
78
+ test "Lookup" do
79
+ assert @list['Milestone 1'] == @milestone
80
+ end
81
+
82
+ test "Task#breadcrumbs" do
83
+ crumbs = @list['Milestone 1']['User login']['Signup'].breadcrumbs
84
+ assert crumbs == [
85
+ @list['Milestone 1'],
86
+ @list['Milestone 1']['User login'],
87
+ @list['Milestone 1']['User login']['Signup']
88
+ ]
89
+ end
90
+
91
+ test "Task#breadcrumbs(false)" do
92
+ crumbs = @list['Milestone 1']['User login']['Signup'].breadcrumbs(false)
93
+ assert crumbs == [
94
+ @list['Milestone 1'],
95
+ @list['Milestone 1']['User login']
96
+ ]
97
+ end
71
98
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tps_reporter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-02-04 00:00:00.000000000Z
12
+ date: 2012-11-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: tilt
16
- requirement: &2152658640 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,15 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *2152658640
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
25
30
  - !ruby/object:Gem::Dependency
26
31
  name: contest
27
- requirement: &2152658200 !ruby/object:Gem::Requirement
32
+ requirement: !ruby/object:Gem::Requirement
28
33
  none: false
29
34
  requirements:
30
35
  - - ! '>='
@@ -32,7 +37,12 @@ dependencies:
32
37
  version: '0'
33
38
  type: :development
34
39
  prerelease: false
35
- version_requirements: *2152658200
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
36
46
  description: A YAML-powered, simple command-line task report builder.
37
47
  email:
38
48
  - rico@sinefunc.com
@@ -41,6 +51,8 @@ executables:
41
51
  extensions: []
42
52
  extra_rdoc_files: []
43
53
  files:
54
+ - .gitignore
55
+ - GUIDE.md
44
56
  - HISTORY.md
45
57
  - README.md
46
58
  - Rakefile
@@ -49,10 +61,14 @@ files:
49
61
  - data/sample.yml
50
62
  - lib/tps.rb
51
63
  - lib/tps/cli_reporter.rb
64
+ - lib/tps/importer.rb
65
+ - lib/tps/sprint.rb
52
66
  - lib/tps/task.rb
53
67
  - lib/tps/task_list.rb
54
68
  - lib/tps/version.rb
55
69
  - test/hello.yml
70
+ - test/sprint_test.rb
71
+ - test/sprints.yml
56
72
  - test/test_helper.rb
57
73
  - test/tps_test.rb
58
74
  - tps_reporter.gemspec
@@ -76,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
92
  version: '0'
77
93
  requirements: []
78
94
  rubyforge_project:
79
- rubygems_version: 1.8.10
95
+ rubygems_version: 1.8.23
80
96
  signing_key:
81
97
  specification_version: 3
82
98
  summary: Task progress sheet reporter.