pivotal-tracker 0.0.9.1

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.
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
+