cullender 0.0.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 (107) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +3 -0
  4. data/Rakefile +34 -0
  5. data/app/assets/javascripts/cullender/application.js +13 -0
  6. data/app/assets/javascripts/cullender/rules.js +46 -0
  7. data/app/assets/stylesheets/cullender/application.css +13 -0
  8. data/app/assets/stylesheets/cullender/rules.css.scss +15 -0
  9. data/app/controllers/cullender/rules_controller.rb +68 -0
  10. data/app/controllers/cullender_controller.rb +105 -0
  11. data/app/helpers/cullender/rules_helper.rb +136 -0
  12. data/app/models/cullender/rule.rb +204 -0
  13. data/app/views/cullender/rules/_form.html.erb +20 -0
  14. data/app/views/cullender/rules/_trigger_fields.html.erb +19 -0
  15. data/app/views/cullender/rules/create.js.erb +3 -0
  16. data/app/views/cullender/rules/index.html.erb +25 -0
  17. data/app/views/cullender/rules/new.html.erb +1 -0
  18. data/app/views/cullender/rules/show.html.erb +3 -0
  19. data/config/locales/en.yml +12 -0
  20. data/config/routes.rb +4 -0
  21. data/lib/cullender.rb +96 -0
  22. data/lib/cullender/controllers/filter_logic.rb +89 -0
  23. data/lib/cullender/controllers/scoped_views.rb +17 -0
  24. data/lib/cullender/core_ext/hash.rb +1 -0
  25. data/lib/cullender/core_ext/hash/deep_delete.rb +13 -0
  26. data/lib/cullender/engine.rb +15 -0
  27. data/lib/cullender/engine/routes.rb +306 -0
  28. data/lib/cullender/mapping.rb +139 -0
  29. data/lib/cullender/version.rb +3 -0
  30. data/lib/generators/cullender/cullender_generator.rb +66 -0
  31. data/lib/generators/cullender/install_generator.rb +18 -0
  32. data/lib/generators/cullender/orm_helpers.rb +32 -0
  33. data/lib/generators/cullender/templates/cullender.rb +3 -0
  34. data/lib/generators/cullender/templates/cullender_migration.rb +13 -0
  35. data/lib/tasks/cullender_tasks.rake +4 -0
  36. data/spec/controllers/cullender/rules_controller_spec.rb +180 -0
  37. data/spec/cullender/controllers/filter_logic_spec.rb +233 -0
  38. data/spec/dummy/README.rdoc +28 -0
  39. data/spec/dummy/Rakefile +6 -0
  40. data/spec/dummy/app/assets/javascripts/application.js +17 -0
  41. data/spec/dummy/app/assets/stylesheets/application.css +14 -0
  42. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  43. data/spec/dummy/app/controllers/events_controller.rb +60 -0
  44. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  45. data/spec/dummy/app/models/event.rb +16 -0
  46. data/spec/dummy/app/views/cullender/rules/_form.html.erb +33 -0
  47. data/spec/dummy/app/views/cullender/rules/index.html.erb +24 -0
  48. data/spec/dummy/app/views/cullender/rules/new.html.erb +1 -0
  49. data/spec/dummy/app/views/cullender/rules/show.html.erb +3 -0
  50. data/spec/dummy/app/views/events/_form.html.erb +21 -0
  51. data/spec/dummy/app/views/events/edit.html.erb +6 -0
  52. data/spec/dummy/app/views/events/index.html.erb +27 -0
  53. data/spec/dummy/app/views/events/new.html.erb +5 -0
  54. data/spec/dummy/app/views/events/show.html.erb +10 -0
  55. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  56. data/spec/dummy/bin/bundle +3 -0
  57. data/spec/dummy/bin/rails +4 -0
  58. data/spec/dummy/bin/rake +4 -0
  59. data/spec/dummy/config.ru +4 -0
  60. data/spec/dummy/config/application.rb +23 -0
  61. data/spec/dummy/config/boot.rb +9 -0
  62. data/spec/dummy/config/database.yml +25 -0
  63. data/spec/dummy/config/environment.rb +5 -0
  64. data/spec/dummy/config/environments/development.rb +27 -0
  65. data/spec/dummy/config/environments/production.rb +80 -0
  66. data/spec/dummy/config/environments/test.rb +36 -0
  67. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  68. data/spec/dummy/config/initializers/cullender.rb +3 -0
  69. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  70. data/spec/dummy/config/initializers/inflections.rb +16 -0
  71. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  72. data/spec/dummy/config/initializers/secret_token.rb +12 -0
  73. data/spec/dummy/config/initializers/session_store.rb +3 -0
  74. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  75. data/spec/dummy/config/locales/cullender.en.yml +12 -0
  76. data/spec/dummy/config/locales/en.yml +23 -0
  77. data/spec/dummy/config/routes.rb +5 -0
  78. data/spec/dummy/db/development.sqlite3 +0 -0
  79. data/spec/dummy/db/migrate/123344556676_create_cullender_tables.rb +12 -0
  80. data/spec/dummy/db/schema.rb +24 -0
  81. data/spec/dummy/db/test.sqlite3 +0 -0
  82. data/spec/dummy/log/development.log +9987 -0
  83. data/spec/dummy/log/test.log +19786 -0
  84. data/spec/dummy/public/404.html +27 -0
  85. data/spec/dummy/public/422.html +26 -0
  86. data/spec/dummy/public/500.html +26 -0
  87. data/spec/dummy/public/favicon.ico +0 -0
  88. data/spec/dummy/tmp/cache/assets/development/sprockets/13fe41fee1fe35b49d145bcc06610705 +0 -0
  89. data/spec/dummy/tmp/cache/assets/development/sprockets/268a828ee4997cf5c19106e5f00fcfb8 +0 -0
  90. data/spec/dummy/tmp/cache/assets/development/sprockets/2f5173deea6c795b8fdde723bb4b63af +0 -0
  91. data/spec/dummy/tmp/cache/assets/development/sprockets/357970feca3ac29060c1e3861e2c0953 +0 -0
  92. data/spec/dummy/tmp/cache/assets/development/sprockets/56b2eb9b46b9aca7e86b5132ddf0d9de +0 -0
  93. data/spec/dummy/tmp/cache/assets/development/sprockets/750887406b3e8dacb2b03f986330932d +0 -0
  94. data/spec/dummy/tmp/cache/assets/development/sprockets/750b42e431d194c41f5b1cde4a257e47 +0 -0
  95. data/spec/dummy/tmp/cache/assets/development/sprockets/921642fe740290e9e5e88a706e5a562c +0 -0
  96. data/spec/dummy/tmp/cache/assets/development/sprockets/a967ed3fc5f8406f3ff72180775f1b40 +0 -0
  97. data/spec/dummy/tmp/cache/assets/development/sprockets/ae74bf4fc3fd20e7f7b98860b8ef0f74 +0 -0
  98. data/spec/dummy/tmp/cache/assets/development/sprockets/c7dbd1f5acc2d5bc078363f4f3c70c54 +0 -0
  99. data/spec/dummy/tmp/cache/assets/development/sprockets/cffd775d018f68ce5dba1ee0d951a994 +0 -0
  100. data/spec/dummy/tmp/cache/assets/development/sprockets/d771ace226fc8215a3572e0aa35bb0d6 +0 -0
  101. data/spec/dummy/tmp/cache/assets/development/sprockets/d995daf8d6f36c27b6e9d1b4672eaf1e +0 -0
  102. data/spec/dummy/tmp/cache/assets/development/sprockets/f7cbd26ba1d28d48de824f0e94586655 +0 -0
  103. data/spec/helpers/cullender/rules_helper_spec.rb +360 -0
  104. data/spec/models/cullender/rule_spec.rb +425 -0
  105. data/spec/requests/rules_spec.rb +19 -0
  106. data/spec/spec_helper.rb +60 -0
  107. metadata +319 -0
