tracker_api 1.7.1 → 1.11.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -1
  3. data/README.md +16 -0
  4. data/lib/tracker_api.rb +19 -0
  5. data/lib/tracker_api/client.rb +16 -35
  6. data/lib/tracker_api/endpoints/attachment.rb +38 -0
  7. data/lib/tracker_api/endpoints/attachments.rb +22 -0
  8. data/lib/tracker_api/endpoints/blockers.rb +20 -0
  9. data/lib/tracker_api/endpoints/comment.rb +9 -2
  10. data/lib/tracker_api/endpoints/iteration.rb +35 -0
  11. data/lib/tracker_api/endpoints/release.rb +17 -0
  12. data/lib/tracker_api/endpoints/releases.rb +20 -0
  13. data/lib/tracker_api/endpoints/reviews.rb +21 -0
  14. data/lib/tracker_api/endpoints/search.rb +1 -1
  15. data/lib/tracker_api/endpoints/stories.rb +10 -0
  16. data/lib/tracker_api/error.rb +12 -2
  17. data/lib/tracker_api/file_utility.rb +16 -0
  18. data/lib/tracker_api/resources/activity.rb +1 -1
  19. data/lib/tracker_api/resources/blocker.rb +18 -0
  20. data/lib/tracker_api/resources/comment.rb +35 -0
  21. data/lib/tracker_api/resources/cycle_time_details.rb +21 -0
  22. data/lib/tracker_api/resources/daily_history_container.rb +13 -0
  23. data/lib/tracker_api/resources/epic.rb +9 -0
  24. data/lib/tracker_api/resources/file_attachment.rb +37 -0
  25. data/lib/tracker_api/resources/iteration.rb +14 -0
  26. data/lib/tracker_api/resources/project.rb +13 -0
  27. data/lib/tracker_api/resources/release.rb +29 -0
  28. data/lib/tracker_api/resources/review.rb +19 -0
  29. data/lib/tracker_api/resources/review_type.rb +15 -0
  30. data/lib/tracker_api/resources/story.rb +49 -6
  31. data/lib/tracker_api/version.rb +1 -1
  32. data/lib/virtus/attribute/nullify_blank.rb +1 -1
  33. data/test/client_test.rb +52 -52
  34. data/test/comment_test.rb +46 -4
  35. data/test/error_test.rb +47 -0
  36. data/test/file_attachment_test.rb +19 -0
  37. data/test/iteration_test.rb +31 -0
  38. data/test/minitest_helper.rb +5 -2
  39. data/test/project_test.rb +59 -47
  40. data/test/release_test.rb +22 -0
  41. data/test/story_test.rb +65 -52
  42. data/test/task_test.rb +3 -3
  43. data/test/vcr/cassettes/create_attachments.json +1 -0
  44. data/test/vcr/cassettes/create_comment.json +1 -1
  45. data/test/vcr/cassettes/create_comment_with_attachment.json +1 -0
  46. data/test/vcr/cassettes/create_story_comment.json +1 -1
  47. data/test/vcr/cassettes/delete_an_attachment.json +1 -0
  48. data/test/vcr/cassettes/delete_attachments.json +1 -0
  49. data/test/vcr/cassettes/delete_comment.json +1 -0
  50. data/test/vcr/cassettes/delete_comments.json +1 -0
  51. data/test/vcr/cassettes/get_current_iteration.json +1 -1
  52. data/test/vcr/cassettes/get_cycle_time_details.json +1 -0
  53. data/test/vcr/cassettes/get_daily_history_container.json +1 -0
  54. data/test/vcr/cassettes/get_releases.json +1 -0
  55. data/test/vcr/cassettes/get_story_in_epic.json +1 -1
  56. data/test/vcr/cassettes/get_story_reviews.json +1 -0
  57. data/test/vcr/cassettes/release_stories.json +1 -0
  58. data/test/vcr/cassettes/search_project.json +1 -1
  59. data/test/workspace_test.rb +5 -5
  60. data/tracker_api.gemspec +3 -2
  61. metadata +68 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cbd6533872fb55f68afdf21309b4187d7061b8a6
4
- data.tar.gz: 71b0f57265c1840a1271f3025406aef56896e5c5
3
+ metadata.gz: cce8ed0f32ae28c44915d1a2aa0dff3f7deb44f9
4
+ data.tar.gz: 6478d86c0e8e8e826e0ffd50b8111758f0dec9c0
5
5
  SHA512:
