ssickles-tire 0.4.2 → 0.4.2.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/lib/tire.rb +3 -18
  2. data/lib/tire/alias.rb +35 -11
  3. data/lib/tire/index.rb +76 -34
  4. data/lib/tire/results/collection.rb +13 -38
  5. data/lib/tire/results/item.rb +0 -19
  6. data/lib/tire/rubyext/to_json.rb +21 -0
  7. data/lib/tire/search.rb +8 -7
  8. data/lib/tire/search/scan.rb +8 -8
  9. data/lib/tire/search/sort.rb +1 -1
  10. data/lib/tire/version.rb +38 -7
  11. data/test/integration/reindex_test.rb +2 -2
  12. data/test/integration/scan_test.rb +1 -1
  13. data/test/test_helper.rb +3 -27
  14. data/test/unit/index_alias_test.rb +17 -3
  15. data/test/unit/index_test.rb +18 -30
  16. data/test/unit/results_collection_test.rb +0 -60
  17. data/test/unit/results_item_test.rb +0 -37
  18. data/test/unit/rubyext_test.rb +3 -3
  19. data/test/unit/search_test.rb +6 -1
  20. data/test/unit/tire_test.rb +0 -15
  21. data/tire.gemspec +13 -30
  22. metadata +41 -153
  23. data/lib/tire/model/callbacks.rb +0 -40
  24. data/lib/tire/model/import.rb +0 -26
  25. data/lib/tire/model/indexing.rb +0 -128
  26. data/lib/tire/model/naming.rb +0 -100
  27. data/lib/tire/model/percolate.rb +0 -99
  28. data/lib/tire/model/persistence.rb +0 -72
  29. data/lib/tire/model/persistence/attributes.rb +0 -143
  30. data/lib/tire/model/persistence/finders.rb +0 -66
  31. data/lib/tire/model/persistence/storage.rb +0 -71
  32. data/lib/tire/model/search.rb +0 -305
  33. data/lib/tire/rubyext/hash.rb +0 -8
  34. data/lib/tire/rubyext/ruby_1_8.rb +0 -54
  35. data/lib/tire/rubyext/symbol.rb +0 -11
  36. data/lib/tire/utils.rb +0 -17
  37. data/test/integration/active_model_indexing_test.rb +0 -51
  38. data/test/integration/active_model_searchable_test.rb +0 -114
  39. data/test/integration/active_record_searchable_test.rb +0 -446
  40. data/test/integration/mongoid_searchable_test.rb +0 -309
  41. data/test/integration/persistent_model_test.rb +0 -117
  42. data/test/models/active_model_article.rb +0 -31
  43. data/test/models/active_model_article_with_callbacks.rb +0 -49
  44. data/test/models/active_model_article_with_custom_document_type.rb +0 -7
  45. data/test/models/active_model_article_with_custom_index_name.rb +0 -7
  46. data/test/models/active_record_models.rb +0 -122
  47. data/test/models/mongoid_models.rb +0 -97
  48. data/test/models/persistent_article.rb +0 -11
  49. data/test/models/persistent_article_in_namespace.rb +0 -12
  50. data/test/models/persistent_article_with_casting.rb +0 -28
  51. data/test/models/persistent_article_with_defaults.rb +0 -11
  52. data/test/models/persistent_articles_with_custom_index_name.rb +0 -10
  53. data/test/models/supermodel_article.rb +0 -17
  54. data/test/models/validated_model.rb +0 -11
  55. data/test/unit/active_model_lint_test.rb +0 -17
  56. data/test/unit/model_callbacks_test.rb +0 -116
  57. data/test/unit/model_import_test.rb +0 -71
  58. data/test/unit/model_persistence_test.rb +0 -516
  59. data/test/unit/model_search_test.rb +0 -899
@@ -4,18 +4,23 @@ module Tire
4
4
 
5
5
  class Search
6
6
 
7
- attr_reader :indices, :json, :query, :facets, :filters, :options, :explain
7
+ attr_reader :indices, :json, :query, :facets, :filters, :options, :explain, :url
8
8
 
9
9
  def initialize(indices=nil, options={}, &block)
10
10
  @indices = Array(indices)
11
- @types = Array(options.delete(:type)).map { |type| Utils.escape(type) }
11
+ @types = Array(options.delete(:type)).map { |type| EscapeUtils.escape_url(type.to_s) }
12
12
  @options = options
