eslurper 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +138 -0
- data/bin/eslurp +29 -0
- data/lib/cacert.pem +3509 -0
- data/lib/slurper.rb +66 -0
- data/lib/story.rb +98 -0
- data/spec/slurper_spec.rb +79 -0
- data/spec/story_spec.rb +110 -0
- metadata +107 -0
data/lib/slurper.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'story'
|
3
|
+
|
4
|
+
YAML::ENGINE.yamler='syck' if RUBY_VERSION > '1.9'
|
5
|
+
|
6
|
+
|
7
|
+
class Slurper
|
8
|
+
|
9
|
+
attr_accessor :story_file, :stories
|
10
|
+
|
11
|
+
def self.slurp(story_file, reverse)
|
12
|
+
slurper = new(story_file)
|
13
|
+
slurper.load_stories
|
14
|
+
slurper.prepare_stories
|
15
|
+
slurper.sort_stories(reverse)
|
16
|
+
slurper.create_stories
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(story_file)
|
20
|
+
self.story_file = story_file
|
21
|
+
end
|
22
|
+
|
23
|
+
def load_stories
|
24
|
+
self.stories = YAML.load(yamlize_story_file)
|
25
|
+
end
|
26
|
+
|
27
|
+
def prepare_stories
|
28
|
+
stories.each { |story| story.prepare }
|
29
|
+
end
|
30
|
+
|
31
|
+
def sort_stories(reverse)
|
32
|
+
stories.sort!
|
33
|
+
stories.reverse! unless reverse
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_stories
|
37
|
+
puts "Preparing to slurp #{stories.size} stories into Tracker..."
|
38
|
+
stories.each_with_index do |story, index|
|
39
|
+
begin
|
40
|
+
if story.save
|
41
|
+
puts "#{index+1}. #{story.name}"
|
42
|
+
else
|
43
|
+
puts "Slurp failed. #{story.errors.full_messages}"
|
44
|
+
end
|
45
|
+
rescue ActiveResource::ConnectionError => e
|
46
|
+
msg = "Slurp failed on story "
|
47
|
+
if story.attributes["name"]
|
48
|
+
msg << story.attributes["name"]
|
49
|
+
else
|
50
|
+
msg << "##{options[:reverse] ? index + 1 : stories.size - index }"
|
51
|
+
end
|
52
|
+
puts msg + ". Error: #{e}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def yamlize_story_file
|
60
|
+
IO.read(story_file).
|
61
|
+
gsub(/^/, " ").
|
62
|
+
gsub(/ ==.*/, "- !ruby/object:Story\n attributes:").
|
63
|
+
gsub(/ description:$/, " description: |")
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
data/lib/story.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'active_resource'
|
2
|
+
|
3
|
+
class Story < ActiveResource::Base
|
4
|
+
include Comparable
|
5
|
+
|
6
|
+
def self.yaml
|
7
|
+
YAML.load_file('slurper_config.yml')
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.config(project_id='project_id')
|
11
|
+
@@config = yaml
|
12
|
+
scheme = if !!@@config['ssl']
|
13
|
+
self.ssl_options = { :verify_mode => OpenSSL::SSL::VERIFY_PEER,
|
14
|
+
:ca_file => File.join(File.dirname(__FILE__), "cacert.pem") }
|
15
|
+
"https"
|
16
|
+
else
|
17
|
+
"http"
|
18
|
+
end
|
19
|
+
self.site = "#{scheme}://www.pivotaltracker.com/services/v3/projects/:project_id"
|
20
|
+
# self.site = "#{scheme}://www.pivotaltracker.com/services/v3/projects/#{@@config[project_id]}"
|
21
|
+
@@config
|
22
|
+
end
|
23
|
+
|
24
|
+
headers['X-TrackerToken'] = config.delete("token")
|
25
|
+
headers['Accept-Encoding'] = '*/*'
|
26
|
+
self.format = ActiveResource::Formats::XmlFormat
|
27
|
+
|
28
|
+
def prepare
|
29
|
+
scrub_description
|
30
|
+
scrub_labels
|
31
|
+
default_requested_by
|
32
|
+
self.description += "\n" + "Label Search\n" + label_search_url + "\n\n" + story_links.join("\n") if epic_story?
|
33
|
+
end
|
34
|
+
|
35
|
+
def epic_story?
|
36
|
+
respond_to?(:labels) ? self.labels.split(',').include?("epic") : false
|
37
|
+
end
|
38
|
+
|
39
|
+
def feature_labels
|
40
|
+
self.labels.split(',').find { |label| label != "epic" }
|
41
|
+
end
|
42
|
+
|
43
|
+
def save
|
44
|
+
@config ||= Story.yaml
|
45
|
+
self.prefix_options = {:project_id => @config['project_id']}
|
46
|
+
super
|
47
|
+
end
|
48
|
+
|
49
|
+
def <=>(another_story)
|
50
|
+
if self.epic_story? && another_story.epic_story?
|
51
|
+
0
|
52
|
+
elsif self.epic_story? && !another_story.epic_story?
|
53
|
+
-1
|
54
|
+
elsif !self.epic_story? && !another_story.epic_story?
|
55
|
+
0
|
56
|
+
else
|
57
|
+
1
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
protected
|
62
|
+
|
63
|
+
def label_search_url
|
64
|
+
@config ||= Story.yaml
|
65
|
+
"https://www.pivotaltracker.com/projects/#{@config['epic_features_project_id']}?label=" + feature_labels
|
66
|
+
end
|
67
|
+
|
68
|
+
def story_links
|
69
|
+
retrieve_user_stories.map { |story| story.url }
|
70
|
+
end
|
71
|
+
|
72
|
+
def retrieve_user_stories
|
73
|
+
@config ||= Story.yaml
|
74
|
+
stories = Story.find(:all, :params => {:filter => 'label:' + feature_labels, :project_id => @config['epic_features_project_id']})
|
75
|
+
end
|
76
|
+
|
77
|
+
def scrub_description
|
78
|
+
if respond_to?(:description)
|
79
|
+
self.description = description.gsub(" ", "").gsub(" \n", "\n")
|
80
|
+
end
|
81
|
+
if respond_to?(:description) && description == ""
|
82
|
+
self.attributes["description"] = nil
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def scrub_labels
|
87
|
+
if respond_to?(:labels)
|
88
|
+
self.labels = labels.gsub(" ", "").gsub(" \n", "\n")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def default_requested_by
|
93
|
+
if (!respond_to?(:requested_by) || requested_by == "") && Story.config["requested_by"]
|
94
|
+
self.attributes["requested_by"] = Story.config["requested_by"]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'slurper'
|
3
|
+
|
4
|
+
describe Slurper do
|
5
|
+
|
6
|
+
context "deals with leading/trailing whitespace" do
|
7
|
+
before do
|
8
|
+
slurper = Slurper.new(File.join(File.dirname(__FILE__), "fixtures", "whitespacey_story.slurper"))
|
9
|
+
slurper.load_stories
|
10
|
+
@story = slurper.stories.first
|
11
|
+
end
|
12
|
+
|
13
|
+
it "strips whitespace from the name" do
|
14
|
+
@story.name.should == "Profit"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context "given an epic feature" do
|
19
|
+
before do
|
20
|
+
slurper = Slurper.new(File.join(File.dirname(__FILE__), "fixtures", "epic_story.slurper"))
|
21
|
+
slurper.load_stories
|
22
|
+
@story = slurper.stories.first
|
23
|
+
end
|
24
|
+
|
25
|
+
it "knows it's an epic story" do
|
26
|
+
@story.epic_story? == true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context "given values for all attributes" do
|
31
|
+
before do
|
32
|
+
slurper = Slurper.new(File.join(File.dirname(__FILE__), "fixtures", "full_story.slurper"))
|
33
|
+
slurper.load_stories
|
34
|
+
@story = slurper.stories.first
|
35
|
+
end
|
36
|
+
|
37
|
+
it "parses the name correctly" do
|
38
|
+
@story.name.should == "Profit"
|
39
|
+
end
|
40
|
+
|
41
|
+
it "parses the label correctly" do
|
42
|
+
@story.labels.should == "money,power,fame"
|
43
|
+
end
|
44
|
+
|
45
|
+
it "parses the story type correctly" do
|
46
|
+
@story.story_type.should == "feature"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "given only a name" do
|
51
|
+
before do
|
52
|
+
slurper = Slurper.new(File.join(File.dirname(__FILE__), "fixtures", "name_only.slurper"))
|
53
|
+
slurper.load_stories
|
54
|
+
@story = slurper.stories.first
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should parse the name correctly" do
|
58
|
+
@story.name.should == "Profit"
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
context "given empty attributes" do
|
64
|
+
before do
|
65
|
+
slurper = Slurper.new(File.join(File.dirname(__FILE__), "fixtures", "empty_attributes.slurper"))
|
66
|
+
slurper.load_stories
|
67
|
+
@story = slurper.stories.first
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should not set any name" do
|
71
|
+
@story.name.should be_nil
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should not set any labels" do
|
75
|
+
@story.labels.should be_nil
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
data/spec/story_spec.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'slurper'
|
3
|
+
|
4
|
+
describe Story do
|
5
|
+
|
6
|
+
context "#prepare" do
|
7
|
+
it "scrubs the description" do
|
8
|
+
story = Story.new
|
9
|
+
story.stub!(:default_requested_by)
|
10
|
+
story.should_receive(:scrub_description)
|
11
|
+
story.prepare
|
12
|
+
end
|
13
|
+
|
14
|
+
it "scrubs the defaults the requested_by attribute" do
|
15
|
+
story = Story.new
|
16
|
+
story.stub!(:scrub_description)
|
17
|
+
story.should_receive(:default_requested_by)
|
18
|
+
story.prepare
|
19
|
+
end
|
20
|
+
|
21
|
+
it "uses http by default" do
|
22
|
+
Story.site.scheme.should == "http"
|
23
|
+
Story.yaml['ssl'].should be_nil
|
24
|
+
end
|
25
|
+
|
26
|
+
it "uses https if set in the config" do
|
27
|
+
Story.stub(:yaml => {"ssl" => true})
|
28
|
+
Story.config['ssl'].should be_true
|
29
|
+
Story.site.scheme.should == "https"
|
30
|
+
Story.ssl_options[:verify_mode].should == 1
|
31
|
+
|
32
|
+
# Not sure what this next line is testing
|
33
|
+
File.open(File.expand_path('lib/cacert.pem')).readlines.find_all{ |l| l.starts_with?("Equifax") }.count.should == 4
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "requested_by attribute" do
|
38
|
+
it "uses the default if not given one" do
|
39
|
+
Story.stub!(:config).and_return({"requested_by" => "Mr. Client"})
|
40
|
+
story = Story.new
|
41
|
+
story.send(:default_requested_by)
|
42
|
+
story.requested_by.should == "Mr. Client"
|
43
|
+
end
|
44
|
+
|
45
|
+
it "uses the default if given a blank requested_by" do
|
46
|
+
Story.stub!(:config).and_return({"requested_by" => "Mr. Client"})
|
47
|
+
story = Story.new(:requested_by => "")
|
48
|
+
story.send(:default_requested_by)
|
49
|
+
story.requested_by.should == "Mr. Client"
|
50
|
+
end
|
51
|
+
|
52
|
+
it "uses the name given in the story file if there is one" do
|
53
|
+
Story.stub!(:config).and_return({"requested_by" => "Mr. Client"})
|
54
|
+
story = Story.new(:requested_by => "Mr. Stakeholder")
|
55
|
+
story.send(:default_requested_by)
|
56
|
+
story.requested_by.should == "Mr. Stakeholder"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context "scrubs the descriptions correctly" do
|
61
|
+
it "when the description is blank" do
|
62
|
+
story = Story.new(:description => "")
|
63
|
+
story.send(:scrub_description)
|
64
|
+
story.description.should be_nil
|
65
|
+
end
|
66
|
+
|
67
|
+
it "when there is no description given" do
|
68
|
+
story = Story.new
|
69
|
+
lambda {
|
70
|
+
story.send(:scrub_description)
|
71
|
+
}.should_not raise_error
|
72
|
+
end
|
73
|
+
|
74
|
+
it "when it contains quotes" do
|
75
|
+
desc = <<-STRING
|
76
|
+
I have a "quote"
|
77
|
+
STRING
|
78
|
+
story = Story.new(:description => desc)
|
79
|
+
story.send(:scrub_description)
|
80
|
+
story.description.should == "I have a \"quote\"\n"
|
81
|
+
end
|
82
|
+
|
83
|
+
it "when it is full of whitespace" do
|
84
|
+
desc = <<-STRING
|
85
|
+
In order to do something
|
86
|
+
As a role
|
87
|
+
I want to click a thingy
|
88
|
+
STRING
|
89
|
+
story = Story.new(:description => desc)
|
90
|
+
story.send(:scrub_description)
|
91
|
+
story.description.should == "In order to do something\nAs a role\nI want to click a thingy\n"
|
92
|
+
end
|
93
|
+
|
94
|
+
it "when it contains acceptance criteria" do
|
95
|
+
desc = <<-STRING
|
96
|
+
In order to do something
|
97
|
+
As a role
|
98
|
+
I want to click a thingy
|
99
|
+
|
100
|
+
Acceptance:
|
101
|
+
- do the thing
|
102
|
+
- don't forget the other thing
|
103
|
+
STRING
|
104
|
+
story = Story.new(:description => desc)
|
105
|
+
story.send(:scrub_description)
|
106
|
+
story.description.should == "In order to do something\nAs a role\nI want to click a thingy\n\nAcceptance:\n- do the thing\n- don't forget the other thing\n"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: eslurper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Wes Gibbs
|
9
|
+
- Adam Lowe
|
10
|
+
- Stephen Caudill
|
11
|
+
- Tim Pope
|
12
|
+
- Jeffrey Walter
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
date: 2011-11-21 00:00:00.000000000Z
|
17
|
+
dependencies:
|
18
|
+
- !ruby/object:Gem::Dependency
|
19
|
+
name: activeresource
|
20
|
+
requirement: &70340789669980 !ruby/object:Gem::Requirement
|
21
|
+
none: false
|
22
|
+
requirements:
|
23
|
+
- - ~>
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 3.1.2
|
26
|
+
type: :runtime
|
27
|
+
prerelease: false
|
28
|
+
version_requirements: *70340789669980
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: rspec
|
31
|
+
requirement: &70340789669360 !ruby/object:Gem::Requirement
|
32
|
+
none: false
|
33
|
+
requirements:
|
34
|
+
- - ~>
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: 1.3.0
|
37
|
+
type: :development
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: *70340789669360
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: ruby-debug19
|
42
|
+
requirement: &70340789668760 !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.11.6
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: *70340789668760
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
name: configuration
|
53
|
+
requirement: &70340789668160 !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ~>
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: 1.2.0
|
59
|
+
type: :development
|
60
|
+
prerelease: false
|
61
|
+
version_requirements: *70340789668160
|
62
|
+
description: ! "\n Slurps stories from the given file (stories.slurper by default)
|
63
|
+
and creates Pivotal Tracker stories from them. Useful during story carding sessions
|
64
|
+
when you want to capture a number of stories quickly without clicking your way through
|
65
|
+
the Tracker UI.\n "
|
66
|
+
email: jeffrey.walter@ericsson.com
|
67
|
+
executables:
|
68
|
+
- eslurp
|
69
|
+
extensions: []
|
70
|
+
extra_rdoc_files:
|
71
|
+
- README.rdoc
|
72
|
+
files:
|
73
|
+
- bin/eslurp
|
74
|
+
- lib/slurper.rb
|
75
|
+
- lib/story.rb
|
76
|
+
- lib/cacert.pem
|
77
|
+
- README.rdoc
|
78
|
+
- spec/slurper_spec.rb
|
79
|
+
- spec/story_spec.rb
|
80
|
+
homepage: http://github.com/jeffreywalter/slurper
|
81
|
+
licenses: []
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options:
|
84
|
+
- --charset=UTF-8
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ! '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: 1.3.6
|
99
|
+
requirements: []
|
100
|
+
rubyforge_project:
|
101
|
+
rubygems_version: 1.8.10
|
102
|
+
signing_key:
|
103
|
+
specification_version: 3
|
104
|
+
summary: takes a formatted story file and puts it on Pivotal Tracker
|
105
|
+
test_files:
|
106
|
+
- spec/slurper_spec.rb
|
107
|
+
- spec/story_spec.rb
|