tracker_api 1.7.1 → 1.11.0

Sign up to get free protection for your applications and to get access to all the features.
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