6
- metadata.gz: 88cff726815d37af8312853895bed38974eefb993d1bbc00050fcf63bf393f2d8a6618d1a72df6ae0cbf0a0124deff6ee48f9798bc13027da0bac8a73c31ca7f
7
- data.tar.gz: a1a864149b4175c72f345ff143b60f16dd2a992d848fd55fd39300287f71e5a0000a0eacdd30783d095d70927ecc2ffbf9207b48616fc62a98ea0a57f5657c06
6
+ metadata.gz: 34b10827d2868cf6e0de4039ef23444b412fcb5d162e6fca2a20258f22d3ceede1e45cb4f7e829cb57ccd8a66a3506144e53eed8f7ae6954b2d2196c6244e0f1
7
+ data.tar.gz: 69eb1bbad1ea7a6ef35b84e36c9b856ebc300eda60369769c242a9ec77fd5cf3c436c7280101341f657d46beab1fc382e16bbb43c074bcb6b89c97b74d71105b
@@ -5,7 +5,8 @@ rvm:
5
5
  - 2.2
6
6
  - 2.3
7
7
  - 2.4
8
- # - "jruby"
8
+ - 2.5
9
+ - "jruby"
9
10
  # - rbx
10
11
  # - "1.8.7"
11
12
  # uncomment this line if your project needs to run something other than `rake`:
data/README.md CHANGED
@@ -68,9 +68,20 @@ comments = story.comments # co
68
68
 
69
69
  comment = story.create_comment(text: "Use the force!") # Create a new comment on the story
70
70
 
71
+ comment = story.create_comment(text: "Use the force again !", # Create a new comment on the story with
72
+ files: ['path/to/an/existing/file']) # file attachments
73
+
71
74
  comment.text += " (please be careful)"
72
75
  comment.save # Update text of an existing comment
76
+ comment.delete # Delete an existing comment
77
+
78
+ comment.create_attachments(files: ['path/to/an/existing/file']) # Add attachments to existing comment
79
+ comment.delete_attachments # Delete all attachments from a comment
73
80
 
81
+ attachments = comment.attachments # Get attachments associated with a comment
82
+ attachments.first.delete # Delete a specific attachment
83
+
84
+ comment.attachments(reload: true) # Re-load the attachments after modification
74
85
  task = story.tasks.first # Get story tasks
75
86
  task.complete = true
76
87
  task.save # Mark a task complete
@@ -95,6 +106,10 @@ client.project(project_id).stories(fields: ':default,tasks') # Eage
95
106
  story.comments(fields: ':default,person') # Eagerly get comments and the person that made the comment for a story
