tps_reporter 0.0.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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.