jmcnevin-paperclip 2.4.5

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 (71) hide show
  1. data/LICENSE +26 -0
  2. data/README.md +414 -0
  3. data/Rakefile +86 -0
  4. data/generators/paperclip/USAGE +5 -0
  5. data/generators/paperclip/paperclip_generator.rb +27 -0
  6. data/generators/paperclip/templates/paperclip_migration.rb.erb +19 -0
  7. data/init.rb +4 -0
  8. data/lib/generators/paperclip/USAGE +8 -0
  9. data/lib/generators/paperclip/paperclip_generator.rb +33 -0
  10. data/lib/generators/paperclip/templates/paperclip_migration.rb.erb +19 -0
  11. data/lib/paperclip.rb +480 -0
  12. data/lib/paperclip/attachment.rb +520 -0
  13. data/lib/paperclip/callback_compatibility.rb +61 -0
  14. data/lib/paperclip/geometry.rb +155 -0
  15. data/lib/paperclip/interpolations.rb +171 -0
  16. data/lib/paperclip/iostream.rb +45 -0
  17. data/lib/paperclip/matchers.rb +33 -0
  18. data/lib/paperclip/matchers/have_attached_file_matcher.rb +57 -0
  19. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +81 -0
  20. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +54 -0
  21. data/lib/paperclip/matchers/validate_attachment_size_matcher.rb +95 -0
  22. data/lib/paperclip/missing_attachment_styles.rb +87 -0
  23. data/lib/paperclip/options.rb +78 -0
  24. data/lib/paperclip/processor.rb +58 -0
  25. data/lib/paperclip/railtie.rb +26 -0
  26. data/lib/paperclip/storage.rb +3 -0
  27. data/lib/paperclip/storage/filesystem.rb +81 -0
  28. data/lib/paperclip/storage/fog.rb +163 -0
  29. data/lib/paperclip/storage/s3.rb +270 -0
  30. data/lib/paperclip/style.rb +95 -0
  31. data/lib/paperclip/thumbnail.rb +105 -0
  32. data/lib/paperclip/upfile.rb +62 -0
  33. data/lib/paperclip/version.rb +3 -0
  34. data/lib/tasks/paperclip.rake +101 -0
  35. data/rails/init.rb +2 -0
  36. data/shoulda_macros/paperclip.rb +124 -0
  37. data/test/attachment_test.rb +1161 -0
  38. data/test/database.yml +4 -0
  39. data/test/fixtures/12k.png +0 -0
  40. data/test/fixtures/50x50.png +0 -0
  41. data/test/fixtures/5k.png +0 -0
  42. data/test/fixtures/animated.gif +0 -0
  43. data/test/fixtures/bad.png +1 -0
  44. data/test/fixtures/double spaces in name.png +0 -0
  45. data/test/fixtures/fog.yml +8 -0
  46. data/test/fixtures/s3.yml +8 -0
  47. data/test/fixtures/spaced file.png +0 -0
  48. data/test/fixtures/text.txt +1 -0
  49. data/test/fixtures/twopage.pdf +0 -0
  50. data/test/fixtures/uppercase.PNG +0 -0
  51. data/test/fog_test.rb +192 -0
  52. data/test/geometry_test.rb +206 -0
  53. data/test/helper.rb +158 -0
  54. data/test/integration_test.rb +781 -0
  55. data/test/interpolations_test.rb +202 -0
  56. data/test/iostream_test.rb +71 -0
  57. data/test/matchers/have_attached_file_matcher_test.rb +24 -0
  58. data/test/matchers/validate_attachment_content_type_matcher_test.rb +87 -0
  59. data/test/matchers/validate_attachment_presence_matcher_test.rb +26 -0
  60. data/test/matchers/validate_attachment_size_matcher_test.rb +51 -0
  61. data/test/options_test.rb +75 -0
  62. data/test/paperclip_missing_attachment_styles_test.rb +80 -0
  63. data/test/paperclip_test.rb +340 -0
  64. data/test/processor_test.rb +10 -0
  65. data/test/storage/filesystem_test.rb +56 -0
  66. data/test/storage/s3_live_test.rb +88 -0
  67. data/test/storage/s3_test.rb +689 -0
  68. data/test/style_test.rb +180 -0
  69. data/test/thumbnail_test.rb +383 -0
  70. data/test/upfile_test.rb +53 -0
  71. metadata +294 -0
