localshred-track-r 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ log/*
2
+ *.swp
3
+ Session.vim
4
+ test/**.yml
5
+ tmp/*
data/README.textile ADDED
@@ -0,0 +1,93 @@
1
+ h1. What is Track-R?
2
+
3
+ Track-R is a library that intends to extend PivotalTracker's API to a higher
4
+ level using Ruby.
5
+
6
+ h2. Installation
7
+
8
+ <pre><code>
9
+ $ gem sources -a http://gems.github.com (you only have to do this once)
10
+ $ sudo gem install jfgomez86-track-r
11
+ </code></pre>
12
+
13
+ h2. Usage
14
+
15
+ To use Track-R you first need to have a valid account on
16
+ "PivotalTracker":http://www.pivotaltracker.com. You also need to have an api
17
+ token. To generate a token, use the "Create New Token" link on the My Profile
18
+ page.
19
+
20
+ If you don't want to generate a token, you can also use Track-R's token
21
+ wrapper:
22
+
23
+ <pre><code>
24
+ token = Token.new(:username => my_username, :password => my_password)
25
+ </code></pre>
26
+
27
+ And use this object to pass it to all the following methods.
28
+
29
+ The next step is to require the library itself:
30
+
31
+ <pre><code>
32
+ require 'track-r'
33
+ </code></pre>
34
+
35
+ Lastly you can fetch your project by creating a new Project instance:
36
+
37
+ <pre><code>
38
+ my_project = Project.new(:project_id => my_project_id, :token => token)
39
+ </code></pre>
40
+
41
+ Want to see it's stories? Then running this:
42
+
43
+ <pre><code>
44
+ my_project.stories
45
+ </code></pre>
46
+
47
+ Will do the job. But you probably want to fetch the backlog stories:
48
+
49
+ <pre><code>
50
+ my_project.backlog
51
+ </code></pre>
52
+
53
+ There you go.
54
+
55
+ Want to create a new story on that project? You have two choices:
56
+
57
+ <pre><code>
58
+ my_project.create_story(:story_type => value, :estimate => value, :current_state => value, :description => value, :name => value, :requested_by => value, :owned_by => value, :created_at => value, :accepted_at => value, :labels)
59
+ </code></pre>
60
+
61
+ Note that most arguments are optional, but please at least specify a name
62
+ and a description.
63
+
64
+ Want to modify the story? Try this:
65
+
66
+ <pre><code>
67
+ story = my_project.story(my_story_id)
68
+ story.name = "A new cool name"
69
+ story.story_type = "Bug"
70
+ story.description = "This is really cool"
71
+ story.save
72
+ </code></pre>
73
+
74
+ Don't like that story anymore? Delete it:
75
+
76
+ <pre><code>
77
+ story.destroy
78
+ </code></pre>
79
+
80
+ Or
81
+
82
+ <pre><code>
83
+ my_project.delete_story(story)
84
+ </code></pre>
85
+
86
+ Or
87
+
88
+ <pre><code>
89
+ my_project.delete_story(story.id)
90
+ </code></pre>
91
+
92
+ I think this should get you started. Find any bugs? Please use github's Issue
93
+ tracker.
data/Rakefile ADDED
@@ -0,0 +1,63 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+ $:.unshift File.join(File.dirname(__FILE__), '/vendor/')
3
+ require 'rubygems'
4
+ require 'rake/gempackagetask'
5
+
6
+ begin
7
+ require 'rake'
8
+ require 'rake/testtask'
9
+ rescue LoadError
10
+ puts 'This script should only be accessed via the "rake" command.'
11
+ puts 'Installation: gem install rake -y'
12
+ exit
13
+ end
14
+ require 'rake/clean'
15
+
16
+ task :default => [:test]
17
+
18
+ Rake::TestTask.new do |t|
19
+ t.libs << "test"
20
+ t.test_files = FileList['test/**/*_test.rb']
21
+ t.verbose = true
22
+ t.warning = false
23
+ #t.options = '--runner=gtk2'
24
+ end
25
+
26
+ namespace :gems do
27
+ desc "Install gems listed in config/gems.yml"
28
+ task :install do
29
+ if !`whoami`.match(/root/)
30
+ print "WARNING: You should be running this as root.\n"
31
+ print "Do you want to continue?(y/N) "
32
+ opt = STDIN.gets
33
+ fail "Not authorized" unless opt.match(/^y/i)
34
+ end
35
+ require 'install-gems/install-gems.rb'
36
+ InstallGems.new(File.join(File.dirname(__FILE__),'/config/gems.yml'))
37
+ end
38
+ end
39
+
40
+ begin
41
+ require 'jeweler'
42
+ Jeweler::Tasks.new do |gemspec|
43
+ gemspec.name = "track-r"
44
+ gemspec.summary = "A wrapper library for pivotal tracker's API"
45
+ gemspec.email = "moc.liamg@68zemogfj".reverse
46
+ gemspec.homepage = "http://github.com/jfgomez86/Track-R"
47
+ gemspec.description = "track-r is a library that provides wrapper classes and methods for accessing PivotalTracker's public API."
48
+ gemspec.version = "1.0.0"
49
+ gemspec.authors = [ "Jose Felix Gomez" ]
50
+ end
51
+ rescue LoadError
52
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
53
+ end
54
+
55
+ #Rake::GemPackageTask.new(spec) do |pkg|
56
+ # pkg.need_tar = true
57
+ #end
58
+
59
+ #namespace :gem do
60
+ # task :build => "pkg/#{spec.name}-#{spec.version}.gem" do
61
+ # puts "generated latest version"
62
+ # end
63
+ #end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
data/config/config.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :site_location: http://www.pivotaltracker.com
3
+ :api_location: http://www.pivotaltracker.com/services/v2
4
+ :api_token: cef78bdff8f2de14c17590e3810a281c
@@ -0,0 +1,3 @@
1
+ require 'rubygems'
2
+ require 'hpricot'
3
+ require 'open-uri'
data/config/gems.yml ADDED
@@ -0,0 +1,5 @@
1
+ ---
2
+ - :name: hpricot
3
+ - :name: thoughtbot-shoulda
4
+ :source: http://gems.github.com
5
+
data/lib/track-r.rb ADDED
@@ -0,0 +1,13 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+ require File.join(File.dirname(__FILE__), '/../config/environment.rb')
3
+ require 'track-r/project'
4
+ require 'track-r/iteration'
5
+ require 'track-r/story'
6
+ require 'track-r/token'
7
+ require 'track-r/tracker'
8
+ require 'cgi'
9
+ require 'net/http'
10
+ require 'timeout'
11
+
12
+ # Load configuration globals
13
+ CONFIG = YAML.load_file(File.join(File.dirname(__FILE__), '/../config/config.yml'))
@@ -0,0 +1,20 @@
1
+ # Generic HTTP wrapper to make calls to the API
2
+ module APIConnector
3
+ attr_accessor :request, :response
4
+ attr_reader :error_message
5
+
6
+ def connect(options={})
7
+ raise(ArgumentError, "You must specify a path to connect to") if !options.include?(:path)
8
+ begin
9
+ @request = APIConnector::Request.new(options)
10
+ @response = @request.connect
11
+ rescue Timeout::Error
12
+ @error_message = "Connection timed out"
13
+ end
14
+ end
15
+
16
+ def error?
17
+ !@error_message.nil?
18
+ end
19
+
20
+ end
@@ -0,0 +1,31 @@
1
+ module APIConnector
2
+ class Request
3
+ attr_reader :token, :path, :headers, :body, :connection_length, :error_message
4
+
5
+ def initialize(options={})
6
+ @token = options[:token] || Token.new
7
+ @path = options[:path] || "/"
8
+ @connection_length = options[:connection_length].to_i || 15 # timeout
9
+ @headers = default_headers.merge! options
10
+ end
11
+
12
+ def connect
13
+ Timeout::timeout(@connection_length) {
14
+ return APIConnector::Response.new(:body => open(api_url, @headers))
15
+ }
16
+ end
17
+
18
+ def error?
19
+ !@error_message.nil?
20
+ end
21
+
22
+ private
23
+
24
+ def default_headers
25
+ {
26
+ "X-TrackerToken" => @token.to_s
27
+ }
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ # Generic HTTP wrapper to make calls to the API
2
+ module APIConnector
3
+ class Response
4
+ attr_reader :headers, :body, :error_message
5
+
6
+ def initialize(options={})
7
+ @headers = options[:headers] || nil
8
+ @body = options[:body] || nil
9
+ @error_message = options[:error_message] || nil
10
+ end
11
+
12
+ def to_h
13
+ Hpricot(@body)
14
+ end
15
+
16
+ def error?
17
+ @connection_error
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,85 @@
1
+ class Iteration
2
+ attr_reader :id, :project_id, :number, :type, :start_date, :finish_date, :stories, :limit, :offset, :token
3
+
4
+ def self.find(options={})
5
+ @token = options[:token] || Token.new
6
+ if options.include?(:project_id)
7
+ @project_id = options[:project_id]
8
+ api_url = "#{CONFIG[:api_location]}/projects/#{@project_id}/iterations/#{options.include?(:type) ? options[:type] : ""}"
9
+ iterations = (Hpricot(open(api_url, {"X-TrackerToken" => @token.to_s}))/'iteration').map do |iteration|
10
+ Iteration.new(:iteration => iteration, :project_id => @project_id)
11
+ end
12
+ end
13
+ end
14
+
15
+ def initialize(options={})
16
+ @token = options[:token] || Token.new
17
+ if options.include?(:project_id) && options.include?(:iteration_id)
18
+ @id = options[:iteration_id]
19
+ @project_id = options[:project_id]
20
+ @number = options[:number]
21
+ @type = assign_type(options[:type])
22
+ @limit = options[:limit]
23
+ @offset = options[:offset]
24
+ @api_url = build_api_url
25
+ @iteration = Hpricot(open(@api_url, {"X-TrackerToken" => @token}))
26
+ elsif options.include?(:iteration) && options.include?(:project_id)
27
+ @project_id = options[:project_id]
28
+ @iteration = options[:iteration]
29
+ else
30
+ raise ArgumentError, "Valid options are: :iteration (receives an Hpricot Object) + :project_id OR :project_id + :iteration_id"
31
+ end
32
+ build_iteration
33
+ end
34
+
35
+ def start_date(format="%m/%d/%Y")
36
+ @finish_date.strftime(format)
37
+ end
38
+
39
+ def finish_date(format="%m/%d/%Y")
40
+ @finish_date.strftime(format)
41
+ end
42
+
43
+ private
44
+
45
+ def build_iteration
46
+ @id ||= @iteration.at('id').inner_html
47
+ @number = @iteration.at('number').inner_html
48
+ @start_date = Date.parse @iteration.at('start').inner_html
49
+ @finish_date = Date.parse @iteration.at('finish').inner_html
50
+ @stories = build_stories
51
+ end
52
+
53
+ def build_stories
54
+ @stories = (@iteration/'story').map do |story|
55
+ Story.new(:story => story, :project_id => @id, :token => @token)
56
+ end
57
+ end
58
+
59
+ def build_api_url
60
+ query_string = attributes.map { |key, value| "#{key.to_s}=#{CGI::escape(value)}" unless value.nil?}.compact.join('&')
61
+ url = "#{CONFIG[:api_location]}/projects/#{@project_id}/iterations/#{@type}?"+query_string
62
+ # if !@offset.nil? || !@limit.nil?
63
+ # url += "?"
64
+ # url += "offset=#{@offset.to_s}&" if !@offset.nil?
65
+ # url += "limit=#{@limit.to_s}&" if !@limit.nil?
66
+ # end
67
+ # url
68
+ end
69
+
70
+ def assign_type(type=nil)
71
+ use_type = nil
72
+ %w{ done current backlog }.each do |valid_type|
73
+ use_type = valid_type if /#{type}/i =~ valid_type
74
+ end
75
+ use_type
76
+ end
77
+
78
+ def attributes
79
+ {
80
+ :limit => @limit,
81
+ :offset => @offset
82
+ }
83
+ end
84
+
85
+ end
@@ -0,0 +1,102 @@
1
+ # Container for project's attributes.
2
+ # Receives a hash with either :project key pointing to an hpricot object or a
3
+ # project_id and a token with which to fetch and build the project object
4
+ class Project
5
+ attr_reader :name, :iteration_length, :id, :week_start_day, :point_scale,
6
+ :api_url, :url, :token
7
+
8
+ def initialize(options = {})
9
+ if options.include?(:project_id)
10
+ @id = options[:project_id]
11
+ @token = options[:token].to_s || CONFIG[:api_token]
12
+ @api_url = "#{CONFIG[:api_location]}/projects/#{@id}"
13
+ @url = "#{CONFIG[:site_location]}/projects/#{@id}"
14
+ @project = Hpricot(open(@api_url, {"X-TrackerToken" => @token}))
15
+ @stories = nil
16
+ elsif options.include?(:project)
17
+ @project = options[:project]
18
+ else
19
+ raise ArgumentError, "Valid options are: :project (receives an Hpricot Object) OR :project_id + :token"
20
+ end
21
+ build_project
22
+ end
23
+
24
+ # Builds an array containing the project's story
25
+ def stories ; @stories || get_stories ; end
26
+
27
+ # Fetches a story with given id
28
+ def story(id)
29
+ Story.new(:story_id => id, :project_id => @id, :token => @token)
30
+ end
31
+
32
+ # Creates a story for this project. Receives a set of valid attributes.
33
+ # Returns a Story object
34
+ # TODO: Validate attributes
35
+ def create_story(attributes = {})
36
+ api_url = URI.parse("#{CONFIG[:api_location]}/projects/#{@id}/stories")
37
+ query_string = attributes.map { |key, value| "story[#{key}]=#{CGI::escape(value)}"}.join('&')
38
+ response = Net::HTTP.start(api_url.host, api_url.port) do |http|
39
+ http.post(api_url.path, query_string.concat("&token=#{@token}"))
40
+ end
41
+
42
+ story = (Hpricot(response.body)/:story)
43
+ Story.new(:story => story, :project_id => @id, :token => @token)
44
+ end
45
+
46
+ # Deletes a story given a Story object or a story_id
47
+ def delete_story(story)
48
+ if story.is_a?(Story)
49
+ api_url = URI.parse("#{CONFIG[:api_location]}/projects/#{@id}/stories/#{story.id}")
50
+ elsif story.is_a?(Integer) || story.to_i.is_a?(Integer)
51
+ api_url = URI.parse("#{CONFIG[:api_location]}/projects/#{@id}/stories/#{story}")
52
+ else
53
+ raise ArgumentError, "Should receive a story id or a Story object."
54
+ end
55
+ response = Net::HTTP.start(api_url.host, api_url.port) do |http|
56
+ http.delete(api_url.path, {"X-TrackerToken" => @token})
57
+ end
58
+ story = (Hpricot(response.body)/:story)
59
+ Story.new(:story => story, :project_id => @id, :token => @token)
60
+ end
61
+
62
+ def get_iterations(iteration_type=nil)
63
+ Iteration.find(:project_id => @id, :type => type)
64
+ end
65
+
66
+ # Gets the stories for a given iteration
67
+ def get_iteration_stories(iteration_type=nil)
68
+ get_iterations(type).stories
69
+ end
70
+
71
+ # Gets the current iteration's stories
72
+ # def current_stories
73
+ # get_stories_by_iteration("current")
74
+ # end
75
+
76
+ protected
77
+
78
+ # Builds a project given an hpricot object stored at instance variable
79
+ # @project
80
+ def build_project
81
+ @id ||= @project.at('id').inner_html
82
+ @api_url ||= "#{CONFIG[:api_location]}/projects/#{@id}"
83
+ @url ||= "http://www.pivotaltracker.com/projects/#{@id}"
84
+ @name = @project.at('name').inner_html
85
+ @iteration_length = @project.at('iteration_length').inner_html
86
+ @week_start_day = @project.at('week_start_day').inner_html
87
+ @point_scale = @project.at('point_scale').inner_html.split(',')
88
+ end
89
+
90
+ # Builds an array containing the project's stories
91
+ def get_stories
92
+ api_url = "#{CONFIG[:api_location]}/projects/#{@id}/stories"
93
+ @stories = (Hpricot(open(api_url, {"X-TrackerToken" => @token.to_s}))/:story).map {|story| Story.new(:story => story, :project_id => @id, :token => @token)}
94
+ end
95
+
96
+ # Builds an array containing the project's stories for a given iteration
97
+ # def get_stories_by_iteration(name)
98
+ # api_url = "#{CONFIG[:api_location]}/projects/#{@id}/iterations/#{name}"
99
+ # @stories = (Hpricot(open(api_url, {"X-TrackerToken" => @token.to_s}))/:story).map {|story| Story.new(:story => story, :project_id => @id, :token => @token)}
100
+ # end
101
+
102
+ end # class Tracker::Project
@@ -0,0 +1,122 @@
1
+ # Container for project's attributes.
2
+ # Receives a hash with either :project key pointing to an hpricot object or a
3
+ # project_id and a token with which to fetch and build the project object
4
+ class Project
5
+ attr_reader :name, :iteration_length, :id, :week_start_day, :point_scale, :api_url, :url, :token, :meta
6
+
7
+ def initialize(options = {})
8
+ @token = options[:token] || Token.new
9
+ if options.include?(:project_id)
10
+ @id = options[:project_id]
11
+ @api_url = "#{CONFIG[:api_location]}/projects/#{@id}"
12
+ @url = "#{CONFIG[:site_location]}/projects/#{@id}"
13
+ @project = Hpricot(open(@api_url, {"X-TrackerToken" => @token.to_s}))
14
+ @stories = nil
15
+ elsif options.include?(:project)
16
+ @project = options[:project]
17
+ else
18
+ raise ArgumentError, "Valid options are: :project (receives an Hpricot Object) OR :project_id + :token"
19
+ end
20
+ build_project
21
+ end
22
+
23
+ # Builds an array containing the project's stories
24
+ def stories ; @stories || get_stories ; end
25
+
26
+ def iterations ; @iterations || get_iterations ; end
27
+
28
+ # Fetches a story with given id
29
+ def story(id)
30
+ Story.new(:story_id => id, :project_id => @id, :token => @token.to_s)
31
+ end
32
+
33
+ # Creates a story for this project. Receives a set of valid attributes.
34
+ # Returns a Story object
35
+ # TODO: Validate attributes
36
+ def create_story(attributes = {})
37
+ api_url = URI.parse("#{CONFIG[:api_location]}/projects/#{@id}/stories")
38
+ query_string = attributes.map { |key, value| "story[#{key}]=#{CGI::escape(value)}"}.join('&')
39
+ response = Net::HTTP.start(api_url.host, api_url.port) do |http|
40
+ http.post(api_url.path, query_string.concat("&token=#{@token.to_s}"))
41
+ end
42
+
43
+ story = (Hpricot(response.body)/:story)
44
+ Story.new(:story => story, :project_id => @id, :token => @token.to_s)
45
+ end
46
+
47
+ # Deletes a story given a Story object or a story_id
48
+ def delete_story(story)
49
+ if story.is_a?(Story)
50
+ api_url = URI.parse("#{CONFIG[:api_location]}/projects/#{@id}/stories/#{story.id}")
51
+ elsif story.is_a?(Integer) || story.to_i.is_a?(Integer)
52
+ api_url = URI.parse("#{CONFIG[:api_location]}/projects/#{@id}/stories/#{story}")
53
+ else
54
+ raise ArgumentError, "Should receive a story id or a Story object."
55
+ end
56
+ response = Net::HTTP.start(api_url.host, api_url.port) do |http|
57
+ http.delete(api_url.path, {"X-TrackerToken" => @token.to_s})
58
+ end
59
+ story = (Hpricot(response.body)/:story)
60
+ Story.new(:story => story, :project_id => @id, :token => @token.to_s)
61
+ end
62
+
63
+ def get_iterations(iteration_type=nil)
64
+ @iterations = Iteration.find(:project_id => @id, :type => iteration_type) || []
65
+ end
66
+
67
+ # Gets the stories for a given iteration
68
+ def get_iteration_stories(iteration_type=nil)
69
+ @stories = get_iterations(iteration_type).stories
70
+ end
71
+
72
+ def num_stories
73
+ stories.size || 0
74
+ end
75
+
76
+ def num_completed_stories
77
+ num_completed_stories = 0
78
+ stories.each do |story|
79
+ num_completed_stories += 1 if completed_statuses.include?(story.current_state)
80
+ end
81
+ num_completed_stories
82
+ end
83
+
84
+ def current_target_date
85
+ get_iterations.last.finish_date
86
+ end
87
+
88
+ def completed_statuses
89
+ %w{ finished delivered accepted }
90
+ end
91
+
92
+ # Builds an array containing the project's stories
93
+ protected
94
+
95
+ # Builds a project given an hpricot object stored at instance variable
96
+ # @project
97
+ def build_project
98
+ @id ||= @project.at('id').inner_html
99
+ @api_url ||= "#{CONFIG[:api_location]}/projects/#{@id}"
100
+ @url ||= "http://www.pivotaltracker.com/projects/#{@id}"
101
+ @name = @project.at('name').inner_html
102
+ @iteration_length = @project.at('iteration_length').inner_html
103
+ @week_start_day = @project.at('week_start_day').inner_html
104
+ @point_scale = @project.at('point_scale').inner_html.split(',')
105
+ @meta = ProjectMeta.find_by_project_id(@id)
106
+ if @meta.nil?
107
+ init_meta
108
+ else
109
+ @meta.sync(self)
110
+ end
111
+ end
112
+
113
+ def get_stories
114
+ api_url = "#{CONFIG[:api_location]}/projects/#{@id}/stories"
115
+ @stories = (Hpricot(open(api_url, {"X-TrackerToken" => @token.to_s}))/:story).map {|story| Story.new(:story => story, :project_id => @id)}
116
+ end
117
+
118
+ def init_meta
119
+ @meta = ProjectMeta.create_from_project(self)
120
+ end
121
+
122
+ end
@@ -0,0 +1,87 @@
1
+ # TODO: Documentation ☻
2
+ class Story
3
+ attr_accessor :story_type, :estimate, :current_state,
4
+ :description, :name, :requested_by, :owned_by, :created_at, :accepted_at,
5
+ :labels
6
+
7
+ attr_reader :id, :url
8
+
9
+ def initialize(options = {})
10
+ @token = options[:token].to_s || Token.new
11
+ if options.include?(:project_id) && options.include?(:story_id)
12
+ @id = options[:story_id]
13
+ @project_id = options[:project_id]
14
+ @url = "http://www.pivotaltracker.com/story/show/#{@id}"
15
+ @api_url = "http://www.pivotaltracker.com/services/v2/projects/#{@project_id}/stories/#{@id}"
16
+ @story = Hpricot(open(@api_url, {"X-TrackerToken" => @token}))
17
+ elsif options.include?(:story) && options.include?(:project_id)
18
+ @project_id = options[:project_id]
19
+ @story = options[:story]
20
+ else
21
+ raise ArgumentError, "Valid options are: :story (receives an Hpricot Object) + :project_id OR :project_id + :story_id + :token"
22
+ end
23
+ build_story
24
+ end
25
+
26
+ def build_story
27
+ @id ||= @story.at('id').inner_html
28
+ @url ||= "http://www.pivotaltracker.com/story/show/#{@id}"
29
+ @api_url ||= "http://www.pivotaltracker.com/services/v2/projects/#{@project_id}/stories/#{@id}"
30
+ @story_type = @story.at('story_type').inner_html unless @story.at('story_type').nil?
31
+ @estimate = @story.at('estimate').inner_html unless @story.at('estimate').nil?
32
+ @current_state = @story.at('current_state').inner_html unless @story.at('current_state').nil?
33
+ @description = @story.at('description').inner_html unless @story.at('description').nil?
34
+ @name = @story.at('name').inner_html unless @story.at('name').nil?
35
+ @requested_by = @story.at('requested_by').inner_html unless @story.at('requested_by').nil?
36
+ @owned_by = @story.at('owned_by').inner_html unless @story.at('owned_by').nil?
37
+ @created_at = @story.at('created_at').inner_html unless @story.at('created_at').nil?
38
+ @accepted_at = @story.at('accepted_at').inner_html unless @story.at('accepted_at').nil?
39
+ @labels = @story.at('labels').inner_html unless @story.at('labels').nil?
40
+ end
41
+
42
+ def save
43
+ parameters = build_story_xml
44
+ api_url = URI.parse("http://www.pivotaltracker.com/services/v2/projects/#{@project_id}/stories/#{@id}")
45
+ response = Net::HTTP.start(api_url.host, api_url.port) do |http|
46
+ http.put(api_url.path, parameters, {'X-TrackerToken' => @token, 'Content-Type' => 'application/xml'})
47
+ end
48
+
49
+ @story = (Hpricot(response.body)/:story)
50
+ build_story
51
+ end
52
+
53
+ # TODO: test this method:
54
+ def destroy
55
+ api_url = URI.parse("http://www.pivotaltracker.com/services/v2/projects/#{@project_id}/stories/#{@id}")
56
+ response = Net::HTTP.start(api_url.host, api_url.port) do |http|
57
+ http.delete(api_url.path, {"X-TrackerToken" => @token})
58
+ end
59
+ end
60
+
61
+ protected
62
+
63
+ def to_param
64
+ query_string = attributes.map { |key, value| "story[#{key}]=#{CGI::escape(value)}" unless value.nil?}.compact.join('&')
65
+ end
66
+
67
+ def build_story_xml
68
+ story_xml = "<story>"
69
+ attributes.each do |key, value|
70
+ story_xml << "<#{key}>#{(value.to_s)}</#{key}>" unless value.nil?
71
+ end
72
+ story_xml << "</story>"
73
+ end
74
+
75
+ def attributes
76
+ {
77
+ "story_type" => @story_type,
78
+ "estimate" => @estimate,
79
+ "current_state" => @current_state,
80
+ "description" => @description,
81
+ "name" => @name,
82
+ "requested_by" => @requested_by,
83
+ "owned_by" => @owned_by,
84
+ "labels" => @labels
85
+ }
86
+ end
87
+ end # class Tracker::Story
@@ -0,0 +1,27 @@
1
+ class Token
2
+ def initialize(options = {})
3
+ @token = case
4
+ when options.include?(:token)
5
+ options[:token]
6
+ when options.include?(:username) && options.include?(:password)
7
+ # If a token is not provided, it can be generated by passing a hash with :user and :password keys
8
+ get_token(options[:username], options[:password])
9
+ when CONFIG.key?(:api_token)
10
+ CONFIG[:api_token]
11
+ else
12
+ raise ArgumentError, "Invalid argument, expecting either <:token> or <:username> and <:password>"
13
+ end
14
+ end
15
+
16
+ def to_s
17
+ @token.to_s
18
+ end
19
+
20
+ protected
21
+
22
+ # According to http://www.pivotaltracker.com/help/api#retrieve_token this should work:
23
+ def get_token(username, password)
24
+ (Hpricot(open("https://www.pivotaltracker.com/services/tokens/active", :http_basic_authentication => [username, password])).at('guid')).inner_html
25
+ end
26
+
27
+ end # class Tracker::Token
@@ -0,0 +1,56 @@
1
+ # The class is a wrapper for getting projects, stories and using tokens. It's
2
+ # main purpose is relating the three allowing to interact with a project, and
3
+ # its stories using a token.
4
+ # Expects a token when initialized.
5
+ class Tracker
6
+
7
+ # To generate a token, use the "Create New Token" link on the My Profile
8
+ # page (http://www.pivotaltracker.com/profile).
9
+ def initialize(token=nil)
10
+ @token = token || Token.new
11
+ raise TypeError unless @token.is_a? Token
12
+ end
13
+
14
+ # Fetches project with given ID
15
+ # Returns a Project object
16
+ def project(project_id)
17
+ @project = Project.new(:project_id => project_id)
18
+ end
19
+
20
+ # Refresh the projects from the server
21
+ # Returns an array of projects
22
+ def sync
23
+ @projects = nil
24
+ get_projects
25
+ end
26
+
27
+ # Alias for get_projects
28
+ # Returns an array of projects
29
+ def projects ; get_projects ; end
30
+
31
+ # Receives a block with the condition to find a project. Should work the
32
+ # same as Enumerable.find method
33
+ # Returns a Project object
34
+ def find_project
35
+ get_projects unless defined?(@projects) && @projects.is_a?(Array)
36
+ @projects.find do |project|
37
+ yield(project)
38
+ end
39
+ end
40
+
41
+ # Uses the ProjectMeta model to find locally stored projects and their api counterparts
42
+ def self.find_local(is_active=nil)
43
+ local = ProjectMeta.find_local(is_active)
44
+ local.map{|meta| Tracker.new.find_project{|project| puts "+++#{meta.project_id} == #{project.id}"; meta.project_id == project.id } } unless local.nil? || local.empty?
45
+ end
46
+
47
+ protected
48
+
49
+ # Fills @projects. NOTE: call sync method to refill/sync @projects
50
+ # Returns an Array stored in @projects
51
+ def get_projects
52
+ api_url = "#{CONFIG[:api_location]}/projects/"
53
+ @projects ||= (Hpricot(open(api_url, {"X-TrackerToken" => @token.to_s}))/:project).map {|project| Project.new(:project => project)}
54
+ end
55
+
56
+ end # class Tracker::Tracker
Binary file
@@ -0,0 +1,28 @@
1
+ ---
2
+ :password: "password"
3
+ :username: an@email.com
4
+ :token: "a_token_hash"
5
+ :project_count: 2
6
+ :project_1:
7
+ :name: AProjectName
8
+ :id: "12345"
9
+ :point_scale:
10
+ - "0"
11
+ - "1"
12
+ - "2"
13
+ - "3"
14
+ :week_start_day: Monday
15
+ :iteration_length: "1"
16
+ :project_2:
17
+ :name: AnotherProjectName
18
+ :id: "6890"
19
+ :point_scale:
20
+ - "0"
21
+ - "1"
22
+ - "2"
23
+ - "3"
24
+ :week_start_day: Sunday
25
+ :iteration_length: "1"
26
+ :story_1:
27
+ :id: 12345
28
+ :name: "Some existing name"
@@ -0,0 +1,6 @@
1
+ require 'rubygems'
2
+ require 'shoulda'
3
+ require 'test/unit'
4
+ require File.join(File.dirname(__FILE__), '/../lib/track-r.rb')
5
+ require File.join(File.dirname(__FILE__), '/../config/environment.rb')
6
+ $config = YAML.load_file(File.join(File.dirname(__FILE__), '/test_config.yml'))
@@ -0,0 +1,44 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class ProjectTest < Test::Unit::TestCase
4
+ context "A given project" do
5
+
6
+ setup do
7
+ @project_id = $config[:project_1][:id]
8
+ token_options = {:username => $config[:username], :password => $config[:password]}
9
+ @tracker = Tracker.new(Token.new(token_options))
10
+ @project = @tracker.project(@project_id)
11
+ end
12
+
13
+ should "show all stories for that project with @project.stories" do
14
+ assert_equal $config[:project_1][:story_count], @project.stories.size
15
+ end
16
+
17
+ should "show a story with @project.story and passing the story id as argument" do
18
+ assert_equal $config[:story_1][:name], @project.story($config[:story_1][:id]).name
19
+ end
20
+
21
+ should "be able to add and remove a story" do
22
+ story_count = @project.stories.size
23
+ attributes = { :name => "Finish Track-R (sorry for cluttering :))",
24
+ :requested_by => "Jose Felix Gomez",
25
+ :description => "This story was made with Track-R library. Sorry for the clutter, you're free to delete me." }
26
+ new_story = @project.create_story(attributes)
27
+ assert_equal "Finish Track-R (sorry for cluttering :))", new_story.name
28
+ @project.delete_story(new_story)
29
+ assert_equal story_count, @project.stories.size
30
+ end
31
+
32
+ should "be able to get the backlog stories" do
33
+ story_count = $config[:project_1][:backlog_stories]
34
+ assert_equal story_count, @project.backlog.size
35
+ end
36
+
37
+ should "be able to get the current iteration stories" do
38
+ story_count = $config[:project_1][:current_stories]
39
+ assert_equal story_count, @project.current.size
40
+ end
41
+
42
+ end
43
+ end
44
+
@@ -0,0 +1,32 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class TestStoryTest < Test::Unit::TestCase
4
+
5
+ context "A given story" do
6
+
7
+ setup do
8
+ token_options = {:username => $config[:username], :password => $config[:password]}
9
+ @tracker = Tracker.new(Token.new(token_options))
10
+ @project_id = $config[:project_1][:id]
11
+ @project = @tracker.project(@project_id)
12
+ attributes = { :name => "Finish Track-R (sorry for cluttering :))",
13
+ :requested_by => "Jose Felix Gomez",
14
+ :description => "This story was made with Track-R library. Sorry for the clutter, you're free to delete me." }
15
+ @story = @project.create_story(attributes)
16
+ end
17
+
18
+ teardown do
19
+ @project.delete_story(@story)
20
+ end
21
+
22
+ should "be updated after story.save call" do
23
+ @story.name = "More power to the shields"
24
+ @story.description = "ZOMG!"
25
+ @story.save
26
+
27
+ assert_equal "More power to the shields", @project.story(@story.id).name
28
+ end
29
+
30
+ end
31
+ end
32
+
@@ -0,0 +1,18 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class TestTokenTest < Test::Unit::TestCase
4
+ context "with a valid username and password" do
5
+
6
+ setup do
7
+ def token_options
8
+ {:username => $config[:username], :password => $config[:password]}
9
+ end
10
+ end
11
+
12
+ should "return a valid token" do
13
+ @token = Token.new(token_options)
14
+ assert_equal $config[:token], @token.to_s
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,46 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class TestTrackerTest < Test::Unit::TestCase
4
+
5
+ context "with an invalid token" do
6
+
7
+ should "raise an TypeError if token is other than Token object" do
8
+ token = "token"
9
+ assert_raise(TypeError) { Tracker.new(token) }
10
+ end
11
+
12
+ end
13
+
14
+ context "with a valid token" do
15
+
16
+ setup do
17
+ @project_id = $config[:project_1][:id]
18
+ def get_token
19
+ token_options = {:username => $config[:username], :password => $config[:password]}
20
+ Token.new(token_options)
21
+ end
22
+ @tracker = Tracker.new(get_token)
23
+ end
24
+
25
+ should "return a valid project by passing the id with correct attributes" do
26
+ project = @tracker.project(@project_id)
27
+ assert_equal $config[:project_1][:name], project.name
28
+ assert_equal $config[:project_1][:point_scale], project.point_scale
29
+ assert_equal $config[:project_1][:week_start_day], project.week_start_day
30
+ assert_equal $config[:project_1][:iteration_length], project.iteration_length
31
+ end
32
+
33
+ should "return an array of all my projects" do
34
+ projects = @tracker.projects
35
+ assert_equal $config[:project_count], projects.size
36
+ end
37
+
38
+ should "be able to find a project using Enumerable.find method" do
39
+ project_1 = (@tracker.find_project {|project| project.name == $config[:project_1][:name]})
40
+ project_2 = (@tracker.find_project {|project| project.id == $config[:project_2][:id]})
41
+ assert_equal $config[:project_1][:name], project_1.name, "Failed to find by name"
42
+ assert_equal $config[:project_2][:name], project_2.name, "Failed to find by id"
43
+ end
44
+ end
45
+
46
+ end
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env ruby
2
+ #-------------------------------------------------------------------------------.
3
+ # InstallGems: Script to do the hard gem installing and letting you rest a bit. ||
4
+ # ||
5
+ # AUTHOR: Jose F. Gomez ||
6
+ # VERSION: 0.2.0 ||
7
+ # DATE: 2009-04-04 ||
8
+ # ||
9
+ # USAGE: install-gems.rb [file.yml] The yml file should have this format per ||
10
+ # gem: ||
11
+ # ||
12
+ # example.yml: ||
13
+ # --- ||
14
+ # - :source: http://gems.github.com ||
15
+ # :version: = 0.1.99.21 ||
16
+ # :name: aslakhellesoy-cucumber ||
17
+ # ||
18
+ # Note that :source and :version are optional and the script checks wheter a ||
19
+ # gem is already installed or not. ||
20
+ # ||
21
+ #-------------------------------------------------------------------------------´
22
+ # HISTORY
23
+ # 0.2.0 -- Made script require-able.
24
+ # 0.1.0 -- Initial Commit
25
+
26
+
27
+ class InstallGems
28
+ require 'yaml'
29
+ require 'rubygems'
30
+
31
+ def initialize(file)
32
+ @gems_yml = YAML.load_file(file)
33
+ install_gems
34
+ end
35
+
36
+ def install_gems
37
+ for gem in @gems_yml
38
+ install_gem(gem)
39
+ end
40
+ end
41
+
42
+ def install_gem(gem)
43
+ unless Gem.available? gem[:name]
44
+ cmd = "gem install "
45
+ cmd += "--version '%s' " % gem[:version] if gem.has_key? :version
46
+ cmd += "--source %s " % gem[:source] if gem.has_key? :source
47
+ cmd += "%s " % gem[:name] if gem.has_key? :name
48
+ puts "Installing: %+28s" % gem[:name]
49
+ out = %x[#{cmd}]
50
+ else
51
+ puts "*** [ #{gem[:name]} ] is already installed on this system."
52
+ end
53
+ end
54
+ end
55
+
56
+ if __FILE__ == $0
57
+ app = InstallGems.new(ARGV[0])
58
+ end
59
+
60
+ #-------------------------------------------------------------------------------.
61
+ # vim: set foldmethod:indent ||
62
+ #-------------------------------------------------------------------------------´
63
+
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: localshred-track-r
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jose Felix Gomez
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-09-15 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: track-r is a library that provides wrapper classes and methods for accessing PivotalTracker's public API.
17
+ email: jfgomez86@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.textile
24
+ files:
25
+ - .gitignore
26
+ - README.textile
27
+ - Rakefile
28
+ - VERSION
29
+ - config/config.yml
30
+ - config/environment.rb
31
+ - config/gems.yml
32
+ - lib/track-r.rb
33
+ - lib/track-r/api_connector/connection.rb
34
+ - lib/track-r/api_connector/request.rb
35
+ - lib/track-r/api_connector/response.rb
36
+ - lib/track-r/iteration.rb
37
+ - lib/track-r/project.bak.rb
38
+ - lib/track-r/project.rb
39
+ - lib/track-r/story.rb
40
+ - lib/track-r/token.rb
41
+ - lib/track-r/tracker.rb
42
+ - pkg/track-r-1.0.0.gem
43
+ - test/test_config.yml.example
44
+ - test/test_helper.rb
45
+ - test/unit/project_test.rb
46
+ - test/unit/story_test.rb
47
+ - test/unit/token_test.rb
48
+ - test/unit/tracker_test.rb
49
+ - vendor/install-gems/install-gems.rb
50
+ has_rdoc: false
51
+ homepage: http://github.com/jfgomez86/Track-R
52
+ licenses:
53
+ post_install_message:
54
+ rdoc_options:
55
+ - --charset=UTF-8
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "0"
63
+ version:
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: "0"
69
+ version:
70
+ requirements: []
71
+
72
+ rubyforge_project:
73
+ rubygems_version: 1.3.5
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: A wrapper library for pivotal tracker's API
77
+ test_files:
78
+ - test/test_helper.rb
79
+ - test/unit/project_test.rb
80
+ - test/unit/story_test.rb
81
+ - test/unit/token_test.rb
82
+ - test/unit/tracker_test.rb