Catch up with changes in master branch
This commit is contained in:
parent
37dbebc579
commit
0a2e4caaa9
|
@ -9,3 +9,7 @@ env:
|
|||
- RAILS=4-0-stable DB=postgres
|
||||
|
||||
|
||||
before_script:
|
||||
- mysql -e 'create database ransack collate utf8_general_ci;'
|
||||
- mysql -e 'use ransack;show variables like "%character%";show variables like "%collation%";'
|
||||
- psql -c 'create database ransack;' -U postgres
|
||||
|
|
|
@ -8,13 +8,17 @@ When filing an issue on the Ransack project, please provide these details:
|
|||
* The version of Ransack *and* the version of Rails.
|
||||
* Any relevant stack traces ("Full trace" preferred)
|
||||
|
||||
In 99% of cases, this information is enough to determine the cause and solution to the problem that is being described.
|
||||
In 99% of cases, this information is enough to determine the cause and
|
||||
solution to the problem that is being described.
|
||||
|
||||
Any issue that is open for 14 days without actionable information or activity will be marked as "stalled" and then closed. Stalled issues can be re-opened if the information requested is provided.
|
||||
Any issue that is open for 14 days without actionable information or activity
|
||||
will be marked as "stalled" and then closed. Stalled issues can be re-opened
|
||||
if the information requested is provided.
|
||||
|
||||
## Pull requests
|
||||
|
||||
We gladly accept pull requests to fix bugs and, in some circumstances, add new features to Ransack.
|
||||
We gladly accept pull requests to fix bugs and, in some circumstances, add new
|
||||
features to Ransack.
|
||||
|
||||
Here's a quick guide:
|
||||
|
||||
|
@ -24,8 +28,7 @@ Here's a quick guide:
|
|||
to know that you have a clean slate:
|
||||
|
||||
$ bundle install
|
||||
$ bundle exec rake test_app
|
||||
$ bundle exec rake
|
||||
$ bundle exec rake spec
|
||||
|
||||
3. Add a test for your change. Only refactoring and documentation changes
|
||||
require no new tests. If you are adding functionality or fixing a bug, we need
|
||||
|
@ -33,7 +36,9 @@ a test!
|
|||
|
||||
4. Make the test pass.
|
||||
|
||||
5. Push to your fork and submit a pull request. If the changes will apply cleanly to the latest stable branches and master branch, you will only need to submit one pull request.
|
||||
5. Push to your fork and submit a pull request. If the changes will apply
|
||||
cleanly to the latest stable branches and master branch, you will only need
|
||||
to submit one pull request.
|
||||
|
||||
At this point you're waiting on us. We like to at least comment on, if not
|
||||
accept, pull requests within three business days (and, typically, one business
|
||||
|
|
5
Gemfile
5
Gemfile
|
@ -1,4 +1,4 @@
|
|||
source "https://rubygems.org"
|
||||
source 'https://rubygems.org'
|
||||
gemspec
|
||||
|
||||
gem 'rake'
|
||||
|
@ -27,4 +27,7 @@ else
|
|||
gem 'activerecord'
|
||||
gem 'actionpack'
|
||||
end
|
||||
if rails == '3-0-stable'
|
||||
gem 'mysql2', '< 0.3'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Ransack
|
||||
|
||||
[![Build Status](https://travis-ci.org/activerecord-hackery/ransack)](https://travis-ci.org/ernie/ransack)
|
||||
[![Build Status](https://travis-ci.org/activerecord-hackery/ransack.png)](https://travis-ci.org/activerecord-hackery/ransack)
|
||||
|
||||
Ransack is a rewrite of [MetaSearch](https://github.com/ernie/meta_search). While it
|
||||
supports many of the same features as MetaSearch, its underlying implementation differs
|
||||
|
@ -23,13 +23,13 @@ gem "ransack" # Last officially released gem (Rails 3 and 4)
|
|||
Or if you want to use the bleeding edge (Rails 3 and 4):
|
||||
|
||||
```ruby
|
||||
gem "ransack", github: "ernie/ransack" # Track git repo
|
||||
gem "ransack", github: "activerecord-hackery/ransack" # Track git repo
|
||||
```
|
||||
|
||||
A dedicated, slimmer version of Ransack for Rails 4 only is here on the "rails-4" branch:
|
||||
A lighter, faster, latest-commits version of Ransack dedicated to Rails 4 is available on the "rails-4" branch:
|
||||
|
||||
```ruby
|
||||
gem "ransack", github: "ernie/ransack", branch: "rails-4"
|
||||
gem "ransack", github: "activerecord-hackery/ransack", branch: "rails-4"
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
|
|
@ -25,6 +25,11 @@ module Ransack
|
|||
column_names + _ransackers.keys
|
||||
end
|
||||
|
||||
def ransortable_attributes(auth_object = nil)
|
||||
# Here so users can overwrite the attributes that show up in the sort_select
|
||||
ransackable_attributes(auth_object)
|
||||
end
|
||||
|
||||
def ransackable_associations(auth_object = nil)
|
||||
reflect_on_all_associations.map { |a| a.name.to_s }
|
||||
end
|
||||
|
|
|
@ -87,7 +87,14 @@ module Ransack
|
|||
module_function
|
||||
# replace % \ to \% \\
|
||||
def escape_wildcards(unescaped)
|
||||
unescaped.to_s.gsub(/\\/){ "\\\\" }.gsub(/%/, "\\%")
|
||||
case ActiveRecord::Base.connection.adapter_name
|
||||
when "SQLite"
|
||||
unescaped
|
||||
else
|
||||
# Necessary for PostgreSQL and MySQL
|
||||
unescaped.to_s.gsub(/([\\|\%|.])/, '\\\\\\1')
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
|
@ -120,7 +120,8 @@ module Ransack
|
|||
end
|
||||
|
||||
def ransackable_attribute?(str, klass)
|
||||
klass.ransackable_attributes(auth_object).include? str
|
||||
klass.ransackable_attributes(auth_object).include?(str) ||
|
||||
klass.ransortable_attributes(auth_object).include?(str)
|
||||
end
|
||||
|
||||
def ransackable_association?(str, klass)
|
||||
|
|
|
@ -19,7 +19,9 @@ module Ransack
|
|||
end
|
||||
|
||||
def valid?
|
||||
bound? && attr
|
||||
bound? && attr &&
|
||||
context.klassify(parent).ransackable_attributes(context.auth_object)
|
||||
.include?(attr_name)
|
||||
end
|
||||
|
||||
def type
|
||||
|
|
|
@ -24,7 +24,9 @@ module Ransack
|
|||
end
|
||||
|
||||
def valid?
|
||||
bound? && attr
|
||||
bound? && attr &&
|
||||
context.klassify(parent).ransortable_attributes(context.auth_object)
|
||||
.include?(attr_name)
|
||||
end
|
||||
|
||||
def name=(name)
|
||||
|
|
|
@ -25,7 +25,7 @@ module Ransack
|
|||
end
|
||||
|
||||
def detect_from_string(str)
|
||||
names_by_decreasing_length.detect {|p| str.end_with?("_#{p}")}
|
||||
names_by_decreasing_length.detect { |p| str.end_with?("_#{p}") }
|
||||
end
|
||||
|
||||
def name_from_attribute_name(attribute_name)
|
||||
|
|
|
@ -2,4 +2,7 @@ Person.blueprint do
|
|||
name
|
||||
email { "test@example.com" }
|
||||
salary
|
||||
only_sort
|
||||
only_search
|
||||
only_admin
|
||||
end
|
|
@ -1,2 +1,9 @@
|
|||
module RansackHelper
|
||||
def quote_table_name(table)
|
||||
ActiveRecord::Base.connection.quote_table_name(table)
|
||||
end
|
||||
|
||||
def quote_column_name(column)
|
||||
ActiveRecord::Base.connection.quote_column_name(column)
|
||||
end
|
||||
end
|
|
@ -20,6 +20,15 @@ module Ransack
|
|||
end
|
||||
|
||||
describe '#ransacker' do
|
||||
# For infix tests
|
||||
def self.sane_adapter?
|
||||
case ::ActiveRecord::Base.connection.adapter_name
|
||||
when "SQLite3", "PostgreSQL"
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
# in schema.rb, class Person:
|
||||
# ransacker :reversed_name, formatter: proc { |v| v.reverse } do |parent|
|
||||
# parent.table[:name]
|
||||
|
@ -36,18 +45,20 @@ module Ransack
|
|||
|
||||
it 'can be accessed through associations' do
|
||||
s = Person.search(children_reversed_name_eq: 'htimS cirA')
|
||||
s.result.to_sql.should match /"children_people"."name" = 'Aric Smith'/
|
||||
s.result.to_sql.should match(
|
||||
/#{quote_table_name("children_people")}.#{quote_column_name("name")} = 'Aric Smith'/
|
||||
)
|
||||
end
|
||||
|
||||
it 'allows an "attribute" to be an InfixOperation' do
|
||||
s = Person.search(doubled_name_eq: 'Aric SmithAric Smith')
|
||||
s.result.first.should eq Person.find_by(name: 'Aric Smith')
|
||||
end if defined?(Arel::Nodes::InfixOperation)
|
||||
end if defined?(Arel::Nodes::InfixOperation) && sane_adapter?
|
||||
|
||||
it "doesn't break #count if using InfixOperations" do
|
||||
s = Person.search(doubled_name_eq: 'Aric SmithAric Smith')
|
||||
s.result.count.should eq 1
|
||||
end if defined?(Arel::Nodes::InfixOperation)
|
||||
end if defined?(Arel::Nodes::InfixOperation) && sane_adapter?
|
||||
|
||||
it "should remove empty key value pairs from the params hash" do
|
||||
s = Person.search(children_reversed_name_eq: '')
|
||||
|
@ -63,14 +74,138 @@ module Ransack
|
|||
s = Person.search(nil)
|
||||
end
|
||||
|
||||
it "should function correctly when using fields with dots in them" do
|
||||
s = Person.search(email_cont: "example.com")
|
||||
s.result.exists?.should be_true
|
||||
end
|
||||
|
||||
it "should function correctly when using fields with % in them" do
|
||||
Person.create!(name: "110%-er")
|
||||
s = Person.search(name_cont: "10%")
|
||||
s.result.exists?.should be_true
|
||||
end
|
||||
|
||||
it "should function correctly when using fields with backslashes in them" do
|
||||
Person.create!(name: "\\WINNER\\")
|
||||
s = Person.search(name_cont: "\\WINNER\\")
|
||||
s.result.exists?.should be_true
|
||||
end
|
||||
|
||||
it 'allows sort by "only_sort" field' do
|
||||
s = Person.search(
|
||||
"s" => { "0" => { "dir" => "asc", "name" => "only_sort" } }
|
||||
)
|
||||
s.result.to_sql.should match(
|
||||
/ORDER BY #{quote_table_name("people")}.#{quote_column_name("only_sort")} ASC/
|
||||
)
|
||||
end
|
||||
|
||||
it "doesn't sort by 'only_search' field" do
|
||||
s = Person.search(
|
||||
"s" => { "0" => { "dir" => "asc", "name" => "only_search" } }
|
||||
)
|
||||
s.result.to_sql.should_not match(
|
||||
/ORDER BY #{quote_table_name("people")}.#{quote_column_name("only_search")} ASC/
|
||||
)
|
||||
end
|
||||
|
||||
it 'allows search by "only_search" field' do
|
||||
s = Person.search(only_search_eq: 'htimS cirA')
|
||||
s.result.to_sql.should match(
|
||||
/WHERE #{quote_table_name("people")}.#{quote_column_name("only_search")} = 'htimS cirA'/
|
||||
)
|
||||
end
|
||||
|
||||
it "can't be searched by 'only_sort'" do
|
||||
s = Person.search(only_sort_eq: 'htimS cirA')
|
||||
s.result.to_sql.should_not match(
|
||||
/WHERE #{quote_table_name("people")}.#{quote_column_name("only_sort")} = 'htimS cirA'/
|
||||
)
|
||||
end
|
||||
|
||||
it 'allows sort by "only_admin" field, if auth_object: :admin' do
|
||||
s = Person.search(
|
||||
{ "s" => { "0" => { "dir" => "asc", "name" => "only_admin" } } },
|
||||
{ auth_object: :admin }
|
||||
)
|
||||
s.result.to_sql.should match(
|
||||
/ORDER BY #{quote_table_name("people")}.#{quote_column_name("only_admin")} ASC/
|
||||
)
|
||||
end
|
||||
|
||||
it "doesn't sort by 'only_admin' field, if auth_object: nil" do
|
||||
s = Person.search(
|
||||
"s" => { "0" => { "dir" => "asc", "name" => "only_admin" } }
|
||||
)
|
||||
s.result.to_sql.should_not match(
|
||||
/ORDER BY #{quote_table_name("people")}.#{quote_column_name("only_admin")} ASC/
|
||||
)
|
||||
end
|
||||
|
||||
it 'allows search by "only_admin" field, if auth_object: :admin' do
|
||||
s = Person.search(
|
||||
{ only_admin_eq: 'htimS cirA' },
|
||||
{ auth_object: :admin }
|
||||
)
|
||||
s.result.to_sql.should match(
|
||||
/WHERE #{quote_table_name("people")}.#{quote_column_name("only_admin")} = 'htimS cirA'/
|
||||
)
|
||||
end
|
||||
|
||||
it "can't be searched by 'only_admin', if auth_object: nil" do
|
||||
s = Person.search(only_admin_eq: 'htimS cirA')
|
||||
s.result.to_sql.should_not match(
|
||||
/WHERE #{quote_table_name("people")}.#{quote_column_name("only_admin")} = 'htimS cirA'/
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ransackable_attributes' do
|
||||
subject { Person.ransackable_attributes }
|
||||
context 'when auth_object is nil' do
|
||||
subject { Person.ransackable_attributes }
|
||||
|
||||
it { should include 'name' }
|
||||
it { should include 'reversed_name' }
|
||||
it { should include 'doubled_name' }
|
||||
it { should include 'name' }
|
||||
it { should include 'reversed_name' }
|
||||
it { should include 'doubled_name' }
|
||||
it { should include 'only_search' }
|
||||
it { should_not include 'only_sort' }
|
||||
it { should_not include 'only_admin' }
|
||||
end
|
||||
|
||||
context 'with auth_object :admin' do
|
||||
subject { Person.ransackable_attributes(:admin) }
|
||||
|
||||
it { should include 'name' }
|
||||
it { should include 'reversed_name' }
|
||||
it { should include 'doubled_name' }
|
||||
it { should include 'only_search' }
|
||||
it { should_not include 'only_sort' }
|
||||
it { should include 'only_admin' }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ransortable_attributes' do
|
||||
context 'when auth_object is nil' do
|
||||
subject { Person.ransortable_attributes }
|
||||
|
||||
it { should include 'name' }
|
||||
it { should include 'reversed_name' }
|
||||
it { should include 'doubled_name' }
|
||||
it { should include 'only_sort' }
|
||||
it { should_not include 'only_search' }
|
||||
it { should_not include 'only_admin' }
|
||||
end
|
||||
|
||||
context 'with auth_object :admin' do
|
||||
subject { Person.ransortable_attributes(:admin) }
|
||||
|
||||
it { should include 'name' }
|
||||
it { should include 'reversed_name' }
|
||||
it { should include 'doubled_name' }
|
||||
it { should include 'only_sort' }
|
||||
it { should_not include 'only_search' }
|
||||
it { should include 'only_admin' }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ransackable_associations' do
|
||||
|
|
|
@ -12,7 +12,7 @@ module Ransack
|
|||
subject.parent_id_cont = 1
|
||||
expect { subject.result }.to_not raise_error
|
||||
end
|
||||
it "escapes % and \\ in value" do
|
||||
it "escapes '%', '.' and '\\\\' in value" do
|
||||
subject.send(:"#{method}=", '%._\\')
|
||||
subject.result.to_sql.should match(regexp)
|
||||
end
|
||||
|
@ -36,7 +36,15 @@ module Ransack
|
|||
end
|
||||
|
||||
describe 'cont' do
|
||||
it_has_behavior 'wildcard escaping', :name_cont, /"people"."name" I?LIKE '%\\%._\\\\%'/ do
|
||||
|
||||
it_has_behavior 'wildcard escaping', :name_cont,
|
||||
(
|
||||
if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
|
||||
/"people"."name" ILIKE '%\\%\\._\\\\%'/
|
||||
else
|
||||
/"people"."name" LIKE '%%._\\%'/
|
||||
end
|
||||
) do
|
||||
subject { @s }
|
||||
end
|
||||
|
||||
|
@ -47,7 +55,14 @@ module Ransack
|
|||
end
|
||||
|
||||
describe 'not_cont' do
|
||||
it_has_behavior 'wildcard escaping', :name_not_cont, /"people"."name" NOT I?LIKE '%\\%._\\\\%'/ do
|
||||
it_has_behavior 'wildcard escaping', :name_not_cont,
|
||||
(
|
||||
if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
|
||||
/"people"."name" NOT ILIKE '%\\%\\._\\\\%'/
|
||||
else
|
||||
/"people"."name" NOT LIKE '%%._\\%'/
|
||||
end
|
||||
) do
|
||||
subject { @s }
|
||||
end
|
||||
|
||||
|
|
|
@ -135,11 +135,9 @@ module Ransack
|
|||
|
||||
it 'evaluates nested conditions' do
|
||||
search = Search.new(Person, children_name_eq: 'Ernie',
|
||||
g: [{
|
||||
m: 'or',
|
||||
name_eq: 'Ernie',
|
||||
children_children_name_eq: 'Ernie'
|
||||
}]
|
||||
g: [
|
||||
{ m: 'or', name_eq: 'Ernie', children_children_name_eq: 'Ernie' }
|
||||
]
|
||||
)
|
||||
search.result.should be_an ActiveRecord::Relation
|
||||
where = search.result.where_values.first
|
||||
|
@ -245,27 +243,21 @@ module Ransack
|
|||
|
||||
it 'creates sorts based on multiple attributes/directions in hash format' do
|
||||
@s.sorts = {
|
||||
'0' => {
|
||||
name: 'id',
|
||||
dir: 'desc'
|
||||
},
|
||||
'1' => {
|
||||
name: 'name',
|
||||
dir: 'asc'
|
||||
}
|
||||
'0' => { name: 'id', dir: 'desc' },
|
||||
'1' => { name: 'name', dir: 'asc' }
|
||||
}
|
||||
@s.sorts.should have(2).items
|
||||
@s.sorts.should be_all {|s| Nodes::Sort === s}
|
||||
id_sort = @s.sorts.detect {|s| s.name == 'id'}
|
||||
name_sort = @s.sorts.detect {|s| s.name == 'name'}
|
||||
@s.sorts.should be_all { |s| Nodes::Sort === s }
|
||||
id_sort = @s.sorts.detect { |s| s.name == 'id' }
|
||||
name_sort = @s.sorts.detect { |s| s.name == 'name' }
|
||||
id_sort.dir.should eq 'desc'
|
||||
name_sort.dir.should eq 'asc'
|
||||
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' }
|
||||
'0' => { name: 'id', dir: 'DESC' },
|
||||
'1' => { name: 'name', dir: 'ASC' }
|
||||
}
|
||||
@s.sorts.should have(2).items
|
||||
@s.sorts.should be_all { |s| Nodes::Sort === s }
|
||||
|
@ -277,8 +269,8 @@ module Ransack
|
|||
|
||||
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 }
|
||||
'0' => { name: 'id', dir: 'DESC' },
|
||||
'1' => { name: 'name', dir: nil }
|
||||
}
|
||||
@s.sorts.should have(2).items
|
||||
@s.sorts.should be_all { |s| Nodes::Sort === s }
|
||||
|
|
|
@ -6,18 +6,22 @@ require 'ransack'
|
|||
Time.zone = 'Eastern Time (US & Canada)'
|
||||
I18n.load_path += Dir[File.join(File.dirname(__FILE__), 'support', '*.yml')]
|
||||
|
||||
Dir[File.expand_path('../{helpers,support,blueprints}/*.rb', __FILE__)].each do |f|
|
||||
Dir[File.expand_path('../{helpers,support,blueprints}/*.rb', __FILE__)]
|
||||
.each do |f|
|
||||
require f
|
||||
end
|
||||
|
||||
Sham.define do
|
||||
name { Faker::Name.name }
|
||||
title { Faker::Lorem.sentence }
|
||||
body { Faker::Lorem.paragraph }
|
||||
salary {|index| 30000 + (index * 1000)}
|
||||
tag_name { Faker::Lorem.words(3).join(' ') }
|
||||
note { Faker::Lorem.words(7).join(' ') }
|
||||
notable_id { |id| id }
|
||||
name { Faker::Name.name }
|
||||
title { Faker::Lorem.sentence }
|
||||
body { Faker::Lorem.paragraph }
|
||||
salary { |index| 30000 + (index * 1000) }
|
||||
tag_name { Faker::Lorem.words(3).join(' ') }
|
||||
note { Faker::Lorem.words(7).join(' ') }
|
||||
only_admin { Faker::Lorem.words(3).join(' ') }
|
||||
only_search { Faker::Lorem.words(3).join(' ') }
|
||||
only_sort { Faker::Lorem.words(3).join(' ') }
|
||||
notable_id { |id| id }
|
||||
end
|
||||
|
||||
RSpec.configure do |config|
|
||||
|
@ -26,7 +30,8 @@ RSpec.configure do |config|
|
|||
config.before(:suite) do
|
||||
puts '=' * 80
|
||||
connection_name = ActiveRecord::Base.connection.adapter_name
|
||||
puts "Running specs against #{connection_name}, ActiveRecord #{ActiveRecord::VERSION::STRING} and ARel #{Arel::VERSION}..."
|
||||
puts "Running specs against #{connection_name}, ActiveRecord #{
|
||||
ActiveRecord::VERSION::STRING} and ARel #{Arel::VERSION}..."
|
||||
puts '=' * 80
|
||||
Schema.create
|
||||
end
|
||||
|
|
|
@ -39,7 +39,25 @@ class Person < ActiveRecord::Base
|
|||
end
|
||||
|
||||
ransacker :doubled_name do |parent|
|
||||
Arel::Nodes::InfixOperation.new('||', parent.table[:name], parent.table[:name])
|
||||
Arel::Nodes::InfixOperation.new(
|
||||
'||', parent.table[:name], parent.table[:name]
|
||||
)
|
||||
end
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
if auth_object == :admin
|
||||
column_names + _ransackers.keys - ['only_sort']
|
||||
else
|
||||
column_names + _ransackers.keys - ['only_sort', 'only_admin']
|
||||
end
|
||||
end
|
||||
|
||||
def self.ransortable_attributes(auth_object = nil)
|
||||
if auth_object == :admin
|
||||
column_names + _ransackers.keys - ['only_search']
|
||||
else
|
||||
column_names + _ransackers.keys - ['only_search', 'only_admin']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -72,6 +90,9 @@ module Schema
|
|||
t.integer :parent_id
|
||||
t.string :name
|
||||
t.string :email
|
||||
t.string :only_search
|
||||
t.string :only_sort
|
||||
t.string :only_admin
|
||||
t.integer :salary
|
||||
t.boolean :awesome, default: false
|
||||
t.timestamps
|
||||
|
@ -120,7 +141,8 @@ module Schema
|
|||
end
|
||||
end
|
||||
|
||||
Comment.make(body: 'First post!',
|
||||
article: Article.make(title: 'Hello, world!'))
|
||||
Comment.make(
|
||||
body: 'First post!', article: Article.make(title: 'Hello, world!')
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue