pdf-storycards 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,10 @@
1
+ == 0.1 / 2008-02-09
2
+
3
+ * Reads stories from STDIN, writes PDF to STDOUT, makes for more Unix-friendly command line tool and more usable library class
4
+ * Turned off munging of story narrative into a sentence by default
5
+ * Introduced concept of metadata (colon-separated name value pairs) that are parsed out of tops of stories (before Story: ...) and the ability to specify metadata keys for the lower left and/or lower right of the cards -- the corresponding values will be printed there.
6
+ * Added a real-world example script: printing cards from Thoughtworks Studio's Mingle. In examples/mingle_cards.rb.
7
+
1
8
  == 0.0.1 / 2007-12-27
2
9
 
3
10
  * Initial release
@@ -3,15 +3,24 @@ Manifest.txt
3
3
  README.txt
4
4
  Rakefile
5
5
  bin/stories2cards
6
+ examples/mingle_cards.rb
6
7
  lib/pdf/storycards.rb
7
8
  lib/pdf/storycards/scenario.rb
8
9
  lib/pdf/storycards/story.rb
9
10
  lib/pdf/storycards/story_capturing_mediator.rb
11
+ lib/pdf/storycards/story_parser.rb
12
+ lib/pdf/storycards/text_file_filter.rb
10
13
  lib/pdf/storycards/writer.rb
11
14
  spec/addition
12
15
  spec/calculator_stories
16
+ spec/calculator_stories_with_metadata
17
+ spec/not_a_text_file.gif
18
+ spec/not_a_text_file.jpg
13
19
  spec/pdf/storycards/story_capturing_mediator_spec.rb
20
+ spec/pdf/storycards/story_parser_spec.rb
14
21
  spec/pdf/storycards/story_spec.rb
22
+ spec/pdf/storycards/text_file_filter_spec.rb
15
23
  spec/pdf/storycards/writer_spec.rb
16
24
  spec/rspec_suite.rb
17
25
  spec/spec_helper.rb
26
+ spec/subtraction_with_metadata
data/README.txt CHANGED
@@ -1,24 +1,29 @@
1
1
  PDF StoryCards
2
- by Luke Melia
3
- http://www.lukemelia.com/
2
+ RDoc <http://pdf-storycards.rubyforge.org/>
3
+ Tracker <http://rubyforge.org/projects/pdf-storycards/>
4
+ by Luke Melia <http://www.lukemelia.com/>
4
5
 
5
6
  == DESCRIPTION:
6
7
 
7
- Provides a script and library to parses stories saved in the RSpec
8
- plain text story format and saves a PDF file with printable 3"x5"
9
- index cards suitable for using in agile planning and prioritization.
8
+ Provides a script and library to parse stories saved in the RSpec
9
+ plain text story format and creates a PDF file with printable 3"x5"
10
+ index cards suitable for using in Agile planning and prioritization.
10
11
 
11
12
  == FEATURES/PROBLEMS:
12
13
 
13
14
  * Create a PDF with each page as a 3x5 sheet, or as 4 cards per 8.5 x 11 sheet
14
- * Currently reads stories from a single file.
15
- * TODO: Take a directory and find all stories in it
16
- * TODO: Take stories via STDIN
15
+ * Included script reads stories from STDIN and writes PDF to STDOUT
17
16
  * TODO: Improve test coverage
17
+ * TODO: Improve documentation
18
18
 
19
19
  == SYNOPSIS:
20
20
 
21
- StorycardPdfWriter.make_pdf("/tmp/stories.txt", "/tmp/storycards.pdf", :style => :card_1up)
21
+ From the command line with
22
+ stories2cards < /path/to/stories.txt
23
+
24
+ Or via Ruby
25
+ story_text = File.read('my_story')
26
+ pdf_content = PDF::Storycards::Writer.make_pdf(story_text, :style => :card_1up)
22
27
 
23
28
  == REQUIREMENTS:
24
29
 
@@ -27,13 +32,13 @@ index cards suitable for using in agile planning and prioritization.
27
32
 
28
33
  == INSTALL:
29
34
 
30
- sudo gem install storycards
35
+ sudo gem install pdf-storycards
31
36
 
32
37
  == LICENSE:
33
38
 
34
39
  (The MIT License)
35
40
 
36
- Copyright (c) 2007 Luke Melia
41
+ Copyright (c) 2007-2008 Luke Melia
37
42
 
38
43
  Permission is hereby granted, free of charge, to any person obtaining
