localshred-track-r 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,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