@@ -0,0 +1,180 @@
1
+ # encoding: utf-8
2
+ require './test/helper'
3
+
4
+ class StyleTest < Test::Unit::TestCase
5
+
6
+ context "A style rule" do
7
+ setup do
8
+ @attachment = attachment :path => ":basename.:extension",
9
+ :styles => { :foo => {:geometry => "100x100#", :format => :png} },
10
+ :whiny => true
11
+ @style = @attachment.options.styles[:foo]
12
+ end
13
+
14
+ should "be held as a Style object" do
15
+ assert_kind_of Paperclip::Style, @style
16
+ end
17
+
18
+ should "get processors from the attachment definition" do
19
+ assert_equal [:thumbnail], @style.processors
20
+ end
21
+
22
+ should "have the right geometry" do
23
+ assert_equal "100x100#", @style.geometry
24
+ end
25
+
26
+ should "be whiny if the attachment is" do
27
+ assert @style.whiny?
28
+ end
29
+
30
+ should "respond to hash notation" do
31
+ assert_equal [:thumbnail], @style[:processors]
32
+ assert_equal "100x100#", @style[:geometry]
33
+ end
34
+ end
35
+
36
+ context "A style rule with properties supplied as procs" do
37
+ setup do
38
+ @attachment = attachment :path => ":basename.:extension",
39
+ :whiny_thumbnails => true,
40
+ :processors => lambda {|a| [:test]},
41
+ :styles => {
42
+ :foo => lambda{|a| "300x300#"},
43
+ :bar => {
44
+ :geometry => lambda{|a| "300x300#"}
45
+ }
46
+ }
47
+ end
48
+
49
+ should "call procs when they are needed" do
50
+ assert_equal "300x300#", @attachment.options.styles[:foo].geometry
51
+ assert_equal "300x300#", @attachment.options.styles[:bar].geometry
52
+ assert_equal [:test], @attachment.options.styles[:foo].processors
53
+ assert_equal [:test], @attachment.options.styles[:bar].processors
54
+ end
55
+ end
56
+
57
+ context "An attachment with style rules in various forms" do
58
+ setup do
59
+ styles = ActiveSupport::OrderedHash.new
60
+ styles[:aslist] = ["100x100", :png]
61
+ styles[:ashash] = {:geometry => "100x100", :format => :png}
62
+ styles[:asstring] = "100x100"
63
+ @attachment = attachment :path => ":basename.:extension",
64
+ :styles => styles
65
+ end
66
+ should "have the right number of styles" do
67
+ assert_kind_of Hash, @attachment.options.styles
68
+ assert_equal 3, @attachment.options.styles.size
69
+ end
70
+
71
+ should "have styles as Style objects" do
72
+ [:aslist, :ashash, :aslist].each do |s|
73
+ assert_kind_of Paperclip::Style, @attachment.options.styles[s]
74
+ end
75
+ end
76
+
77
+ should "have the right geometries" do
78
+ [:aslist, :ashash, :aslist].each do |s|
79
+ assert_equal @attachment.options.styles[s].geometry, "100x100"
80
+ end
81
+ end
82
+
83
+ should "have the right formats" do
84
+ assert_equal @attachment.options.styles[:aslist].format, :png
85
+ assert_equal @attachment.options.styles[:ashash].format, :png
86
+ assert_nil @attachment.options.styles[:asstring].format
87
+ end
88
+
89
+ should "retain order" do
90
+ assert_equal [:aslist, :ashash, :asstring], @attachment.options.styles.keys
91
+ end
92
+ end
93
+
94
+ context "An attachment with :convert_options" do
95
+ setup do
96
+ @attachment = attachment :path => ":basename.:extension",
97
+ :styles => {:thumb => "100x100", :large => "400x400"},
98
+ :convert_options => {:all => "-do_stuff", :thumb => "-thumbnailize"}
99
+ @style = @attachment.options.styles[:thumb]
100
+ @file = StringIO.new("...")
101
+ @file.stubs(:original_filename).returns("file.jpg")
102
+ end
103
+
104
+ before_should "not have called extra_options_for(:thumb/:large) on initialization" do
105
+ @attachment.expects(:extra_options_for).never
106
+ end
107
+
108
+ should "call extra_options_for(:thumb/:large) when convert options are requested" do
109
+ @attachment.expects(:extra_options_for).with(:thumb)
110
+ @attachment.options.styles[:thumb].convert_options
111
+ end
112
+ end
113
+
114
+ context "An attachment with :source_file_options" do
115
+ setup do
116
+ @attachment = attachment :path => ":basename.:extension",
117
+ :styles => {:thumb => "100x100", :large => "400x400"},
118
+ :source_file_options => {:all => "-density 400", :thumb => "-depth 8"}
119
+ @style = @attachment.options.styles[:thumb]
120
+ @file = StringIO.new("...")
121
+ @file.stubs(:original_filename).returns("file.jpg")
122
+ end
123
+
124
+ before_should "not have called extra_source_file_options_for(:thumb/:large) on initialization" do
125
+ @attachment.expects(:extra_source_file_options_for).never
126
+ end
127
+
128
+ should "call extra_options_for(:thumb/:large) when convert options are requested" do
129
+ @attachment.expects(:extra_source_file_options_for).with(:thumb)
130
+ @attachment.options.styles[:thumb].source_file_options
131
+ end
132
+ end
133
+
134
+ context "A style rule with its own :processors" do
135
+ setup do
136
+ @attachment = attachment :path => ":basename.:extension",
137
+ :styles => {
138
+ :foo => {
139
+ :geometry => "100x100#",
140
+ :format => :png,
141
+ :processors => [:test]
142
+ }
143
+ },
144
+ :processors => [:thumbnail]
145
+ @style = @attachment.options.styles[:foo]
146
+ end
147
+
148
+ should "not get processors from the attachment" do
149
+ @attachment.expects(:processors).never
150
+ assert_not_equal [:thumbnail], @style.processors
151
+ end
152
+
153
+ should "report its own processors" do
154
+ assert_equal [:test], @style.processors
155
+ end
156
+
157
+ end
158
+
159
+ context "A style rule with :processors supplied as procs" do
160
+ setup do
161
+ @attachment = attachment :path => ":basename.:extension",
162
+ :styles => {
163
+ :foo => {
164
+ :geometry => "100x100#",
165
+ :format => :png,
166
+ :processors => lambda{|a| [:test]}
167
+ }
168
+ },
169
+ :processors => [:thumbnail]
170
+ end
171
+
172
+ should "defer processing of procs until they are needed" do
173
+ assert_kind_of Proc, @attachment.options.styles[:foo].instance_variable_get("@processors")
174
+ end
175
+
176
+ should "call procs when they are needed" do
177
+ assert_equal [:test], @attachment.options.styles[:foo].processors
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,383 @@
1
+ require './test/helper'
2
+
3
+ class ThumbnailTest < Test::Unit::TestCase
4
+
5
+ context "A Paperclip Tempfile" do
6
+ setup do
7
+ @tempfile = Paperclip::Tempfile.new(["file", ".jpg"])
8
+ end
9
+
10
+ should "have its path contain a real extension" do
11
+ assert_equal ".jpg", File.extname(@tempfile.path)
12
+ end
13
+
14
+ should "be a real Tempfile" do
15
+ assert @tempfile.is_a?(::Tempfile)
16
+ end
17
+ end
18
+
19
+ context "Another Paperclip Tempfile" do
20
+ setup do
21
+ @tempfile = Paperclip::Tempfile.new("file")
22
+ end
23
+
24
+ should "not have an extension if not given one" do
25
+ assert_equal "", File.extname(@tempfile.path)
26
+ end
27
+
28
+ should "still be a real Tempfile" do
29
+ assert @tempfile.is_a?(::Tempfile)
30
+ end
31
+ end
32
+
33
+ context "An image" do
34
+ setup do
35
+ @file = File.new(File.join(File.dirname(__FILE__), "fixtures", "5k.png"), 'rb')
36
+ end
37
+
38
+ teardown { @file.close }
39
+
40
+ [["600x600>", "434x66"],
41
+ ["400x400>", "400x61"],
42
+ ["32x32<", "434x66"]
43
+ ].each do |args|
44
+ context "being thumbnailed with a geometry of #{args[0]}" do
45
+ setup do
46
+ @thumb = Paperclip::Thumbnail.new(@file, :geometry => args[0])
47
+ end
48
+
49
+ should "start with dimensions of 434x66" do
50
+ cmd = %Q[identify -format "%wx%h" "#{@file.path}"]
51
+ assert_equal "434x66", `#{cmd}`.chomp
52
+ end
53
+
54
+ should "report the correct target geometry" do
55
+ assert_equal args[0], @thumb.target_geometry.to_s
56
+ end
57
+
58
+ context "when made" do
59
+ setup do
60
+ @thumb_result = @thumb.make
61
+ end
62
+
63
+ should "be the size we expect it to be" do
64
+ cmd = %Q[identify -format "%wx%h" "#{@thumb_result.path}"]
65
+ assert_equal args[1], `#{cmd}`.chomp
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ context "being thumbnailed at 100x50 with cropping" do
72
+ setup do
73
+ @thumb = Paperclip::Thumbnail.new(@file, :geometry => "100x50#")
74
+ end
75
+
76
+ should "let us know when a command isn't found versus a processing error" do
77
+ old_path = ENV['PATH']
78
+ begin
79
+ ENV['PATH'] = ''
80
+ assert_raises(Paperclip::CommandNotFoundError) do
81
+ @thumb.make
82
+ end
83
+ ensure
84
+ ENV['PATH'] = old_path
85
+ end
86
+ end
87
+
88
+ should "report its correct current and target geometries" do
89
+ assert_equal "100x50#", @thumb.target_geometry.to_s
90
+ assert_equal "434x66", @thumb.current_geometry.to_s
91
+ end
92
+
93
+ should "report its correct format" do
94
+ assert_nil @thumb.format
95
+ end
96
+
97
+ should "have whiny turned on by default" do
98
+ assert @thumb.whiny
99
+ end
100
+
101
+ should "have convert_options set to nil by default" do
102
+ assert_equal nil, @thumb.convert_options
103
+ end
104
+
105
+ should "have source_file_options set to nil by default" do
106
+ assert_equal nil, @thumb.source_file_options
107
+ end
108
+
109
+ should "send the right command to convert when sent #make" do
110
+ Paperclip.expects(:run).with do |*arg|
111
+ arg[0] == 'convert' &&
112
+ arg[1] == ':source -resize "x50" -crop "100x50+114+0" +repage :dest' &&
113
+ arg[2][:source] == "#{File.expand_path(@thumb.file.path)}[0]"
114
+ end
115
+ @thumb.make
116
+ end
117
+
118
+ should "create the thumbnail when sent #make" do
119
+ dst = @thumb.make
120
+ assert_match /100x50/, `identify "#{dst.path}"`
121
+ end
122
+ end
123
+
124
+ context "being thumbnailed with source file options set" do
125
+ setup do
126
+ @thumb = Paperclip::Thumbnail.new(@file,
127
+ :geometry => "100x50#",
128
+ :source_file_options => "-strip")
129
+ end
130
+
131
+ should "have source_file_options value set" do
132
+ assert_equal ["-strip"], @thumb.source_file_options
133
+ end
134
+
135
+ should "send the right command to convert when sent #make" do
136
+ Paperclip.expects(:run).with do |*arg|
137
+ arg[0] == 'convert' &&
138
+ arg[1] == '-strip :source -resize "x50" -crop "100x50+114+0" +repage :dest' &&
139
+ arg[2][:source] == "#{File.expand_path(@thumb.file.path)}[0]"
140
+ end
141
+ @thumb.make
142
+ end
143
+
144
+ should "create the thumbnail when sent #make" do
145
+ dst = @thumb.make
146
+ assert_match /100x50/, `identify "#{dst.path}"`
147
+ end
148
+
149
+ context "redefined to have bad source_file_options setting" do
150
+ setup do
151
+ @thumb = Paperclip::Thumbnail.new(@file,
152
+ :geometry => "100x50#",
153
+ :source_file_options => "-this-aint-no-option")
154
+ end
155
+
156
+ should "error when trying to create the thumbnail" do
157
+ assert_raises(Paperclip::PaperclipError) do
158
+ @thumb.make
159
+ end
160
+ end
161
+ end
162
+ end
163
+
164
+ context "being thumbnailed with convert options set" do
165
+ setup do
166
+ @thumb = Paperclip::Thumbnail.new(@file,
167
+ :geometry => "100x50#",
168
+ :convert_options => "-strip -depth 8")
169
+ end
170
+
171
+ should "have convert_options value set" do
172
+ assert_equal %w"-strip -depth 8", @thumb.convert_options
173
+ end
174
+
175
+ should "send the right command to convert when sent #make" do
176
+ Paperclip.expects(:run).with do |*arg|
177
+ arg[0] == 'convert' &&
178
+ arg[1] == ':source -resize "x50" -crop "100x50+114+0" +repage -strip -depth 8 :dest' &&
179
+ arg[2][:source] == "#{File.expand_path(@thumb.file.path)}[0]"
180
+ end
181
+ @thumb.make
182
+ end
183
+
184
+ should "create the thumbnail when sent #make" do
185
+ dst = @thumb.make
186
+ assert_match /100x50/, `identify "#{dst.path}"`
187
+ end
188
+
189
+ context "redefined to have bad convert_options setting" do
190
+ setup do
191
+ @thumb = Paperclip::Thumbnail.new(@file,
192
+ :geometry => "100x50#",
193
+ :convert_options => "-this-aint-no-option")
194
+ end
195
+
196
+ should "error when trying to create the thumbnail" do
197
+ assert_raises(Paperclip::PaperclipError) do
198
+ @thumb.make
199
+ end
200
+ end
201
+
202
+ should "let us know when a command isn't found versus a processing error" do
203
+ old_path = ENV['PATH']
204
+ begin
205
+ ENV['PATH'] = ''
206
+ assert_raises(Paperclip::CommandNotFoundError) do
207
+ @thumb.make
208
+ end
209
+ ensure
210
+ ENV['PATH'] = old_path
211
+ end
212
+ end
213
+ end
214
+ end
215
+
216
+ context "being thumbnailed with a blank geometry string" do
217
+ setup do
218
+ @thumb = Paperclip::Thumbnail.new(@file,
219
+ :geometry => "",
220
+ :convert_options => "-gravity center -crop \"300x300+0-0\"")
221
+ end
222
+
223
+ should "not get resized by default" do
224
+ assert !@thumb.transformation_command.include?("-resize")
225
+ end
226
+ end
227
+
228
+ context "passing a custom file geometry parser" do
229
+ should "produce the appropriate transformation_command" do
230
+ GeoParser = Class.new do
231
+ def self.from_file(file)
232
+ new
233
+ end
234
+ def transformation_to(target, should_crop)
235
+ ["SCALE", "CROP"]
236
+ end
237
+ end
238
+
239
+ thumb = Paperclip::Thumbnail.new(@file, :geometry => '50x50', :file_geometry_parser => GeoParser)
240
+
241
+ transformation_command = thumb.transformation_command
242
+
243
+ assert transformation_command.include?('-crop'),
244
+ %{expected #{transformation_command.inspect} to include '-crop'}
245
+ assert transformation_command.include?('"CROP"'),
246
+ %{expected #{transformation_command.inspect} to include '"CROP"'}
247
+ assert transformation_command.include?('-resize'),
248
+ %{expected #{transformation_command.inspect} to include '-resize'}
249
+ assert transformation_command.include?('"SCALE"'),
250
+ %{expected #{transformation_command.inspect} to include '"SCALE"'}
251
+ end
252
+ end
253
+
254
+ context "passing a custom geometry string parser" do
255
+ should "produce the appropriate transformation_command" do
256
+ GeoParser = Class.new do
257
+ def self.parse(s)
258
+ new
259
+ end
260
+
261
+ def to_s
262
+ "151x167"
263
+ end
264
+ end
265
+
266
+ thumb = Paperclip::Thumbnail.new(@file, :geometry => '50x50', :string_geometry_parser => GeoParser)
267
+
268
+ transformation_command = thumb.transformation_command
269
+
270
+ assert transformation_command.include?('"151x167"'),
271
+ %{expected #{transformation_command.inspect} to include '151x167'}
272
+ end
273
+ end
274
+ end
275
+
276
+ context "A multipage PDF" do
277
+ setup do
278
+ @file = File.new(File.join(File.dirname(__FILE__), "fixtures", "twopage.pdf"), 'rb')
279
+ end
280
+
281
+ teardown { @file.close }
282
+
283
+ should "start with two pages with dimensions 612x792" do
284
+ cmd = %Q[identify -format "%wx%h" "#{@file.path}"]
285
+ assert_equal "612x792"*2, `#{cmd}`.chomp
286
+ end
287
+
288
+ context "being thumbnailed at 100x100 with cropping" do
289
+ setup do
290
+ @thumb = Paperclip::Thumbnail.new(@file, :geometry => "100x100#", :format => :png)
291
+ end
292
+
293
+ should "report its correct current and target geometries" do
294
+ assert_equal "100x100#", @thumb.target_geometry.to_s
295
+ assert_equal "612x792", @thumb.current_geometry.to_s
296
+ end
297
+
298
+ should "report its correct format" do
299
+ assert_equal :png, @thumb.format
300
+ end
301
+
302
+ should "create the thumbnail when sent #make" do
303
+ dst = @thumb.make
304
+ assert_match /100x100/, `identify "#{dst.path}"`
305
+ end
306
+ end
307
+ end
308
+
309
+ context "An animated gif" do
310
+ setup do
311
+ @file = File.new(File.join(File.dirname(__FILE__), "fixtures", "animated.gif"), 'rb')
312
+ end
313
+
314
+ teardown { @file.close }
315
+
316
+ should "start with 12 frames with size 100x100" do
317
+ cmd = %Q[identify -format "%wx%h" "#{@file.path}"]
318
+ assert_equal "100x100"*12, `#{cmd}`.chomp
319
+ end
320
+
321
+ context "with static output" do
322
+ setup do
323
+ @thumb = Paperclip::Thumbnail.new(@file, :geometry => "50x50", :format => :jpg)
324
+ end
325
+
326
+ should "create the single frame thumbnail when sent #make" do
327
+ dst = @thumb.make
328
+ cmd = %Q[identify -format "%wx%h" "#{dst.path}"]
329
+ assert_equal "50x50", `#{cmd}`.chomp
330
+ end
331
+ end
332
+
333
+ context "with animated output format" do
334
+ setup do
335
+ @thumb = Paperclip::Thumbnail.new(@file, :geometry => "50x50", :format => :gif)
336
+ end
337
+
338
+ should "create the 12 frames thumbnail when sent #make" do
339
+ dst = @thumb.make
340
+ cmd = %Q[identify -format "%wx%h" "#{dst.path}"]
341
+ assert_equal "50x50"*12, `#{cmd}`.chomp
342
+ end
343
+
344
+ should "use the -coalesce option" do
345
+ assert_equal @thumb.transformation_command.first, "-coalesce"
346
+ end
347
+ end
348
+
349
+ context "with omitted output format" do
350
+ setup do
351
+ @thumb = Paperclip::Thumbnail.new(@file, :geometry => "50x50")
352
+ end
353
+
354
+ should "create the 12 frames thumbnail when sent #make" do
355
+ dst = @thumb.make
356
+ cmd = %Q[identify -format "%wx%h" "#{dst.path}"]
357
+ assert_equal "50x50"*12, `#{cmd}`.chomp
358
+ end
359
+
360
+ should "use the -coalesce option" do
361
+ assert_equal @thumb.transformation_command.first, "-coalesce"
362
+ end
363
+ end
364
+
365
+ context "with animated option set to false" do
366
+ setup do
367
+ @thumb = Paperclip::Thumbnail.new(@file, :geometry => "50x50", :animated => false)
368
+ end
369
+
370
+ should "output the gif format" do
371
+ dst = @thumb.make
372
+ cmd = %Q[identify "#{dst.path}"]
373
+ assert_match /GIF/, `#{cmd}`.chomp
374
+ end
375
+
376
+ should "create the single frame thumbnail when sent #make" do
377
+ dst = @thumb.make
378
+ cmd = %Q[identify -format "%wx%h" "#{dst.path}"]
379
+ assert_equal "50x50", `#{cmd}`.chomp
380
+ end
381
+ end
382
+ end
383
+ end