13
-
14
13
  @path = ['/', @indices.join(','), @types.join(','), '_search'].compact.join('/').squeeze('/')
15
14
 
15
+ self.url = options.delete(:url) || Configuration.url
16
+
16
17
  block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given?
17
18
  end
18
19
 
20
+ def url=( url )
21
+ @url = url + @path
22
+ end
23
+
19
24
  def results
20
25
  @results || (perform; @results)
21
26
  end
@@ -24,10 +29,6 @@ module Tire
24
29
  @response || (perform; @response)
25
30
  end
26
31
 
27
- def url
28
- Configuration.url + @path
29
- end
30
-
31
32
  def params
32
33
  @options.empty? ? '' : '?' + @options.to_param
33
34
  end
@@ -42,22 +42,22 @@ module Tire
42
42
  class Scan
43
43
  include Enumerable
44
44
 
45
- attr_reader :indices, :options, :search
45
+ attr_reader :indices, :options, :search, :url
46
46
 
47
47
  def initialize(indices=nil, options={}, &block)
48
48
  @indices = Array(indices)
49
49
  @options = options.update(:search_type => 'scan', :scroll => '10m')
50
50
  @seen = 0
51
+ @url = options.fetch(:url, Configuration.url) + '/_search/scroll'
51
52
  @search = Search.new(@indices, @options, &block)
52
53
  end
53
54
 
54
- def url; Configuration.url + "/_search/scroll"; end
55
- def params; @options.empty? ? '' : '?' + @options.to_param; end
56
- def results; @results || (__perform; @results); end
57
- def response; @response || (__perform; @response); end
58
- def json; @json || (__perform; @json); end
59
- def total; @total || (__perform; @total); end
60
- def seen; @seen || (__perform; @seen); end
55
+ def params() @options.empty? ? '' : '?' + @options.to_param; end
56
+ def results() @results || (__perform; @results); end
57
+ def response() @response || (__perform; @response); end
58
+ def json() @json || (__perform; @json); end
59
+ def total() @total || (__perform; @total); end
60
+ def seen() @seen || (__perform; @seen); end
61
61
 
62
62
  def scroll_id
63
63
  @scroll_id ||= @search.perform.json['_scroll_id']
@@ -17,7 +17,7 @@ module Tire
17
17
  end
18
18
 
19
19
  def to_json
20
- @value.to_json
20
+ MultiJson.encode(@value)
21
21
  end
22
22
  end
23
23
 
@@ -1,9 +1,46 @@
1
1
  module Tire
2
- VERSION = "0.4.2"
2
+ VERSION = "0.4.2.7"
3
3
 
4
4
  CHANGELOG =<<-END
5
5
  IMPORTANT CHANGES LATELY:
6
6
 
7
+ Version 0.4.2.7
8
+ ---------------
9
+ * Bumped escape_utils dependency to ~> 0.3.x
10
+
11
+ Version 0.4.2.6
12
+ ---------------
13
+ * Add support for parent / child documents.
14
+
15
+ Version 0.4.2.4
16
+ ---------------
17
+ * Add routing support.
18
+
19
+ Version 0.4.2.4
20
+ ---------------
21
+ * Fixing url routing for index alais commands.
22
+
23
+ Version 0.4.2.3
24
+ ---------------
25
+ * Making the library multi-cluster capable
26
+
27
+ Version 0.4.2.2
28
+ ---------------
29
+ * Tire::Index now removes _id/_type keys from document hashes.
30
+
31
+ Version 0.4.2.1
32
+ ---------------
33
+ * Removed all depedence on ActiveModel
34
+ * Fixed tests to work with any version of ActiveSupport
35
+ * Removed wonky 1.9 backports for URL encoding
36
+ * Fixed tests to work under 1.8.7
37
+
38
+ Version 0.4.2
39
+ -------------
40
+ * Fixed incorrect handling of PUT requests in the Curb client
41
+ * Fixed, that blocks passed to `Tire::Index.new` or `Tire.index` losed the scope
42
+ * Added `Tire::Alias`, interface and DSL to manage aliases as resources
43
+
7
44
  Version 0.4.1
8
45
  -------------
