pivotal-tracker-api 0.2.3 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +8 -2
- data/Gemfile.lock +65 -47
- data/README.md +7 -7
- data/VERSION +1 -1
- data/lib/pivotal-tracker-api.rb +10 -2
- data/lib/pivotal-tracker-api/activity.rb +52 -18
- data/lib/pivotal-tracker-api/analytics.rb +23 -0
- data/lib/pivotal-tracker-api/base.rb +14 -2
- data/lib/pivotal-tracker-api/client.rb +43 -21
- data/lib/pivotal-tracker-api/comment.rb +76 -27
- data/lib/pivotal-tracker-api/core_ext/string.rb +3 -0
- data/lib/pivotal-tracker-api/cycle_time_details.rb +43 -0
- data/lib/pivotal-tracker-api/file_attachment.rb +56 -0
- data/lib/pivotal-tracker-api/iteration.rb +73 -11
- data/lib/pivotal-tracker-api/label.rb +32 -0
- data/lib/pivotal-tracker-api/me.rb +16 -0
- data/lib/pivotal-tracker-api/person.rb +13 -9
- data/lib/pivotal-tracker-api/project.rb +76 -23
- data/lib/pivotal-tracker-api/service.rb +202 -0
- data/lib/pivotal-tracker-api/story.rb +173 -132
- data/lib/pivotal-tracker-api/story_transition.rb +81 -0
- data/lib/pivotal-tracker-api/string_extensions.rb +61 -0
- data/lib/pivotal-tracker-api/task.rb +56 -12
- data/pivotal-tracker-api.gemspec +28 -15
- data/test/helper.rb +1 -0
- data/test/test_activity.rb +79 -0
- data/test/test_analytics.rb +38 -0
- data/test/test_cycle_time_details.rb +69 -0
- data/test/test_iteration.rb +182 -0
- data/test/test_label.rb +29 -0
- data/test/test_me.rb +52 -0
- data/test/test_service.rb +67 -0
- data/test/test_story.rb +557 -0
- data/test/test_story_transition.rb +80 -0
- data/test/test_string_extensions.rb +37 -0
- metadata +29 -27
- data/lib/pivotal-tracker-api/attachment.rb +0 -28
- data/lib/pivotal-tracker-api/pivotal_service.rb +0 -141
- data/test/test_pivotal-tracker-api.rb +0 -7
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module PivotalAPI
|
4
|
+
class Service
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def set_token(token)
|
9
|
+
PivotalAPI::Client.token = token
|
10
|
+
end
|
11
|
+
|
12
|
+
def me(username, password)
|
13
|
+
PivotalAPI::Client.username = username
|
14
|
+
PivotalAPI::Client.password = password
|
15
|
+
response = PivotalAPI::Client.ssl_get("/me")
|
16
|
+
json_me = JSON.parse(response, {:symbolize_names => true})
|
17
|
+
me = PivotalAPI::Me.from_json(json_me)
|
18
|
+
PivotalAPI::Client.token = me.api_token
|
19
|
+
me
|
20
|
+
end
|
21
|
+
|
22
|
+
def activity(opts={})
|
23
|
+
# opts:
|
24
|
+
# :project_id - REQUIRED - A valid pivotal project ID
|
25
|
+
# :story_id - OPTIONAL - A valid pivotal story ID. NOTE: Optional if requesting project activity. Required if requesting story activity.
|
26
|
+
# :fields - OPTIONAL - specific fields to ask pivotal to return for each activity
|
27
|
+
# :parameters - OPTIONAL - See list of parameters here https://www.pivotaltracker.com/help/api/rest/v5#Activity
|
28
|
+
#
|
29
|
+
# Default Parameters: {limit: 20}
|
30
|
+
|
31
|
+
raise ArgumentError.new("missing required key/value :project_id") unless opts[:project_id]
|
32
|
+
|
33
|
+
project_id = opts[:project_id]
|
34
|
+
opts[:parameters] = {} unless opts[:parameters]
|
35
|
+
opts[:parameters][:limit] = 20 unless opts[:parameters][:limit]
|
36
|
+
opts[:parameters][:fields] = opts[:fields] if opts[:fields] && opts[:parameters][:fields].nil?
|
37
|
+
|
38
|
+
api_url = "/projects/#{project_id}/"
|
39
|
+
api_url += "stories/#{opts[:story_id]}/" if opts[:story_id]
|
40
|
+
api_url += "activity?"
|
41
|
+
api_url.append_pivotal_params(opts[:parameters])
|
42
|
+
|
43
|
+
puts "\n****** URL: #{api_url}\n\n"
|
44
|
+
|
45
|
+
response = PivotalAPI::Client.get(api_url)
|
46
|
+
json_activity = JSON.parse(response, {:symbolize_names => true})
|
47
|
+
PivotalAPI::Activity.from_json(json_activity)
|
48
|
+
end
|
49
|
+
|
50
|
+
def comments(project_id, story, fields)
|
51
|
+
# opts:
|
52
|
+
# :project_id - REQUIRED - A valid pivotal project ID
|
53
|
+
# :story_id - REQUIRED - A valid pivotal story ID
|
54
|
+
# :fields - OPTIONAL - specific fields to ask pivotal to return for each activity
|
55
|
+
# :parameters - OPTIONAL - See list of parameters here https://www.pivotaltracker.com/help/api/rest/v5#Activity
|
56
|
+
#
|
57
|
+
# Default Parameters: {limit: 20}
|
58
|
+
|
59
|
+
raise ArgumentError.new("missing required key/value :project_id") unless opts[:project_id]
|
60
|
+
raise ArgumentError.new("missing required key/value :story_id") unless opts[:story_id]
|
61
|
+
|
62
|
+
project_id = opts[:project_id]
|
63
|
+
story_id = opts[:story_id]
|
64
|
+
opts[:parameters] = {} unless opts[:parameters]
|
65
|
+
opts[:parameters][:limit] = 20 unless opts[:parameters][:limit]
|
66
|
+
opts[:parameters][:fields] = opts[:fields] if opts[:fields] && opts[:parameters][:fields].nil?
|
67
|
+
|
68
|
+
api_url = "/projects/#{project_id}/stories/#{story_id}/comments"
|
69
|
+
api_url.append_pivotal_params(opts[:parameters])
|
70
|
+
|
71
|
+
puts "\n****** URL: #{api_url}\n\n"
|
72
|
+
|
73
|
+
response = PivotalAPI::Client.get_with_caching(api_url)
|
74
|
+
json_comments = JSON.parse(response, {:symbolize_names => true})
|
75
|
+
PivotalAPI::Comments.from_json(json_comments)
|
76
|
+
end
|
77
|
+
|
78
|
+
def projects(opts={})
|
79
|
+
# opts:
|
80
|
+
# :fields - OPTIONAL - specific fields to ask pivotal to return for each project
|
81
|
+
|
82
|
+
opts[:parameters] = {} unless opts[:parameters]
|
83
|
+
opts[:parameters][:fields] = opts[:fields] if opts[:fields] && opts[:parameters][:fields].nil?
|
84
|
+
|
85
|
+
api_url = '/projects'
|
86
|
+
api_url.append_pivotal_params(opts[:parameters])
|
87
|
+
|
88
|
+
puts "\n****** URL: #{api_url}\n\n"
|
89
|
+
|
90
|
+
response = PivotalAPI::Client.get(api_url)
|
91
|
+
json_projects = JSON.parse(response, {:symbolize_names => true})
|
92
|
+
PivotalAPI::Projects.from_json(json_projects)
|
93
|
+
end
|
94
|
+
|
95
|
+
def project(opts={})
|
96
|
+
# opts:
|
97
|
+
# :project_id - REQUIRED - A valid pivotal project ID
|
98
|
+
# :fields - OPTIONAL - specific fields to ask pivotal to return for the project
|
99
|
+
|
100
|
+
raise ArgumentError.new("missing required key/value :project_id") unless opts[:project_id]
|
101
|
+
|
102
|
+
project_id = opts[:project_id]
|
103
|
+
opts[:parameters] = {} unless opts[:parameters]
|
104
|
+
opts[:parameters][:fields] = opts[:fields] if opts[:fields] && opts[:parameters][:fields].nil?
|
105
|
+
|
106
|
+
return @project if !@project.nil? && @project.id == project_id.to_i
|
107
|
+
|
108
|
+
api_url = "/projects/#{project_id}"
|
109
|
+
api_url.append_pivotal_params(opts[:parameters])
|
110
|
+
|
111
|
+
puts "\n****** URL: #{api_url}\n\n"
|
112
|
+
|
113
|
+
response = PivotalAPI::Client.get(api_url)
|
114
|
+
json_project = JSON.parse(response, {:symbolize_names => true})
|
115
|
+
@project = PivotalAPI::Project.from_json(json_project)
|
116
|
+
end
|
117
|
+
|
118
|
+
def iterations(opts={})
|
119
|
+
# opts:
|
120
|
+
# :project_id - REQUIRED - A valid pivotal project ID
|
121
|
+
# :fields - OPTIONAL - specific fields to ask pivotal to return for each iteration
|
122
|
+
# :parameters - OPTIONAL - See list of parameters here https://www.pivotaltracker.com/help/api/rest/v5#Iterations
|
123
|
+
#
|
124
|
+
# Default Parameters: {scope: 'current', limit: 1, offset: 0}
|
125
|
+
|
126
|
+
raise ArgumentError.new("missing required key/value :project_id") unless opts[:project_id]
|
127
|
+
|
128
|
+
project_id = opts[:project_id]
|
129
|
+
opts[:parameters] = {} unless opts[:parameters]
|
130
|
+
opts[:parameters][:scope] = 'current' unless opts[:parameters][:scope]
|
131
|
+
opts[:parameters][:limit] = 1 unless opts[:parameters][:limit]
|
132
|
+
opts[:parameters][:offset] = 0 unless opts[:parameters][:offset]
|
133
|
+
opts[:parameters][:fields] = opts[:fields] if opts[:fields] && opts[:parameters][:fields].nil?
|
134
|
+
|
135
|
+
api_url = "/projects/#{project_id}/iterations"
|
136
|
+
api_url.append_pivotal_params(opts[:parameters])
|
137
|
+
|
138
|
+
puts "\n****** URL: #{api_url}\n\n"
|
139
|
+
|
140
|
+
response = PivotalAPI::Client.get_with_caching(api_url)
|
141
|
+
json_iterations = JSON.parse(response, {:symbolize_names => true})
|
142
|
+
PivotalAPI::Iterations.from_json(json_iterations)
|
143
|
+
end
|
144
|
+
|
145
|
+
def stories(opts={})
|
146
|
+
# opts:
|
147
|
+
# :project_id - REQUIRED - A valid pivotal project ID
|
148
|
+
# :fields - OPTIONAL - specific fields to ask pivotal to return for each story
|
149
|
+
# :parameters - OPTIONAL - See list of parameters here https://www.pivotaltracker.com/help/api/rest/v5#projects_project_id_stories_get
|
150
|
+
|
151
|
+
raise ArgumentError.new("missing required key/value :project_id") unless opts[:project_id]
|
152
|
+
|
153
|
+
project_id = opts[:project_id]
|
154
|
+
opts[:parameters] = {} unless opts[:parameters]
|
155
|
+
opts[:parameters][:limit] = 20 unless opts[:parameters][:limit]
|
156
|
+
opts[:parameters][:offset] = 0 unless opts[:parameters][:offset]
|
157
|
+
opts[:parameters][:fields] = opts[:fields] if opts[:fields] && opts[:parameters][:fields].nil?
|
158
|
+
|
159
|
+
api_url = "/projects/#{project_id}/stories"
|
160
|
+
api_url.append_pivotal_params(opts[:parameters])
|
161
|
+
|
162
|
+
puts "\n****** URL: #{api_url}\n\n"
|
163
|
+
|
164
|
+
response = PivotalAPI::Client.get(api_url)
|
165
|
+
json_story = JSON.parse(response, {:symbolize_names => true})
|
166
|
+
PivotalAPI::Stories.from_json(json_story)
|
167
|
+
end
|
168
|
+
|
169
|
+
def story(opts={})
|
170
|
+
# opts:
|
171
|
+
# :project_id - REQUIRED - A valid pivotal project ID
|
172
|
+
# :story_id - REQUIRED - A valid pivotal story ID
|
173
|
+
# :fields - OPTIONAL - specific fields to ask pivotal to return for each story
|
174
|
+
|
175
|
+
raise ArgumentError.new("missing required key/value :project_id") unless opts[:project_id]
|
176
|
+
raise ArgumentError.new("missing required key/value :story_id") unless opts[:story_id]
|
177
|
+
|
178
|
+
project_id = opts[:project_id]
|
179
|
+
opts[:parameters] = {} unless opts[:parameters]
|
180
|
+
opts[:parameters][:fields] = opts[:fields] if opts[:fields] && opts[:parameters][:fields].nil?
|
181
|
+
|
182
|
+
api_url = "/projects/#{project_id}/stories/#{story_id}"
|
183
|
+
api_url.append_pivotal_params(opts[:parameters])
|
184
|
+
|
185
|
+
puts "\n****** URL: #{api_url}\n\n"
|
186
|
+
|
187
|
+
response = PivotalAPI::Client.get(api_url)
|
188
|
+
json_story = JSON.parse(response, {:symbolize_names => true})
|
189
|
+
PivotalAPI::Story.from_json(json_story)
|
190
|
+
end
|
191
|
+
|
192
|
+
def update_story(story_id, project_id, updates={})
|
193
|
+
raise ArgumentError.new("missing required parameter project_id") unless project_id
|
194
|
+
raise ArgumentError.new("missing required parameter sotry_id") unless story_id
|
195
|
+
|
196
|
+
api_url = "/projects/#{project.id}/stories/#{story_id}"
|
197
|
+
PivotalAPI::Client.put(api_url, updates)
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -1,41 +1,178 @@
|
|
1
|
-
|
1
|
+
# PROPERTIES
|
2
|
+
# id int
|
3
|
+
# — Database id of the story. This field is read only. This field is always returned.
|
4
|
+
#
|
5
|
+
# project_id int
|
6
|
+
# — id of the project.
|
7
|
+
#
|
8
|
+
# name string[5000]
|
9
|
+
# Required On Create — Name of the story. This field is required on create.
|
10
|
+
#
|
11
|
+
# description string[20000]
|
12
|
+
# — In-depth explanation of the story requirements.
|
13
|
+
#
|
14
|
+
# story_type enumerated string
|
15
|
+
# — Type of story.
|
16
|
+
# Valid enumeration values: feature, bug, chore, release
|
17
|
+
#
|
18
|
+
# current_state enumerated string
|
19
|
+
# — Story's state of completion.
|
20
|
+
# Valid enumeration values: accepted, delivered, finished, started, rejected, planned, unstarted, unscheduled
|
21
|
+
#
|
22
|
+
# estimate float
|
23
|
+
# — Point value of the story.
|
24
|
+
#
|
25
|
+
# accepted_at datetime
|
26
|
+
# — Acceptance time.
|
27
|
+
#
|
28
|
+
# deadline datetime
|
29
|
+
# — Due date/time (for a release-type story).
|
30
|
+
#
|
31
|
+
# requested_by_id int
|
32
|
+
# — The id of the person who requested the story. In API responses, this attribute may be requested_by_id or requested_by.
|
33
|
+
#
|
34
|
+
# owned_by_id int
|
35
|
+
# — The id of the person who owns the story. In API responses, this attribute may be owned_by_id or owned_by.
|
36
|
+
#
|
37
|
+
# owner_ids List[int]
|
38
|
+
# — IDs of the current story owners. By default this will be included in responses as an array of nested structures, using the key owners. In API responses, this attribute may be owner_ids or owners.
|
39
|
+
#
|
40
|
+
# label_ids List[int]
|
41
|
+
# — IDs of labels currently applied to story. By default this will be included in responses as an array of nested structures, using the key labels. In API responses, this attribute may be label_ids or labels.
|
42
|
+
#
|
43
|
+
# task_ids List[int]
|
44
|
+
# — IDs of tasks currently on the story. This field is writable only on create. This field is excluded by default. In API responses, this attribute may be task_ids or tasks.
|
45
|
+
#
|
46
|
+
# follower_ids List[int]
|
47
|
+
# — IDs of people currently following the story. This field is excluded by default. In API responses, this attribute may be follower_ids or followers.
|
48
|
+
#
|
49
|
+
# comment_ids List[int]
|
50
|
+
# — IDs of comments currently on the story. This field is writable only on create. This field is excluded by default. In API responses, this attribute may be comment_ids or comments.
|
51
|
+
#
|
52
|
+
# created_at datetime
|
53
|
+
# — Creation time. This field is writable only on create.
|
54
|
+
#
|
55
|
+
# updated_at datetime
|
56
|
+
# — Time of last update. This field is read only.
|
57
|
+
#
|
58
|
+
# before_id int
|
59
|
+
# — ID of the story that the current story is located before. Null if story is last one in the project. This field is excluded by default.
|
60
|
+
#
|
61
|
+
# after_id int
|
62
|
+
# — ID of the story that the current story is located after. Null if story is the first one in the project. This field is excluded by default.
|
63
|
+
#
|
64
|
+
# integration_id int
|
65
|
+
# — ID of the integration API that is linked to this story. In API responses, this attribute may be integration_id or integration.
|
66
|
+
#
|
67
|
+
# external_id string[255]
|
68
|
+
# — The integration's specific ID for the story. (Note that this attribute does not indicate an association to another resource.)
|
69
|
+
#
|
70
|
+
# url string
|
71
|
+
# — The url for this story in Tracker. This field is read only.
|
72
|
+
#
|
73
|
+
# transitions List[story_transition]
|
74
|
+
# — All state transitions for the story. This field is read only. This field is excluded by default.
|
75
|
+
#
|
76
|
+
# cycle_time_details cycle_time_details
|
77
|
+
# — All information regarding a story's cycle time and state transitions (duration and occurrences). This field is read only. This field is excluded by default.
|
78
|
+
#
|
79
|
+
# kind string
|
80
|
+
# — The type of this object: story. This field is read only.
|
81
|
+
|
82
|
+
module PivotalAPI
|
2
83
|
class Story < Base
|
3
84
|
|
4
|
-
attr_accessor :project_id, :follower_ids, :
|
5
|
-
:
|
6
|
-
:
|
7
|
-
:
|
85
|
+
attr_accessor :project_id, :follower_ids, :followers, :updated_at, :current_state,
|
86
|
+
:name, :comment_ids, :url, :story_type, :label_ids, :description,
|
87
|
+
:requested_by_id, :external_id, :deadline, :owner_ids, :owners,
|
88
|
+
:created_at, :estimate, :kind, :id, :task_ids, :integration_id,
|
89
|
+
:accepted_at, :comments, :tasks, :has_attachments, :requested_by,
|
90
|
+
:labels, :transitions, :after_id,
|
91
|
+
:before_id, :cycle_time_details
|
8
92
|
|
9
93
|
def self.fields
|
10
94
|
['url', 'name', 'description', 'story_type',
|
11
95
|
'estimate', 'current_state', 'requested_by',
|
12
|
-
'
|
13
|
-
'deadline', "comments(#{
|
96
|
+
'owners', 'labels', 'integration_id',
|
97
|
+
'deadline', "comments(#{PivotalAPI::Comment.fields.join(',')})",
|
98
|
+
'tasks', 'transitions', 'followers', 'cycle_time_details',
|
99
|
+
'accepted_at']
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.from_json(json)
|
103
|
+
parse_json_story(json)
|
104
|
+
end
|
105
|
+
|
106
|
+
def hours
|
107
|
+
return 0 if transitions.nil?
|
108
|
+
duration_hrs = 0
|
109
|
+
started = nil
|
110
|
+
transitions.each do |transition|
|
111
|
+
case transition.state
|
112
|
+
when 'started'
|
113
|
+
started = Time.parse(transition.occurred_at.to_s)
|
114
|
+
when 'finished'
|
115
|
+
duration_hrs += hours_between(started, Time.parse(transition.occurred_at.to_s)) if started
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
if current_state == 'accepted'
|
120
|
+
duration_hrs += hours_between(started, Time.parse(accepted_at.to_s))
|
121
|
+
elsif current_state != 'accepted' && started
|
122
|
+
duration_hrs += hours_between(started, Time.now)
|
123
|
+
end
|
124
|
+
|
125
|
+
duration_hrs
|
126
|
+
end
|
127
|
+
|
128
|
+
def overdue?
|
129
|
+
hours >= estimate
|
14
130
|
end
|
15
131
|
|
16
|
-
|
17
|
-
|
18
|
-
|
132
|
+
protected
|
133
|
+
|
134
|
+
def hours_between(start_time, end_time)
|
135
|
+
return 0 unless start_time && end_time
|
136
|
+
seconds = start_time.business_time_until(end_time)
|
137
|
+
minutes = seconds / 60
|
138
|
+
hours = minutes / 60
|
139
|
+
hours.round
|
140
|
+
end
|
141
|
+
|
142
|
+
def self.parse_json_story(json_story)
|
19
143
|
estimate = json_story[:estimate] ? json_story[:estimate].to_i : -1
|
20
|
-
current_state = json_story[:current_state]
|
21
144
|
parsed_story = new({
|
22
|
-
id:
|
145
|
+
id: json_story[:id].to_i,
|
23
146
|
url: json_story[:url],
|
24
|
-
project_id: project_id,
|
147
|
+
project_id: json_story[:project_id],
|
25
148
|
name: json_story[:name],
|
26
149
|
description: json_story[:description],
|
27
150
|
story_type: json_story[:story_type],
|
28
151
|
estimate: estimate,
|
29
|
-
current_state: current_state,
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
152
|
+
current_state: json_story[:current_state],
|
153
|
+
requested_by_id: json_story[:requested_by_id],
|
154
|
+
requested_by: Person.from_json(json_story[:requested_by]),
|
155
|
+
owner_ids: json_story[:owner_ids],
|
156
|
+
owners: People.from_json(json_story[:owners]),
|
157
|
+
follower_ids: json_story[:follower_ids],
|
158
|
+
followers: People.from_json(json_story[:followers]),
|
159
|
+
label_ids: json_story[:label_ids],
|
160
|
+
labels: Labels.from_json(json_story[:labels]),
|
34
161
|
integration_id: json_story[:integration_id],
|
35
|
-
deadline: json_story[:deadline]
|
162
|
+
deadline: (DateTime.parse(json_story[:deadline]) if json_story[:deadline]),
|
163
|
+
transitions: StoryTransitions.from_json(json_story[:transitions]),
|
164
|
+
updated_at: (DateTime.parse(json_story[:updated_at]) if json_story[:updated_at]),
|
165
|
+
created_at: (DateTime.parse(json_story[:created_at]) if json_story[:created_at]),
|
166
|
+
comment_ids: json_story[:comment_ids],
|
167
|
+
kind: json_story[:kind],
|
168
|
+
task_ids: json_story[:task_ids],
|
169
|
+
tasks: Tasks.from_json(json_story[:tasks]),
|
170
|
+
accepted_at: (DateTime.parse(json_story[:accepted_at]) if json_story[:accepted_at]),
|
171
|
+
cycle_time_details: CycleTimeDetails.from_json(json_story[:cycle_time_details]),
|
172
|
+
external_id: json_story[:external_id]
|
36
173
|
})
|
37
174
|
|
38
|
-
parsed_story.comments =
|
175
|
+
parsed_story.comments = Comments.from_json(json_story[:comments])
|
39
176
|
parsed_story.has_attachments = false
|
40
177
|
if !parsed_story.comments.nil? && parsed_story.comments.count > 0
|
41
178
|
parsed_story.comments.each do |note|
|
@@ -45,125 +182,29 @@ module Scorer
|
|
45
182
|
end
|
46
183
|
end
|
47
184
|
end
|
48
|
-
|
185
|
+
|
49
186
|
parsed_story
|
50
187
|
end
|
51
188
|
|
52
|
-
def self.parse_json_stories(json_stories, project_id)
|
53
|
-
stories = Array.new
|
54
|
-
json_stories.each do |story|
|
55
|
-
stories << parse_json_story(story, project_id)
|
56
|
-
end
|
57
|
-
stories
|
58
|
-
end
|
59
|
-
|
60
|
-
def self.parse_tasks(tasks, story)
|
61
|
-
parsed_tasks = Array.new
|
62
|
-
if tasks
|
63
|
-
tasks.each do |task|
|
64
|
-
parsed_tasks << Scorer::Task.new({
|
65
|
-
id: task[:id].to_i,
|
66
|
-
description: task[:description],
|
67
|
-
complete: task[:complete],
|
68
|
-
created_at: DateTime.parse(task[:created_at].to_s).to_s,
|
69
|
-
story: story
|
70
|
-
})
|
71
|
-
end
|
72
|
-
end
|
73
|
-
parsed_tasks
|
74
|
-
end
|
75
|
-
|
76
|
-
def self.parse_labels(labels)
|
77
|
-
parsed_labels = ''
|
78
|
-
labels.each do |label|
|
79
|
-
parsed_labels = parsed_labels + "#{label[:name]},"
|
80
|
-
end
|
81
|
-
parsed_labels
|
82
|
-
end
|
83
|
-
|
84
|
-
def self.get_story_started_at(project_id, story_id)
|
85
|
-
events = Hash.new
|
86
|
-
current_started_at = nil
|
87
|
-
current_accepted_at = nil
|
88
|
-
activity = PivotalService.activity(project_id, story_id, 40)
|
89
|
-
activity.each do |event|
|
90
|
-
case event[:highlight]
|
91
|
-
when 'started'
|
92
|
-
started_at = event[:occurred_at]
|
93
|
-
if current_started_at.nil? || current_started_at < started_at || current_accepted_at === started_at
|
94
|
-
current_started_at = started_at
|
95
|
-
events[:started_at] = current_started_at
|
96
|
-
end
|
97
|
-
when 'accepted'
|
98
|
-
accepted_at = event[:occurred_at]
|
99
|
-
if current_accepted_at.nil? || current_accepted_at < accepted_at
|
100
|
-
current_accepted_at = accepted_at
|
101
|
-
events[:accepted_at] = current_accepted_at
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|
105
|
-
events
|
106
|
-
end
|
107
|
-
|
108
|
-
def self.get_story_status(event_times, points, current_state)
|
109
|
-
status = {status: 'ok', hours: -1}
|
110
|
-
if !event_times.nil? && !event_times[:started_at].nil? && points > -1 && current_state != 'unstarted'
|
111
|
-
|
112
|
-
# Times
|
113
|
-
started_at_time = Time.parse(event_times[:started_at])
|
114
|
-
|
115
|
-
# Due Dates
|
116
|
-
due_date = (points.to_i.business_hours.after(started_at_time)).to_datetime
|
117
|
-
almost_due_date = ((points - 1).to_i.business_hours.after(started_at_time)).to_datetime
|
118
|
-
|
119
|
-
if current_state == 'accepted' && !event_times[:accepted_at].nil?
|
120
|
-
accepted_at = event_times[:accepted_at]
|
121
|
-
hours = get_hours_between_times(started_at_time, Time.parse(accepted_at))
|
122
|
-
if accepted_at.to_datetime > due_date || hours >= points
|
123
|
-
status = {status: 'overdue', hours: hours}
|
124
|
-
elsif accepted_at >= almost_due_date || hours == (points - 1)
|
125
|
-
status = {status: 'almost_due', hours: hours}
|
126
|
-
else
|
127
|
-
status = {status: 'ok', hours: hours}
|
128
|
-
end
|
129
|
-
else
|
130
|
-
now = DateTime.now
|
131
|
-
hours = get_hours_between_times(started_at_time, Time.parse(now.to_s))
|
132
|
-
if now >= due_date || hours >= points
|
133
|
-
status = {status: 'overdue', hours: hours}
|
134
|
-
elsif now >= almost_due_date || hours == (points - 1)
|
135
|
-
status = {status: 'almost_due', hours: hours}
|
136
|
-
else
|
137
|
-
status = {status: 'ok', hours: hours}
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
end
|
142
|
-
status
|
143
|
-
end
|
144
|
-
|
145
|
-
protected
|
146
|
-
|
147
|
-
def self.get_hours_between_times(time1, time2)
|
148
|
-
|
149
|
-
# Check to see if both times occurred outside of business hours.
|
150
|
-
# If so, calculate the total time in between each time
|
151
|
-
if Time::roll_forward(time1) == Time::roll_forward(time2)
|
152
|
-
return (((time2 - time1) / 60) / 60).round
|
153
|
-
end
|
154
|
-
|
155
|
-
((time1.business_time_until(time2) / 60) / 60).round
|
156
|
-
end
|
157
|
-
|
158
189
|
def self.est_time_zone(time)
|
159
190
|
time.in_time_zone("Eastern Time (US & Canada)")
|
160
191
|
end
|
161
192
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
193
|
+
end
|
194
|
+
|
195
|
+
class Stories < Story
|
196
|
+
|
197
|
+
def self.from_json(json)
|
198
|
+
parse_json_stories(json)
|
166
199
|
end
|
167
|
-
|
200
|
+
|
201
|
+
protected
|
202
|
+
|
203
|
+
def self.parse_json_stories(json_stories)
|
204
|
+
stories = []
|
205
|
+
json_stories.each { |story| stories << parse_json_story(story) }
|
206
|
+
stories
|
207
|
+
end
|
208
|
+
|
168
209
|
end
|
169
210
|
end
|