tsenart-pivotal-tracker 0.4.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.
Files changed (49) hide show
  1. data/Gemfile +16 -0
  2. data/Gemfile.lock +48 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +77 -0
  5. data/Rakefile +50 -0
  6. data/VERSION +1 -0
  7. data/lib/pivotal-tracker/activity.rb +46 -0
  8. data/lib/pivotal-tracker/attachment.rb +16 -0
  9. data/lib/pivotal-tracker/client.rb +41 -0
  10. data/lib/pivotal-tracker/extensions.rb +11 -0
  11. data/lib/pivotal-tracker/iteration.rb +34 -0
  12. data/lib/pivotal-tracker/membership.rb +20 -0
  13. data/lib/pivotal-tracker/note.rb +58 -0
  14. data/lib/pivotal-tracker/project.rb +58 -0
  15. data/lib/pivotal-tracker/proxy.rb +66 -0
  16. data/lib/pivotal-tracker/story.rb +148 -0
  17. data/lib/pivotal-tracker/task.rb +53 -0
  18. data/lib/pivotal-tracker/validation.rb +68 -0
  19. data/lib/pivotal-tracker.rb +40 -0
  20. data/lib/pivotal_tracker.rb +2 -0
  21. data/pivotal-tracker.gemspec +121 -0
  22. data/spec/fixtures/activity.xml +177 -0
  23. data/spec/fixtures/created_note.xml +14 -0
  24. data/spec/fixtures/created_story.xml +14 -0
  25. data/spec/fixtures/iterations_all.xml +237 -0
  26. data/spec/fixtures/iterations_backlog.xml +163 -0
  27. data/spec/fixtures/iterations_current.xml +47 -0
  28. data/spec/fixtures/iterations_done.xml +33 -0
  29. data/spec/fixtures/memberships.xml +42 -0
  30. data/spec/fixtures/notes.xml +33 -0
  31. data/spec/fixtures/project.xml +51 -0
  32. data/spec/fixtures/project_activity.xml +177 -0
  33. data/spec/fixtures/projects.xml +103 -0
  34. data/spec/fixtures/stale_fish.yml +100 -0
  35. data/spec/fixtures/stories.xml +293 -0
  36. data/spec/fixtures/tasks.xml +24 -0
  37. data/spec/spec.opts +1 -0
  38. data/spec/spec_helper.rb +31 -0
  39. data/spec/support/stale_fish_fixtures.rb +67 -0
  40. data/spec/unit/pivotal-tracker/activity_spec.rb +32 -0
  41. data/spec/unit/pivotal-tracker/attachment_spec.rb +62 -0
  42. data/spec/unit/pivotal-tracker/client_spec.rb +87 -0
  43. data/spec/unit/pivotal-tracker/iteration_spec.rb +52 -0
  44. data/spec/unit/pivotal-tracker/membership_spec.rb +20 -0
  45. data/spec/unit/pivotal-tracker/note_spec.rb +61 -0
  46. data/spec/unit/pivotal-tracker/project_spec.rb +55 -0
  47. data/spec/unit/pivotal-tracker/story_spec.rb +185 -0
  48. data/spec/unit/pivotal-tracker/task_spec.rb +21 -0
  49. metadata +236 -0
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source 'http://rubygems.org'
2
+
3
+ group :runtime do
4
+ gem 'rest-client', '~> 1.6.0'
5
+ gem 'happymapper', '>= 0.3.2'
6
+ gem 'builder'
7
+ gem 'nokogiri', '~> 1.4'
8
+ end
9
+
10
+ group :test do
11
+ gem 'rspec', '~> 1.3.0', :require => 'spec'
12
+ gem 'rake'
13
+ gem 'jeweler'
14
+ gem 'stale_fish', '~> 1.3.0'
15
+ gem "ruby-debug"
16
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,48 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activesupport (2.3.8)
5
+ builder (2.1.2)
6
+ columnize (0.3.2)
7
+ fakeweb (1.2.8)
8
+ gemcutter (0.6.1)
9
+ git (1.2.5)
10
+ happymapper (0.3.2)
11
+ libxml-ruby (~> 1.1.3)
12
+ jeweler (1.4.0)
13
+ gemcutter (>= 0.1.0)
14
+ git (>= 1.2.5)
15
+ rubyforge (>= 2.0.0)
16
+ json_pure (1.4.5)
17
+ libxml-ruby (1.1.4)
18
+ linecache (0.43)
19
+ mime-types (1.16)
20
+ nokogiri (1.4.4)
21
+ rake (0.8.7)
22
+ rest-client (1.6.0)
23
+ mime-types (>= 1.16)
24
+ rspec (1.3.2)
25
+ ruby-debug (0.10.4)
26
+ columnize (>= 0.1)
27
+ ruby-debug-base (~> 0.10.4.0)
28
+ ruby-debug-base (0.10.4)
29
+ linecache (>= 0.3)
30
+ rubyforge (2.0.4)
31
+ json_pure (>= 1.1.7)
32
+ stale_fish (1.3.0)
33
+ activesupport
34
+ fakeweb
35
+
36
+ PLATFORMS
37
+ ruby
38
+
39
+ DEPENDENCIES
40
+ builder
41
+ happymapper (>= 0.3.2)
42
+ jeweler
43
+ nokogiri (~> 1.4)
44
+ rake
45
+ rest-client (~> 1.6.0)
46
+ rspec (~> 1.3.0)
47
+ ruby-debug
48
+ stale_fish (~> 1.3.0)
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Justin Smestad
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,77 @@
1
+ = pivotal-tracker.rb
2
+
3
+ Ruby wrapper for Pivotal Tracker API, no frameworks required. Simply Ruby.
4
+
5
+ == Features
6
+
7
+ * Compatible with Pivotal Tracker API version 3
8
+ * ActiveRecord-style Wrapper API
9
+ * Support for SSL protected repositories
10
+
11
+ == Overview
12
+
13
+ PivotalTracker::Client.token('myusername@email.com', 'secretpassword') # Automatically fetch API Token
14
+ PivotalTracker::Client.token = 'jkfduisj97823974j2kl24899234' # Manually set API Token
15
+
16
+ @projects = PivotalTracker::Project.all # return all projects
17
+ @a_project = PivotalTracker::Project.find(84739) # find project with a given ID
18
+
19
+ @a_project.stories.all # return all stories for "a_project"
20
+ @a_project.stories.all(:label => 'overdue', :story_type => ['bug', 'chore']) # return all stories that match the passed filters
21
+ @a_project.stories.find(847762630) # find story with a given ID
22
+
23
+ @a_project.stories.create(:name => 'My Story', :story_type => 'feature') # create a story for this project
24
+
25
+ # all tracker defined filters are allowed, as well as :limit & :offset for pagination
26
+
27
+ # The below pattern below is planned to be added to the final release:
28
+
29
+ @a_project.stories << PivotalTracker::Story.new(84739, :name => 'Ur Story') # same as earlier story creation, useful for copying/cloning from proj
30
+
31
+
32
+ @story = @a_project.stories.find(847762630)
33
+ @story.notes.all # return all notes (comments) for a story
34
+ @story.notes.create(:text => 'A new comment', :noted_at => '06/29/2010 05:00 EST') # add a new note
35
+
36
+
37
+ @story.attachments # return an array of all attachment items (data only, not the files)
38
+ @story.upload_attachment(file_path) # add a file attachment to @story that can be found at file_path
39
+
40
+
41
+ # All 4 examples below return a PivotalTracker::Story from the new project, with the same story ID
42
+
43
+ @story.move_to_project(123456) # move @story to the project with ID 123456
44
+ @story.move_to_project('123456') # same as above
45
+ @story.move_to_project(@project) # move @story to @project
46
+ @story.move_to_project(@another_story) # move @story into the same project as @another_story
47
+
48
+
49
+
50
+ The API is based on the following this gist: http://gist.github.com/283120
51
+
52
+ == Getting Started
53
+
54
+ * Installing:
55
+
56
+ $ gem install pivotal-tracker
57
+
58
+ * Contributing (requires Bundler >= 0.9.7):
59
+
60
+ $ git clone git://github.com/jsmestad/pivotal-tracker
61
+ $ cd pivotal-tracker
62
+ $ bundle install
63
+ $ bundle exec rake
64
+
65
+ == Additional Information
66
+
67
+ * Wiki: http://wiki.github.com/jsmestad/pivotal-tracker
68
+ * Documentation: http://rdoc.info/projects/jsmestad/pivotal-tracker
69
+ * Pivotal API v3 Docs: http://www.pivotaltracker.com/help/api?version=v3
70
+
71
+ == Contributors along the way
72
+
73
+ * Justin Smestad (http://github.com/jsmestad)
74
+ * Josh Nichols (http://github.com/technicalpickles)
75
+ * Terence Lee (http://github.com/hone)
76
+ * Jon Mischo (http://github.com/supertaz)
77
+ * Gabor Ratky (http://github.com/rgabo)
data/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ # encoding: utf-8
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "tsenart-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/tsenart/pivotal-tracker"
11
+ gem.authors = ["Justin Smestad", "Josh Nichols", "Terence Lee", "Tomás Senart"]
12
+
13
+ gem.add_dependency 'rest-client', '~> 1.6.0'
14
+ gem.add_dependency 'happymapper', '>= 0.3.2'
15
+ gem.add_dependency 'builder'
16
+ gem.add_dependency 'nokogiri', '~> 1.4.3.1'
17
+
18
+ gem.add_development_dependency 'rspec'
19
+ gem.add_development_dependency 'bundler', '>= 0.9.26'
20
+ gem.add_development_dependency 'jeweler'
21
+ gem.add_development_dependency 'stale_fish', '~> 1.3.0'
22
+ end
23
+ Jeweler::GemcutterTasks.new
24
+ rescue LoadError
25
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
26
+ end
27
+
28
+ require 'spec/rake/spectask'
29
+ Spec::Rake::SpecTask.new(:spec) do |spec|
30
+ spec.libs << 'lib' << 'spec'
31
+ spec.spec_files = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
35
+ spec.libs << 'lib' << 'spec'
36
+ spec.pattern = 'spec/**/*_spec.rb'
37
+ spec.rcov = true
38
+ end
39
+
40
+ task :default => :spec
41
+
42
+ begin
43
+ require 'yard'
44
+ YARD::Rake::YardocTask.new
45
+ rescue LoadError
46
+ task :yardoc do
47
+ abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
48
+ end
49
+ end
50
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.4.0
@@ -0,0 +1,46 @@
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
+ #NOTE currently forces UTC as the timezone
27
+ options_string << "occurred_since_date=#{URI.escape options[:occurred_since_date].strftime("%Y/%m/%d %H:%M:%S UTC")}"
28
+ end
29
+
30
+ return "?#{options_string.join('&')}"
31
+ end
32
+
33
+ end
34
+
35
+ element :id, Integer
36
+ element :version, Integer
37
+ element :event_type, String
38
+ element :occurred_at, DateTime
39
+ element :author, String
40
+ element :project_id, Integer
41
+ element :description, String
42
+
43
+ has_many :stories, Story
44
+
45
+ end
46
+ end
@@ -0,0 +1,16 @@
1
+ module PivotalTracker
2
+ class Attachment
3
+ include HappyMapper
4
+
5
+ tag 'attachment'
6
+
7
+ element :id, Integer
8
+ element :filename, String
9
+ element :description, String
10
+ element :uploaded_by, String
11
+ element :uploaded_at, DateTime
12
+ element :url, String
13
+ element :status, String
14
+
15
+ end
16
+ end
@@ -0,0 +1,41 @@
1
+ module PivotalTracker
2
+ class Client
3
+
4
+ class NoToken < StandardError; end
5
+
6
+ class << self
7
+ attr_writer :use_ssl, :token
8
+
9
+ def use_ssl
10
+ @use_ssl || false
11
+ end
12
+
13
+ def token(username, password, method='post')
14
+ return @token if @token
15
+ response = if method == 'post'
16
+ RestClient.post 'https://www.pivotaltracker.com/services/v3/tokens/active', :username => username, :password => password
17
+ else
18
+ RestClient.get "https://#{username}:#{password}@www.pivotaltracker.com/services/v3/tokens/active"
19
+ end
20
+ @token= Nokogiri::XML(response.body).search('guid').inner_html
21
+ end
22
+
23
+ # this is your connection for the entire module
24
+ def connection(options={})
25
+ raise NoToken if @token.to_s.empty?
26
+
27
+ @connections ||= {}
28
+
29
+ @connections[@token] ||= RestClient::Resource.new("#{protocol}://www.pivotaltracker.com/services/v3", :headers => {'X-TrackerToken' => @token, 'Content-Type' => 'application/xml'})
30
+ end
31
+
32
+ protected
33
+
34
+ def protocol
35
+ use_ssl ? 'https' : 'http'
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+ 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,34 @@
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
+
11
+ def current(project)
12
+ array = parse(Client.connection["projects/#{project.id}/iterations/current"].get)
13
+ array.first if array
14
+ end
15
+
16
+ def done(project, options={})
17
+ params = PivotalTracker.encode_options(options)
18
+ parse(Client.connection["/projects/#{project.id}/iterations/done#{params}"].get)
19
+ end
20
+
21
+ def backlog(project, options={})
22
+ params = PivotalTracker.encode_options(options)
23
+ parse(Client.connection["/projects/#{project.id}/iterations/backlog#{params}"].get)
24
+ end
25
+ end
26
+
27
+ element :id, Integer
28
+ element :number, Integer
29
+ element :start, DateTime
30
+ element :finish, DateTime
31
+ has_many :stories, Story
32
+
33
+ end
34
+ 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,58 @@
1
+ module PivotalTracker
2
+ class Note
3
+ include HappyMapper
4
+
5
+ class << self
6
+ def all(story, options={})
7
+ notes = parse(Client.connection["/projects/#{story.project_id}/stories/#{story.id}/notes"].get)
8
+ notes.each { |n| n.project_id, n.story_id = story.project_id, story.id }
9
+ return notes
10
+ end
11
+ end
12
+
13
+ attr_accessor :project_id, :story_id
14
+
15
+ element :id, Integer
16
+ element :text, String
17
+ element :author, String
18
+ element :noted_at, DateTime
19
+ has_one :story, Story
20
+
21
+ def initialize(attributes={})
22
+ if attributes[:owner]
23
+ self.story = attributes.delete(:owner)
24
+ self.project_id = self.story.project_id
25
+ self.story_id = self.story.id
26
+ end
27
+
28
+ update_attributes(attributes)
29
+ end
30
+
31
+ def create
32
+ response = Client.connection["/projects/#{project_id}/stories/#{story_id}/notes"].post(self.to_xml, :content_type => 'application/xml')
33
+ return Note.parse(response)
34
+ end
35
+
36
+ # Pivotal Tracker API doesn't seem to support updating or deleting notes at this time.
37
+
38
+ protected
39
+
40
+ def to_xml
41
+ builder = Nokogiri::XML::Builder.new do |xml|
42
+ xml.note {
43
+ #xml.author "#{author}"
44
+ xml.text_ "#{text}"
45
+ xml.noted_at "#{noted_at}"
46
+ }
47
+ end
48
+ return builder.to_xml
49
+ end
50
+
51
+ def update_attributes(attrs)
52
+ attrs.each do |key, value|
53
+ self.send("#{key}=", value.is_a?(Array) ? value.join(',') : value )
54
+ end
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,58 @@
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 :account, String
22
+ element :week_start_day, String
23
+ element :point_scale, String
24
+ element :week_start_day, String
25
+ element :velocity_scheme, String
26
+ element :iteration_length, Integer
27
+ element :initial_velocity, Integer
28
+ element :current_velocity, Integer
29
+ element :last_activity_at, DateTime
30
+ element :use_https, Boolean
31
+
32
+ def activities
33
+ @activities ||= Proxy.new(self, Activity)
34
+ end
35
+
36
+ def iterations
37
+ @iterations ||= Proxy.new(self, Iteration)
38
+ end
39
+
40
+ def stories
41
+ @stories ||= Proxy.new(self, Story)
42
+ end
43
+
44
+ def memberships
45
+ @memberships ||= Proxy.new(self, Membership)
46
+ end
47
+
48
+ def iteration(group)
49
+ case group.to_sym
50
+ when :done then Iteration.done(self)
51
+ when :current then Iteration.current(self)
52
+ when :backlog then Iteration.backlog(self)
53
+ else
54
+ raise ArgumentError, "Invalid group. Use :done, :current or :backlog instead."
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,66 @@
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 @target.find(param, @owner.id) if @target.respond_to?("find")
21
+ return proxy_found(options).detect { |document| document.id == param }
22
+ end
23
+
24
+ def <<(*objects)
25
+ objects.flatten.each do |object|
26
+ if obj = object.create
27
+ return obj
28
+ else
29
+ return object
30
+ end
31
+ end
32
+ end
33
+
34
+ def create(args)
35
+ object = @target.new(args.merge({:owner => @owner}))
36
+ if obj = object.create
37
+ return obj
38
+ else
39
+ return object
40
+ end
41
+ end
42
+
43
+ protected
44
+
45
+ def proxy_found(options)
46
+ # Check to see if options have changed
47
+ if @opts == options
48
+ @found ||= load_found(options)
49
+ else
50
+ load_found(options)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def method_missing(method, *args, &block)
57
+ @target.send(method, *args, &block)
58
+ end
59
+
60
+ def load_found(options)
61
+ @opts = options
62
+ @target.all(@owner, @opts)
63
+ end
64
+
65
+ end
66
+ end