9
46
  * Added a Index#settings method to retrieve index settings as a Hash
@@ -12,11 +49,5 @@ module Tire
12
49
  * Added basic support for index aliases
13
50
  * Changed, that Index#bulk_store runs against an index endpoint, not against `/_bulk`
14
51
  * Refactorings, fixes, Ruby 1.8 compatibility
15
-
16
- Version 0.4.2
17
- -------------
18
- * Fixed incorrect handling of PUT requests in the Curb client
19
- * Fixed, that blocks passed to `Tire::Index.new` or `Tire.index` losed the scope
20
- * Added `Tire::Alias`, interface and DSL to manage aliases as resources
21
52
  END
22
53
  end
@@ -9,7 +9,7 @@ module Tire
9
9
  setup do
10
10
  Tire.index('reindex-test-new').delete
11
11
 
12
- documents = (1..100).map { |i| { id: i, type: 'test', title: "Document #{i}" } }
12
+ documents = (1..100).map { |i| { :id => i, :type => 'test', :title => "Document #{i}" } }
13
13
 
14
14
  Tire.index 'reindex-test' do
15
15
  delete
@@ -25,7 +25,7 @@ module Tire
25
25
  end
26
26
 
27
27
  should "reindex the index into a new index with different settings" do
28
- Tire.index('reindex-test').reindex 'reindex-test-new', settings: { number_of_shards: 3 }
28
+ Tire.index('reindex-test').reindex 'reindex-test-new', :settings => { :number_of_shards => 3 }
29
29
 
30
30
  Tire.index('reindex-test-new').refresh
31
31
  assert_equal 100, Tire.search('reindex-test-new').results.total
@@ -7,7 +7,7 @@ module Tire
7
7
 
8
8
  context "Scan" do
9
9
  setup do
10
- documents = (1..100).map { |i| { id: i, type: 'test', title: "Document #{i}" } }
10
+ documents = (1..100).map { |i| { :id => i, :type => 'test', :title => "Document #{i}" } }
11
11
 
12
12
  Tire.index 'scantest' do
13
13
  delete
@@ -4,32 +4,15 @@ require 'bundler/setup'
4
4
  require 'pathname'
5
5
  require 'test/unit'
6
6
 
7
- require 'yajl/json_gem'
8
- require 'sqlite3'
7
+ require 'yajl'
8
+ #require 'yajl/json_gem'
9
9
 
10
10
  require 'shoulda'
11
11
  require 'turn/autorun' unless ENV["TM_FILEPATH"] || ENV["CI"] || defined?(RUBY_VERSION) && RUBY_VERSION < '1.9'
12
12
  require 'mocha'
13
13
 
14
- require 'active_support/core_ext/hash/indifferent_access'
15
-
16
14
  require 'tire'
17
15
 
18
- # Require basic model files
19
- #
20
- require File.dirname(__FILE__) + '/models/active_model_article'
21
- require File.dirname(__FILE__) + '/models/active_model_article_with_callbacks'
22
- require File.dirname(__FILE__) + '/models/active_model_article_with_custom_document_type'
23
- require File.dirname(__FILE__) + '/models/active_model_article_with_custom_index_name'
24
- require File.dirname(__FILE__) + '/models/active_record_models'
25
- require File.dirname(__FILE__) + '/models/article'
26
- require File.dirname(__FILE__) + '/models/persistent_article'
27
- require File.dirname(__FILE__) + '/models/persistent_article_in_namespace'
28
- require File.dirname(__FILE__) + '/models/persistent_article_with_casting'
29
- require File.dirname(__FILE__) + '/models/persistent_article_with_defaults'
30
- require File.dirname(__FILE__) + '/models/persistent_articles_with_custom_index_name'
31
- require File.dirname(__FILE__) + '/models/validated_model'
32
-
33
16
  class Test::Unit::TestCase
34
17
 
35
18
  def mock_response(body, code=200, headers={})
@@ -74,14 +57,7 @@ module Test::Integration
74
57
  def teardown
75
58
  %w[
76
59
  articles-test
77
- active_record_articles
78
- active_model_article_with_custom_as_serializations
79
- active_record_class_with_tire_methods
80
- mongoid_articles
81
- mongoid_class_with_tire_methods
82
- supermodel_articles
83
- dynamic_index
84
- model_with_nested_documents ].each do |index|
60
+ dynamic_index ].each do |index|
85
61
  ::RestClient.delete "#{URL}/#{index}" rescue nil
