config_scripts 0.4.1

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 (83) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +2 -0
  4. data/.yardopts +1 -0
  5. data/CHANGELOG.md +17 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.lock +124 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +55 -0
  10. data/Rakefile +1 -0
  11. data/TODO.md +8 -0
  12. data/config_scripts.gemspec +31 -0
  13. data/lib/config_scripts/generators/config_script.rb +24 -0
  14. data/lib/config_scripts/generators/migrations.rb +36 -0
  15. data/lib/config_scripts/generators.rb +2 -0
  16. data/lib/config_scripts/scripts/script.rb +135 -0
  17. data/lib/config_scripts/scripts/script_history.rb +39 -0
  18. data/lib/config_scripts/scripts.rb +9 -0
  19. data/lib/config_scripts/seeds/seed_set.rb +321 -0
  20. data/lib/config_scripts/seeds/seed_type.rb +361 -0
  21. data/lib/config_scripts/seeds.rb +8 -0
  22. data/lib/config_scripts/tasks/pending_migrations.rake +11 -0
  23. data/lib/config_scripts/tasks/seeds.rake +18 -0
  24. data/lib/config_scripts/tasks.rb +12 -0
  25. data/lib/config_scripts/version.rb +4 -0
  26. data/lib/config_scripts.rb +9 -0
  27. data/spec/dummy/README.rdoc +28 -0
  28. data/spec/dummy/Rakefile +6 -0
  29. data/spec/dummy/app/assets/images/.keep +0 -0
  30. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  31. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  32. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  33. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  34. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  35. data/spec/dummy/app/mailers/.keep +0 -0
  36. data/spec/dummy/app/models/.keep +0 -0
  37. data/spec/dummy/app/models/concerns/.keep +0 -0
  38. data/spec/dummy/app/models/hair_color.rb +2 -0
  39. data/spec/dummy/app/models/person.rb +4 -0
  40. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  41. data/spec/dummy/bin/bundle +3 -0
  42. data/spec/dummy/bin/rails +4 -0
  43. data/spec/dummy/bin/rake +4 -0
  44. data/spec/dummy/config/application.rb +23 -0
  45. data/spec/dummy/config/boot.rb +5 -0
  46. data/spec/dummy/config/database.yml +25 -0
  47. data/spec/dummy/config/environment.rb +5 -0
  48. data/spec/dummy/config/environments/development.rb +29 -0
  49. data/spec/dummy/config/environments/production.rb +80 -0
  50. data/spec/dummy/config/environments/test.rb +36 -0
  51. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  52. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  53. data/spec/dummy/config/initializers/inflections.rb +16 -0
  54. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  55. data/spec/dummy/config/initializers/secret_token.rb +12 -0
  56. data/spec/dummy/config/initializers/session_store.rb +3 -0
  57. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  58. data/spec/dummy/config/locales/en.yml +23 -0
  59. data/spec/dummy/config/routes.rb +56 -0
  60. data/spec/dummy/config.ru +4 -0
  61. data/spec/dummy/db/migrate/20140208014550_create_config_scripts.rb +7 -0
  62. data/spec/dummy/db/migrate/20140208161829_create_people.rb +9 -0
  63. data/spec/dummy/db/migrate/20140208182050_create_hair_colors.rb +9 -0
  64. data/spec/dummy/db/migrate/20140208182101_add_hair_color_to_person.rb +6 -0
  65. data/spec/dummy/db/migrate/20140208225801_add_scope_to_people.rb +6 -0
  66. data/spec/dummy/db/migrate/20140209132911_add_hex_value_to_hair_color.rb +5 -0
  67. data/spec/dummy/db/schema.rb +38 -0
  68. data/spec/dummy/lib/assets/.keep +0 -0
  69. data/spec/dummy/log/.keep +0 -0
  70. data/spec/dummy/public/404.html +58 -0
  71. data/spec/dummy/public/422.html +58 -0
  72. data/spec/dummy/public/500.html +57 -0
  73. data/spec/dummy/public/favicon.ico +0 -0
  74. data/spec/generators/config_script_spec.rb +23 -0
  75. data/spec/generators/migrations_spec.rb +23 -0
  76. data/spec/scripts/script_history_spec.rb +53 -0
  77. data/spec/scripts/script_spec.rb +282 -0
  78. data/spec/seeds/seed_set_spec.rb +371 -0
  79. data/spec/seeds/seed_type_spec.rb +439 -0
  80. data/spec/spec_helper.rb +38 -0
  81. data/templates/config_script.rb +9 -0
  82. data/templates/create_config_scripts_migration.rb +7 -0
  83. metadata +321 -0
