westarete-tracker-tools 0.3.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.
- data/.document +5 -0
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +59 -0
- data/LICENSE.txt +20 -0
- data/README.md +190 -0
- data/Rakefile +79 -0
- data/VERSION +1 -0
- data/autotest/discover.rb +1 -0
- data/bin/backlog +47 -0
- data/bin/done +53 -0
- data/bin/finished +25 -0
- data/bin/validate +29 -0
- data/ci +21 -0
- data/lib/westarete-tracker-tools.rb +6 -0
- data/lib/westarete-tracker-tools/project.rb +69 -0
- data/lib/westarete-tracker-tools/story.rb +137 -0
- data/spec/fixtures/vcr/WestAreteTrackerTools_Project.yml +525 -0
- data/spec/fixtures/vcr/WestAreteTrackerTools_Story.yml +777 -0
- data/spec/helper.rb +17 -0
- data/spec/project_spec.rb +132 -0
- data/spec/story_spec.rb +408 -0
- data/spec/vcr_helper.rb +11 -0
- data/westarete-tracker-tools.gemspec +109 -0
- metadata +248 -0
data/spec/helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
|
11
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
12
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
13
|
+
require 'westarete-tracker-tools'
|
14
|
+
|
15
|
+
include WestAreteTrackerTools
|
16
|
+
|
17
|
+
require 'vcr_helper'
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe Project do
|
4
|
+
use_vcr_cassette :record => :new_episodes, :match_requests_on => [:uri, :method]
|
5
|
+
before do
|
6
|
+
@project = Project.find_by_name('westarete-tracker-tools-test')
|
7
|
+
end
|
8
|
+
describe '.all' do
|
9
|
+
before do
|
10
|
+
@projects = Project.all
|
11
|
+
end
|
12
|
+
it 'returns all active projects' do
|
13
|
+
@projects.length.should == 15
|
14
|
+
@projects.first.should respond_to(:name)
|
15
|
+
@projects.collect(&:name).should == [
|
16
|
+
'altoonabustest.com',
|
17
|
+
'Background Questionnaires',
|
18
|
+
'BellefonteDentistry.com',
|
19
|
+
'Captcha',
|
20
|
+
'centralinsgrp.com',
|
21
|
+
'coolblue.com',
|
22
|
+
'famfound.net',
|
23
|
+
'ionmodern.com',
|
24
|
+
'LifeViz',
|
25
|
+
'oneononefit.com',
|
26
|
+
'PAIG',
|
27
|
+
'pasubway.com',
|
28
|
+
'ReDi Index',
|
29
|
+
'trailspace.com',
|
30
|
+
'westarete-tracker-tools-test'
|
31
|
+
]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
describe '.find_by_name' do
|
35
|
+
context 'if the name exists' do
|
36
|
+
it 'returns the project' do
|
37
|
+
@project.name.should == 'westarete-tracker-tools-test'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
context 'if the name does not exist' do
|
41
|
+
before do
|
42
|
+
@project = Project.find_by_name('not there')
|
43
|
+
end
|
44
|
+
it 'returns nil' do
|
45
|
+
@project.should be_nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
describe "#backlog" do
|
50
|
+
before do
|
51
|
+
@stories = @project.backlog
|
52
|
+
end
|
53
|
+
it 'returns all of the stories in the backlog, including the current iteration' do
|
54
|
+
@stories.length.should == 17
|
55
|
+
@stories.each { |s| s.should be_kind_of(Story) }
|
56
|
+
@stories.first.name.should == 'Newly accepted feature'
|
57
|
+
@stories.last.name.should == 'Release'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
describe '#current' do
|
61
|
+
before do
|
62
|
+
@stories = @project.current
|
63
|
+
end
|
64
|
+
it 'returns all of the stories in the current iteration' do
|
65
|
+
@stories.collect(&:name).should == [
|
66
|
+
'Newly accepted feature',
|
67
|
+
'Rejected feature',
|
68
|
+
'Delivered feature',
|
69
|
+
'Finished feature',
|
70
|
+
'Started feature',
|
71
|
+
]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
describe '#done' do
|
75
|
+
context 'when given no arguments' do
|
76
|
+
before do
|
77
|
+
@stories = @project.done
|
78
|
+
end
|
79
|
+
it 'returns all stories that have been completed in any iteration' do
|
80
|
+
@stories.collect(&:name).should == [
|
81
|
+
'Old accepted feature',
|
82
|
+
'Accepted feature',
|
83
|
+
'Newly accepted feature',
|
84
|
+
]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
context 'when given a -2' do
|
88
|
+
before do
|
89
|
+
@stories = @project.done(-2)
|
90
|
+
end
|
91
|
+
it 'returns all stories that were completed two iterations ago' do
|
92
|
+
@stories.collect(&:name).should == ['Old accepted feature']
|
93
|
+
end
|
94
|
+
end
|
95
|
+
context 'when given a -1' do
|
96
|
+
before do
|
97
|
+
@stories = @project.done(-1)
|
98
|
+
end
|
99
|
+
it 'returns all stories that were completed in last iteration' do
|
100
|
+
@stories.collect(&:name).should == ['Accepted feature']
|
101
|
+
end
|
102
|
+
end
|
103
|
+
context 'when given a 0' do
|
104
|
+
before do
|
105
|
+
@stories = @project.done(0)
|
106
|
+
end
|
107
|
+
it 'returns all stories that have been completed during the current iteration' do
|
108
|
+
@stories.collect(&:name).should == ['Newly accepted feature']
|
109
|
+
end
|
110
|
+
end
|
111
|
+
context 'when given a 1' do
|
112
|
+
it 'raises an ArgumentError' do
|
113
|
+
lambda { @project.done(1) }.should raise_error(ArgumentError)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
describe '#name' do
|
118
|
+
it "returns the project's name" do
|
119
|
+
@project.name.should == 'westarete-tracker-tools-test'
|
120
|
+
end
|
121
|
+
end
|
122
|
+
describe '#tracker_id' do
|
123
|
+
it "returns Pivotal Tracker's id for the project" do
|
124
|
+
@project.tracker_id.should == 213679
|
125
|
+
end
|
126
|
+
end
|
127
|
+
describe '#url' do
|
128
|
+
it "returns Pivotal Tracker's url for the project" do
|
129
|
+
@project.url.should == 'http://www.pivotaltracker.com/projects/213679'
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
data/spec/story_spec.rb
ADDED
@@ -0,0 +1,408 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe Story do
|
4
|
+
use_vcr_cassette :record => :new_episodes, :match_requests_on => [:uri, :method]
|
5
|
+
before do
|
6
|
+
@project = Project.find_by_name('westarete-tracker-tools-test')
|
7
|
+
@backlog = @project.backlog
|
8
|
+
@story = @backlog.detect { |s| s.name == 'Valid story' }
|
9
|
+
end
|
10
|
+
describe 'validations' do
|
11
|
+
context 'a bug' do
|
12
|
+
subject { @backlog.detect { |s| s.name == 'Bug' } }
|
13
|
+
it { should be_valid }
|
14
|
+
end
|
15
|
+
context 'a chore' do
|
16
|
+
subject { @backlog.detect { |s| s.name == 'Chore' } }
|
17
|
+
it { should be_valid }
|
18
|
+
end
|
19
|
+
context 'a feature' do
|
20
|
+
context 'that is valid' do
|
21
|
+
subject { @story }
|
22
|
+
it { should be_valid }
|
23
|
+
end
|
24
|
+
context 'that has no risk' do
|
25
|
+
before do
|
26
|
+
@story = @backlog.detect { |s| s.name == 'Story with no risk' }
|
27
|
+
end
|
28
|
+
it 'complains about the missing risk' do
|
29
|
+
@story.should_not be_valid
|
30
|
+
@story.errors[:risk].should include("can't be blank")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
context 'that has no price' do
|
34
|
+
before do
|
35
|
+
@story = @backlog.detect { |s| s.name == 'Story with comments, but no price' }
|
36
|
+
end
|
37
|
+
it 'complains about the missing price' do
|
38
|
+
@story.should_not be_valid
|
39
|
+
@story.errors[:price].should include("can't be blank")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
context 'that has no point estimate' do
|
43
|
+
before do
|
44
|
+
@story = @backlog.detect { |s| s.name == 'Story with no point estimate' }
|
45
|
+
end
|
46
|
+
it 'complains about the missing point estimate' do
|
47
|
+
@story.should_not be_valid
|
48
|
+
@story.errors[:points].should include("can't be blank")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
context 'that has no description' do
|
52
|
+
before do
|
53
|
+
@story = @backlog.detect { |s| s.name == 'Story with no description' }
|
54
|
+
end
|
55
|
+
it 'complains about the missing description' do
|
56
|
+
@story.should_not be_valid
|
57
|
+
@story.errors[:description].should include("can't be blank")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
context "that has a description that's too short" do
|
61
|
+
before do
|
62
|
+
@story = @backlog.detect { |s| s.name == 'Story with a description that is too short' }
|
63
|
+
end
|
64
|
+
it 'complains about the BS description' do
|
65
|
+
@story.should_not be_valid
|
66
|
+
@story.errors[:description].should include("is too short (minimum is 70 characters)")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
context 'that is a feature' do
|
72
|
+
describe '#feature?' do
|
73
|
+
subject { @story.feature? }
|
74
|
+
it { should be_true }
|
75
|
+
end
|
76
|
+
describe '#bug?' do
|
77
|
+
subject { @story.bug? }
|
78
|
+
it { should be_false }
|
79
|
+
end
|
80
|
+
describe '#chore?' do
|
81
|
+
subject { @story.chore? }
|
82
|
+
it { should be_false }
|
83
|
+
end
|
84
|
+
describe '#release?' do
|
85
|
+
subject { @story.release? }
|
86
|
+
it { should be_false }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
context 'that is a bug' do
|
90
|
+
before do
|
91
|
+
@story = @backlog.detect { |s| s.name == 'Bug' }
|
92
|
+
end
|
93
|
+
describe '#feature?' do
|
94
|
+
subject { @story.feature? }
|
95
|
+
it { should be_false }
|
96
|
+
end
|
97
|
+
describe '#bug?' do
|
98
|
+
subject { @story.bug? }
|
99
|
+
it { should be_true }
|
100
|
+
end
|
101
|
+
describe '#chore?' do
|
102
|
+
subject { @story.chore? }
|
103
|
+
it { should be_false }
|
104
|
+
end
|
105
|
+
describe '#release?' do
|
106
|
+
subject { @story.release? }
|
107
|
+
it { should be_false }
|
108
|
+
end
|
109
|
+
describe '#points' do
|
110
|
+
subject { @story.points }
|
111
|
+
it { should be_nil }
|
112
|
+
end
|
113
|
+
describe '#price' do
|
114
|
+
subject { @story.price }
|
115
|
+
it { should be_nil }
|
116
|
+
end
|
117
|
+
describe '#risk' do
|
118
|
+
subject { @story.risk }
|
119
|
+
it { should be_nil }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
context 'that is a chore' do
|
123
|
+
before do
|
124
|
+
@story = @backlog.detect { |s| s.name == 'Chore' }
|
125
|
+
end
|
126
|
+
describe '#feature?' do
|
127
|
+
subject { @story.feature? }
|
128
|
+
it { should be_false }
|
129
|
+
end
|
130
|
+
describe '#bug?' do
|
131
|
+
subject { @story.bug? }
|
132
|
+
it { should be_false }
|
133
|
+
end
|
134
|
+
describe '#chore?' do
|
135
|
+
subject { @story.chore? }
|
136
|
+
it { should be_true }
|
137
|
+
end
|
138
|
+
describe '#release?' do
|
139
|
+
subject { @story.release? }
|
140
|
+
it { should be_false }
|
141
|
+
end
|
142
|
+
describe '#points' do
|
143
|
+
subject { @story.points }
|
144
|
+
it { should be_nil }
|
145
|
+
end
|
146
|
+
describe '#price' do
|
147
|
+
subject { @story.price }
|
148
|
+
it { should be_nil }
|
149
|
+
end
|
150
|
+
describe '#risk' do
|
151
|
+
subject { @story.risk }
|
152
|
+
it { should be_nil }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
context 'that is a release' do
|
156
|
+
before do
|
157
|
+
@story = @backlog.detect { |s| s.name == 'Release' }
|
158
|
+
end
|
159
|
+
describe '#feature?' do
|
160
|
+
subject { @story.feature? }
|
161
|
+
it { should be_false }
|
162
|
+
end
|
163
|
+
describe '#bug?' do
|
164
|
+
subject { @story.bug? }
|
165
|
+
it { should be_false }
|
166
|
+
end
|
167
|
+
describe '#chore?' do
|
168
|
+
subject { @story.chore? }
|
169
|
+
it { should be_false }
|
170
|
+
end
|
171
|
+
describe '#release?' do
|
172
|
+
subject { @story.release? }
|
173
|
+
it { should be_true }
|
174
|
+
end
|
175
|
+
describe '#points' do
|
176
|
+
subject { @story.points }
|
177
|
+
it { should be_nil }
|
178
|
+
end
|
179
|
+
describe '#price' do
|
180
|
+
subject { @story.price }
|
181
|
+
it { should be_nil }
|
182
|
+
end
|
183
|
+
describe '#risk' do
|
184
|
+
subject { @story.risk }
|
185
|
+
it { should be_nil }
|
186
|
+
end
|
187
|
+
end
|
188
|
+
describe '#accepted_at' do
|
189
|
+
context 'for a story that is not started' do
|
190
|
+
subject { @story.accepted_at }
|
191
|
+
it { should be_nil }
|
192
|
+
end
|
193
|
+
context 'for a story that is in progress' do
|
194
|
+
subject do
|
195
|
+
@project.backlog.detect { |story| story.name == 'Started feature' }.accepted_at
|
196
|
+
end
|
197
|
+
it { should be_nil }
|
198
|
+
end
|
199
|
+
context 'for a story that is complete' do
|
200
|
+
before do
|
201
|
+
@story = @project.done.detect { |story| story.name == 'Accepted feature' }
|
202
|
+
end
|
203
|
+
it 'returns a date and time' do
|
204
|
+
@story.accepted_at.should be_kind_of(DateTime)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
describe '#comments' do
|
209
|
+
context 'when there are comments' do
|
210
|
+
before do
|
211
|
+
@story = @project.backlog.detect { |s| s.name == 'Story with comments and two prices' }
|
212
|
+
end
|
213
|
+
it 'returns an array of note objects in chronological order' do
|
214
|
+
@story.comments.length.should == 2
|
215
|
+
@story.comments[0].noted_at.should < @story.comments[1].noted_at
|
216
|
+
end
|
217
|
+
end
|
218
|
+
context 'when there are no comments' do
|
219
|
+
before do
|
220
|
+
@story = @project.backlog.detect { |s| s.name == 'Story with no comments' }
|
221
|
+
end
|
222
|
+
it 'returns an empty array' do
|
223
|
+
@story.comments.should be_empty
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
describe '#complete?' do
|
228
|
+
context 'for a story that is not started' do
|
229
|
+
subject { @story.complete? }
|
230
|
+
it { should be_false }
|
231
|
+
end
|
232
|
+
context 'for a story that is in progress' do
|
233
|
+
subject do
|
234
|
+
@project.backlog.detect { |story| story.name == 'Started feature' }.complete?
|
235
|
+
end
|
236
|
+
it { should be_false }
|
237
|
+
end
|
238
|
+
context 'for a story that is complete' do
|
239
|
+
subject do
|
240
|
+
@project.done.detect { |story| story.name == 'Accepted feature' }.complete?
|
241
|
+
end
|
242
|
+
it { should be_true }
|
243
|
+
end
|
244
|
+
end
|
245
|
+
describe '#current_state' do
|
246
|
+
it 'returns a string that describes the current state' do
|
247
|
+
@story.current_state.should == 'unstarted'
|
248
|
+
end
|
249
|
+
end
|
250
|
+
describe '#description' do
|
251
|
+
it 'returns the description body' do
|
252
|
+
@story.description.should =~ /^As a /
|
253
|
+
end
|
254
|
+
end
|
255
|
+
describe '#finished?' do
|
256
|
+
context 'for a story that is not yet started' do
|
257
|
+
subject do
|
258
|
+
@project.backlog.detect { |story| story.name == 'Not yet started feature' }.finished?
|
259
|
+
end
|
260
|
+
it { should be_false }
|
261
|
+
end
|
262
|
+
context 'for a story that is started' do
|
263
|
+
subject do
|
264
|
+
@project.backlog.detect { |story| story.name == 'Started feature' }.finished?
|
265
|
+
end
|
266
|
+
it { should be_false }
|
267
|
+
end
|
268
|
+
context 'for a story that is finished' do
|
269
|
+
subject do
|
270
|
+
@project.backlog.detect { |story| story.name == 'Finished feature' }.finished?
|
271
|
+
end
|
272
|
+
it { should be_true }
|
273
|
+
end
|
274
|
+
context 'for a story that is delivered' do
|
275
|
+
subject do
|
276
|
+
@project.backlog.detect { |story| story.name == 'Delivered feature' }.finished?
|
277
|
+
end
|
278
|
+
it { should be_true }
|
279
|
+
end
|
280
|
+
context 'for a story that is rejected' do
|
281
|
+
subject do
|
282
|
+
@project.backlog.detect { |story| story.name == 'Rejected feature' }.finished?
|
283
|
+
end
|
284
|
+
it { should be_true }
|
285
|
+
end
|
286
|
+
context 'for a story that is accepted' do
|
287
|
+
subject do
|
288
|
+
@project.done.detect { |story| story.name == 'Accepted feature' }.finished?
|
289
|
+
end
|
290
|
+
it { should be_false }
|
291
|
+
end
|
292
|
+
end
|
293
|
+
describe '#incomplete?' do
|
294
|
+
context 'for a story that is not started' do
|
295
|
+
subject { @story.incomplete? }
|
296
|
+
it { should be_true }
|
297
|
+
end
|
298
|
+
context 'for a story that is in progress' do
|
299
|
+
subject do
|
300
|
+
@project.backlog.detect { |story| story.name == 'Started feature' }.incomplete?
|
301
|
+
end
|
302
|
+
it { should be_true }
|
303
|
+
end
|
304
|
+
context 'for a story that is complete' do
|
305
|
+
subject do
|
306
|
+
@project.done.detect { |story| story.name == 'Accepted feature' }.incomplete?
|
307
|
+
end
|
308
|
+
it { should be_false }
|
309
|
+
end
|
310
|
+
end
|
311
|
+
describe "#name" do
|
312
|
+
it "returns the story's name, without the risk rating" do
|
313
|
+
@story.name.should == 'Valid story'
|
314
|
+
end
|
315
|
+
end
|
316
|
+
describe '#owner' do
|
317
|
+
context 'when there is an owner' do
|
318
|
+
before do
|
319
|
+
@story = @project.backlog.detect { |story| story.name == 'Started feature' }
|
320
|
+
end
|
321
|
+
it "should return the owner's full name" do
|
322
|
+
@story.owner.should == 'Scott Woods'
|
323
|
+
end
|
324
|
+
end
|
325
|
+
context 'when there is no owner' do
|
326
|
+
subject do
|
327
|
+
@project.backlog.detect { |story| story.name == 'Not yet started feature' }.owner
|
328
|
+
end
|
329
|
+
it { should be_nil }
|
330
|
+
end
|
331
|
+
end
|
332
|
+
describe '#points' do
|
333
|
+
context 'if the story has been estimated' do
|
334
|
+
it 'returns the point estimate for the story' do
|
335
|
+
@story.points.should == 2
|
336
|
+
end
|
337
|
+
end
|
338
|
+
context 'if the story has not been estimated' do
|
339
|
+
before do
|
340
|
+
@story = @backlog.detect { |s| s.name == 'Story with no point estimate' }
|
341
|
+
end
|
342
|
+
it 'returns nil' do
|
343
|
+
@story.points.should be_nil
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
347
|
+
describe '#price' do
|
348
|
+
context 'when there are no comments' do
|
349
|
+
before do
|
350
|
+
@story = @backlog.detect { |s| s.name == 'Story with no comments' }
|
351
|
+
end
|
352
|
+
it 'returns nil' do
|
353
|
+
@story.price.should be_nil
|
354
|
+
end
|
355
|
+
end
|
356
|
+
context 'when there are comments, but none mention cost or price' do
|
357
|
+
before do
|
358
|
+
@story = @backlog.detect { |s| s.name == 'Story with comments, but no price' }
|
359
|
+
end
|
360
|
+
it 'returns nil' do
|
361
|
+
@story.price.should be_nil
|
362
|
+
end
|
363
|
+
end
|
364
|
+
context 'when there are multiple comments that mention price' do
|
365
|
+
before do
|
366
|
+
@story = @backlog.detect { |s| s.name == 'Story with comments and two prices' }
|
367
|
+
end
|
368
|
+
it 'uses the most recent comment by Scott Woods' do
|
369
|
+
@story.price.should == 100
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
describe "#requester" do
|
374
|
+
it "returns the requester's name" do
|
375
|
+
@story.requester.should == 'Scott Woods'
|
376
|
+
end
|
377
|
+
end
|
378
|
+
describe '#risk' do
|
379
|
+
context 'if a risk is present' do
|
380
|
+
it 'returns the risk rating for the story' do
|
381
|
+
@story.risk.should == 'R'
|
382
|
+
end
|
383
|
+
end
|
384
|
+
context 'if no risk is present' do
|
385
|
+
before do
|
386
|
+
@story = @backlog.detect { |s| s.name == 'Story with no risk' }
|
387
|
+
end
|
388
|
+
it 'returns nil' do
|
389
|
+
@story.risk.should be_nil
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
describe '#story_type' do
|
394
|
+
it 'returns a string of the story type' do
|
395
|
+
@story.story_type.should == 'feature'
|
396
|
+
end
|
397
|
+
end
|
398
|
+
describe '#tracker_id' do
|
399
|
+
it "returns Pivotal Tracker's id for the story" do
|
400
|
+
@story.tracker_id.should == 9072189
|
401
|
+
end
|
402
|
+
end
|
403
|
+
describe '#url' do
|
404
|
+
it 'returns the URL for the story' do
|
405
|
+
@story.url.should == 'http://www.pivotaltracker.com/story/show/9072189'
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|