86
62
  end
87
63
  end
@@ -12,9 +12,15 @@ module Tire
12
12
  should "have a name and defaults" do
13
13
  @alias = Alias.new :name => 'dummy'
14
14
  assert_equal 'dummy', @alias.name
15
+ assert_equal Configuration.url, @alias.url
15
16
  assert @alias.indices.empty?
16
17
  end
17
18
 
19
+ should "have a configurable base URL" do
20
+ @alias = Alias.new :url => 'http://example.com:9200'
21
+ assert_equal 'http://example.com:9200', @alias.url
22
+ end
23
+
18
24
  should "have indices" do
19
25
  @alias = Alias.new :indices => ['index_A', 'index_B']
20
26
  assert_equal ['index_A', 'index_B'], @alias.indices.to_a
@@ -94,7 +100,7 @@ module Tire
94
100
  a['add']['alias'] == 'alias_martha'
95
101
  end
96
102
  end.returns(mock_response('{}'), 200)
97
-
103
+
98
104
  a = Alias.find('alias_martha')
99
105
  a.indices.push 'index_A'
100
106
  a.save
@@ -109,7 +115,7 @@ module Tire
109
115
  a['remove']['alias'] == 'alias_martha'
110
116
  end
111
117
  end.returns(mock_response('{}'), 200)
112
-
118
+
113
119
  a = Alias.find('alias_martha')
114
120
  a.indices.delete 'index_A'
115
121
  a.save
@@ -120,7 +126,7 @@ module Tire
120
126
  # puts json
121
127
  MultiJson.decode(json)['actions'].all? { |a| a['add']['routing'] == 'martha' }
122
128
  end.returns(mock_response('{}'), 200)
123
-
129
+
124
130
  a = Alias.find('alias_martha')
125
131
  a.routing('martha')
126
132
  a.save
@@ -181,6 +187,14 @@ module Tire
181
187
  a = Alias.find('alias_martha')
182
188
  assert_instance_of Alias, a
183
189
  assert_equal ['index_B', 'index_C'], a.indices.to_a.sort
190
+
191
+ Configuration.client.expects(:get).with do |url, json|
192
+ url == "http://example.com:9200/_aliases"
193
+ end.returns( mock_response( %q|{"index_A":{"aliases":{}},"index_B":{"aliases":{"alias_john":{"filter":{"term":{"user":"john"}}},"alias_martha":{"filter":{"term":{"user":"martha"}}}}},"index_C":{"aliases":{"alias_martha":{"filter":{"term":{"user":"martha"}}}}}}|), 200 )
194
+
195
+ a = Alias.find('http://example.com:9200', 'alias_martha')
196
+ assert_instance_of Alias, a
197
+ assert_equal ['index_B', 'index_C'], a.indices.to_a.sort
184
198
  end
185
199
 
186
200
  should "find an alias and configure it with a block" do
@@ -18,6 +18,11 @@ module Tire
18
18
  assert_equal "#{Configuration.url}/#{@index.name}", @index.url
19
19
  end
20
20
 
21
+ should "have a configurable URL endpoint" do
22
+ @index.url = 'http://example.com'
23
+ assert_equal "http://example.com/#{@index.name}", @index.url
24
+ end
25
+
21
26
  should "return HTTP response" do
22
27
  assert_respond_to @index, :response
23
28
 
@@ -211,21 +216,21 @@ module Tire
211
216
 
212
217
  should "set type from Hash :type property" do
213
218
  Configuration.client.expects(:post).with do |url,document|
214
- url == "#{@index.url}/article/"
219
+ url == "#{@index.url}/article"
215
220
  end.returns(mock_response('{"ok":true,"_id":"test"}'))
216
221
  @index.store :type => 'article', :title => 'Test'
217
222
  end
218
223
 
219
224
  should "set type from Hash :_type property" do
220
225
  Configuration.client.expects(:post).with do |url,document|
221
- url == "#{@index.url}/article/"
226
+ url == "#{@index.url}/article"
222
227
  end.returns(mock_response('{"ok":true,"_id":"test"}'))
223
228
  @index.store :_type => 'article', :title => 'Test'