39
44
  a copy of this software and associated documentation files (the
data/Rakefile CHANGED
@@ -13,11 +13,12 @@ Hoe.new('pdf-storycards', PDF::Storycards::VERSION) do |p|
13
13
  p.author = 'Luke Melia'
14
14
  p.email = 'luke@lukemelia.com'
15
15
  p.summary = 'Utilities for generating printable story cards for agile planning and measurement'
16
- p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
17
- # p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
16
+ p.description = p.paragraphs_of('README.txt', 1..5).join("\n\n")
17
+ p.url = 'http://rubyforge.org/projects/pdf-storycards/'
18
18
  p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
19
19
  p.extra_deps << ['pdf-writer', '>= 1.1.7']
20
20
  p.extra_deps << ['rspec', '>= 1.1.1']
21
+ p.remote_rdoc_dir = '' # Release to root
21
22
  end
22
23
 
23
24
  # vim: syntax=Ruby
@@ -9,31 +9,34 @@
9
9
  # This command outputs a PDF consisting of one card for each story and
10
10
  # narrative defined in stories.txt and saves it as stories.pdf
11
11
  #
12
- # stories2cards stories.txt
12
+ # stories2cards < stories.txt
13
13
  #
14
14
  # Other examples
15
15
  #
16
- # stories2cards stories.txt -o /tmp/cards.pdf
17
- # stories2cards stories.txt --style=4up
16
+ # stories2cards -o /tmp/cards.pdf < stories.txt
17
+ # stories2cards --style=4up < stories.txt
18
18
  #
19
19
  # == Usage
20
- # stories2cards [options] text_file_with_stories
20
+ # stories2cards [options] < text_file_with_stories
21
21
  #
22
22
  # For help use stories2cards -h
23
23
  #
24
24
  # == Options
25
25
  # -h, --help Displays help message
26
26
  # -v, --version Display the version, then exit
27
- # -o, --output=PATH Specify the path to the PDF file
28
- # to be created, defaults to storycards.pdf
27
+ # -o, --output=PATH (optional) Specify the path to the PDF file
28
+ # to be created, writes to stdout by default
29
29
  # -s, --style=1up|4up Defaults to 1up
30
- # -b, --[no-]borders Print borders (only has effect with 4up style
30
+ # -l, --lowerleft=key (optional) MetaData key for value to be printed in lower left
31
+ # -r, --lowerright=key (optional) MetaData key for value to be printed in lower right
32
+ # -b, --[no-]borders Print borders (only has effect with 4up style)
33
+ # -m, --makesentences, Make english narratives into sentences
31
34
  #
32
35
  # == Author
33
36
  # Luke Melia
34
37
  #
35
38
  # == Copyright
36
- # Copyright (c) 2007 Luke Melia. Licensed under the MIT License
39
+ # Copyright (c) 2007-2008 Luke Melia. Licensed under the MIT License
37
40
  # http://www.opensource.org/licenses/mit-license.php
38
41
 
39
42
  require 'optparse'
@@ -56,20 +59,21 @@ class App
56
59
 
57
60
  attr_reader :options
58
61
 
59
- def initialize(arguments, stdin)
62
+ def initialize(arguments, stdin, stdout)
60
63
  @arguments = arguments
61
64
  @stdin = stdin
65
+ @stdout = stdout
62
66
 
63
67
  # Set defaults
64
68
  @options = OpenStruct.new
65
- @options.output = "storycards.pdf"
66
69
  @options.style = :"1up"
67
70
  end
68
71
 
69
72
  # Parse options, check arguments, then process the command
70
73
  def run
71
74
  if parsed_options? && arguments_valid?
72
- process_arguments
75
+ process_arguments
76
+ @story_text = @stdin.read
73
77
  process_command
74
78
  else
75
79
  output_usage
@@ -86,7 +90,10 @@ class App
86
90
  opts.on('-h', '--help') { output_help }
87
91
  opts.on('-o', '--output [PATH]') { |path| @options.output = path }
88
92
  opts.on('-s', '--style [STYLE]', [:"1up", :"4up"]) { |style| @options.style = style }
89
- opts.on("-b", "--[no-]borders", "Print borders") { |b| options.borders = b }
93
+ opts.on('-l', '--lowerleft [METADATA_KEY]') { |metadata_key| @options.lower_left = metadata_key }
94
+ opts.on('-r', '--lowerright [METADATA_KEY]') { |metadata_key| @options.lower_right = metadata_key }
95
+ opts.on("-b", "--[no-]borders", "Print borders") { |b| @options.borders = b }
96
+ opts.on('-m', '--makesentences', "Make english narratives into sentences") { |m| @options.make_sentences = m }
90
97
  opts.parse!(@arguments) rescue return false
91
98
 
92
99
  true
@@ -94,14 +101,12 @@ class App
94
101
 
95
102
  # True if required arguments were provided
96
103
  def arguments_valid?
97
- return false unless @arguments.length == 1
98
- return false unless File.exists?(@arguments[0])
104
+ return false if @arguments.length > 0
99
105
  return true
100
106
  end
101
107
 
102
108
  # Setup the arguments
103
109
  def process_arguments
104
- @source_file = @arguments[0]
105
110
  end
106
111
 
107
112
  def output_help
@@ -118,12 +123,18 @@ class App
118
123
  end
119
124
 
120
125
  def process_command
121
- style = :card_1up if @options.style == :"1up"
122
- style = :letter_4up if @options.style == :"4up"
123
- PDF::Storycards::Writer.make_pdf(@source_file, @options.output, :style => style)
126
+ @options.style = :card_1up if @options.style == :"1up"
127
+ @options.style = :letter_4up if @options.style == :"4up"
128
+ options_as_hash = @options.instance_variable_get('@table')
129
+ pdf_text = PDF::Storycards::Writer.make_pdf(@story_text, options_as_hash)
130
+ if @options.output
131
+ File.open(@options.output, 'w') {|f| f.write(pdf_text) }
132
+ else
133
+ @stdout.print pdf_text
134
+ end
124
135
  end
125
- end
136
+ end
126
137
 
127
138
  # Create and run the application
128
- app = App.new(ARGV, STDIN)
139
+ app = App.new(ARGV, $stdin, $stdout)
129
140
  app.run
@@ -0,0 +1,29 @@
1
+ require 'rubygems'
2
+ require 'active_record'
3
+ require 'pdf/storycards'
4
+
5
+ ActiveRecord::Base.establish_connection(
6
+ :adapter => "mysql",
7
+ :host => "localhost",
8
+ :username => "root",
9
+ :password => "",
10
+ :database => "mingle"
11
+ )
12
+
13
+ target_sprint_number = 4
14
+
15
+ class WeplayLaunchCard < ActiveRecord::Base
16
+ end
17
+
18
+ cards = WeplayLaunchCard.find(:all, :conditions => ["card_type_name = 'story' AND cp_target_sprint_number = ?", target_sprint_number], :order => 'cp_storyrank')
19
+ stories = cards.collect do |card|
20
+ story = "CardNumber: #{card.number}\n"
21
+ story << "Estimate: #{card.cp_estimated_effort}\n\n" unless card.cp_estimated_effort.nil?
22
+ story << "Story: #{card.name}\n\n"
23
+ story << " #{card.description.gsub("\n","\n ")}\n\n" unless card.description.nil?
24
+ story
25
+ end
26
+ story_text = stories.join(".\n")
27
+
28
+ pdf_text = PDF::Storycards::Writer.make_pdf(story_text, :style => :letter_4up, :borders => true, :lower_left => 'Estimate', :lower_right => 'CardNumber')
29
+ $stdout.print pdf_text
@@ -1,10 +1,12 @@
1
1
  module PDF
2
2
  module Storycards
3
- VERSION = '0.0.1'
3
+ VERSION = '0.1.0'
4
4
  end
5
5
  end
6
6
 
7
+ require 'pdf/storycards/text_file_filter'
7
8
  require 'pdf/storycards/scenario'
8
9
  require 'pdf/storycards/story'
9
10
  require 'pdf/storycards/story_capturing_mediator'
11
+ require 'pdf/storycards/story_parser'
10
12
  require 'pdf/storycards/writer'
@@ -1,7 +1,10 @@
1
1
  module PDF
2
2
  module Storycards
3
+ # The string Story#narrative is imbued with a method called to_sentence, which formats the
4
+ # story narrative as a single sentence without newlines.
3
5
  class Story
4
6
  def initialize(title, narrative)
7
+ @meta_data = {}
5
8
  @title = title
6
9
  @narrative = narrative
7
10
  def @narrative.to_sentence
@@ -10,7 +13,8 @@ module PDF
10
13
  @scenarios = []
11
14
  end
12
15
 
13
- attr_accessor :title, :narrative
16
+ attr_reader :title, :narrative
17
+ attr_accessor :meta_data
14
18
 
15
19
  def add_scenario(scenario)
16
20
  @scenarios << scenario
@@ -1,8 +1,5 @@
1
1
  # RSpec's story parser needs to be constructed with a Mediator. The mediator defined in the class
2
2
  # below simply collects stories and exposes the collection as StoryCapturingMediator#stories.
3
- #
4
- # It also sprinkles in a magic method on Story#narrative called to_sentence, which formats the
5
- # story narrative as a single sentence without newlines.
6
3
  module PDF
7
4
  module Storycards
8
5
 
@@ -0,0 +1,41 @@
1
+ module PDF
2
+ module Storycards
3
+ class StoryParser
4
+
5
+ def parse_stories(story_text)
6
+ all_stories = []
7
+ story_groups = story_text.split(/^\.$/)
8
+ story_groups.each do |story_group|
9
+ all_stories << parse_story_group(story_group)
10
+ end
11
+ return all_stories.flatten
12
+ end
13
+
14
+ def parse_story_group(story_text)
15
+ meta_data = extract_meta_data(story_text)
16
+ story_mediator = StoryCapturingMediator.new
17
+ rspec_story_parser = Spec::Story::Runner::StoryParser.new(story_mediator)
18
+ rspec_story_parser.parse(story_text.split("\n"))
19
+ stories = story_mediator.stories
20
+ stories.each do |story|
21
+ story.meta_data = meta_data
22
+ end
23
+ stories
24
+ end
25
+
26
+ def extract_meta_data(story_text)
27
+ meta_data = {}
28
+ story_text.split("\n").each do |line|
29
+ match_data = /^([^ :]+):(.*)$/.match(line)
30
+ unless match_data.nil?
31
+ name = match_data[1].strip
32
+ value = match_data[2].strip
33
+ break if name == 'Story'
34
+ meta_data[name] = value
35
+ end
36
+ end
37
+ meta_data
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,31 @@
1
+ module PDF
2
+ module Storycards
3
+ class TextFileFilter
4
+ include Enumerable
5
+
6
+ def initialize(*paths)
7
+ @paths = paths
8
+ @text_files = paths.select{|path| nonbinary?(path)}
9
+ @text_files.map!{ |path| Pathname.new(path).cleanpath.to_s }
10
+ end
11
+
12
+ def each(&block)
13
+ @text_files.each(&block)
14
+ end
15
+
16
+ private
17
+
18
+ NON_ASCII_PRINTABLE = /[^\x20-\x7e\s]/
19
+
20
+ def nonbinary?(path, size = 1024)
21
+ open(path) do |io|
22
+ while buf = io.read(size)
23
+ return false if NON_ASCII_PRINTABLE =~ buf
24
+ end
25
+ return true
26
+ end
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -5,12 +5,14 @@ module PDF
5
5
  module Storycards
6
6
  class Writer
7
7
 
8
- def self.make_pdf( path, output_path = 'storycards.pdf', opts = {} )
9
- output_path = 'storycards.pdf' if output_path.nil?
8
+ CARD_WIDTH = PDF::Writer.in2pts(5)
9
+ CARD_HEIGHT = PDF::Writer.in2pts(3)
10
+
11
+ def self.make_pdf( story_text, opts )
10
12
  opts[:style] = :card_1up unless opts[:style]
11
- opts[:border] = false unless opts[:border]
13
+ opts[:borders] = false unless opts[:borders]
12
14
 
13
- stories = self.parse_stories(path)
15
+ stories = StoryParser.new.parse_stories(story_text)
14
16
 
15
17
  if opts[:style] == :card_1up
16
18
  pdf = PDF::Writer.new(:paper => [7.62, 12.7], :orientation => :landscape) #3x5 card
@@ -18,18 +20,16 @@ module PDF
18
20
  pdf.move_pointer(-60)
19
21
 
20
22
  stories.each_with_index do |story, index|
21
- pdf.y = 0 if index > 0
22
- print_card_text(pdf, story)
23
+ pdf.start_new_page if index > 0
24
+ print_card_corners(pdf, story, opts[:lower_left], opts[:lower_right], 0, 0)
25
+ print_card_text(pdf, story, 0, opts[:make_sentences])
23
26
  end
24
27
 
25
- pdf.save_as(output_path)
26
- return output_path
28
+ return pdf.render
27
29
 
28
30
  elsif opts[:style] == :letter_4up
29
31
 
30
32
  pdf = PDF::Writer.new(:paper => "LETTER", :orientation => :landscape)
31
- card_width = PDF::Writer.in2pts(5)
32
- card_height = PDF::Writer.in2pts(3)
33
33
  margin = PDF::Writer.in2pts(0.33)
34
34
  gutter = margin
35
35
  padding = 10
@@ -37,54 +37,64 @@ module PDF
37
37
  pdf.start_columns(2, gutter)
38
38
  stories.each_with_index do |story, index|
39
39
 
40
- is_laying_out_left_hand_column = (index < 2)
40
+ is_laying_out_left_hand_column = (index % 4 < 2)
41
41
  if is_laying_out_left_hand_column
42
42
  rect_x = margin
43
43
  else
44
- rect_x = pdf.page_width - margin - card_width
44
+ rect_x = pdf.page_width - margin - CARD_WIDTH
45
45
  end
46
46
 
47
47
  is_laying_out_top_row = (index % 2 == 0)
48
48
  if is_laying_out_top_row
49
49
  pdf.start_new_page unless index == 0
50
50
  pdf.y = pdf.page_height - margin - padding
51
- rect_y = pdf.page_height - margin - card_height
51
+ rect_y = pdf.page_height - margin - CARD_HEIGHT
52
52
  else
53
53
  rect_y = margin
54
- pdf.y = margin + card_height - padding
54
+ pdf.y = margin + CARD_HEIGHT - padding
55
55
  end
56
56
 
57
- print_border(pdf, rect_x, rect_y, card_width, card_height) if opts[:border]
58
- print_card_text(pdf, story, padding)
57
+ print_border(pdf, rect_x, rect_y) if opts[:borders]
58
+ print_card_corners(pdf, story, opts[:lower_left], opts[:lower_right], rect_x, rect_y)
59
+ print_card_text(pdf, story, padding, opts[:make_sentences])
59
60
  end
60
61
 
61
- pdf.save_as(output_path)
62
- return output_path
62
+ return pdf.render
63
63
 
64
+ else
65
+ raise "Unknown style: #{opts[:style]}"
64
66
  end
65
67
  end
66
68
  private
67
69
 
68
- def self.print_border(pdf, x, y, width, height)
70
+ def self.print_border(pdf, x, y)
69
71
  pdf.stroke_color(Color::RGB::Black)
70
72
  pdf.stroke_style(PDF::Writer::StrokeStyle.new(1))
71
- pdf.rectangle(rect_x, rect_y, card_width, card_height).close_stroke
72
- end
73
-
74
- def self.parse_stories(path)
75
- story_mediator = StoryCapturingMediator.new
76
- story_parser = Spec::Story::Runner::StoryParser.new(story_mediator)
77
- story_text = File.read(path)
78
- story_parser.parse(story_text.split("\n"))
79
- return story_mediator.stories
73
+ pdf.rectangle(x, y, CARD_WIDTH, CARD_HEIGHT).close_stroke
80
74
  end
81
-
82
- def self.print_card_text(pdf, story, padding = 0)
75
+
76
+ def self.print_card_text(pdf, story, padding = 0, make_sentences = false)
77
+ top = pdf.y
83
78
  pdf.text "<b>#{story.title}</b>", :font_size => 18, :justification => :center,
84
79
  :left => padding, :right => padding
85
80
  pdf.move_pointer(12)
86
- pdf.text story.narrative.to_sentence, :justification => :left, :font_size => 14,
87
- :left => padding, :right => padding
81
+ narrative = story.narrative
82
+ narrative = narrative.to_sentence if make_sentences
83
+ pdf.text narrative, :justification => :left, :font_size => 14,
84
+ :left => padding, :right => padding
85
+ end
86
+
87
+ def self.print_card_corners(pdf, story, lower_left_meta_data_key, lower_right_meta_data_key, card_x, card_y)
88
+ text_y = card_y + 16 + 8
89
+ text_width = CARD_WIDTH/2 - PDF::Writer.in2pts(0.4)
90
+ if story.meta_data[lower_left_meta_data_key]
91
+ text_x = card_x + PDF::Writer.in2pts(0.4)
92
+ pdf.add_text_wrap text_x, text_y, text_width, story.meta_data[lower_left_meta_data_key], 16, :left
93
+ end
94
+ if story.meta_data[lower_right_meta_data_key]
95
+ text_x = card_x + CARD_WIDTH/2
96
+ pdf.add_text_wrap text_x, text_y, text_width, story.meta_data[lower_right_meta_data_key], 16, :right
97
+ end
88
98
  end
89
99
  end
90
100
  end
@@ -0,0 +1,56 @@
1
+ This is a story about a calculator. The text up here above the Story: declaration
2
+ won't be processed, so you can write whatever you wish!
3
+
4
+ CardNumber: 12
5
+ Estimate: Very Easy
6
+
7
+ Story: simple multiplication
8
+
9
+ As an accountant
10
+ I want to multiply numbers
11
+ So that I can count beans
12
+
13
+ Scenario: multiply one and one
14
+ Given a value of 1
15
+ And a multiplier of 1
16
+
17
+ When the multiplier is applied to the value
18
+
19
+ Then the product should be 1
20
+ And the corks should be popped
21
+
22
+ .
23
+ CardNumber: 15
24
+ Estimate: Very Hard
25
+
26
+ Story: simple subtraction
27
+
28
+ As an accountant
29
+ I want to subtract numbers
30
+ So that I can count beans
31
+
32
+ Scenario: subtract one minus one
33
+ Given an argument of 1
34
+ And an argument of 1
35
+
36
+ When the second argument is subtracted from the first argument
37
+
38
+ Then the different should be 0
39
+ And the corks should be popped
40
+
41
+ Scenario: subtract two from five
42
+ Given an argument of 5
43
+ And an argument of 2
44
+
45
+ When the second argument is subtracted from the first argument
46
+
47
+ Then the difference should be 3
48
+ Then it should snow
49
+
50
+ Scenario: subtract two more
51
+ GivenScenario subtract two from five
52
+ And an argument of 2
53
+
54
+ When the result is reduced by the argument
55
+
56
+ Then the result should be 1
Binary file
Binary file
@@ -0,0 +1,43 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper.rb'
2
+
3
+ module PDF
4
+ class Writer
5
+ def save_as(path)
6
+ #no op
7
+ end
8
+ end
9
+ end
10
+
11
+ describe PDF::Storycards::Writer do
12
+
13
+ before(:all) do
14
+ @single_story_path = File.dirname(__FILE__) + '/../../addition'
15
+ @single_story_with_meta_data_path = File.dirname(__FILE__) + '/../../subtraction_with_metadata'
16
+ @multiple_stories_with_meta_data_path = File.dirname(__FILE__) + '/../../calculator_stories_with_metadata'
17
+ end
18
+
19
+ it "should parse an example plain text story file and create a new story" do
20
+ story_text = File.read(@single_story_path)
21
+ stories = PDF::Storycards::StoryParser.new.parse_stories(story_text)
22
+ stories.first.title.should == 'simple addition'
23
+ stories.first.narrative.should == "As an accountant\nI want to add numbers\nSo that I can count beans"
24
+ end
25
+
26
+ it "should parse extract metadata from plain text story file and set it on the story" do
27
+ story_text = File.read(@single_story_with_meta_data_path)
28
+ stories = PDF::Storycards::StoryParser.new.parse_stories(story_text)
29
+ stories.first.meta_data['CardNumber'].should == '152'
30
+ stories.first.meta_data['Estimate'].should == 'Medium'
31
+ end
32
+
33
+ it "should treat a single . on a line as a separator between story groups" do
34
+ story_text = File.read(@multiple_stories_with_meta_data_path)
35
+ stories = PDF::Storycards::StoryParser.new.parse_stories(story_text)
36
+ stories.first.title.should == 'simple multiplication'
37
+ stories.first.meta_data['CardNumber'].should == '12'
38
+ stories.first.meta_data['Estimate'].should == 'Very Easy'
39
+ stories.last.title.should == 'simple subtraction'
40
+ stories.last.meta_data['CardNumber'].should == '15'
41
+ stories.last.meta_data['Estimate'].should == 'Very Hard'
42
+ end
43
+ end
@@ -0,0 +1,30 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper.rb'
2
+ require 'pathname'
3
+
4
+ describe PDF::Storycards::TextFileFilter do
5
+ before(:all) do
6
+ @text_file_path_1 = File.dirname(__FILE__) + '/../../addition'
7
+ @text_file_clean_path_1 = Pathname.new(@text_file_path_1).cleanpath.to_s
8
+ @text_file_path_2 = File.dirname(__FILE__) + '/../../calculator_stories'
9
+ @non_text_file_path_1 = File.dirname(__FILE__) + '/../../not_a_text_file.jpg'
10
+ @non_text_file_clean_path_1 = Pathname.new(@non_text_file_path_1).cleanpath.to_s
11
+ @non_text_file_path_2 = File.dirname(__FILE__) + '/../../not_a_text_file.gif'
12
+ @directory = File.dirname(__FILE__) + '/../../../'
13
+ end
14
+
15
+ it "should expose an empty collection when initialized with nothing" do
16
+ filter = PDF::Storycards::TextFileFilter.new()
17
+ filter.map.should == []
18
+ end
19
+
20
+ it "should expose a collection of the clean path to one text file when initialized with only that text file" do
21
+ filter = PDF::Storycards::TextFileFilter.new(@text_file_path_1)
22
+ filter.map.should == [@text_file_clean_path_1]
23
+ end
24
+
25
+ it "should expose an empty collection when initialized with a single non-text file" do
26
+ filter = PDF::Storycards::TextFileFilter.new(@non_text_file_path_1)
27
+ filter.map.should == []
28
+ end
29
+
30
+ end
@@ -11,28 +11,22 @@ end
11
11
  describe PDF::Storycards::Writer do
12
12
 
13
13
  before(:all) do
14
- @single_story_path = File.dirname(__FILE__) + '/../../addition'
15
- @multiple_stories_path = File.dirname(__FILE__) + '/../../calculator_stories'
16
- end
17
-
18
- it "should parse an example plain text story file and create a new story" do
19
- stories = PDF::Storycards::Writer.send(:parse_stories, @single_story_path)
20
- stories.first.title.should == 'simple addition'
21
- stories.first.narrative.should == "As an accountant\nI want to add numbers\nSo that I can count beans"
14
+ multiple_stories_path = File.dirname(__FILE__) + '/../../calculator_stories'
15
+ @story_text = File.read(multiple_stories_path)
22
16
  end
23
17
 
24
18
  it "should not print border when printing 1-up" do
25
19
  PDF::Storycards::Writer.should_not_receive(:print_border)
26
- PDF::Storycards::Writer.make_pdf(@multiple_stories_path, 'storycards.pdf', :style => :card_1up)
20
+ PDF::Storycards::Writer.make_pdf(@story_text, :output => 'storycards.pdf', :style => :card_1up)
27
21
  end
28
22
 
29
23
  it "should not print border when printing 4-up with default options" do
30
24
  PDF::Storycards::Writer.should_not_receive(:print_border)
31
- PDF::Storycards::Writer.make_pdf(@multiple_stories_path, 'storycards.pdf', :style => :letter_4up)
25
+ PDF::Storycards::Writer.make_pdf(@story_text, :output => 'storycards.pdf', :style => :letter_4up)
32
26
  end
33
27
 
34
28
  it "should print border when printing 4-up with border option set to true" do
35
29
  PDF::Storycards::Writer.should_receive(:print_border).twice
36
- PDF::Storycards::Writer.make_pdf(@multiple_stories_path, 'storycards.pdf', :style => :letter_4up, :border => true)
30
+ PDF::Storycards::Writer.make_pdf(@story_text, :output => 'storycards.pdf', :style => :letter_4up, :borders => true)
37
31
  end
38
32
  end
@@ -1,3 +1,4 @@
1
+ require 'rubygems'
1
2
  require 'stringio'
2
3
  require 'spec'
3
4
  require 'spec/mocks'
@@ -0,0 +1,40 @@
1
+ This is a story about a calculator. The text up here above the Story: declaration
2
+ won't be processed, so you can write whatever you wish!
3
+
4
+ There is some metadata below. It's also above the Story: declaration, so it won't be
5
+ processed by storyrunner, but pdf-storycards can work with it.
6
+
7
+ CardNumber: 152
8
+ Estimate: Medium
9
+
10
+ Story: simple subtraction
11
+
12
+ As an accountant
13
+ I want to subtract numbers
14
+ So that I can count beans
15
+
16
+ Scenario: subtract one minus one
17
+ Given an argument of 1
18
+ And an argument of 1
19
+
20
+ When the second argument is subtracted from the first argument
21
+
22
+ Then the different should be 0
23
+ And the corks should be popped
24
+
25
+ Scenario: subtract two from five
26
+ Given an argument of 5
27
+ And an argument of 2
28
+
29
+ When the second argument is subtracted from the first argument
30
+
31
+ Then the difference should be 3
32
+ Then it should snow
33
+
34
+ Scenario: subtract two more
35
+ GivenScenario subtract two from five
36
+ And an argument of 2
37
+
38
+ When the result is reduced by the argument
39
+
40
+ Then the result should be 1
metadata CHANGED
@@ -1,21 +1,80 @@
1
1
  --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.4
3
+ specification_version: 1
2
4
  name: pdf-storycards
3
5
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
6
+ version: 0.1.0
7
+ date: 2008-02-10 00:00:00 -05:00
8
+ summary: Utilities for generating printable story cards for agile planning and measurement
9
+ require_paths:
10
+ - lib
11
+ email: luke@lukemelia.com
12
+ homepage: http://rubyforge.org/projects/pdf-storycards/
13
+ rubyforge_project: pdf-storycards
14
+ description: "== DESCRIPTION: Provides a script and library to parse stories saved in the RSpec plain text story format and creates a PDF file with printable 3\"x5\" index cards suitable for using in Agile planning and prioritization. == FEATURES/PROBLEMS: * Create a PDF with each page as a 3x5 sheet, or as 4 cards per 8.5 x 11 sheet * Included script reads stories from STDIN and writes PDF to STDOUT * TODO: Improve test coverage * TODO: Improve documentation == SYNOPSIS: From the command line with stories2cards < /path/to/stories.txt Or via Ruby story_text = File.read('my_story') pdf_content = PDF::Storycards::Writer.make_pdf(story_text, :style => :card_1up) == REQUIREMENTS:"
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
5
25
  platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
6
29
  authors:
7
30
  - Luke Melia
8
- autorequire:
9
- bindir: bin
10
- cert_chain: []
31
+ files:
32
+ - History.txt
33
+ - Manifest.txt
34
+ - README.txt
35
+ - Rakefile
36
+ - bin/stories2cards
37
+ - examples/mingle_cards.rb
38
+ - lib/pdf/storycards.rb
39
+ - lib/pdf/storycards/scenario.rb
40
+ - lib/pdf/storycards/story.rb
41
+ - lib/pdf/storycards/story_capturing_mediator.rb
42
+ - lib/pdf/storycards/story_parser.rb
43
+ - lib/pdf/storycards/text_file_filter.rb
44
+ - lib/pdf/storycards/writer.rb
45
+ - spec/addition
46
+ - spec/calculator_stories
47
+ - spec/calculator_stories_with_metadata
48
+ - spec/not_a_text_file.gif
49
+ - spec/not_a_text_file.jpg
50
+ - spec/pdf/storycards/story_capturing_mediator_spec.rb
51
+ - spec/pdf/storycards/story_parser_spec.rb
52
+ - spec/pdf/storycards/story_spec.rb
53
+ - spec/pdf/storycards/text_file_filter_spec.rb
54
+ - spec/pdf/storycards/writer_spec.rb
55
+ - spec/rspec_suite.rb
56
+ - spec/spec_helper.rb
57
+ - spec/subtraction_with_metadata
58
+ test_files: []
59
+
60
+ rdoc_options:
61
+ - --main
62
+ - README.txt
63
+ extra_rdoc_files:
64
+ - History.txt
65
+ - Manifest.txt
66
+ - README.txt
67
+ executables:
68
+ - stories2cards
69
+ extensions: []
70
+
71
+ requirements: []
11
72
 
12
- date: 2007-12-29 00:00:00 -05:00
13
- default_executable:
14
73
  dependencies:
15
74
  - !ruby/object:Gem::Dependency
16
75
  name: pdf-writer
17
76
  version_requirement:
18
- version_requirements: !ruby/object:Gem::Requirement
77
+ version_requirements: !ruby/object:Gem::Version::Requirement
19
78
  requirements:
20
79
  - - ">="
21
80
  - !ruby/object:Gem::Version
@@ -24,7 +83,7 @@ dependencies:
24
83
  - !ruby/object:Gem::Dependency
25
84
  name: rspec
26
85
  version_requirement:
27
- version_requirements: !ruby/object:Gem::Requirement
86
+ version_requirements: !ruby/object:Gem::Version::Requirement
28
87
  requirements:
29
88
  - - ">="
30
89
  - !ruby/object:Gem::Version
@@ -33,66 +92,9 @@ dependencies:
33
92
  - !ruby/object:Gem::Dependency
34
93
  name: hoe
35
94
  version_requirement:
36
- version_requirements: !ruby/object:Gem::Requirement
95
+ version_requirements: !ruby/object:Gem::Version::Requirement
37
96
  requirements:
38
97
  - - ">="
39
98
  - !ruby/object:Gem::Version
40
- version: 1.4.0
99
+ version: 1.5.0
41
100
  version:
42
- description: "== FEATURES/PROBLEMS: * Create a PDF with each page as a 3x5 sheet, or as 4 cards per 8.5 x 11 sheet * Currently reads stories from a single file. * TODO: Take a directory and find all stories in it * TODO: Take stories via STDIN * TODO: Improve test coverage == SYNOPSIS: StorycardPdfWriter.make_pdf(\"/tmp/stories.txt\", \"/tmp/storycards.pdf\", :style => :card_1up) == REQUIREMENTS:"
43
- email: luke@lukemelia.com
44
- executables:
45
- - stories2cards
46
- extensions: []
47
-
48
- extra_rdoc_files:
49
- - History.txt
50
- - Manifest.txt
51
- - README.txt
52
- files:
53
- - History.txt
54
- - Manifest.txt
55
- - README.txt
56
- - Rakefile
57
- - bin/stories2cards
58
- - lib/pdf/storycards.rb
59
- - lib/pdf/storycards/scenario.rb
60
- - lib/pdf/storycards/story.rb
61
- - lib/pdf/storycards/story_capturing_mediator.rb
62
- - lib/pdf/storycards/writer.rb
63
- - spec/addition
64
- - spec/calculator_stories
65
- - spec/pdf/storycards/story_capturing_mediator_spec.rb
66
- - spec/pdf/storycards/story_spec.rb
67
- - spec/pdf/storycards/writer_spec.rb
68
- - spec/rspec_suite.rb
69
- - spec/spec_helper.rb
70
- has_rdoc: true
71
- homepage: http://www.zenspider.com/ZSS/Products/pdf-storycards/
72
- post_install_message:
73
- rdoc_options:
74
- - --main
75
- - README.txt
76
- require_paths:
77
- - lib
78
- required_ruby_version: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: "0"
83
- version:
84
- required_rubygems_version: !ruby/object:Gem::Requirement
85
- requirements:
86
- - - ">="
87
- - !ruby/object:Gem::Version
88
- version: "0"
89
- version:
90
- requirements: []
91
-
92
- rubyforge_project: pdf-storycards
93
- rubygems_version: 1.0.1
94
- signing_key:
95
- specification_version: 2
96
- summary: Utilities for generating printable story cards for agile planning and measurement
97
- test_files: []
98
-