mundane-search 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Guardfile +11 -0
- data/README.md +61 -9
- data/Rakefile +1 -1
- data/bin/coderay +16 -0
- data/bin/erubis +16 -0
- data/bin/guard +16 -0
- data/bin/pry +16 -0
- data/bin/rackup +16 -0
- data/bin/rake +1 -1
- data/bin/sprockets +16 -0
- data/bin/thor +16 -0
- data/bin/tilt +16 -0
- data/lib/columns_hash.rb +38 -0
- data/lib/mundane-search.rb +14 -6
- data/lib/mundane-search/buildable.rb +25 -0
- data/lib/mundane-search/builder.rb +32 -18
- data/lib/mundane-search/filter_canister.rb +27 -3
- data/lib/mundane-search/filters.rb +7 -8
- data/lib/mundane-search/filters/attribute_match.rb +6 -7
- data/lib/mundane-search/filters/attribute_substring.rb +14 -0
- data/lib/mundane-search/filters/base.rb +5 -2
- data/lib/mundane-search/filters/blank_params_are_nil.rb +1 -1
- data/lib/mundane-search/filters/operator.rb +18 -0
- data/lib/mundane-search/filters/shortcuts.rb +33 -0
- data/lib/mundane-search/filters/typical.rb +28 -3
- data/lib/mundane-search/initial_stack.rb +13 -0
- data/lib/mundane-search/railtie.rb +7 -0
- data/lib/mundane-search/result.rb +23 -8
- data/lib/mundane-search/result_model.rb +65 -0
- data/lib/mundane-search/stack.rb +38 -0
- data/lib/mundane-search/version.rb +1 -1
- data/lib/mundane-search/view_helpers.rb +24 -0
- data/mundane-search.gemspec +7 -0
- data/script/console +6 -0
- data/spec/active_record_setup.rb +2 -45
- data/spec/buildable_integration_spec.rb +14 -0
- data/spec/buildable_spec.rb +19 -0
- data/spec/builder_integration_spec.rb +26 -5
- data/spec/builder_spec.rb +13 -18
- data/spec/columns_hash_spec.rb +37 -0
- data/spec/demo_data.rb +50 -0
- data/spec/filter_canister_spec.rb +46 -0
- data/spec/filters/attribute_match_integration_spec.rb +2 -2
- data/spec/filters/attribute_match_spec.rb +27 -0
- data/spec/filters/attribute_substring_spec.rb +27 -0
- data/spec/filters/base_spec.rb +39 -7
- data/spec/filters/blank_params_are_nil_spec.rb +11 -0
- data/spec/filters/operator_integration_spec.rb +20 -0
- data/spec/filters/operator_spec.rb +28 -0
- data/spec/filters/shortcuts_integration_spec.rb +16 -0
- data/spec/filters/shortcuts_spec.rb +15 -0
- data/spec/filters/typical_spec.rb +68 -0
- data/spec/form_integration_spec.rb +29 -0
- data/spec/initial_stack_spec.rb +13 -0
- data/spec/minitest_helper.rb +93 -4
- data/spec/result_integration_spec.rb +24 -0
- data/spec/result_model_spec.rb +50 -0
- data/spec/result_spec.rb +10 -28
- data/spec/search_form_for_integration_spec.rb +19 -0
- data/spec/simple_form_integration_spec.rb +36 -0
- data/spec/simple_search_form_for_integration_spec.rb +25 -0
- data/spec/stack_spec.rb +40 -0
- metadata +167 -6
- data/lib/mundane-search/filters/helpers.rb +0 -44
- data/lib/mundane-search/initial_result.rb +0 -7
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative 'minitest_helper'
|
2
|
+
|
3
|
+
describe MundaneSearch::Buildable do
|
4
|
+
let(:built) do
|
5
|
+
Class.new do
|
6
|
+
include MundaneSearch::Buildable
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should limit results using exact match filter" do
|
11
|
+
built.use MundaneSearch::Filters::ExactMatch, param_key: "foo"
|
12
|
+
built.call(collection, params).must_equal(['bar'])
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative 'minitest_helper'
|
2
|
+
|
3
|
+
describe MundaneSearch::Buildable do
|
4
|
+
let(:built) do
|
5
|
+
Class.new do
|
6
|
+
include MundaneSearch::Buildable
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should have a search builder object" do
|
11
|
+
built.builder.must_be_kind_of MundaneSearch::Builder
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should add proxy methods" do
|
15
|
+
[:use, :result_for, :call, :employ].each do |method|
|
16
|
+
built.must_respond_to method
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -1,14 +1,35 @@
|
|
1
1
|
require_relative 'minitest_helper'
|
2
2
|
|
3
3
|
describe MundaneSearch::Builder do
|
4
|
-
|
4
|
+
let(:built) { MundaneSearch::Builder.new }
|
5
|
+
|
6
|
+
it "should return unchanged collection on call" do
|
7
|
+
built.call(collection, params).must_equal(collection)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should tollerate nil params (convert to empty hash)" do
|
11
|
+
built.call(collection, nil).must_equal(collection)
|
12
|
+
end
|
5
13
|
|
6
14
|
it "should limit results using exact match filter" do
|
15
|
+
built.use MundaneSearch::Filters::ExactMatch, param_key: "foo"
|
16
|
+
built.call(collection, params).must_equal(['bar'])
|
17
|
+
end
|
7
18
|
|
8
|
-
|
9
|
-
|
10
|
-
|
19
|
+
it "should translate symbols into MundaneSearch::Filter class (if available)" do
|
20
|
+
built.use :exact_match, param_key: "foo"
|
21
|
+
built.call(collection, params).must_equal(['bar'])
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should translate strings into MundaneSearch::Filter class (if available)" do
|
25
|
+
built.use "exact_match", param_key: "foo"
|
26
|
+
built.call(collection, params).must_equal(['bar'])
|
27
|
+
end
|
11
28
|
|
29
|
+
it "should looks for Object:: level filters when translating strings" do
|
30
|
+
Object.const_set :MockFilter, Class.new(MundaneSearch::Filters::ExactMatch)
|
31
|
+
built.use :mock_filter, param_key: "foo"
|
12
32
|
built.call(collection, params).must_equal(['bar'])
|
33
|
+
Object.send(:remove_const, :MockFilter)
|
13
34
|
end
|
14
|
-
end
|
35
|
+
end
|
data/spec/builder_spec.rb
CHANGED
@@ -1,25 +1,20 @@
|
|
1
1
|
require_relative 'minitest_helper'
|
2
2
|
|
3
3
|
describe MundaneSearch::Builder do
|
4
|
-
|
4
|
+
let(:built) { MundaneSearch::Builder.new }
|
5
|
+
let(:null_filter) { Object.new }
|
6
|
+
let(:canister) { Minitest::Mock.new }
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
it "should return unchanged collection on call" do
|
13
|
-
builder.call(collection, params).must_equal(collection)
|
14
|
-
end
|
15
|
-
end
|
8
|
+
# what does a builder actually do?
|
9
|
+
# 1. Store search configuration (use, result_class, filter_canister)
|
10
|
+
# a. Can "use" (et al) be called?
|
11
|
+
# i. use can be verified
|
12
|
+
# 2. Run search (call, result_for)
|
13
|
+
# a. ... by creating a result object
|
16
14
|
|
17
15
|
it "should use middleware" do
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
canister = builder.filters.first
|
22
|
-
canister.must_be_kind_of MundaneSearch::FilterCanister
|
23
|
-
canister.filter.must_equal NothingFilterForTest
|
16
|
+
expectation = ->(filter, *args) { filter.must_equal null_filter }
|
17
|
+
built.define_singleton_method :build_filter_canister, ->{ expectation }
|
18
|
+
built.use(null_filter)
|
24
19
|
end
|
25
|
-
end
|
20
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require_relative 'minitest_helper'
|
2
|
+
|
3
|
+
describe ColumnsHash do
|
4
|
+
[:binary, :boolean, :date, :datetime, :decimal, :float, :integer, :primary_key, :string, :text, :time, :timestamp].each do |type|
|
5
|
+
it "should respond to all ActiveRecord type #{type}" do
|
6
|
+
ColumnsHash.generate({type: type}).must_be_kind_of ColumnsHash::Attribute
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should have 255 limit when string" do
|
11
|
+
ColumnsHash.generate({type: :string}).limit.must_equal 255
|
12
|
+
end
|
13
|
+
|
14
|
+
[:float, :integer].each do |type|
|
15
|
+
it "should be number? true when #{type}" do
|
16
|
+
ColumnsHash.generate({type: type}).number?.must_equal true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "applied to class" do
|
21
|
+
let(:columned_class) do
|
22
|
+
Class.new do
|
23
|
+
include ColumnsHash
|
24
|
+
end
|
25
|
+
end
|
26
|
+
let(:columned) { columned_class.new }
|
27
|
+
|
28
|
+
it "should have a columns_hash" do
|
29
|
+
columned_class.columns_hash.must_be_kind_of Hash
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should store (and retrieve) a column type" do
|
33
|
+
columned_class.attribute_column(:title, :string)
|
34
|
+
columned.column_for_attribute(:title).type.must_equal :string
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/spec/demo_data.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
def demo_data
|
2
|
+
[
|
3
|
+
{
|
4
|
+
title: "A Tale of Two Cities",
|
5
|
+
author: "Charles Dickens",
|
6
|
+
publication_date: Date.parse('31-12-1859'),
|
7
|
+
first_purchased_at: Time.utc(1859,"jan",1,20,15,1),
|
8
|
+
sold: 200_000_000
|
9
|
+
},
|
10
|
+
{
|
11
|
+
title: "The Lord of the Rings",
|
12
|
+
author: "J. R. R. Tolkien",
|
13
|
+
publication_date: Date.parse('31-12-1954'),
|
14
|
+
first_purchased_at: Time.utc(1954,"jan",1,20,15,1),
|
15
|
+
sold: 150_000_000
|
16
|
+
},
|
17
|
+
{
|
18
|
+
title: "The Little Prince (Le Petit Prince)",
|
19
|
+
author: "Antoine de Saint-Exupery",
|
20
|
+
publication_date: Date.parse('31-12-1943'),
|
21
|
+
first_purchased_at: Time.utc(1943,"jan",1,20,15,1),
|
22
|
+
sold: 140_000_000
|
23
|
+
},
|
24
|
+
{
|
25
|
+
title: "The Hobbit",
|
26
|
+
author: "J. R. R. Tolkien",
|
27
|
+
publication_date: Date.parse('31-12-1937'),
|
28
|
+
first_purchased_at: Time.utc(1937,"jan",1,20,15,1),
|
29
|
+
sold: 100_000_000
|
30
|
+
},
|
31
|
+
{
|
32
|
+
title: "Hong lou meng (Dream of the Red Chamber/The Story of the Stone)",
|
33
|
+
author: "Cao Xueqin",
|
34
|
+
publication_date: Date.parse('31-12-1754'),
|
35
|
+
first_purchased_at: Time.utc(1754,"jan",1,20,15,1),
|
36
|
+
sold: 100_000_000
|
37
|
+
},
|
38
|
+
{
|
39
|
+
title: "And Then There Were None",
|
40
|
+
author: "Agatha Christie",
|
41
|
+
publication_date: Date.parse('31-12-1939'),
|
42
|
+
first_purchased_at: Time.utc(1939,"jan",1,20,15,1),
|
43
|
+
sold: 100_000_000
|
44
|
+
}
|
45
|
+
]
|
46
|
+
end
|
47
|
+
|
48
|
+
def open_struct_books
|
49
|
+
demo_data.collect {|d| OpenStruct.new(d) }
|
50
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require_relative 'minitest_helper'
|
2
|
+
|
3
|
+
describe MundaneSearch::FilterCanister do
|
4
|
+
it "should build filters with options" do
|
5
|
+
filter = Minitest::Mock.new
|
6
|
+
options = {param_key: 'baz'}
|
7
|
+
result = Object.new
|
8
|
+
filter.expect(:constants, [])
|
9
|
+
filter.expect(:new, result, [collection, params, options])
|
10
|
+
filter_canister = MundaneSearch::FilterCanister.new(filter, options)
|
11
|
+
filter_canister.build(collection, params).must_equal(result)
|
12
|
+
filter.verify
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should use subconstant based on class of collection" do
|
16
|
+
object_filter = Minitest::Mock.new
|
17
|
+
collection, result, options = Object.new, Object.new, Object.new
|
18
|
+
object_filter.expect(:new, result, [collection, params, options])
|
19
|
+
|
20
|
+
base_filter = Class.new
|
21
|
+
base_filter::Object = object_filter
|
22
|
+
|
23
|
+
filter_canister = MundaneSearch::FilterCanister.new(base_filter, options)
|
24
|
+
filter_canister.build(collection, params).must_equal(result)
|
25
|
+
object_filter.verify
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should report param_key" do
|
29
|
+
param_key = Object.new
|
30
|
+
filter_canister = MundaneSearch::FilterCanister.new(nil, param_key: param_key)
|
31
|
+
filter_canister.param_key.must_equal param_key
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should report param_key_type" do
|
35
|
+
param_key_type = Object.new
|
36
|
+
filter_canister = MundaneSearch::FilterCanister.new(nil, param_key_type: param_key_type)
|
37
|
+
filter_canister.param_key_type.must_equal param_key_type
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should take param_key_type from filter, if not supplied in options" do
|
41
|
+
filter = Minitest::Mock.new
|
42
|
+
filter.expect(:param_key_type, :date)
|
43
|
+
filter_canister = MundaneSearch::FilterCanister.new(filter)
|
44
|
+
filter_canister.param_key_type.must_equal :date
|
45
|
+
end
|
46
|
+
end
|
@@ -10,8 +10,8 @@ describe MundaneSearch::Filters::AttributeMatch do
|
|
10
10
|
let(:all_books) { Book.scoped }
|
11
11
|
let(:a_tale_of_two_cities) { Book.first }
|
12
12
|
|
13
|
-
it "should
|
14
|
-
built = Builder.new do
|
13
|
+
it "should match based on param_key" do
|
14
|
+
built = MundaneSearch::Builder.new do
|
15
15
|
use MundaneSearch::Filters::AttributeMatch, param_key: "author"
|
16
16
|
end
|
17
17
|
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative '../minitest_helper'
|
2
|
+
require_relative '../demo_data'
|
3
|
+
|
4
|
+
describe MundaneSearch::Filters::AttributeMatch do
|
5
|
+
let(:books) { open_struct_books }
|
6
|
+
let(:a_tale) { books.first }
|
7
|
+
it "should match param key" do
|
8
|
+
am = MundaneSearch::Filters::AttributeMatch
|
9
|
+
filter = am.new(books, {'title' => "A Tale of Two Cities"}, {param_key: 'title'})
|
10
|
+
|
11
|
+
filter.filtered_collection.must_equal([a_tale])
|
12
|
+
end
|
13
|
+
|
14
|
+
describe MundaneSearch::Filters::AttributeMatch::ActiveRecord do
|
15
|
+
let(:am) { MundaneSearch::Filters::AttributeMatch::ActiveRecord }
|
16
|
+
it "should filter with 'where'" do
|
17
|
+
collection = Minitest::Mock.new
|
18
|
+
params = { 'title' => "A Tale of Two Cities" }
|
19
|
+
result = Object.new
|
20
|
+
|
21
|
+
filter = am.new(collection, params, param_key: 'title')
|
22
|
+
collection.expect(:where, result, [params])
|
23
|
+
filter.filtered_collection.must_equal(result)
|
24
|
+
collection.verify
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative '../minitest_helper'
|
2
|
+
require_relative '../demo_data'
|
3
|
+
|
4
|
+
describe MundaneSearch::Filters::AttributeSubstring do
|
5
|
+
let(:books) { open_struct_books }
|
6
|
+
let(:a_tale) { books.first }
|
7
|
+
it "should match param key" do
|
8
|
+
am = MundaneSearch::Filters::AttributeSubstring
|
9
|
+
filter = am.new(books, {'title' => "Tale of Two"}, {param_key: 'title'})
|
10
|
+
|
11
|
+
filter.filtered_collection.must_equal([a_tale])
|
12
|
+
end
|
13
|
+
|
14
|
+
describe MundaneSearch::Filters::AttributeSubstring::ActiveRecord do
|
15
|
+
let(:am) { MundaneSearch::Filters::AttributeSubstring::ActiveRecord }
|
16
|
+
it "should filter with 'where'" do
|
17
|
+
collection = Minitest::Mock.new
|
18
|
+
params = { 'title' => "Tale of Two" }
|
19
|
+
result = Object.new
|
20
|
+
|
21
|
+
filter = am.new(collection, params, param_key: 'title')
|
22
|
+
collection.expect(:where, result, [["title LIKE ?", "%Tale of Two%"]])
|
23
|
+
filter.filtered_collection.must_equal(result)
|
24
|
+
collection.verify
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/spec/filters/base_spec.rb
CHANGED
@@ -1,13 +1,45 @@
|
|
1
1
|
require_relative '../minitest_helper'
|
2
2
|
|
3
3
|
describe MundaneSearch::Filters::Base do
|
4
|
-
|
4
|
+
let(:base) { MundaneSearch::Filters::Base.new(collection, params) }
|
5
|
+
let(:standin) { Object.new }
|
5
6
|
|
6
|
-
|
7
|
-
|
7
|
+
describe "#call" do
|
8
|
+
it "should equal [filtered_collection, filtered_params]" do
|
9
|
+
collection, params = Object.new, Object.new
|
10
|
+
base.expects(:filtered_collection).returns(collection)
|
11
|
+
base.expects(:filtered_params).returns(params)
|
12
|
+
base.call.must_equal [collection, params]
|
13
|
+
end
|
8
14
|
|
9
|
-
|
10
|
-
|
11
|
-
|
15
|
+
it "should equal [collection, params] when apply? is false" do
|
16
|
+
collection, params = Object.new, Object.new
|
17
|
+
def base.filtered_params ; end
|
18
|
+
def base.filtered_collection ; end
|
19
|
+
base.expects(:apply?).returns(false)
|
20
|
+
base.expects(:collection).returns(collection)
|
21
|
+
base.expects(:params).returns(params)
|
22
|
+
base.call.must_equal [collection, params]
|
23
|
+
end
|
12
24
|
end
|
13
|
-
|
25
|
+
|
26
|
+
describe "#apply?" do
|
27
|
+
it "should be true" do
|
28
|
+
assert base.apply?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "#filtered_collection" do
|
33
|
+
it "should contain collection" do
|
34
|
+
base.expects(:collection).returns(standin)
|
35
|
+
base.filtered_collection.must_equal standin
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "#filtered_params" do
|
40
|
+
it "should contain params" do
|
41
|
+
base.expects(:params).returns(standin)
|
42
|
+
base.filtered_params.must_equal standin
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require_relative '../minitest_helper'
|
2
|
+
|
3
|
+
describe MundaneSearch::Filters::BlankParamsAreNil do
|
4
|
+
it "should remove empty values from params" do
|
5
|
+
params = { empty_string: '', empty_array: [], empty_hash: {} }
|
6
|
+
bpan = MundaneSearch::Filters::BlankParamsAreNil.new([], params)
|
7
|
+
bpan.filtered_params[:empty_string].must_be_nil
|
8
|
+
bpan.filtered_params[:empty_array].must_be_nil
|
9
|
+
bpan.filtered_params[:empty_hash].must_be_nil
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative '../minitest_helper'
|
2
|
+
require_relative '../active_record_setup'
|
3
|
+
|
4
|
+
describe MundaneSearch::Filters::Operator do
|
5
|
+
before do
|
6
|
+
DatabaseCleaner.clean
|
7
|
+
populate_books!
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:all_books) { Book.scoped }
|
11
|
+
let(:a_tale_of_two_cities) { Book.first }
|
12
|
+
|
13
|
+
it "should match based on param_key" do
|
14
|
+
built = MundaneSearch::Builder.new do
|
15
|
+
use MundaneSearch::Filters::Operator, { param_key: 'sold', operator: :> }
|
16
|
+
end
|
17
|
+
|
18
|
+
built.call(all_books, {"sold" => 199_999_999}).must_equal([a_tale_of_two_cities])
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative '../minitest_helper'
|
2
|
+
require_relative '../demo_data'
|
3
|
+
|
4
|
+
describe MundaneSearch::Filters::Operator do
|
5
|
+
let(:books) { open_struct_books }
|
6
|
+
let(:a_tale) { books.first }
|
7
|
+
|
8
|
+
it "should match param key" do
|
9
|
+
o = MundaneSearch::Filters::Operator
|
10
|
+
filter = o.new(books, {'sold' => 199_999_999}, { param_key: 'sold', operator: :> })
|
11
|
+
|
12
|
+
filter.filtered_collection.must_equal([a_tale])
|
13
|
+
end
|
14
|
+
|
15
|
+
describe MundaneSearch::Filters::Operator::ActiveRecord do
|
16
|
+
let(:o) { MundaneSearch::Filters::Operator::ActiveRecord }
|
17
|
+
it "should filter with 'where'" do
|
18
|
+
collection = Minitest::Mock.new
|
19
|
+
params = { 'sold' => 199_999_999 }
|
20
|
+
result = Object.new
|
21
|
+
|
22
|
+
filter = o.new(collection, params, { param_key: 'sold', operator: :> })
|
23
|
+
collection.expect(:where, result, [["sold > ?", 199_999_999]])
|
24
|
+
filter.filtered_collection.must_equal(result)
|
25
|
+
collection.verify
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative '../minitest_helper'
|
2
|
+
require_relative '../demo_data'
|
3
|
+
|
4
|
+
describe MundaneSearch::Filters::Shortcuts do
|
5
|
+
|
6
|
+
let(:all_books) { open_struct_books }
|
7
|
+
let(:a_tale_of_two_cities) { all_books.first }
|
8
|
+
|
9
|
+
it "should match based on param_key" do
|
10
|
+
built = MundaneSearch::Builder.new do
|
11
|
+
employ :attribute_filter, param_key: 'title'
|
12
|
+
end
|
13
|
+
|
14
|
+
built.call(all_books, {"title" => "A Tale of Two Cities"}).must_equal([a_tale_of_two_cities])
|
15
|
+
end
|
16
|
+
end
|