224
229
  end
225
230
 
226
231
  should "set type from Object _type method" do
227
232
  Configuration.client.expects(:post).with do |url,document|
228
- url == "#{@index.url}/article/"
233
+ url == "#{@index.url}/article"
229
234
  end.returns(mock_response('{"ok":true,"_id":"test"}'))
230
235
 
231
236
  article = Class.new do
@@ -237,7 +242,7 @@ module Tire
237
242
 
238
243
  should "set type from Object type method" do
239
244
  Configuration.client.expects(:post).with do |url,document|
240
- url == "#{@index.url}/article/"
245
+ url == "#{@index.url}/article"
241
246
  end.returns(mock_response('{"ok":true,"_id":"test"}'))
242
247
 
243
248
  article = Class.new do
@@ -249,7 +254,7 @@ module Tire
249
254
 
250
255
  should "properly encode namespaced document types" do
251
256
  Configuration.client.expects(:post).with do |url,document|
252
- url == "#{@index.url}/my_namespace%2Fmy_model/"
257
+ url == "#{@index.url}/my_namespace%2Fmy_model"
253
258
  end.returns(mock_response('{"ok":true,"_id":"123"}'))
254
259
 
255
260
  module MyNamespace
@@ -263,7 +268,7 @@ module Tire
263
268
  end
264
269
 
265
270
  should "set default type" do
266
- Configuration.client.expects(:post).with("#{@index.url}/document/", '{"title":"Test"}').returns(mock_response('{"ok":true,"_id":"test"}'))
271
+ Configuration.client.expects(:post).with("#{@index.url}/document", '{"title":"Test"}').returns(mock_response('{"ok":true,"_id":"test"}'))
267
272
  @index.store :title => 'Test'
268
273
  end
269
274
 
@@ -310,7 +315,7 @@ module Tire
310
315
  Configuration.reset :wrapper
311
316
 
312
317
  Configuration.client.stubs(:post).with do |url, payload|
313
- url == "#{@index.url}/article/" &&
318
+ url == "#{@index.url}/article" &&
314
319
  payload =~ /"title":"Test"/
315
320
  end.
316
321
  returns(mock_response('{"ok":true,"_id":"id-1"}'))
@@ -447,23 +452,6 @@ module Tire
447
452
  @index.bulk_store [ {:id => '1', :title => 'One'}, {:id => '2', :title => 'Two'} ]
448
453
  end
449
454
 
450
- should "serialize ActiveModel instances" do
451
- Configuration.client.expects(:post).with do |url, json|
452
- url == "#{ActiveModelArticle.index.url}/_bulk" &&
453
- json =~ /"_index":"active_model_articles"/ &&
454
- json =~ /"_type":"active_model_article"/ &&
455
- json =~ /"_id":"1"/ &&
456
- json =~ /"_id":"2"/ &&
457
- json =~ /"title":"One"/ &&
458
- json =~ /"title":"Two"/
459
- end.returns(mock_response('{}', 200))
460
-
461
- one = ActiveModelArticle.new 'title' => 'One'; one.id = '1'
462
- two = ActiveModelArticle.new 'title' => 'Two'; two.id = '2'
463
-
464
- ActiveModelArticle.index.bulk_store [ one, two ]
465
- end
466
-
467
455
  context "namespaced models" do
468
456
  should "not URL-escape the document_type" do
469
457
  Configuration.client.expects(:post).with do |url, json|
@@ -514,13 +502,13 @@ module Tire
514
502
 
515
503
  should "display error message when collection item does not have ID" do
516
504
  Configuration.client.expects(:post).with do |url, json|
517
- url == "#{ActiveModelArticle.index.url}/_bulk"
505
+ url == "#{@index.url}/_bulk"
518
506
  end.returns(mock_response('success', 200))
519
507
 
520
508
  STDERR.expects(:puts).once
521
509
 
522
510
  documents = [ { :title => 'Bogus' }, { :title => 'Real', :id => 1 } ]
523
- ActiveModelArticle.index.bulk_store documents
511
+ @index.bulk_store documents
524
512
  end
525
513
 
526
514
  end
@@ -745,7 +733,7 @@ module Tire
745
733
  should "percolate document against all registered queries" do
746
734
  Configuration.client.expects(:post).
747
735
  with do |url, payload|