@@ -0,0 +1,425 @@
1
+ require 'spec_helper'
2
+
3
+ describe Cullender::Rule do
4
+
5
+ # describe "#fire" do
6
+ # before(:each) do
7
+ # @rule = Rule.new
8
+ # end
9
+ # context "with events" do
10
+ # before(:each) do
11
+ # @event = Factory(:event)
12
+ # Event.stub(:filter_between).with(nil, nil, @rule.triggers).and_return([@event])
13
+ # end
14
+ # context "with labels" do
15
+ # before(:each) do
16
+ # @rule.labels = ["label1", "label2"]
17
+ # end
18
+ # it "adds labels to the events" do
19
+ # @rule.fire(nil, nil)
20
+ # @event.should have(2).tags
21
+ # @event.tags.should include "label1"
22
+ # @event.tags.should include "label2"
23
+ # end
24
+ # end
25
+ # end
26
+ # context "with no events" do
27
+ # before(:each) do
28
+ # Event.stub(:filter_between).with(nil, nil, @rule.triggers).and_return([])
29
+ # end
30
+ # it "returns nil" do
31
+ # @rule.fire(nil, nil).should be_nil
32
+ # end
33
+ # end
34
+ # end
35
+
36
+ def single_attr(key = "attr", value = 100)
37
+ {key => {"o" => "term", "v" => value}}
38
+ end
39
+
40
+
41
+ describe ".to_query_proc" do
42
+ before(:each) do
43
+ @rule = Cullender::Rule.new
44
+ end
45
+ it "returns a Proc object" do
46
+ # @rule.should_receive(:to_query).and_return(::Tire::Search::Query.new)
47
+ query_proc = @rule.to_query_proc
48
+ query_proc.should be_a_kind_of(Proc)
49
+ end
50
+ end
51
+
52
+
53
+ describe "#number_of_fields" do
54
+ before(:each) do
55
+ @rule = Cullender::Rule.new
56
+ end
57
+ context "with triggers not set" do
58
+ it "returns 0" do
59
+ @rule.send(:number_of_fields).should == 0
60
+ end
61
+ end
62
+ context "single level" do
63
+ context "single field" do
64
+ before(:each) do
65
+ @rule.triggers = single_attr
66
+ end
67
+ it "returns the count" do
68
+ @rule.send(:number_of_fields).should == 1
69
+ end
70
+ end
71
+
72
+ context "multiple fields" do
73
+ before(:each) do
74
+ @rule.triggers = single_attr("attr1", 100).merge!(single_attr("attr2", 200))
75
+ end
76
+ it "returns the count" do
77
+ @rule.send(:number_of_fields).should == 2
78
+ end
79
+ end
80
+ end
81
+ context "deep nesting" do
82
+ context "single level" do
83
+ before(:each) do
84
+ @rule.triggers = {
85
+ "1" => single_attr("attr1", 100).merge!(single_attr("attr2", 200)),
86
+ "2" => single_attr("attr3", 300).merge!(single_attr("attr4", 400))
87
+ }
88
+ end
89
+ it "returns the count" do
90
+ @rule.send(:number_of_fields).should == 4
91
+ end
92
+ end
93
+ context "different levels" do
94
+ before(:each) do
95
+ @rule.triggers = {
96
+ "1" => {
97
+ "1" => single_attr("attr1", 100).merge!(single_attr("attr2", 200)),
98
+ "2" => single_attr("attr3", 300).merge!(single_attr("attr4", 400))
99
+ },
100
+ "2" => single_attr("attr5", 500).merge!(single_attr("attr6", 600)),
101
+ "3" => single_attr("attr7", 700).merge!(single_attr("attr8", 800))
102
+ }
103
+ end
104
+ it "returns the count" do
105
+ @rule.send(:number_of_fields).should == 8
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+
112
+
113
+ describe "#to_query", :current => true do
114
+ before(:each) do
115
+ @rule = Cullender::Rule.new
116
+ end
117
+ context "a single attr" do
118
+ before(:each) do
119
+ @rule.triggers = single_attr
120
+ end
121
+ it "parses the filter for matching" do
122
+ result = @rule.to_query.to_hash
123
+ result.should == {
124
+ :term => {
125
+ "attr" => {:term => 100}
126
+ }
127
+ }
128
+ end
129
+ end
130
+
131
+ context "multiple filter 'AND'ed together" do
132
+ before(:each) do
133
+ @rule.triggers = single_attr("attr1", 100).merge!(single_attr("attr2", 200))
134
+ end
135
+ it "parses the filter for matching" do
136
+ result = @rule.to_query.to_hash
137
+ result.should == {:bool=>{:must=>[{:term=>{"attr1"=>{:term=>100}}}, {:term=>{"attr2"=>{:term=>200}}}]}}
138
+ # {
139
+ # :filtered => {
140
+ # :filter => {
141
+ # :and => [
142
+ # {:term => {:attr1 => "100"}},
143
+ # {:term => {:attr2 => "200"}}
144
+ # ]
145
+ # }
146
+ # }
147
+ # }
148
+ end
149
+ end
150
+
151
+ context "multiple 'AND'ed filters 'OR'ed together" do
152
+ before(:each) do
153
+ @rule.triggers = {
154
+ "1" => single_attr("attr1", 100).merge!(single_attr("attr2", 200)),
155
+ "2" => single_attr("attr3", 300).merge!(single_attr("attr4", 400))
156
+ }
157
+ end
158
+ it "parses the filter for matching" do
159
+ result = @rule.to_query.to_hash
160
+ result.should == {
161
+ :bool=>{
162
+ :should=>[
163
+ {
164
+ :bool=>{
165
+ :must=>[
166
+ {:term=>{"attr1"=>{:term=>100}}},
167
+ {:term=>{"attr2"=>{:term=>200}}}
168
+ ]
169
+ }
170
+ },{
171
+ :bool=>{
172
+ :must=>[
173
+ {:term=>{"attr3"=>{:term=>300}}},
174
+ {:term=>{"attr4"=>{:term=>400}}}
175
+ ]
176
+ }
177
+ }
178
+ ]
179
+ }
180
+ }
181
+
182
+ # {
183
+ # :filtered => {
184
+ # :filter => {
185
+ # :and => [
186
+ # :or => [
187
+ # {:and => [
188
+ # {:term => {:attr1 => "100"}},
189
+ # {:term => {:attr2 => "200"}}
190
+ # ]},
191
+ # {:and => [
192
+ # {:term => {:attr3 => "300"}},
193
+ # {:term => {:attr4 => "400"}}
194
+ # ]}
195
+ # ]
196
+ # ]
197
+ # }
198
+ # }
199
+ # }
200
+ end
201
+ end
202
+
203
+ context "deeply nested multiple 'AND'ed filters 'OR'ed together" do
204
+ before(:each) do
205
+ @rule.triggers = {
206
+ "1" => {
207
+ "1" => single_attr("attr1", 100).merge!(single_attr("attr2", 200)),
208
+ "2" => single_attr("attr3", 300).merge!(single_attr("attr4", 400))
209
+ },
210
+ "2" => {
211
+ "1" => single_attr("attr5", 500).merge!(single_attr("attr6", 600)),
212
+ "2" => single_attr("attr7", 700).merge!(single_attr("attr8", 800)),
213
+ }
214
+ }
215
+ end
216
+ it "parses the filter for matching" do
217
+ result = @rule.to_query.to_hash
218
+ result.should == {
219
+ :bool=>{
220
+ :should=>[
221
+ {
222
+ :bool=>{
223
+ :should=>[
224
+ {
225
+ :bool=>{
226
+ :must=>[
227
+ {:term=>{"attr1"=>{:term=>100}}},
228
+ {:term=>{"attr2"=>{:term=>200}}}
229
+ ]
230
+ }
231
+ },{
232
+ :bool=>{
233
+ :must=>[
234
+ {:term=>{"attr3"=>{:term=>300}}},
235
+ {:term=>{"attr4"=>{:term=>400}}}
236
+ ]
237
+ }
238
+ }
239
+ ]
240
+ }
241
+ },{
242
+ :bool=>{
243
+ :should=>[
244
+ {
245
+ :bool=>{
246
+ :must=>[
247
+ {:term=>{"attr5"=>{:term=>500}}},
248
+ {:term=>{"attr6"=>{:term=>600}}}
249
+ ]
250
+ }
251
+ },{
252
+ :bool=>{
253
+ :must=>[
254
+ {:term=>{"attr7"=>{:term=>700}}},
255
+ {:term=>{"attr8"=>{:term=>800}}}
256
+ ]
257
+ }
258
+ }
259
+ ]
260
+ }
261
+ }
262
+ ]
263
+ }
264
+ }
265
+ # {
266
+ # :filtered => {
267
+ # :filter => {
268
+ # :and => [
269
+ # :or => [
270
+ # {:or => [
271
+ # {:and => [
272
+ # {:term => {:attr1 => "100"}},
273
+ # {:term => {:attr2 => "200"}}
274
+ # ]},
275
+ # {:and => [
276
+ # {:term => {:attr3 => "300"}},
277
+ # {:term => {:attr4 => "400"}}
278
+ # ]}
279
+ # ]},
280
+ # {:or => [
281
+ # {:and => [
282
+ # {:term => {:attr5 => "500"}},
283
+ # {:term => {:attr6 => "600"}}
284
+ # ]},
285
+ # {:and => [
286
+ # {:term => {:attr7 => "700"}},
287
+ # {:term => {:attr8 => "800"}}
288
+ # ]}
289
+ # ]}
290
+ # ]
291
+ # ]
292
+ # }
293
+ # }
294
+ # }
295
+ end
296
+ end
297
+
298
+ end
299
+
300
+
301
+ # describe "#to_search" do
302
+ # before(:each) do
303
+ # @rule = Cullender::Rule.new
304
+ # end
305
+ # context "a single filter" do
306
+ # before(:each) do
307
+ # @rule.triggers = single_attr
308
+ # end
309
+ # it "parses the filter for matching" do
310
+ # result = @rule.to_search.to_hash
311
+ # result.should == {
312
+ # :filter => {
313
+ # :term => {:attr => "100"}
314
+ # }
315
+ # }
316
+ # end
317
+ # end
318
+
319
+ # context "multiple filter 'AND'ed together" do
320
+ # before(:each) do
321
+ # @rule.triggers = single_attr("attr1", 100).merge!(single_attr("attr2", 200))
322
+ # end
323
+ # it "parses the filter for matching" do
324
+ # result = @rule.to_search.to_hash
325
+ # result.should == {
326
+ # :filter => {
327
+ # :and => [
328
+ # {:term => {:attr1 => "100"}},
329
+ # {:term => {:attr2 => "200"}}
330
+ # ]
331
+ # }
332
+ # }
333
+ # end
334
+ # end
335
+
336
+ # context "multiple 'AND'ed filters 'OR'ed together" do
337
+ # before(:each) do
338
+ # @rule.triggers = {
339
+ # "1" => single_attr("attr1", 100).merge!(single_attr("attr2", 200)),
340
+ # "2" => single_attr("attr3", 300).merge!(single_attr("attr4", 400))
341
+ # }
342
+ # end
343
+ # it "parses the filter for matching" do
344
+ # result = @rule.to_search.to_hash
345
+ # result.should == {
346
+ # :filter => {
347
+ # :or => [
348
+ # {:and => [
349
+ # {:term => {:attr1 => "100"}},
350
+ # {:term => {:attr2 => "200"}}
351
+ # ]},
352
+ # {:and => [
353
+ # {:term => {:attr3 => "300"}},
354
+ # {:term => {:attr4 => "400"}}
355
+ # ]}
356
+ # ]
357
+ # }
358
+ # }
359
+ # end
360
+ # end
361
+
362
+ # context "deeply nested multiple 'AND'ed filters 'OR'ed together" do
363
+ # before(:each) do
364
+ # @rule.triggers = {
365
+ # "1" => {
366
+ # "2" => single_attr("attr1", 100).merge!(single_attr("attr2", 200)),
367
+ # "1" => single_attr("attr3", 300).merge!(single_attr("attr4", 400))
368
+ # },
369
+ # "2" => {
370
+ # "1" => single_attr("attr5", 500).merge!(single_attr("attr6", 600)),
371
+ # "2" => single_attr("attr7", 700).merge!(single_attr("attr8", 800)),
372
+ # }
373
+ # }
374
+ # end
375
+ # it "parses the filter for matching" do
376
+ # result = @rule.to_search.to_hash
377
+ # result.should == {
378
+ # :filter => {
379
+ # :or => [
380
+ # {:or => [
381
+ # {:and => [
382
+ # {:term => {:attr1 => "100"}},
383
+ # {:term => {:attr2 => "200"}}
384
+ # ]},
385
+ # {:and => [
386
+ # {:term => {:attr3 => "300"}},
387
+ # {:term => {:attr4 => "400"}}
388
+ # ]}
389
+ # ]},
390
+ # {:or => [
391
+ # {:and => [
392
+ # {:term => {:attr5 => "500"}},
393
+ # {:term => {:attr6 => "600"}}
394
+ # ]},
395
+ # {:and => [
396
+ # {:term => {:attr7 => "700"}},
397
+ # {:term => {:attr8 => "800"}}
398
+ # ]}
399
+ # ]}
400
+ # ]
401
+ # }
402
+ # }
403
+ # end
404
+ # end
405
+
406
+ # end
407
+
408
+
409
+
410
+
411
+
412
+ end
413
+ # == Schema Information
414
+ #
415
+ # Table name: rules
416
+ #
417
+ # id :integer(4) not null, primary key
418
+ # name :string(255)
419
+ # description :string(255)
420
+ # type :string(255)
421
+ # enabled :boolean(1)
422
+ # created_at :datetime not null
423
+ # updated_at :datetime not null
424
+ #
425
+
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Rules" do
4
+
5
+
6
+ describe "POST /rules" do
7
+ context "percolation" do
8
+ before(:each) do
9
+ ::Tire.index('_percolator').refresh
10
+ end
11
+ # test the creating of a rule and percolate command in elasticsearch
12
+ it "works! (now write some real specs)" do
13
+ Cullender::Rule.create(:name => "example", :triggers => {})
14
+
15
+ end
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,60 @@
1
+ # Configure Rails Environment
2
+ ENV["RAILS_ENV"] = "test"
3
+
4
+ require File.expand_path("../dummy/config/environment.rb", __FILE__)
5
+ require "rails/test_help"
6
+ require 'rspec/rails'
7
+ require 'rspec/autorun'
8
+ require 'capybara/rspec'
9
+ # require 'database_cleaner'
10
+
11
+ Rails.backtrace_cleaner.remove_silencers!
12
+
13
+ # Load support files
14
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
15
+
16
+ RSpec.configure do |config|
17
+
18
+ # == Mock Framework
19
+ #
20
+ # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
21
+ #
22
+ # config.mock_with :mocha
23
+ # config.mock_with :flexmock
24
+ # config.mock_with :rr
25
+ config.mock_with :rspec
26
+
27
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
28
+ # config.fixture_path = "#{::Rails.root}/spec/fixtures"
29
+
30
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
31
+ # examples within a transaction, remove the following line or assign false
32
+ # instead of true.
33
+ config.use_transactional_fixtures = false
34
+
35
+ # If true, the base class of anonymous controllers will be inferred
36
+ # automatically. This will be the default behavior in future versions of
37
+ # rspec-rails.
38
+ config.infer_base_class_for_anonymous_controllers = false
39
+
40
+
41
+ config.around do |example|
42
+ ActiveRecord::Base.transaction do
43
+ example.run
44
+ raise ActiveRecord::Rollback
45
+ end
46
+ end
47
+
48
+ # config.before(:suite) do
49
+ # DatabaseCleaner.strategy = :transaction
50
+ # DatabaseCleaner.clean_with(:truncation)
51
+ # end
52
+
53
+ # config.before(:each) do
54
+ # DatabaseCleaner.start
55
+ # end
56
+
57
+ # config.after(:each) do
58
+ # DatabaseCleaner.clean
59
+ # end
60
+ end