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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +8 -2
  3. data/Gemfile.lock +65 -47
  4. data/README.md +7 -7
  5. data/VERSION +1 -1
  6. data/lib/pivotal-tracker-api.rb +10 -2
  7. data/lib/pivotal-tracker-api/activity.rb +52 -18
  8. data/lib/pivotal-tracker-api/analytics.rb +23 -0
  9. data/lib/pivotal-tracker-api/base.rb +14 -2
  10. data/lib/pivotal-tracker-api/client.rb +43 -21
  11. data/lib/pivotal-tracker-api/comment.rb +76 -27
  12. data/lib/pivotal-tracker-api/core_ext/string.rb +3 -0
  13. data/lib/pivotal-tracker-api/cycle_time_details.rb +43 -0
  14. data/lib/pivotal-tracker-api/file_attachment.rb +56 -0
  15. data/lib/pivotal-tracker-api/iteration.rb +73 -11
  16. data/lib/pivotal-tracker-api/label.rb +32 -0
  17. data/lib/pivotal-tracker-api/me.rb +16 -0
  18. data/lib/pivotal-tracker-api/person.rb +13 -9
  19. data/lib/pivotal-tracker-api/project.rb +76 -23
  20. data/lib/pivotal-tracker-api/service.rb +202 -0
  21. data/lib/pivotal-tracker-api/story.rb +173 -132
  22. data/lib/pivotal-tracker-api/story_transition.rb +81 -0
  23. data/lib/pivotal-tracker-api/string_extensions.rb +61 -0
  24. data/lib/pivotal-tracker-api/task.rb +56 -12
  25. data/pivotal-tracker-api.gemspec +28 -15
  26. data/test/helper.rb +1 -0
  27. data/test/test_activity.rb +79 -0
  28. data/test/test_analytics.rb +38 -0
  29. data/test/test_cycle_time_details.rb +69 -0
  30. data/test/test_iteration.rb +182 -0
  31. data/test/test_label.rb +29 -0
  32. data/test/test_me.rb +52 -0
  33. data/test/test_service.rb +67 -0
  34. data/test/test_story.rb +557 -0
  35. data/test/test_story_transition.rb +80 -0
  36. data/test/test_string_extensions.rb +37 -0
  37. metadata +29 -27
  38. data/lib/pivotal-tracker-api/attachment.rb +0 -28
  39. data/lib/pivotal-tracker-api/pivotal_service.rb +0 -141
  40. 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
- module Scorer
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, :updated_at, :current_state, :name, :comment_ids, :url, :story_type,
5
- :label_ids, :description, :requested_by_id, :planned_iteration_number, :external_id, :deadline,
6
- :owned_by_id, :owned_by, :created_at, :estimate, :kind, :id, :task_ids, :integration_id, :accepted_at,
7
- :comments, :tasks, :has_attachments, :requested_by, :labels, :notes, :started_at, :status
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
- 'owned_by', 'labels', 'integration_id',
13
- 'deadline', "comments(#{Scorer::Comment.fields.join(',')})", 'tasks']
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
- def self.parse_json_story(json_story, project_id)
17
- requested_by = json_story[:requested_by][:name] if !json_story[:requested_by].nil?
18
- story_id = json_story[:id].to_i
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: story_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
- requested_by: requested_by,
31
- owned_by_id: json_story[:owned_by_id],
32
- owned_by: json_story[:owned_by],
33
- labels: parse_labels(json_story[:labels]),
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 = Scorer::Comment.parse_json_comments(json_story[:comments], parsed_story)
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
- parsed_story.tasks = Task.parse_tasks(json_story[:tasks], json_story)
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
- def update_attributes(attrs)
163
- attrs.each do |key, value|
164
- self.send("#{key}=", value.is_a?(Array) ? value.join(',') : value )
165
- end
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