96
107
  ```
97
108
 
109
+ ## Error Handling
110
+ `TrackerApi::Errors::ClientError` is raised for 4xx HTTP status codes
111
+ `TrackerApi::Errors::ServerError` is raised for 5xx HTTP status codes
112
+
98
113
  ## Warning
99
114
 
100
115
  Direct mutation of an attribute value skips coercion and dirty tracking. Please use direct assignment or the specialized add_* methods to get expected behavior.
@@ -118,6 +133,7 @@ story.save
118
133
 
119
134
  - Add missing resources and endpoints
120
135
  - Add create, update, delete for resources
136
+ - Error handling for [error responses](https://www.pivotaltracker.com/help/api#Error_Responses)
121
137
 
122
138
  ## Semantic Versioning
123
139
  http://semver.org/
@@ -3,6 +3,8 @@ require 'tracker_api/version'
3
3
  # dependencies
4
4
  require 'faraday'
5
5
  require 'faraday_middleware'
6
+ require 'pathname'
7
+ require 'mimemagic'
6
8
 
7
9
  if defined?(ActiveSupport)
8
10
  require 'active_support/core_ext/object/blank'
@@ -26,15 +28,20 @@ module TrackerApi
26
28
  autoload :Error, 'tracker_api/error'
27
29
  autoload :Client, 'tracker_api/client'
28
30
  autoload :Logger, 'tracker_api/logger'
31
+ autoload :FileUtility, 'tracker_api/file_utility'
29
32
 
30
33
  module Errors
31
34
  class UnexpectedData < StandardError; end
35
+ class ClientError < Error; end
36
+ class ServerError < Error; end
32
37
  end
33
38
 
34
39
  module Endpoints
35
40
  autoload :Activity, 'tracker_api/endpoints/activity'
41
+ autoload :Blockers, 'tracker_api/endpoints/blockers'
36
42
  autoload :Epic, 'tracker_api/endpoints/epic'
37
43
  autoload :Epics, 'tracker_api/endpoints/epics'
44
+ autoload :Iteration, 'tracker_api/endpoints/iteration'
38
45
  autoload :Iterations, 'tracker_api/endpoints/iterations'
39
46
  autoload :Labels, 'tracker_api/endpoints/labels'
40
47
  autoload :Me, 'tracker_api/endpoints/me'
@@ -55,6 +62,11 @@ module TrackerApi
55
62
  autoload :Webhook, 'tracker_api/endpoints/webhook'
56
63
  autoload :Webhooks, 'tracker_api/endpoints/webhooks'
57
64
  autoload :StoryTransitions, 'tracker_api/endpoints/story_transitions'
65
+ autoload :Attachment, 'tracker_api/endpoints/attachment'
66
+ autoload :Attachments, 'tracker_api/endpoints/attachments'
67
+ autoload :Releases, 'tracker_api/endpoints/releases'
68
+ autoload :Release, 'tracker_api/endpoints/release'
69
+ autoload :Reviews, 'tracker_api/endpoints/reviews'
58
70
  end
59
71
 
60
72
  module Resources
@@ -64,6 +76,7 @@ module TrackerApi
64
76
  end
65
77
  autoload :Activity, 'tracker_api/resources/activity'
66
78
  autoload :Account, 'tracker_api/resources/account'
79
+ autoload :Blocker, 'tracker_api/resources/blocker'
67
80
  autoload :Change, 'tracker_api/resources/change'
68
81
  autoload :Epic, 'tracker_api/resources/epic'
69
82
  autoload :EpicsSearchResult, 'tracker_api/resources/epics_search_result'
@@ -85,5 +98,11 @@ module TrackerApi
85
98
  autoload :Comment, 'tracker_api/resources/comment'
86
99
  autoload :Webhook, 'tracker_api/resources/webhook'
87
100
  autoload :StoryTransition, 'tracker_api/resources/story_transition'
101
+ autoload :FileAttachment, 'tracker_api/resources/file_attachment'
102
+ autoload :Release, 'tracker_api/resources/release'
103
+ autoload :CycleTimeDetails, 'tracker_api/resources/cycle_time_details'
104
+ autoload :DailyHistoryContainer, 'tracker_api/resources/daily_history_container'
105
+ autoload :Review, 'tracker_api/resources/review'
106
+ autoload :ReviewType, 'tracker_api/resources/review_type'
88
107
  end
89
108
  end
@@ -25,7 +25,7 @@ module TrackerApi
25
25
  @url = Addressable::URI.parse(url).to_s
26
26
  @api_version = options.fetch(:api_version, '/services/v5')
27
27
  @logger = options.fetch(:logger, ::Logger.new(nil))
28
- adapter = options.fetch(:adapter, :excon)
28
+ adapter = options.fetch(:adapter) { defined?(JRUBY_VERSION) ? :net_http : :excon }
29
29
  connection_options = options.fetch(:connection_options, { ssl: { verify: true } })
30
30
  @auto_paginate = options.fetch(:auto_paginate, true)
31
31
  @token = options[:token]
@@ -36,7 +36,7 @@ module TrackerApi
36
36
  @connection = Faraday.new({ url: @url }.merge(connection_options)) do |builder|
37
37
  # response
38
38
  builder.use Faraday::Response::RaiseError
39
- builder.response :json
39
+ builder.response :json, content_type: /\bjson/ # e.g., 'application/json; charset=utf-8'
40
40
 
41
41
  # request
42
42
  builder.request :multipart
@@ -48,40 +48,15 @@ module TrackerApi
48
48
  end
49
49
  end
50
50
 
51
- # Make a HTTP GET request
51
+ # HTTP requests methods
52
52
  #
53
53
  # @param path [String] The path, relative to api endpoint
54
54
  # @param options [Hash] Query and header params for request
55
55
  # @return [Faraday::Response]
56
- def get(path, options = {})
57
- request(:get, parse_query_and_convenience_headers(path, options))
58
- end
59
-
60
- # Make a HTTP POST request
61
- #
62
- # @param path [String] The path, relative to api endpoint
63
- # @param options [Hash] Query and header params for request
64
- # @return [Faraday::Response]
65
- def post(path, options = {})
66
- request(:post, parse_query_and_convenience_headers(path, options))
67
- end
68
-
69
- # Make a HTTP PUT request
70
- #
71
- # @param path [String] The path, relative to api endpoint
72
- # @param options [Hash] Query and header params for request
73
- # @return [Faraday::Response]
74
- def put(path, options = {})
75
- request(:put, parse_query_and_convenience_headers(path, options))
76
- end
77
-
78
- # Make a HTTP DELETE request
79
- #
80
- # @param path [String] The path, relative to api endpoint
81
- # @param options [Hash] Query and header params for request
82
- # @return [Faraday::Response]
83
- def delete(path, options = {})
84
- request(:delete, parse_query_and_convenience_headers(path, options))
56
+ %i{get post patch put delete}.each do |verb|
57
+ define_method verb do |path, options = {}|
58
+ request(verb, parse_query_and_convenience_headers(path, options))
59
+ end
85
60
  end
86
61
 
87
62
  # Make one or more HTTP GET requests, optionally fetching
@@ -215,7 +190,8 @@ module TrackerApi
215
190
  opts[:params] = options[:params] || {}
216
191
  opts[:token] = options[:token] || @token
217
192
  headers = { 'User-Agent' => USER_AGENT,
218
- 'X-TrackerToken' => opts.fetch(:token) }.merge(options.fetch(:headers, {}))
193
+ 'X-TrackerToken' => opts.fetch(:token),
194
+ 'Accept' => 'application/json' }.merge(options.fetch(:headers, {}))
219
195
 
220
196
  CONVENIENCE_HEADERS.each do |h|
221
197
  if header = options[h]
@@ -247,8 +223,13 @@ module TrackerApi
247
223
  req.body = body
248
224
  end
249
225
  response
250
- rescue Faraday::Error::ClientError => e
251
- raise TrackerApi::Error.new(e)
226
+ rescue Faraday::ClientError, Faraday::ServerError => e
227
+ status_code = e.response[:status]
228
+ case status_code
229
+ when 400..499 then raise TrackerApi::Errors::ClientError.new(e)
230
+ when 500..599 then raise TrackerApi::Errors::ServerError.new(e)
231
+ else raise "Expected 4xx or 5xx HTTP status code; got #{status_code} instead."
232
+ end
252
233
  end
253
234
 
254
235
  class Pagination
@@ -0,0 +1,38 @@
1
+ module TrackerApi
2
+ module Endpoints
3
+ class Attachment
4
+ attr_accessor :client
5
+
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ def create(comment, file)
11
+ data = client.post("/projects/#{comment.project_id}/uploads", body: FileUtility.get_file_upload(file)).body
12
+ Resources::FileAttachment.new({ comment: comment }.merge(data))
13
+ end
14
+
15
+ # TODO : Discuss before implementing this as it orphans the file.
16
+ # It deletes source, but the file name appears in the comments
17
+ # def delete(comment, file_attachment_id)
18
+ # client.delete("/projects/#{comment.project_id}/stories/#{comment.story_id}/comments/#{comment.id}/file_attachments/#{file_attachment_id}").body
19
+ # end
20
+
21
+ def get(comment)
22
+ data = client.get("/projects/#{comment.project_id}/stories/#{comment.story_id}/comments/#{comment.id}?fields=file_attachments").body["file_attachments"]
23
+ raise Errors::UnexpectedData, 'Array of file attachments expected' unless data.is_a? Array
24
+
25
+ data.map do |file_attachment|
26
+ Resources::FileAttachment.new({ comment: comment }.merge(file_attachment))
27
+ end
28
+ end
29
+
30
+ # TODO : Implement this properly.
31
+ # This results in either content of the file or an S3 link.
32
+ # the S3 link is also present in big_url attribute.
33
+ # def download(download_path)
34
+ # client.get(download_path, url: '').body
35
+ # end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,22 @@
1
+ module TrackerApi
2
+ module Endpoints
3
+ class Attachments
4
+ attr_accessor :client
5
+
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+
11
+ def create(comment, files)
12
+ return [] if files.to_a.empty?
13
+ #Check files before upload to make it all or none.
14
+ FileUtility.check_files_exist(files)
15
+ attachment = Endpoints::Attachment.new(client)
16
+ files.map do | file |
17
+ attachment.create(comment, file)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ module TrackerApi
2
+ module Endpoints
3
+ class Blockers
4
+ attr_accessor :client
5
+
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ def get(project_id, story_id, params = {})
11
+ data = client.get("/projects/#{project_id}/stories/#{story_id}/blockers", params: params).body
12
+ raise Errors::UnexpectedData, 'Array of Blockers expected' unless data.is_a? Array
13
+
14
+ data.map do |blocker|
15
+ Resources::Blocker.new({ client: client, project_id: project_id }.merge(blocker))
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -15,13 +15,20 @@ module TrackerApi
15
15
  def update(comment, params={})
16
16
  raise ArgumentError, 'Valid comment required to update.' unless comment.instance_of?(Resources::Comment)
17
17
 
18
- data = client.put("/projects/#{comment.project_id}/stories/#{comment.story_id}/comments/#{comment.id}",
19
- params: params).body
18
+ path = "/projects/#{comment.project_id}/stories/#{comment.story_id}/comments/#{comment.id}"
19
+ path += "?fields=:default,file_attachments" if params.represented.file_attachment_ids_to_add.present? || params.represented.file_attachment_ids_to_remove.present?
20
+ data = client.put(path, params: params).body
20
21
 
21
22
  comment.attributes = data
22
23
  comment.clean!
23
24
  comment
24
25
  end
26
+
27
+ def delete(comment)
28
+ raise ArgumentError, 'Valid comment required to update.' unless comment.instance_of?(Resources::Comment)
29
+
30
+ client.delete("/projects/#{comment.project_id}/stories/#{comment.story_id}/comments/#{comment.id}").body
31
+ end
25
32
  end
26
33
  end
27
34
  end
@@ -0,0 +1,35 @@
1
+ module TrackerApi
2
+ module Endpoints
3
+ class Iteration
4
+ attr_accessor :client
5
+
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ def get(project_id, iteration_number)
11
+ data = client.get("/projects/#{project_id}/iterations/#{iteration_number}").body
12
+
13
+ Resources::Iteration.new({ client: client, project_id: project_id }.merge(data))
14
+ end
15
+
16
+ def get_analytics_cycle_time_details(project_id, iteration_number)
17
+ data = client.paginate("/projects/#{project_id}/iterations/#{iteration_number}/analytics/cycle_time_details")
18
+ raise Errors::UnexpectedData, 'Array of cycle time details expected' unless data.is_a? Array
19
+
20
+ data.map do |cycle_time_details|
21
+ Resources::CycleTimeDetails.new(
22
+ { project_id: project_id, iteration_number: iteration_number }.merge(cycle_time_details)
23
+ )
24
+ end
25
+ end
26
+
27
+ def get_history(project_id, iteration_number)
28
+ data = client.get("/projects/#{project_id}/history/iterations/#{iteration_number}/days").body
29
+ raise Errors::UnexpectedData, 'Hash of history data expected' unless data.is_a? Hash
30
+
31
+ Resources::DailyHistoryContainer.new({ project_id: project_id, iteration_number: iteration_number }.merge(data))
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,17 @@
1
+ module TrackerApi
2
+ module Endpoints
3
+ class Release
4
+ attr_accessor :client
5
+
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ def get(project_id, id, params={})
11
+ data = client.get("/projects/#{project_id}/releases/#{id}", params: params).body
12
+
13
+ Resources::Release.new({ client: client }.merge(data))
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ module TrackerApi
2
+ module Endpoints
3
+ class Releases
4
+ attr_accessor :client
5
+
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ def get(project_id, params={})
11
+ data = client.paginate("/projects/#{project_id}/releases", params: params)
12
+ raise Errors::UnexpectedData, 'Array of releases expected' unless data.is_a? Array
13
+
14
+ data.map do |release|
15
+ Resources::Release.new({ client: client, project_id: project_id }.merge(release))
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ module TrackerApi
2
+ module Endpoints
3
+ class Reviews
4
+ attr_accessor :client
5
+
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ def get(project_id, story_id, params={})
11
+ params[:fields] ||= ":default,review_type"
12
+ data = client.paginate("/projects/#{project_id}/stories/#{story_id}/reviews", params: params)
13
+ raise Errors::UnexpectedData, 'Successful responses to this request return an array containing zero or more instances of the review resource. This response was not an array.' unless data.is_a? Array
14
+
15
+ data.map do |review|
16
+ Resources::Review.new({ client: client, project_id: project_id }.merge(review))
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -10,7 +10,7 @@ module TrackerApi
10
10
  def get(project_id, query, options={})
11
11
  raise ArgumentError, 'Valid query string required to search' unless query.is_a?(String)
12
12
 
13
- options.key?(:body) ? options[:body][:query] = query : options[:body] = { query: query }
13
+ options[:params] = { query: query }
14
14
  data = client.get("/projects/#{project_id}/search", options).body
15
15
 
16
16
  raise Errors::UnexpectedData, 'Hash of search results expect' unless data.is_a? Hash
@@ -17,6 +17,16 @@ module TrackerApi
17
17
  Resources::Story.new({ client: client, project_id: project_id }.merge(story))
18
18
  end
19
19
  end
20
+
21
+ def get_release(project_id, release_id, params={})
22
+ data = client.paginate("/projects/#{project_id}/releases/#{release_id}/stories", params: params)
23
+
24
+ raise Errors::UnexpectedData, 'Array of stories expected' unless data.is_a? Array
25
+
26
+ data.map do |story|
27
+ Resources::Story.new({ client: client, project_id: project_id }.merge(story))
28
+ end
29
+ end
20
30
  end
21
31
  end
22
32
  end