2014-08-02 13:33:51 -04:00
|
|
|
require 'mongoid_spec_helper'
|
|
|
|
|
|
|
|
module Ransack
|
|
|
|
describe Search do
|
|
|
|
describe '#initialize' do
|
|
|
|
it "removes empty conditions before building" do
|
|
|
|
expect_any_instance_of(Search).to receive(:build).with({})
|
|
|
|
Search.new(Person, :name_eq => '')
|
|
|
|
end
|
|
|
|
|
|
|
|
it "keeps conditions with a false value before building" do
|
|
|
|
expect_any_instance_of(Search).to receive(:build).with({"name_eq" => false})
|
|
|
|
Search.new(Person, :name_eq => false)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "keeps conditions with a value before building" do
|
|
|
|
expect_any_instance_of(Search).to receive(:build).with({"name_eq" => 'foobar'})
|
|
|
|
Search.new(Person, :name_eq => 'foobar')
|
|
|
|
end
|
|
|
|
|
|
|
|
it "removes empty suffixed conditions before building" do
|
|
|
|
expect_any_instance_of(Search).to receive(:build).with({})
|
|
|
|
Search.new(Person, :name_eq_any => [''])
|
|
|
|
end
|
|
|
|
|
|
|
|
it "keeps suffixed conditions with a false value before building" do
|
|
|
|
expect_any_instance_of(Search).to receive(:build).with({"name_eq_any" => [false]})
|
|
|
|
Search.new(Person, :name_eq_any => [false])
|
|
|
|
end
|
|
|
|
|
|
|
|
it "keeps suffixed conditions with a value before building" do
|
|
|
|
expect_any_instance_of(Search).to receive(:build).with({"name_eq_any" => ['foobar']})
|
|
|
|
Search.new(Person, :name_eq_any => ['foobar'])
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'does not raise exception for string :params argument' do
|
|
|
|
expect { Search.new(Person, '') }.not_to raise_error
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '#build' do
|
|
|
|
it 'creates conditions for top-level attributes' do
|
|
|
|
search = Search.new(Person, :name_eq => 'Ernie')
|
|
|
|
condition = search.base[:name_eq]
|
|
|
|
expect(condition).to be_a Nodes::Condition
|
|
|
|
expect(condition.predicate.name).to eq 'eq'
|
|
|
|
expect(condition.attributes.first.name).to eq 'name'
|
|
|
|
expect(condition.value).to eq 'Ernie'
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'joins' do
|
|
|
|
before { pending 'not implemented for mongoid' }
|
|
|
|
|
|
|
|
it 'creates conditions for association attributes' do
|
|
|
|
search = Search.new(Person, :children_name_eq => 'Ernie')
|
|
|
|
condition = search.base[:children_name_eq]
|
|
|
|
expect(condition).to be_a Nodes::Condition
|
|
|
|
expect(condition.predicate.name).to eq 'eq'
|
|
|
|
expect(condition.attributes.first.name).to eq 'children_name'
|
|
|
|
expect(condition.value).to eq 'Ernie'
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'creates conditions for polymorphic belongs_to association attributes' do
|
|
|
|
search = Search.new(Note, :notable_of_Person_type_name_eq => 'Ernie')
|
|
|
|
condition = search.base[:notable_of_Person_type_name_eq]
|
|
|
|
expect(condition).to be_a Nodes::Condition
|
|
|
|
expect(condition.predicate.name).to eq 'eq'
|
|
|
|
expect(condition.attributes.first.name).to eq 'notable_of_Person_type_name'
|
|
|
|
expect(condition.value).to eq 'Ernie'
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'creates conditions for multiple polymorphic belongs_to association attributes' do
|
|
|
|
search = Search.new(Note,
|
|
|
|
:notable_of_Person_type_name_or_notable_of_Article_type_title_eq => 'Ernie')
|
|
|
|
condition = search.
|
|
|
|
base[:notable_of_Person_type_name_or_notable_of_Article_type_title_eq]
|
|
|
|
expect(condition).to be_a Nodes::Condition
|
|
|
|
expect(condition.predicate.name).to eq 'eq'
|
|
|
|
expect(condition.attributes.first.name).to eq 'notable_of_Person_type_name'
|
|
|
|
expect(condition.attributes.last.name).to eq 'notable_of_Article_type_title'
|
|
|
|
expect(condition.value).to eq 'Ernie'
|
|
|
|
end
|
|
|
|
before { skip }
|
|
|
|
it 'accepts arrays of groupings with joins' do
|
|
|
|
search = Search.new(Person,
|
|
|
|
g: [
|
|
|
|
{ :m => 'or', :name_eq => 'Ernie', :children_name_eq => 'Ernie' },
|
|
|
|
{ :m => 'or', :name_eq => 'Bert', :children_name_eq => 'Bert' },
|
|
|
|
]
|
|
|
|
)
|
|
|
|
ors = search.groupings
|
|
|
|
expect(ors.size).to eq(2)
|
|
|
|
or1, or2 = ors
|
|
|
|
expect(or1).to be_a Nodes::Grouping
|
|
|
|
expect(or1.combinator).to eq 'or'
|
|
|
|
expect(or2).to be_a Nodes::Grouping
|
|
|
|
expect(or2.combinator).to eq 'or'
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'accepts "attributes" hashes for groupings' do
|
|
|
|
search = Search.new(Person,
|
|
|
|
g: {
|
|
|
|
'0' => { m: 'or', name_eq: 'Ernie', children_name_eq: 'Ernie' },
|
|
|
|
'1' => { m: 'or', name_eq: 'Bert', children_name_eq: 'Bert' },
|
|
|
|
}
|
|
|
|
)
|
|
|
|
ors = search.groupings
|
|
|
|
expect(ors.size).to eq(2)
|
|
|
|
or1, or2 = ors
|
|
|
|
expect(or1).to be_a Nodes::Grouping
|
|
|
|
expect(or1.combinator).to eq 'or'
|
|
|
|
expect(or2).to be_a Nodes::Grouping
|
|
|
|
expect(or2.combinator).to eq 'or'
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'accepts "attributes" hashes for conditions' do
|
|
|
|
search = Search.new(Person,
|
|
|
|
:c => {
|
|
|
|
'0' => { :a => ['name'], :p => 'eq', :v => ['Ernie'] },
|
|
|
|
'1' => { :a => ['children_name', 'parent_name'],
|
|
|
|
:p => 'eq', :v => ['Ernie'], :m => 'or' }
|
|
|
|
}
|
|
|
|
)
|
|
|
|
conditions = search.base.conditions
|
|
|
|
expect(conditions.size).to eq(2)
|
|
|
|
expect(conditions.map { |c| c.class })
|
|
|
|
.to eq [Nodes::Condition, Nodes::Condition]
|
|
|
|
end
|
|
|
|
|
|
|
|
before { skip }
|
|
|
|
it 'does not evaluate the query on #inspect' do
|
|
|
|
search = Search.new(Person, :children_id_in => [1, 2, 3])
|
|
|
|
expect(search.inspect).not_to match /ActiveRecord/
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'discards empty conditions' do
|
|
|
|
search = Search.new(Person, :children_name_eq => '')
|
|
|
|
condition = search.base[:children_name_eq]
|
|
|
|
expect(condition).to be_nil
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'accepts arrays of groupings' do
|
|
|
|
search = Search.new(Person,
|
|
|
|
g: [
|
|
|
|
{ :m => 'or', :name_eq => 'Ernie', :email_eq => 'ernie@example.org' },
|
|
|
|
{ :m => 'or', :name_eq => 'Bert', :email_eq => 'bert@example.org' },
|
|
|
|
]
|
|
|
|
)
|
|
|
|
ors = search.groupings
|
|
|
|
expect(ors.size).to eq(2)
|
|
|
|
or1, or2 = ors
|
|
|
|
expect(or1).to be_a Nodes::Grouping
|
|
|
|
expect(or1.combinator).to eq 'or'
|
|
|
|
expect(or2).to be_a Nodes::Grouping
|
|
|
|
expect(or2.combinator).to eq 'or'
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'creates conditions for custom predicates that take arrays' do
|
|
|
|
Ransack.configure do |config|
|
|
|
|
config.add_predicate 'ary_pred', :wants_array => true
|
|
|
|
end
|
|
|
|
|
|
|
|
search = Search.new(Person, :name_ary_pred => ['Ernie', 'Bert'])
|
|
|
|
condition = search.base[:name_ary_pred]
|
|
|
|
expect(condition).to be_a Nodes::Condition
|
|
|
|
expect(condition.predicate.name).to eq 'ary_pred'
|
|
|
|
expect(condition.attributes.first.name).to eq 'name'
|
|
|
|
expect(condition.value).to eq ['Ernie', 'Bert']
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with an invalid condition' do
|
|
|
|
subject { Search.new(Person, :unknown_attr_eq => 'Ernie') }
|
|
|
|
|
|
|
|
context "when ignore_unknown_conditions is false" do
|
|
|
|
before do
|
2014-12-17 13:34:08 -05:00
|
|
|
Ransack.configure { |c| c.ignore_unknown_conditions = false }
|
2014-08-02 13:33:51 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
specify { expect { subject }.to raise_error ArgumentError }
|
|
|
|
end
|
|
|
|
|
|
|
|
context "when ignore_unknown_conditions is true" do
|
|
|
|
before do
|
2014-12-17 13:34:08 -05:00
|
|
|
Ransack.configure { |c| c.ignore_unknown_conditions = true }
|
2014-08-02 13:33:51 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
specify { expect { subject }.not_to raise_error }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '#result' do
|
|
|
|
let(:people_name_field) {
|
|
|
|
"#{quote_table_name("people")}.#{quote_column_name("name")}"
|
|
|
|
}
|
|
|
|
# let(:children_people_name_field) {
|
|
|
|
# "#{quote_table_name("children_people")}.#{quote_column_name("name")}"
|
|
|
|
# }
|
|
|
|
|
|
|
|
it 'evaluates arrays of groupings' do
|
|
|
|
search = Search.new(Person,
|
|
|
|
:g => [
|
|
|
|
{ :m => 'or', :name_eq => 'Ernie', :email_eq => 'ernie@example.org' },
|
|
|
|
{ :m => 'or', :name_eq => 'Bert', :email_eq => 'bert@example.org' }
|
|
|
|
]
|
|
|
|
)
|
|
|
|
expect(search.result).to be_an Mongoid::Criteria
|
|
|
|
selector = search.result.selector
|
|
|
|
expect(selector.keys).to eq ['$and']
|
|
|
|
first, second = selector.values.first
|
|
|
|
expect(first).to eq({ '$or' => [ { 'name' => 'Ernie' }, { 'email' => 'ernie@example.org' } ] })
|
|
|
|
expect(second).to eq({ '$or' => [ { 'name' => 'Bert' }, { 'email' => 'bert@example.org' } ] })
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with joins' do
|
|
|
|
before { pending 'not implemented for mongoid' }
|
|
|
|
|
|
|
|
it 'evaluates conditions contextually' do
|
|
|
|
search = Search.new(Person, :children_name_eq => 'Ernie')
|
|
|
|
expect(search.result).to be_an ActiveRecord::Relation
|
|
|
|
where = search.result.where_values.first
|
|
|
|
expect(where.to_sql).to match /#{children_people_name_field} = 'Ernie'/
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'evaluates compound conditions contextually' do
|
|
|
|
search = Search.new(Person, :children_name_or_name_eq => 'Ernie')
|
|
|
|
expect(search.result).to be_an ActiveRecord::Relation
|
|
|
|
where = search.result.where_values.first
|
|
|
|
expect(where.to_sql).to match /#{children_people_name_field
|
|
|
|
} = 'Ernie' OR #{people_name_field} = 'Ernie'/
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'evaluates polymorphic belongs_to association conditions contextually' do
|
|
|
|
search = Search.new(Note, :notable_of_Person_type_name_eq => 'Ernie')
|
|
|
|
expect(search.result).to be_an ActiveRecord::Relation
|
|
|
|
where = search.result.where_values.first
|
|
|
|
expect(where.to_sql).to match /#{people_name_field} = 'Ernie'/
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'evaluates nested conditions' do
|
|
|
|
search = Search.new(Person, :children_name_eq => 'Ernie',
|
|
|
|
:g => [
|
|
|
|
{ :m => 'or',
|
|
|
|
:name_eq => 'Ernie',
|
|
|
|
:children_children_name_eq => 'Ernie'
|
|
|
|
}
|
|
|
|
]
|
|
|
|
)
|
|
|
|
expect(search.result).to be_an ActiveRecord::Relation
|
|
|
|
where = search.result.where_values.first
|
|
|
|
expect(where.to_sql).to match /#{children_people_name_field} = 'Ernie'/
|
|
|
|
expect(where.to_sql).to match /#{people_name_field} = 'Ernie'/
|
|
|
|
expect(where.to_sql).to match /#{quote_table_name("children_people_2")
|
|
|
|
}.#{quote_column_name("name")} = 'Ernie'/
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'evaluates arrays of groupings' do
|
|
|
|
search = Search.new(Person,
|
|
|
|
:g => [
|
|
|
|
{ :m => 'or', :name_eq => 'Ernie', :children_name_eq => 'Ernie' },
|
|
|
|
{ :m => 'or', :name_eq => 'Bert', :children_name_eq => 'Bert' }
|
|
|
|
]
|
|
|
|
)
|
|
|
|
expect(search.result).to be_an ActiveRecord::Relation
|
|
|
|
where = search.result.where_values.first
|
|
|
|
sql = where.to_sql
|
|
|
|
first, second = sql.split(/ AND /)
|
|
|
|
expect(first).to match /#{people_name_field} = 'Ernie'/
|
|
|
|
expect(first).to match /#{children_people_name_field} = 'Ernie'/
|
|
|
|
expect(second).to match /#{people_name_field} = 'Bert'/
|
|
|
|
expect(second).to match /#{children_people_name_field} = 'Bert'/
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns distinct records when passed :distinct => true' do
|
|
|
|
search = Search.new(
|
|
|
|
Person, :g => [
|
|
|
|
{ :m => 'or',
|
|
|
|
:comments_body_cont => 'e',
|
|
|
|
:articles_comments_body_cont => 'e'
|
|
|
|
}
|
|
|
|
]
|
|
|
|
)
|
|
|
|
if ActiveRecord::VERSION::MAJOR == 3
|
|
|
|
all_or_load, uniq_or_distinct = :all, :uniq
|
|
|
|
else
|
|
|
|
all_or_load, uniq_or_distinct = :load, :distinct
|
|
|
|
end
|
|
|
|
expect(search.result.send(all_or_load).size).
|
|
|
|
to eq(9000)
|
|
|
|
expect(search.result(:distinct => true).size).
|
|
|
|
to eq(10)
|
|
|
|
expect(search.result.send(all_or_load).send(uniq_or_distinct)).
|
|
|
|
to eq search.result(:distinct => true).send(all_or_load)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '#sorts=' do
|
|
|
|
before do
|
|
|
|
@s = Search.new(Person)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'creates sorts based on a single attribute/direction' do
|
|
|
|
@s.sorts = 'id desc'
|
|
|
|
expect(@s.sorts.size).to eq(1)
|
|
|
|
sort = @s.sorts.first
|
|
|
|
expect(sort).to be_a Nodes::Sort
|
|
|
|
expect(sort.name).to eq 'id'
|
|
|
|
expect(sort.dir).to eq 'desc'
|
|
|
|
expect(@s.result.options).to eq({ :sort => { '_id' => -1 } })
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'creates sorts based on a single attribute and uppercase direction' do
|
|
|
|
@s.sorts = 'id DESC'
|
|
|
|
expect(@s.sorts.size).to eq(1)
|
|
|
|
sort = @s.sorts.first
|
|
|
|
expect(sort).to be_a Nodes::Sort
|
|
|
|
expect(sort.name).to eq 'id'
|
|
|
|
expect(sort.dir).to eq 'desc'
|
|
|
|
expect(@s.result.options).to eq({ :sort => { '_id' => -1 } })
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'creates sorts based on a single attribute and without direction' do
|
|
|
|
@s.sorts = 'id'
|
|
|
|
expect(@s.sorts.size).to eq(1)
|
|
|
|
sort = @s.sorts.first
|
|
|
|
expect(sort).to be_a Nodes::Sort
|
|
|
|
expect(sort.name).to eq 'id'
|
|
|
|
expect(sort.dir).to eq 'asc'
|
|
|
|
expect(@s.result.options).to eq({ :sort => { '_id' => 1 } })
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'creates sorts based on multiple attributes/directions in array format' do
|
|
|
|
@s.sorts = ['id desc', { :name => 'name', :dir => 'asc' }]
|
|
|
|
expect(@s.sorts.size).to eq(2)
|
|
|
|
sort1, sort2 = @s.sorts
|
|
|
|
expect(sort1).to be_a Nodes::Sort
|
|
|
|
expect(sort1.name).to eq 'id'
|
|
|
|
expect(sort1.dir).to eq 'desc'
|
|
|
|
expect(sort2).to be_a Nodes::Sort
|
|
|
|
expect(sort2.name).to eq 'name'
|
|
|
|
expect(sort2.dir).to eq 'asc'
|
|
|
|
expect(@s.result.options).to eq({ :sort=>{"_id"=>-1, "name"=>1} })
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'creates sorts based on multiple attributes and uppercase directions in array format' do
|
|
|
|
@s.sorts = ['id DESC', { :name => 'name', :dir => 'ASC' }]
|
|
|
|
expect(@s.sorts.size).to eq(2)
|
|
|
|
sort1, sort2 = @s.sorts
|
|
|
|
expect(sort1).to be_a Nodes::Sort
|
|
|
|
expect(sort1.name).to eq 'id'
|
|
|
|
expect(sort1.dir).to eq 'desc'
|
|
|
|
expect(sort2).to be_a Nodes::Sort
|
|
|
|
expect(sort2.name).to eq 'name'
|
|
|
|
expect(sort2.dir).to eq 'asc'
|
|
|
|
expect(@s.result.options).to eq({ :sort=>{"_id"=>-1, "name"=>1} })
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'creates sorts based on multiple attributes and different directions in array format' do
|
|
|
|
@s.sorts = ['id DESC', { name: 'name', dir: nil }]
|
|
|
|
expect(@s.sorts.size).to eq(2)
|
|
|
|
sort1, sort2 = @s.sorts
|
|
|
|
expect(sort1).to be_a Nodes::Sort
|
|
|
|
expect(sort1.name).to eq 'id'
|
|
|
|
expect(sort1.dir).to eq 'desc'
|
|
|
|
expect(sort2).to be_a Nodes::Sort
|
|
|
|
expect(sort2.name).to eq 'name'
|
|
|
|
expect(sort2.dir).to eq 'asc'
|
|
|
|
expect(@s.result.options).to eq({ :sort=>{"_id"=>-1, "name"=>1} })
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'creates sorts based on multiple attributes/directions in hash format' do
|
|
|
|
@s.sorts = {
|
|
|
|
'0' => { :name => 'id', :dir => 'desc' },
|
|
|
|
'1' => { :name => 'name', :dir => 'asc' }
|
|
|
|
}
|
|
|
|
expect(@s.sorts.size).to eq(2)
|
|
|
|
expect(@s.sorts).to be_all { |s| Nodes::Sort === s }
|
|
|
|
id_sort = @s.sorts.detect { |s| s.name == 'id' }
|
|
|
|
name_sort = @s.sorts.detect { |s| s.name == 'name' }
|
|
|
|
expect(id_sort.dir).to eq 'desc'
|
|
|
|
expect(name_sort.dir).to eq 'asc'
|
|
|
|
expect(@s.result.options).to eq({ :sort=>{"_id"=>-1, "name"=>1} })
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'creates sorts based on multiple attributes and uppercase directions in hash format' do
|
|
|
|
@s.sorts = {
|
|
|
|
'0' => { :name => 'id', :dir => 'DESC' },
|
|
|
|
'1' => { :name => 'name', :dir => 'ASC' }
|
|
|
|
}
|
|
|
|
expect(@s.sorts.size).to eq(2)
|
|
|
|
expect(@s.sorts).to be_all { |s| Nodes::Sort === s }
|
|
|
|
id_sort = @s.sorts.detect { |s| s.name == 'id' }
|
|
|
|
name_sort = @s.sorts.detect { |s| s.name == 'name' }
|
|
|
|
expect(id_sort.dir).to eq 'desc'
|
|
|
|
expect(name_sort.dir).to eq 'asc'
|
|
|
|
expect(@s.result.options).to eq({ :sort=>{"_id"=>-1, "name"=>1} })
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'creates sorts based on multiple attributes and different directions in hash format' do
|
|
|
|
@s.sorts = {
|
|
|
|
'0' => { :name => 'id', :dir => 'DESC' },
|
|
|
|
'1' => { :name => 'name', :dir => nil }
|
|
|
|
}
|
|
|
|
expect(@s.sorts.size).to eq(2)
|
|
|
|
expect(@s.sorts).to be_all { |s| Nodes::Sort === s }
|
|
|
|
id_sort = @s.sorts.detect { |s| s.name == 'id' }
|
|
|
|
name_sort = @s.sorts.detect { |s| s.name == 'name' }
|
|
|
|
expect(id_sort.dir).to eq 'desc'
|
|
|
|
expect(name_sort.dir).to eq 'asc'
|
|
|
|
expect(@s.result.options).to eq({ :sort=>{"_id"=>-1, "name"=>1} })
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'overrides existing sort' do
|
|
|
|
@s.sorts = 'id asc'
|
|
|
|
expect(@s.result.first.id.to_s).to eq Person.min(:id).to_s
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '#method_missing' do
|
|
|
|
before do
|
|
|
|
@s = Search.new(Person)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'raises NoMethodError when sent an invalid attribute' do
|
|
|
|
expect { @s.blah }.to raise_error NoMethodError
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'sets condition attributes when sent valid attributes' do
|
|
|
|
@s.name_eq = 'Ernie'
|
|
|
|
expect(@s.name_eq).to eq 'Ernie'
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with joins' do
|
|
|
|
before { pending 'not implemented for mongoid' }
|
|
|
|
it 'allows chaining to access nested conditions' do
|
|
|
|
@s.groupings = [
|
|
|
|
{ :m => 'or', :name_eq => 'Ernie', :children_name_eq => 'Ernie' }
|
|
|
|
]
|
|
|
|
expect(@s.groupings.first.children_name_eq).to eq 'Ernie'
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|