pivotal-tracker 0.0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/.gitignore +7 -0
  2. data/Gemfile +16 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +60 -0
  5. data/Rakefile +46 -0
  6. data/VERSION +1 -0
  7. data/lib/pivotal-tracker/activity.rb +45 -0
  8. data/lib/pivotal-tracker/client.rb +34 -0
  9. data/lib/pivotal-tracker/extensions.rb +11 -0
  10. data/lib/pivotal-tracker/iteration.rb +20 -0
  11. data/lib/pivotal-tracker/membership.rb +20 -0
  12. data/lib/pivotal-tracker/note.rb +17 -0
  13. data/lib/pivotal-tracker/project.rb +42 -0
  14. data/lib/pivotal-tracker/proxy.rb +65 -0
  15. data/lib/pivotal-tracker/story.rb +87 -0
  16. data/lib/pivotal-tracker/task.rb +48 -0
  17. data/lib/pivotal-tracker.rb +40 -0
  18. data/lib/pivotal_tracker.rb +2 -0
  19. data/pivotal-tracker.gemspec +95 -0
  20. data/spec/fixtures/activity.xml +176 -0
  21. data/spec/fixtures/created_story.xml +14 -0
  22. data/spec/fixtures/memberships.xml +29 -0
  23. data/spec/fixtures/project.xml +42 -0
  24. data/spec/fixtures/project_activity.xml +170 -0
  25. data/spec/fixtures/projects.xml +396 -0
  26. data/spec/fixtures/stale_fish.yml +58 -0
  27. data/spec/fixtures/stories.xml +66 -0
  28. data/spec/fixtures/tasks.xml +24 -0
  29. data/spec/spec.opts +1 -0
  30. data/spec/spec_helper.rb +27 -0
  31. data/spec/support/stale_fish_fixtures.rb +43 -0
  32. data/spec/unit/pivotal-tracker/activity_spec.rb +23 -0
  33. data/spec/unit/pivotal-tracker/iteration_spec.rb +8 -0
  34. data/spec/unit/pivotal-tracker/membership_spec.rb +20 -0
  35. data/spec/unit/pivotal-tracker/project_spec.rb +47 -0
  36. data/spec/unit/pivotal-tracker/story_spec.rb +27 -0
  37. data/spec/unit/pivotal-tracker/task_spec.rb +21 -0
  38. metadata +162 -0
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+
7
+ .bundle
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source :gemcutter
2
+
3
+ group :runtime do
4
+ gem 'rest-client', '~> 1.4.1'
5
+ gem 'happymapper', '>= 0.2.4'
6
+ gem 'builder'
7
+ gem 'nokogiri', '~> 1.4.1'
8
+ end
9
+
10
+ group :test do
11
+ gem 'rspec', :require => 'spec'
12
+ gem 'rake'
13
+ gem 'bundler', '~> 0.9.5'
14
+ gem 'jeweler'
15
+ gem 'stale_fish', '~> 1.3.0'
16
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Justin Smestad, Josh Nichols
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,60 @@
1
+ = pivotal-tracker.rb
2
+
3
+ Ruby wrapper for Pivotal Tracker API, no frameworks required. Simply Ruby.
4
+
5
+ == Note
6
+
7
+ Version 0.0.8 and above are incompatible with previous versions.
8
+
9
+ == Features
10
+
11
+ * Compatible with Pivotal Tracker API version 3
12
+ * ActiveRecord-style Wrapper API
13
+ * Support for SSL protected repositories
14
+
15
+ == Overview
16
+
17
+ PivotalTracker::Client.token('myusername@email.com', 'secretpassword') # Automatically fetch API Token
18
+ PivotalTracker::Client.token = 'jkfduisj97823974j2kl24899234' # Manually set API Token
19
+
20
+ @projects = PivotalTracker::Project.all # return all projects
21
+ @a_project = PivotalTracker::Project.find(84739) # find project with a given ID
22
+
23
+ @a_project.stories.all # return all stories for "a_project"
24
+ @a_project.stories.all(:label => 'overdue', :story_type => ['bug', 'chore']) # return all stories that match the passed filters
25
+ @a_project.stories.find(847762630) # find story with a given ID
26
+
27
+ @a_project.stories.create(:name => 'My Story', :story_type => 'feature') # create a story for this project
28
+
29
+ # all tracker defined filters are allowed, as well as :limit & :offset for pagination
30
+
31
+ # The below are planned to be added to the final release:
32
+
33
+ @a_project.stories << PivotalTracker::Story.new(84739, :name => 'Ur Story') # same as above, useful for copying/cloning from proj
34
+
35
+
36
+ The API is based on the following this gist: http://gist.github.com/283120
37
+
38
+ == Getting Started
39
+
40
+ * Installing:
41
+
42
+ $ gem install pivotal-tracker
43
+
44
+ * Contributing (requires Bundler >= 0.9.7):
45
+
46
+ $ git clone git://github.com/jsmestad/pivotal-tracker
47
+ $ cd pivotal-tracker
48
+ $ bundle install
49
+ $ bundle exec rake
50
+
51
+ == Additional Information
52
+
53
+ Wiki: http://wiki.github.com/jsmestad/pivotal-tracker
54
+ Documentation: http://rdoc.info/projects/jsmestad/pivotal-tracker
55
+
56
+ == Contributers
57
+
58
+ * Justin Smestad (http://github.com/jsmestad)
59
+ * Josh Nichols (http://github.com/technicalpickles)
60
+ * Terence Lee (http://github.com/hone)
data/Rakefile ADDED
@@ -0,0 +1,46 @@
1
+ require 'rake'
2
+ require 'bundler'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "pivotal-tracker"
8
+ gem.summary = %Q{Ruby wrapper for the Pivotal Tracker API}
9
+ gem.email = "justin.smestad@gmail.com"
10
+ gem.homepage = "http://github.com/jsmestad/pivotal-tracker"
11
+ gem.authors = ["Justin Smestad", "Josh Nichols", "Terence Lee"]
12
+
13
+ bundle = Bundler::Definition.from_gemfile('Gemfile')
14
+ bundle.dependencies.each do |dep|
15
+ next unless dep.groups.include?(:runtime)
16
+ gem.add_dependency(dep.name, dep.requirement.to_s)
17
+ end
18
+ end
19
+ Jeweler::GemcutterTasks.new
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
22
+ end
23
+
24
+ require 'spec/rake/spectask'
25
+ Spec::Rake::SpecTask.new(:spec) do |spec|
26
+ spec.libs << 'lib' << 'spec'
27
+ spec.spec_files = FileList['spec/**/*_spec.rb']
28
+ end
29
+
30
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
31
+ spec.libs << 'lib' << 'spec'
32
+ spec.pattern = 'spec/**/*_spec.rb'
33
+ spec.rcov = true
34
+ end
35
+
36
+ task :default => :spec
37
+
38
+ begin
39
+ require 'yard'
40
+ YARD::Rake::YardocTask.new
41
+ rescue LoadError
42
+ task :yardoc do
43
+ abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
44
+ end
45
+ end
46
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.9.1
@@ -0,0 +1,45 @@
1
+ module PivotalTracker
2
+ class Activity
3
+ include HappyMapper
4
+ class << self
5
+ def all(project=nil, options={})
6
+ params = self.encode_options(options)
7
+ if project
8
+ parse(Client.connection["/projects/#{project.id}/activities#{params}"].get)
9
+ else
10
+ parse(Client.connection["/activities#{params}"].get)
11
+ end
12
+ end
13
+
14
+ protected
15
+
16
+ def encode_options(options)
17
+ return nil if !options.is_a?(Hash) || options.empty?
18
+
19
+ options_string = []
20
+ options_string << "limit=#{options.delete(:limit)}" if options[:limit]
21
+ options_string << "newer_than_version=#{options.delete(:newer_than_version)}" if options[:newer_than_version]
22
+
23
+ if options[:occurred_since]
24
+ options_string << "occurred_since_date=#{options[:occurred_since].utc}"
25
+ elsif options[:occurred_since_date]
26
+ options_string << "occurred_since_date=#{options[:occurred_since]}"
27
+ end
28
+
29
+ return "?#{options_string.join('&')}"
30
+ end
31
+
32
+ end
33
+
34
+ element :id, Integer
35
+ element :version, Integer
36
+ element :event_type, String
37
+ element :occurred_at, DateTime
38
+ element :author, String
39
+ element :project_id, Integer
40
+ element :description, String
41
+
42
+ has_many :stories, Story
43
+
44
+ end
45
+ end
@@ -0,0 +1,34 @@
1
+ module PivotalTracker
2
+ class Client
3
+
4
+ class << self
5
+ attr_writer :use_ssl, :token
6
+
7
+ def use_ssl
8
+ @use_ssl || false
9
+ end
10
+
11
+ def token(username, password, method='post')
12
+ response = if method == 'post'
13
+ RestClient.post 'https://www.pivotaltracker.com/services/v3/tokens/active', :username => username, :password => password
14
+ else
15
+ RestClient.get "https://#{username}:#{password}@www.pivotaltracker.com/services/v3/tokens/active"
16
+ end
17
+ @token ||= Nokogiri::XML(response.body).search('guid').inner_html
18
+ end
19
+
20
+ # this is your connection for the entire module
21
+ def connection(options={})
22
+ @connection ||= RestClient::Resource.new("#{protocol}://www.pivotaltracker.com/services/v3", :headers => {'X-TrackerToken' => @token, 'Content-Type' => 'application/xml'})
23
+ end
24
+
25
+ protected
26
+
27
+ def protocol
28
+ use_ssl ? 'https' : 'http'
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,11 @@
1
+ # Happymapper patch for RestClient API Change (response => response.body)
2
+
3
+ module HappyMapper
4
+ module ClassMethods
5
+ alias_method :orig_parse, :parse
6
+ def parse(xml, options={})
7
+ xml = xml.to_s if xml.is_a?(RestClient::Response)
8
+ orig_parse(xml, options)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ module PivotalTracker
2
+ class Iteration
3
+ include HappyMapper
4
+
5
+ class << self
6
+ def all(project, options={})
7
+ params = PivotalTracker.encode_options(options)
8
+ parse(Client.connection["/projects/#{project.id}/iterations#{params}"].get)
9
+ end
10
+ end
11
+
12
+ element :id, Integer
13
+ element :number, Integer
14
+ element :start, DateTime
15
+ element :finish, DateTime
16
+
17
+ has_many :stories, Story
18
+
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module PivotalTracker
2
+ class Membership
3
+ include HappyMapper
4
+
5
+ class << self
6
+ def all(project, options={})
7
+ parse(Client.connection["/projects/#{project.id}/memberships"].get)
8
+ end
9
+ end
10
+
11
+ element :id, Integer
12
+ element :role, String
13
+
14
+ # Flattened Attributes from <person>...</person>
15
+ element :name, String, :deep => true
16
+ element :email, String, :deep => true
17
+ element :initials, String, :deep => true
18
+
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ module PivotalTracker
2
+ class Note
3
+ include HappyMapper
4
+
5
+ class << self
6
+ # def all(story, options={})
7
+ # parse(Client.connection["/projects/#{project_id}/stories/#{story_id}/notes"].get)
8
+ # end
9
+ end
10
+
11
+ element :id, Integer
12
+ element :text, String
13
+ element :author, String
14
+ element :noted_at, DateTime
15
+ # has_one :story, Story
16
+ end
17
+ end
@@ -0,0 +1,42 @@
1
+ module PivotalTracker
2
+ class Project
3
+ include HappyMapper
4
+
5
+ class << self
6
+ def all
7
+ @found = parse(Client.connection['/projects'].get)
8
+ end
9
+
10
+ def find(id)
11
+ if @found
12
+ @found.detect { |document| document.id == id }
13
+ else
14
+ parse(Client.connection["/projects/#{id}"].get)
15
+ end
16
+ end
17
+ end
18
+
19
+ element :id, Integer
20
+ element :name, String
21
+ element :iteration_length, Integer
22
+ element :week_start_day, String
23
+ element :point_scale, String
24
+
25
+ def activities
26
+ @activities ||= Proxy.new(self, Activity)
27
+ end
28
+
29
+ def iterations
30
+ @iterations ||= Proxy.new(self, Iteration)
31
+ end
32
+
33
+ def stories
34
+ @stories ||= Proxy.new(self, Story)
35
+ end
36
+
37
+ def memberships
38
+ @memberships ||= Proxy.new(self, Membership)
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,65 @@
1
+ class BasicObject #:nodoc:
2
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|instance_eval|proxy_|^object_id$)/ }
3
+ end unless defined?(BasicObject)
4
+
5
+ module PivotalTracker
6
+ class Proxy < BasicObject
7
+
8
+ def initialize(owner, target)
9
+ @owner = owner
10
+ @target = target
11
+ @opts = nil
12
+ end
13
+
14
+ def all(options={})
15
+ proxy_found(options)
16
+ end
17
+
18
+ def find(param, options={})
19
+ return all(options) if param == :all
20
+ return proxy_found(options).detect { |document| document.id == param }
21
+ end
22
+
23
+ def <<(*objects)
24
+ objects.flatten.each do |object|
25
+ if obj = object.create
26
+ return obj
27
+ else
28
+ return object
29
+ end
30
+ end
31
+ end
32
+
33
+ def create(args)
34
+ object = @target.new(args.merge({:owner => @owner}))
35
+ if obj = object.create
36
+ return obj
37
+ else
38
+ return object
39
+ end
40
+ end
41
+
42
+ protected
43
+
44
+ def proxy_found(options)
45
+ # Check to see if options have changed
46
+ if @opts == options
47
+ @found ||= load_found(options)
48
+ else
49
+ load_found(options)
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def method_missing(method, *args, &block)
56
+ @target.send(method, *args, &block)
57
+ end
58
+
59
+ def load_found(options)
60
+ @opts = options
61
+ @target.all(@owner, @opts)
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,87 @@
1
+ module PivotalTracker
2
+ class Story
3
+ include HappyMapper
4
+
5
+ class << self
6
+ def all(project, options={})
7
+ params = PivotalTracker.encode_options(options)
8
+ stories = parse(Client.connection["/projects/#{project.id}/stories#{params}"].get)
9
+ stories.each { |s| s.project_id = project.id }
10
+ return stories
11
+ end
12
+ end
13
+
14
+ attr_accessor :project_id
15
+
16
+ element :id, Integer
17
+ element :story_type, String
18
+ element :url, String
19
+ element :estimate, Integer
20
+ element :current_state, String
21
+ element :name, String
22
+ element :requested_by, String
23
+ element :owned_by, String
24
+ element :created_at, DateTime
25
+ element :accepted_at, DateTime
26
+ element :labels, String
27
+ element :description, String
28
+ element :jira_id, Integer
29
+ element :jira_url, String
30
+
31
+ def initialize(attributes={})
32
+ self.project_id = attributes.delete(:owner).id if attributes[:owner]
33
+
34
+ update_attributes(attributes)
35
+ end
36
+
37
+ def create
38
+ return self if project_id.nil?
39
+ response = Client.connection["/projects/#{project_id}/stories"].post(self.to_xml, :content_type => 'application/xml')
40
+ return Story.parse(response)
41
+ end
42
+
43
+ def update(attrs={})
44
+ update_attributes(attrs)
45
+ response = Client.connection["/projects/#{project_id}/stories/#{id}"].put(self.to_xml, :content_type => 'application/xml')
46
+ return Story.parse(response)
47
+ end
48
+
49
+ def delete
50
+ Client.connection["/projects/#{project_id}/stories/#{id}"].delete
51
+ end
52
+
53
+ def tasks
54
+ @tasks ||= Proxy.new(self, Task)
55
+ end
56
+
57
+ def project=(proj_id)
58
+ self.project_id = proj_id
59
+ end
60
+
61
+ protected
62
+
63
+ def to_xml
64
+ builder = Nokogiri::XML::Builder.new do |xml|
65
+ xml.story {
66
+ xml.name "#{name}"
67
+ xml.story_type "#{story_type}"
68
+ xml.estimate "#{estimate}"
69
+ xml.current_state "#{current_state}"
70
+ xml.requested_by "#{requested_by}"
71
+ xml.owned_by "#{owned_by}"
72
+ xml.labels "#{labels}"
73
+ xml.description "#{description}"
74
+ # xml.jira_id "#{jira_id}"
75
+ # xml.jira_url "#{jira_url}"
76
+ }
77
+ end
78
+ return builder.to_xml
79
+ end
80
+
81
+ def update_attributes(attrs)
82
+ attrs.each do |key, value|
83
+ self.send("#{key}=", value.is_a?(Array) ? value.join(',') : value )
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,48 @@
1
+ module PivotalTracker
2
+ class Task
3
+ include HappyMapper
4
+
5
+ class << self
6
+ def all(story, options={})
7
+ tasks = parse(Client.connection["/projects/#{story.project_id}/stories/#{story.id}/tasks"].get)
8
+ tasks.each { |t| t.project_id, t.story_id = story.project_id, story.id }
9
+ return tasks
10
+ end
11
+ end
12
+
13
+ attr_accessor :project_id, :story_id
14
+
15
+ element :id, Integer
16
+ element :description, String
17
+ element :position, Integer
18
+ element :complete, Boolean
19
+ element :created_at, DateTime
20
+
21
+ def create
22
+ response = Client.connection["/projects/#{project_id}/stories/#{story_id}/tasks"].post(self.to_xml, :content_type => 'application/xml')
23
+ return Task.parse(response)
24
+ end
25
+
26
+ def update
27
+ response = Client.connection["/projects/#{project_id}/stories/#{story_id}/tasks/#{id}"].put(self.to_xml, :content_type => 'application/xml')
28
+ return Task.parse(response)
29
+ end
30
+
31
+ def delete
32
+ Client.connection["/projects/#{project_id}/stories/#{story_id}/tasks/#{id}"].delete
33
+ end
34
+
35
+ protected
36
+
37
+ def to_xml
38
+ builder = Nokogiri::XML::Builder.new do |xml|
39
+ xml.task {
40
+ xml.description "#{description}"
41
+ # xml.position "#{position}"
42
+ xml.complete "#{complete}"
43
+ }
44
+ end
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,40 @@
1
+ require 'cgi'
2
+ require 'rest_client'
3
+ require 'happymapper'
4
+ require 'nokogiri'
5
+
6
+
7
+ require File.join(File.dirname(__FILE__), 'pivotal-tracker', 'extensions')
8
+ require File.join(File.dirname(__FILE__), 'pivotal-tracker', 'proxy')
9
+ require File.join(File.dirname(__FILE__), 'pivotal-tracker', 'client')
10
+ require File.join(File.dirname(__FILE__), 'pivotal-tracker', 'project')
11
+ require File.join(File.dirname(__FILE__), 'pivotal-tracker', 'story')
12
+ require File.join(File.dirname(__FILE__), 'pivotal-tracker', 'task')
13
+ require File.join(File.dirname(__FILE__), 'pivotal-tracker', 'membership')
14
+ require File.join(File.dirname(__FILE__), 'pivotal-tracker', 'activity')
15
+ require File.join(File.dirname(__FILE__), 'pivotal-tracker', 'iteration')
16
+ require File.join(File.dirname(__FILE__), 'pivotal-tracker', 'note')
17
+
18
+ module PivotalTracker
19
+
20
+ # define error types
21
+ class ProjectNotSpecified < StandardError; end
22
+
23
+ def self.encode_options(options)
24
+ return nil if !options.is_a?(Hash) || options.empty?
25
+
26
+ options_string = []
27
+ options_string << "limit=#{options.delete(:limit)}" if options[:limit]
28
+ options_string << "offset=#{options.delete(:offset)}" if options[:offset]
29
+
30
+ filters = []
31
+ options.each do |key, value|
32
+ values = value.is_a?(Array) ? value.map {|x| CGI.escape(x) }.join(',') : CGI.escape(value)
33
+ filters << "#{key}%3A#{values}" # %3A => :
34
+ end
35
+ options_string << "filter=#{filters.join('%20')}" unless filters.empty? # %20 => &amp;
36
+
37
+ return "?#{options_string.join('&')}"
38
+ end
39
+
40
+ end
@@ -0,0 +1,2 @@
1
+ require "pivotal-tracker"
2
+