748
- url == "#{@index.url}/article/?percolate=*" &&
736
+ url == "#{@index.url}/article?percolate=%2A" &&
749
737
  payload =~ /"title":"Test"/
750
738
  end.
751
739
  returns(mock_response('{"ok":true,"_id":"test","matches":["alerts"]}'))
@@ -755,7 +743,7 @@ module Tire
755
743
  should "percolate document against specific queries" do
756
744
  Configuration.client.expects(:post).
757
745
  with do |url, payload|
758
- url == "#{@index.url}/article/?percolate=tag:alerts" &&
746
+ url == "#{@index.url}/article?percolate=tag%3Aalerts" &&
759
747
  payload =~ /"title":"Test"/
760
748
  end.
761
749
  returns(mock_response('{"ok":true,"_id":"test","matches":["alerts"]}'))
@@ -823,10 +811,10 @@ module Tire
823
811
  def index_something
824
812
  @tags = ['block', 'scope', 'revenge']
825
813
 
826
- Index.any_instance.expects(:store).with(title: 'Title From Outer Space', tags: ['block', 'scope', 'revenge'])
814
+ Index.any_instance.expects(:store).with(:title => 'Title From Outer Space', :tags => ['block', 'scope', 'revenge'])
827
815
 
828
816
  Tire::Index.new 'outer-space' do |index|
829
- index.store title: @my_title, tags: @tags
817
+ index.store(:title => @my_title, :tags => @tags)
830
818
  end
831
819
  end
832
820
 
@@ -214,66 +214,6 @@ module Tire
214
214
 
215
215
  end
216
216
 
217
- context "with eager loading" do
218
- setup do
219
- @response = { 'hits' => { 'hits' => [ {'_id' => 1, '_type' => 'active_record_article'},
220
- {'_id' => 2, '_type' => 'active_record_article'},
221
- {'_id' => 3, '_type' => 'active_record_article'}] } }
222
- ActiveRecordArticle.stubs(:inspect).returns("<ActiveRecordArticle>")
223
- end
224
-
225
- should "load the records via model find method from database" do
226
- ActiveRecordArticle.expects(:find).with([1,2,3]).
227
- returns([ Results::Item.new(:id => 3),
228
- Results::Item.new(:id => 1),
229
- Results::Item.new(:id => 2) ])
230
- Results::Collection.new(@response, :load => true).results
231
- end
232
-
233
- should "pass the :load option Hash to model find metod" do
234
- ActiveRecordArticle.expects(:find).with([1,2,3], :include => 'comments').
235
- returns([ Results::Item.new(:id => 3),
236
- Results::Item.new(:id => 1),
237
- Results::Item.new(:id => 2) ])
238
- Results::Collection.new(@response, :load => { :include => 'comments' }).results
239
- end
240
-
241
- should "preserve the order of records returned from search" do
242
- ActiveRecordArticle.expects(:find).with([1,2,3]).
243
- returns([ Results::Item.new(:id => 3),
244
- Results::Item.new(:id => 1),
245
- Results::Item.new(:id => 2) ])
246
- assert_equal [1,2,3], Results::Collection.new(@response, :load => true).results.map(&:id)
247
- end
248
-
249
- should "raise error when model class cannot be inferred from _type" do
250
- assert_raise(NameError) do
251
- response = { 'hits' => { 'hits' => [ {'_id' => 1, '_type' => 'hic_sunt_leones'}] } }
252
- Results::Collection.new(response, :load => true).results
253
- end
254
- end
255
-
256
- should "raise error when _type is missing" do
257
- assert_raise(NoMethodError) do
258
- response = { 'hits' => { 'hits' => [ {'_id' => 1}] } }
259
- Results::Collection.new(response, :load => true).results
260
- end
261
- end
262
-
263
- should "return empty array for empty hits" do
264
- response = { 'hits' => {
265
- 'hits' => [],
266
- 'total' => 4
267
- },
268
- 'took' => 1 }
269
- @collection = Results::Collection.new( response, :load => true )
270
- assert @collection.empty?, 'Collection should be empty'
271
- assert @collection.results.empty?, 'Collection results should be empty'
272
- assert_equal 0, @collection.size
273
- end
274
-
275
- end
276
-
277
217
  end
278
218
 
279
219
  end