@@ -0,0 +1,58 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The page you were looking for doesn't exist (404)</title>
5
+ <style>
6
+ body {
7
+ background-color: #EFEFEF;
8
+ color: #2E2F30;
9
+ text-align: center;
10
+ font-family: arial, sans-serif;
11
+ }
12
+
13
+ div.dialog {
14
+ width: 25em;
15
+ margin: 4em auto 0 auto;
16
+ border: 1px solid #CCC;
17
+ border-right-color: #999;
18
+ border-left-color: #999;
19
+ border-bottom-color: #BBB;
20
+ border-top: #B00100 solid 4px;
21
+ border-top-left-radius: 9px;
22
+ border-top-right-radius: 9px;
23
+ background-color: white;
24
+ padding: 7px 4em 0 4em;
25
+ }
26
+
27
+ h1 {
28
+ font-size: 100%;
29
+ color: #730E15;
30
+ line-height: 1.5em;
31
+ }
32
+
33
+ body > p {
34
+ width: 33em;
35
+ margin: 0 auto 1em;
36
+ padding: 1em 0;
37
+ background-color: #F7F7F7;
38
+ border: 1px solid #CCC;
39
+ border-right-color: #999;
40
+ border-bottom-color: #999;
41
+ border-bottom-left-radius: 4px;
42
+ border-bottom-right-radius: 4px;
43
+ border-top-color: #DADADA;
44
+ color: #666;
45
+ box-shadow:0 3px 8px rgba(50, 50, 50, 0.17);
46
+ }
47
+ </style>
48
+ </head>
49
+
50
+ <body>
51
+ <!-- This file lives in public/404.html -->
52
+ <div class="dialog">
53
+ <h1>The page you were looking for doesn't exist.</h1>
54
+ <p>You may have mistyped the address or the page may have moved.</p>
55
+ </div>
56
+ <p>If you are the application owner check the logs for more information.</p>
57
+ </body>
58
+ </html>
@@ -0,0 +1,58 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The change you wanted was rejected (422)</title>
5
+ <style>
6
+ body {
7
+ background-color: #EFEFEF;
8
+ color: #2E2F30;
9
+ text-align: center;
10
+ font-family: arial, sans-serif;
11
+ }
12
+
13
+ div.dialog {
14
+ width: 25em;
15
+ margin: 4em auto 0 auto;
16
+ border: 1px solid #CCC;
17
+ border-right-color: #999;
18
+ border-left-color: #999;
19
+ border-bottom-color: #BBB;
20
+ border-top: #B00100 solid 4px;
21
+ border-top-left-radius: 9px;
22
+ border-top-right-radius: 9px;
23
+ background-color: white;
24
+ padding: 7px 4em 0 4em;
25
+ }
26
+
27
+ h1 {
28
+ font-size: 100%;
29
+ color: #730E15;
30
+ line-height: 1.5em;
31
+ }
32
+
33
+ body > p {
34
+ width: 33em;
35
+ margin: 0 auto 1em;
36
+ padding: 1em 0;
37
+ background-color: #F7F7F7;
38
+ border: 1px solid #CCC;
39
+ border-right-color: #999;
40
+ border-bottom-color: #999;
41
+ border-bottom-left-radius: 4px;
42
+ border-bottom-right-radius: 4px;
43
+ border-top-color: #DADADA;
44
+ color: #666;
45
+ box-shadow:0 3px 8px rgba(50, 50, 50, 0.17);
46
+ }
47
+ </style>
48
+ </head>
49
+
50
+ <body>
51
+ <!-- This file lives in public/422.html -->
52
+ <div class="dialog">
53
+ <h1>The change you wanted was rejected.</h1>
54
+ <p>Maybe you tried to change something you didn't have access to.</p>
55
+ </div>
56
+ <p>If you are the application owner check the logs for more information.</p>
57
+ </body>
58
+ </html>
@@ -0,0 +1,57 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>We're sorry, but something went wrong (500)</title>
5
+ <style>
6
+ body {
7
+ background-color: #EFEFEF;
8
+ color: #2E2F30;
9
+ text-align: center;
10
+ font-family: arial, sans-serif;
11
+ }
12
+
13
+ div.dialog {
14
+ width: 25em;
15
+ margin: 4em auto 0 auto;
16
+ border: 1px solid #CCC;
17
+ border-right-color: #999;
18
+ border-left-color: #999;
19
+ border-bottom-color: #BBB;
20
+ border-top: #B00100 solid 4px;
21
+ border-top-left-radius: 9px;
22
+ border-top-right-radius: 9px;
23
+ background-color: white;
24
+ padding: 7px 4em 0 4em;
25
+ }
26
+
27
+ h1 {
28
+ font-size: 100%;
29
+ color: #730E15;
30
+ line-height: 1.5em;
31
+ }
32
+
33
+ body > p {
34
+ width: 33em;
35
+ margin: 0 auto 1em;
36
+ padding: 1em 0;
37
+ background-color: #F7F7F7;
38
+ border: 1px solid #CCC;
39
+ border-right-color: #999;
40
+ border-bottom-color: #999;
41
+ border-bottom-left-radius: 4px;
42
+ border-bottom-right-radius: 4px;
43
+ border-top-color: #DADADA;
44
+ color: #666;
45
+ box-shadow:0 3px 8px rgba(50, 50, 50, 0.17);
46
+ }
47
+ </style>
48
+ </head>
49
+
50
+ <body>
51
+ <!-- This file lives in public/500.html -->
52
+ <div class="dialog">
53
+ <h1>We're sorry, but something went wrong.</h1>
54
+ </div>
55
+ <p>If you are the application owner check the logs for more information.</p>
56
+ </body>
57
+ </html>
File without changes
@@ -0,0 +1,23 @@
1
+ require 'generator_spec'
2
+
3
+ describe ConfigScripts::ConfigScriptGenerator, type: :generator do
4
+ destination File.expand_path("../../../tmp", __FILE__)
5
+ arguments ['TestConfigScript']
6
+
7
+ describe "config_script" do
8
+ let(:expected_path) { "db/config_scripts/#{Time.now.to_s(:number)}_test_config_script.rb" }
9
+ before do
10
+ prepare_destination
11
+ Timecop.freeze
12
+ run_generator
13
+ end
14
+
15
+ after do
16
+ Timecop.return
17
+ end
18
+
19
+ it "creates a file in the config_scripts directory, with a config script class" do
20
+ assert_file expected_path, /class TestConfigScriptConfig < ConfigScripts::Scripts::Script/
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ describe ConfigScripts::MigrationsGenerator, type: :generator do
2
+ destination File.expand_path("../../../tmp", __FILE__)
3
+
4
+ describe "create_migrations" do
5
+ before do
6
+ prepare_destination
7
+ run_generator
8
+ end
9
+
10
+ it "creates a migration for adding the config scripts table" do
11
+ assert_file "db/migrate/#{Time.now.to_s(:number)}_create_config_scripts.rb", /create_table :config_scripts/
12
+ end
13
+
14
+ context "when running repeatedly" do
15
+ it "says that it has skipped it" do
16
+ output = capture :stderr do
17
+ run_generator
18
+ end
19
+ expect(output).to be =~ /Another migration is already named create_config_scripts/
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,53 @@
1
+ describe ConfigScripts::Scripts::ScriptHistory do
2
+ let(:klass) { ConfigScripts::Scripts::ScriptHistory }
3
+ let(:timestamp1) { 5.minutes.ago.to_s(:number) }
4
+ let(:timestamp2) { 10.minutes.ago.to_s(:number) }
5
+
6
+ describe "entries_for_timestamp" do
7
+ let!(:entry1) { klass.create(name: timestamp1) }
8
+ let!(:entry2) { klass.create(name: timestamp1) }
9
+ let!(:entry3) { klass.create(name: timestamp2) }
10
+
11
+ subject { klass.entries_for_timestamp(timestamp1) }
12
+
13
+ it "gets all the entries whose timestamp has the value given" do
14
+ expect(subject).to include entry1
15
+ expect(subject).to include entry2
16
+ end
17
+
18
+ it "does not include entries with other timestamps" do
19
+ expect(subject).not_to include entry3
20
+ end
21
+ end
22
+
23
+ describe "script_was_run?" do
24
+ let!(:entry1) { klass.create(name: timestamp2) }
25
+
26
+ context "with a timestamp that has an entry in the table" do
27
+ subject { klass.script_was_run?(timestamp2) }
28
+ it { should be_true }
29
+ end
30
+
31
+ context "with a timestamp that has no entry in the table" do
32
+ subject { klass.script_was_run?(timestamp1) }
33
+ it { should be_false }
34
+ end
35
+ end
36
+
37
+ describe "record_timestamp" do
38
+ it "adds an entry to the table" do
39
+ expect(klass.script_was_run?(timestamp1)).to be_false
40
+ klass.record_timestamp(timestamp1)
41
+ expect(klass.script_was_run?(timestamp1)).to be_true
42
+ end
43
+ end
44
+
45
+ describe "remove_timestamp" do
46
+ it "removes the entry from the table" do
47
+ klass.create(name: timestamp2)
48
+ expect(klass.script_was_run?(timestamp2)).to be_true
49
+ klass.remove_timestamp(timestamp2)
50
+ expect(klass.script_was_run?(timestamp2)).to be_false
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,282 @@
1
+ describe ConfigScripts::Scripts::Script do
2
+ let(:klass) { ConfigScripts::Scripts::Script }
3
+
4
+ describe "class methods" do
5
+ describe "script_directory" do
6
+ subject { klass.script_directory}
7
+ it "is the db/config_scripts directory" do
8
+ expect(subject).to eq Rails.root.join('db', 'config_scripts')
9
+ end
10
+ end
11
+
12
+ describe "pending_scripts" do
13
+ let!(:filename1) { "20140208150000_script_1" }
14
+ let!(:filename2) { "20140208200000_script_2" }
15
+
16
+ subject { klass.pending_scripts }
17
+
18
+ before do
19
+ Dir.stub glob: ["/tmp/#{filename1}.rb", "/tmp/#{filename2}.rb"]
20
+ ConfigScripts::Scripts::ScriptHistory.record_timestamp('20140208150000')
21
+ end
22
+
23
+ it "uses Dir to get the files" do
24
+ subject
25
+ expect(Dir).to have_received(:glob).with(File.join(klass.script_directory, "*.rb"))
26
+ end
27
+
28
+ it "includes filenames that don't have entries in the script histories" do
29
+ expect(subject).to include filename2
30
+ end
31
+
32
+ it "does not include filenames that have entries in the script history" do
33
+ expect(subject).not_to include filename1
34
+ end
35
+ end
36
+
37
+ describe "run_pending_scripts" do
38
+ class TestConfigScriptConfig < ConfigScripts::Scripts::Script; end
39
+
40
+ let!(:timestamp1) { '20140208150000' }
41
+ let!(:timestamp2) { '20140208200000' }
42
+ let!(:timestamp3) { '20140208250000' }
43
+
44
+ let(:script_filenames) { [timestamp1, timestamp2, timestamp3].collect { |stamp| "#{stamp}_test_config_script" } }
45
+
46
+ let!(:script1) { TestConfigScriptConfig.new(timestamp1) }
47
+ let!(:script2) { TestConfigScriptConfig.new(timestamp2) }
48
+ let!(:script3) { TestConfigScriptConfig.new(timestamp3) }
49
+
50
+ before do
51
+ klass.stub pending_scripts: script_filenames
52
+ klass.stub :require
53
+ klass.stub :puts
54
+ [script1, script2, script3].each do |script|
55
+ script.stub(:run)
56
+ end
57
+
58
+ TestConfigScriptConfig.stub(:new).with(timestamp1).and_return(script1)
59
+ TestConfigScriptConfig.stub(:new).with(timestamp2).and_return(script2)
60
+ TestConfigScriptConfig.stub(:new).with(timestamp3).and_return(script3)
61
+
62
+ FileUtils.mkdir_p(Rails.root.join("tmp", "cache"))
63
+ end
64
+
65
+ let(:scripts) {[
66
+ {
67
+ filename: script_filenames[0],
68
+ script: script1,
69
+ run: true
70
+ },
71
+ {
72
+ filename: script_filenames[1],
73
+ script: script2,
74
+ run: true
75
+ },
76
+ {
77
+ filename: script_filenames[2],
78
+ script: script3,
79
+ run: true
80
+ }
81
+ ]}
82
+
83
+
84
+ shared_examples "ran scripts" do
85
+ it "requires only the scripts it needs to run" do
86
+ scripts.each do |script|
87
+ path = Rails.root.join("db", "config_scripts", "#{script[:filename]}.rb")
88
+ if script[:run]
89
+ expect(klass).to have_received(:require).with(path)
90
+ else
91
+ expect(klass).not_to have_received(:require).with(path)
92
+ end
93
+ end
94
+ end
95
+
96
+ it "creates a config script for each timestamp with the appropriate class" do
97
+ scripts.each do |script|
98
+ timestamp = script[:filename].first(14)
99
+ if script[:run]
100
+ expect(TestConfigScriptConfig).to have_received(:new).with(timestamp)
101
+ else
102
+ expect(TestConfigScriptConfig).not_to have_received(:new).with(timestamp)
103
+ end
104
+ end
105
+ end
106
+
107
+ it "calls the up command on each script item" do
108
+ scripts.each do |script|
109
+ if script[:run]
110
+ expect(script[:script]).to have_received(:run).with(:up)
111
+ else
112
+ expect(script[:script]).not_to have_received(:run)
113
+ end
114
+ end
115
+ end
116
+
117
+ it "outputs the name of the scripts that it is running" do
118
+ scripts.each do |script|
119
+ if script[:run]
120
+ expect(klass).to have_received(:puts).with("Running #{script[:filename]}")
121
+ else
122
+ expect(klass).not_to have_received(:puts).with("Running #{script[:filename]}")
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ context "with no problems running the scripts" do
129
+ it_behaves_like "ran scripts"
130
+
131
+ before do
132
+ klass.run_pending_scripts
133
+ end
134
+ end
135
+
136
+ context "with a problems running the scripts" do
137
+ it_behaves_like "ran scripts"
138
+
139
+ before do
140
+ script2.stub(:run).and_raise 'Error in script'
141
+ scripts[2][:run] = false
142
+ begin
143
+ klass.run_pending_scripts
144
+ rescue
145
+ end
146
+ end
147
+ end
148
+
149
+ context "with a class name it cannot find" do
150
+ let(:bad_filename) { "20140208170000_missing_class" }
151
+
152
+ before do
153
+ scripts[1][:run] = false
154
+ scripts[2][:run] = false
155
+ klass.stub pending_scripts: [script_filenames[0], bad_filename, script_filenames[1], script_filenames[2]]
156
+ klass.run_pending_scripts
157
+ end
158
+
159
+ it_behaves_like "ran scripts"
160
+
161
+ it "requires the path for the bad script" do
162
+ path = Rails.root.join("db", "config_scripts", "#{bad_filename}.rb")
163
+ expect(klass).to have_received(:require).with(path)
164
+ end
165
+
166
+ it "outputs a name error for the missing class" do
167
+ expect(klass).to have_received(:puts).with("Aborting: could not find class MissingClassConfig")
168
+ end
169
+ end
170
+ end
171
+
172
+ describe "list_pending_scripts" do
173
+ before do
174
+ klass.stub pending_scripts: ['script1.rb', 'script2.rb']
175
+ klass.stub :puts
176
+ klass.list_pending_scripts
177
+ end
178
+
179
+ it "prints out the name of each script" do
180
+ expect(klass).to have_received(:puts).with('script1.rb')
181
+ expect(klass).to have_received(:puts).with('script2.rb')
182
+ end
183
+ end
184
+ end
185
+
186
+ describe "creating" do
187
+ let(:timestamp) { '20140101153500' }
188
+ subject { klass.new(timestamp) }
189
+
190
+ it "sets the timestamp" do
191
+ expect(subject.timestamp).to eq timestamp
192
+ end
193
+ end
194
+
195
+ describe "running" do
196
+ let(:timestamp) { 1.day.ago.to_s(:number)}
197
+ let(:script) { ConfigScripts::Scripts::Script.new(timestamp) }
198
+
199
+ describe "up" do
200
+ it "raises an exception" do
201
+ expect(lambda{script.up}).to raise_exception("Not supported")
202
+ end
203
+ end
204
+
205
+ describe "down" do
206
+ it "raises an exception" do
207
+ expect(lambda{script.down}).to raise_exception("Not supported")
208
+ end
209
+ end
210
+
211
+ describe "run" do
212
+ {up: true, down: false}.each do |direction, expect_timestamp|
213
+ describe "direction" do
214
+ before do
215
+ if !expect_timestamp
216
+ ConfigScripts::Scripts::ScriptHistory.record_timestamp(timestamp)
217
+ end
218
+
219
+ script.stub :puts
220
+
221
+ Rails.cache.write("cached_item", "cached_value")
222
+ end
223
+
224
+ context "with a success" do
225
+ before do
226
+ script.stub(direction) do
227
+ Person.create(name: 'John Doe')
228
+ end
229
+ script.run(direction)
230
+ end
231
+
232
+ it "performs the changes in the #{direction} method" do
233
+ expect(Person.count).to eq 1
234
+ expect(Person.first.name).to eq "John Doe"
235
+ end
236
+
237
+ it "cleares the cache" do
238
+ expect(Rails.cache.read("cached_item")).to be_nil
239
+ end
240
+
241
+ it "{expect_timestamp ? 'adds' : 'removes'} the timestamp" do
242
+ expect(ConfigScripts::Scripts::ScriptHistory.script_was_run?(timestamp)).to eq expect_timestamp
243
+ end
244
+ end
245
+
246
+ context "with an exception in the #{direction} method" do
247
+ before do
248
+ script.stub(direction) do
249
+ Person.create(name: 'John Doe')
250
+ raise
251
+ end
252
+ end
253
+
254
+ it "re-raises the exception" do
255
+ expect(lambda{script.run(direction)}).to raise_exception
256
+ end
257
+
258
+ it "does not persist the changes in the #{direction} method" do
259
+ script.run(direction) rescue nil
260
+ expect(Person.count).to eq 0
261
+ end
262
+
263
+ it "does not clear the cache" do
264
+ script.run(direction) rescue nil
265
+ expect(Rails.cache.read('cached_item')).to eq 'cached_value'
266
+ end
267
+
268
+ it "does not #{expect_timestamp ? 'add' : 'remove'} the timestamp" do
269
+ script.run(direction) rescue nil
270
+ expect(ConfigScripts::Scripts::ScriptHistory.script_was_run?(timestamp)).not_to eq expect_timestamp
271
+ end
272
+
273
+ it "puts an error out to the logs" do
274
+ script.run(direction) rescue nil
275
+ expect(script).to have_received(:puts).with("Error running script for ConfigScripts::Scripts::Script: ")
276
+ end
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
282
+ end