listings 0.0.3 → 0.1.0
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.
- data/app/assets/javascripts/listings.js +1 -1
- data/app/views/listings/_filters.html.haml +5 -5
- data/app/views/listings/_table_content.html.haml +1 -1
- data/lib/listings.rb +1 -0
- data/lib/listings/base.rb +34 -35
- data/lib/listings/base_field_descriptor.rb +21 -0
- data/lib/listings/base_field_view.rb +39 -0
- data/lib/listings/column_descriptor.rb +7 -47
- data/lib/listings/column_view.rb +18 -27
- data/lib/listings/configuration_methods.rb +27 -10
- data/lib/listings/filter_descriptor.rb +7 -0
- data/lib/listings/filter_view.rb +19 -0
- data/lib/listings/sources.rb +7 -0
- data/lib/listings/sources/active_record_data_source.rb +159 -0
- data/lib/listings/sources/data_source.rb +76 -0
- data/lib/listings/sources/object_data_source.rb +91 -0
- data/lib/listings/version.rb +1 -1
- data/lib/rspec/listings_helpers.rb +27 -5
- data/spec/dummy/app/controllers/welcome_controller.rb +3 -0
- data/spec/dummy/app/listings/array_listing.rb +1 -0
- data/spec/dummy/app/listings/tracks_fixed_order_listing.rb +13 -0
- data/spec/dummy/app/listings/tracks_listing.rb +18 -0
- data/spec/dummy/app/models/album.rb +4 -0
- data/spec/dummy/app/models/object_album.rb +8 -0
- data/spec/dummy/app/models/object_track.rb +8 -0
- data/spec/dummy/app/models/track.rb +7 -0
- data/spec/dummy/app/views/welcome/index.html.haml +7 -1
- data/spec/dummy/app/views/welcome/tracks.html.haml +3 -0
- data/spec/dummy/config/routes.rb +1 -0
- data/spec/dummy/db/migrate/20150611185824_create_albums.rb +9 -0
- data/spec/dummy/db/migrate/20150611185922_create_tracks.rb +12 -0
- data/spec/dummy/db/schema.rb +17 -1
- data/spec/dummy/db/seeds.rb +6 -0
- data/spec/factories/albums.rb +25 -0
- data/spec/factories/post.rb +1 -0
- data/spec/factories/tracks.rb +14 -0
- data/spec/factories/traits.rb +9 -0
- data/spec/lib/filter_parser_spec.rb +5 -0
- data/spec/lib/sources/active_record_data_source_spec.rb +359 -0
- data/spec/lib/sources/object_data_source_spec.rb +231 -0
- data/spec/listings/tracks_fixed_order_listing_spec.rb +19 -0
- data/spec/listings/tracks_listing_spec.rb +27 -0
- data/spec/models/album_spec.rb +9 -0
- data/spec/models/post_spec.rb +2 -2
- data/spec/models/track_spec.rb +8 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/support/query_counter.rb +29 -0
- metadata +49 -3
@@ -0,0 +1,18 @@
|
|
1
|
+
class TracksListing < Listings::Base
|
2
|
+
|
3
|
+
model Track
|
4
|
+
|
5
|
+
filter album: :name
|
6
|
+
filter album: :id, title: 'The Album Id' do |value|
|
7
|
+
"#{value}!"
|
8
|
+
end
|
9
|
+
|
10
|
+
column :order
|
11
|
+
column :title, searchable: true
|
12
|
+
column album: :name, searchable: true do |track, album_name|
|
13
|
+
"#{album_name} (Buy!)"
|
14
|
+
end
|
15
|
+
|
16
|
+
column album: :id, title: 'The Album Id'
|
17
|
+
|
18
|
+
end
|
@@ -1,5 +1,11 @@
|
|
1
1
|
%h1 Welcome#index
|
2
2
|
|
3
|
+
%dl
|
4
|
+
%dt
|
5
|
+
= link_to 'Tracks', tracks_path
|
6
|
+
%dd
|
7
|
+
ActiveRecord based listing with association handling
|
8
|
+
|
3
9
|
%dl
|
4
10
|
%dt
|
5
11
|
= link_to 'Posts', posts_path
|
@@ -20,6 +26,6 @@
|
|
20
26
|
|
21
27
|
%dl
|
22
28
|
%dt
|
23
|
-
= link_to 'Array',
|
29
|
+
= link_to 'Array', array_path
|
24
30
|
%dd
|
25
31
|
Array based listing
|
data/spec/dummy/config/routes.rb
CHANGED
data/spec/dummy/db/schema.rb
CHANGED
@@ -11,7 +11,13 @@
|
|
11
11
|
#
|
12
12
|
# It's strongly recommended to check this file into your version control system.
|
13
13
|
|
14
|
-
ActiveRecord::Schema.define(:version =>
|
14
|
+
ActiveRecord::Schema.define(:version => 20150611185922) do
|
15
|
+
|
16
|
+
create_table "albums", :force => true do |t|
|
17
|
+
t.string "name"
|
18
|
+
t.datetime "created_at", :null => false
|
19
|
+
t.datetime "updated_at", :null => false
|
20
|
+
end
|
15
21
|
|
16
22
|
create_table "posts", :force => true do |t|
|
17
23
|
t.string "title"
|
@@ -21,4 +27,14 @@ ActiveRecord::Schema.define(:version => 20140728151246) do
|
|
21
27
|
t.string "category"
|
22
28
|
end
|
23
29
|
|
30
|
+
create_table "tracks", :force => true do |t|
|
31
|
+
t.string "title"
|
32
|
+
t.integer "order"
|
33
|
+
t.integer "album_id"
|
34
|
+
t.datetime "created_at", :null => false
|
35
|
+
t.datetime "updated_at", :null => false
|
36
|
+
end
|
37
|
+
|
38
|
+
add_index "tracks", ["album_id"], :name => "index_tracks_on_album_id"
|
39
|
+
|
24
40
|
end
|
data/spec/dummy/db/seeds.rb
CHANGED
@@ -1,5 +1,11 @@
|
|
1
|
+
require 'factory_girl'
|
2
|
+
# Dir[Rails.root.join("spec/factories/*.rb")].each {|f| require f}
|
1
3
|
|
2
4
|
(1..100).each do |sn|
|
3
5
|
Post.create! title: "post n-#{sn}", author: "john-#{(sn % 4) + 1}", category: "category-#{(sn % 3) + 1}"
|
4
6
|
end
|
5
7
|
|
8
|
+
|
9
|
+
(1..10).each do |sn|
|
10
|
+
FactoryGirl.create :album
|
11
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
factory :album do
|
3
|
+
name
|
4
|
+
|
5
|
+
transient do
|
6
|
+
tracks_count 5
|
7
|
+
end
|
8
|
+
|
9
|
+
after(:create) do |album, evaluator|
|
10
|
+
create_list(:track, evaluator.tracks_count, album: album)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
factory :object_album do
|
15
|
+
name
|
16
|
+
|
17
|
+
transient do
|
18
|
+
tracks_count 5
|
19
|
+
end
|
20
|
+
|
21
|
+
after(:build) do |album, evaluator|
|
22
|
+
album.tracks = build_list(:object_track, evaluator.tracks_count, album: album)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/spec/factories/post.rb
CHANGED
@@ -5,6 +5,8 @@ describe Listings do
|
|
5
5
|
assert_parse_filter "author:me", {author: "me"}, ""
|
6
6
|
assert_parse_filter "Author:me", {author: "me"}, ""
|
7
7
|
|
8
|
+
assert_parse_filter "101", {}, "101"
|
9
|
+
|
8
10
|
assert_parse_filter "author:me category:foo", {author: "me", category: "foo"}, ""
|
9
11
|
assert_parse_filter " author: me category: foo", {author: "me", category: "foo"}, ""
|
10
12
|
assert_parse_filter " author: 'me' category: foo", {author: "me", category: "foo"}, ""
|
@@ -21,6 +23,9 @@ describe Listings do
|
|
21
23
|
assert_parse_filter "author:'John Doe:s'", {author: "John Doe:s"}, ""
|
22
24
|
|
23
25
|
assert_parse_filter "bar author:'me:s' baz category:\"foo foo\"", {author: "me:s", category: "foo foo"}, "bar baz"
|
26
|
+
|
27
|
+
assert_parse_filter "album_name:me", {album_name: "me"}, ""
|
28
|
+
assert_parse_filter "album_name:'me 2' ", {album_name: "me 2"}, ""
|
24
29
|
end
|
25
30
|
|
26
31
|
def assert_parse_filter(text, hash, left_text)
|
@@ -0,0 +1,359 @@
|
|
1
|
+
include Listings::Sources
|
2
|
+
|
3
|
+
RSpec.describe ActiveRecordDataSource do
|
4
|
+
|
5
|
+
def show_query(ds)
|
6
|
+
# with_active_record_logging do
|
7
|
+
ds.items.to_a
|
8
|
+
# end
|
9
|
+
end
|
10
|
+
|
11
|
+
def with_active_record_logging
|
12
|
+
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
13
|
+
puts
|
14
|
+
yield
|
15
|
+
ActiveRecord::Base.logger = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
context "simple active record model" do
|
19
|
+
let(:total_count) { 40 }
|
20
|
+
|
21
|
+
let!(:posts) { create_list(:post, total_count) }
|
22
|
+
let(:title) { ds.build_field :title }
|
23
|
+
let(:author) { ds.build_field :author }
|
24
|
+
|
25
|
+
shared_examples "project all authors" do
|
26
|
+
it "should matching values" do
|
27
|
+
expect(ds.values_for_filter(author)).to eq(['author1', 'author2', 'author3'])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
shared_examples "activerecord datasource with all item" do
|
32
|
+
describe "DataSource factory" do
|
33
|
+
it "should create from with class name" do
|
34
|
+
expect(ds).to be_a ActiveRecordDataSource
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "items" do
|
39
|
+
it "should return all items" do
|
40
|
+
expect(ds.items.count).to be(total_count)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should enumerate all items" do
|
44
|
+
expect(begin
|
45
|
+
c = 0
|
46
|
+
ds.items.each do
|
47
|
+
c = c + 1
|
48
|
+
end
|
49
|
+
c
|
50
|
+
end).to be(total_count)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "paginate" do
|
55
|
+
let(:page_size) { 5 }
|
56
|
+
|
57
|
+
before(:each) { ds.paginate(2, page_size) }
|
58
|
+
|
59
|
+
it "should get only paged items" do
|
60
|
+
expect(ds.items.count).to be(page_size)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should keep total_count" do
|
64
|
+
expect(ds.items.total_count).to be(total_count)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "field" do
|
69
|
+
it "should project attribute value" do
|
70
|
+
expect(title.value_for(ds.items.first)).to eq(posts.first.title)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should have key" do
|
74
|
+
expect(title.key).to eq('title')
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "scope" do
|
79
|
+
before(:each) do
|
80
|
+
ds.scope do |items|
|
81
|
+
items.even
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should return scoped items" do
|
86
|
+
expect(ds.items.count).to be(total_count / 2)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe "search" do
|
91
|
+
before(:each) do
|
92
|
+
create_list(:post, 10, title: 'title-magic-string')
|
93
|
+
create_list(:post, 10, author: 'author-magic-string')
|
94
|
+
|
95
|
+
ds.search([title, author], 'magic')
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should return matching items" do
|
99
|
+
expect(ds.items.count).to be(20)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "sort" do
|
104
|
+
before(:each) do
|
105
|
+
expect(posts.map(&:title)).to_not eq(posts.map(&:title).sort)
|
106
|
+
ds.sort(title)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should return matching items" do
|
110
|
+
expect(ds.items.map { |e| title.value_for(e) }).to eq(posts.map(&:title).sort)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe "sort desc" do
|
115
|
+
before(:each) do
|
116
|
+
expect(posts.map(&:title)).to_not eq(posts.map(&:title).sort.reverse)
|
117
|
+
ds.sort(title, DataSource::DESC)
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should return matching items" do
|
121
|
+
expect(ds.items.map { |e| title.value_for(e) }).to eq(posts.map(&:title).sort.reverse)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe "filter" do
|
126
|
+
before(:each) do
|
127
|
+
create_list(:post, 10, author: 'author1')
|
128
|
+
create_list(:post, 10, author: 'author2')
|
129
|
+
|
130
|
+
ds.filter(author, 'author1')
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should return matching items" do
|
134
|
+
expect(ds.items.count).to be(10)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "values_for_filter" do
|
139
|
+
let(:total_count) { 0 } # skip default test posts
|
140
|
+
|
141
|
+
before(:each) do
|
142
|
+
create_list(:post, 10, author: 'author3')
|
143
|
+
create_list(:post, 10, author: 'author1')
|
144
|
+
create_list(:post, 10, author: 'author2')
|
145
|
+
create_list(:post, 10, author: nil)
|
146
|
+
end
|
147
|
+
|
148
|
+
context "without search" do
|
149
|
+
it_behaves_like "project all authors"
|
150
|
+
end
|
151
|
+
|
152
|
+
context "with scope" do
|
153
|
+
before(:each) do
|
154
|
+
ds.scope do |items|
|
155
|
+
items.where(author: ['author1', 'author2'])
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
it "should matching values" do
|
160
|
+
expect(ds.values_for_filter(author)).to eq(['author1', 'author2'])
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
context "with search" do
|
165
|
+
before(:each) do
|
166
|
+
ds.search([author], 'author2')
|
167
|
+
end
|
168
|
+
|
169
|
+
it_behaves_like "project all authors"
|
170
|
+
end
|
171
|
+
|
172
|
+
context "with filter" do
|
173
|
+
before(:each) do
|
174
|
+
ds.filter(author, 'author2')
|
175
|
+
end
|
176
|
+
|
177
|
+
it_behaves_like "project all authors"
|
178
|
+
end
|
179
|
+
|
180
|
+
context "with paging" do
|
181
|
+
before(:each) do
|
182
|
+
ds.paginate(1, 1)
|
183
|
+
end
|
184
|
+
|
185
|
+
it_behaves_like "project all authors"
|
186
|
+
end
|
187
|
+
|
188
|
+
context "with sort" do
|
189
|
+
before(:each) do
|
190
|
+
ds.sort(title)
|
191
|
+
end
|
192
|
+
|
193
|
+
it_behaves_like "project all authors"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
context "using class" do
|
199
|
+
let(:ds) { DataSource.for(Post) }
|
200
|
+
it_behaves_like "activerecord datasource with all item"
|
201
|
+
end
|
202
|
+
|
203
|
+
context "using relation" do
|
204
|
+
let(:ds) { DataSource.for(Post.where('1 = 1')) }
|
205
|
+
it_behaves_like "activerecord datasource with all item"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
context "active record model with belongs_to" do
|
210
|
+
let(:total_count) { 6 } # skip default test posts
|
211
|
+
let!(:albums) { create_list(:album, total_count) }
|
212
|
+
let!(:ds) { DataSource.for(Track) }
|
213
|
+
let!(:track_title) { ds.build_field :title }
|
214
|
+
|
215
|
+
shared_examples "listing with projected values" do
|
216
|
+
describe "projected field" do
|
217
|
+
it "should project attribute value" do
|
218
|
+
expect(album_name.value_for(ds.items.first)).to eq(Track.first.album.name)
|
219
|
+
end
|
220
|
+
|
221
|
+
it "should have key" do
|
222
|
+
expect(album_name.key).to eq('album_name')
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
it "should deal with intermediate nils" do
|
227
|
+
track_without_album = create(:track, album: nil)
|
228
|
+
expect(album_name.value_for(track_without_album)).to be_nil
|
229
|
+
end
|
230
|
+
|
231
|
+
it "should perform a eager_load" do
|
232
|
+
show_query ds
|
233
|
+
end
|
234
|
+
|
235
|
+
it "should perform a single query" do
|
236
|
+
expect(ActiveRecord::Base.count_queries do
|
237
|
+
album_name.value_for(ds.items.first)
|
238
|
+
album_id.value_for(ds.items.first)
|
239
|
+
end).to eq(1)
|
240
|
+
end
|
241
|
+
|
242
|
+
describe "scope" do
|
243
|
+
before(:each) do
|
244
|
+
ds.scope do |items|
|
245
|
+
items.even
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
it "should return scoped items" do
|
250
|
+
expect(ds.items.count).to be(Track.count / 2)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
describe "search" do
|
255
|
+
before(:each) do
|
256
|
+
create(:album, tracks_count: 5, name: 'album-name-magic-string-1')
|
257
|
+
create(:album, tracks_count: 5, name: 'album-name-magic-string-2')
|
258
|
+
create_list(:album, 2).each do |album_with_tracks_to_match|
|
259
|
+
create_list(:track, 5, title: 'title-magic-string', album: album_with_tracks_to_match)
|
260
|
+
end
|
261
|
+
|
262
|
+
ds.search([track_title, album_name], 'magic')
|
263
|
+
end
|
264
|
+
|
265
|
+
it "should return matching items" do
|
266
|
+
show_query ds
|
267
|
+
expect(ds.items.count).to be(20)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
describe "filter" do
|
272
|
+
before(:each) do
|
273
|
+
create(:album, tracks_count: 5, name: 'album-name-magic-string-1')
|
274
|
+
create(:album, tracks_count: 5, name: 'album-name-magic-string-2')
|
275
|
+
|
276
|
+
ds.filter(album_name, 'album-name-magic-string-1')
|
277
|
+
end
|
278
|
+
|
279
|
+
it "should return matching items" do
|
280
|
+
expect(ds.items.count).to be(5)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
describe "sort" do
|
285
|
+
def all_track_albumns_name
|
286
|
+
Track.all.map { |t| t.album.name }
|
287
|
+
end
|
288
|
+
|
289
|
+
before(:each) do
|
290
|
+
expect(all_track_albumns_name).to_not eq(all_track_albumns_name.sort)
|
291
|
+
ds.sort(album_name)
|
292
|
+
end
|
293
|
+
|
294
|
+
it "should return matching items" do
|
295
|
+
expect(ds.items.map { |e| album_name.value_for(e) }).to eq(all_track_albumns_name.sort)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
describe "sort desc" do
|
300
|
+
def all_track_albumns_name
|
301
|
+
Track.all.map { |t| t.album.name }
|
302
|
+
end
|
303
|
+
|
304
|
+
before(:each) do
|
305
|
+
expect(all_track_albumns_name).to_not eq(all_track_albumns_name.sort.reverse)
|
306
|
+
ds.sort(album_name, DataSource::DESC)
|
307
|
+
end
|
308
|
+
|
309
|
+
it "should return matching items" do
|
310
|
+
expect(ds.items.map { |e| album_name.value_for(e) }).to eq(all_track_albumns_name.sort.reverse)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
describe "values_for_filter" do
|
315
|
+
let(:total_count) { 0 } # skip default test albums
|
316
|
+
|
317
|
+
before(:each) do
|
318
|
+
create(:album, tracks_count: 5, name: 'album-name-1')
|
319
|
+
create(:album, tracks_count: 5, name: 'album-name-3')
|
320
|
+
create(:album, tracks_count: 5, name: 'album-name-2')
|
321
|
+
create(:album, tracks_count: 5, name: nil)
|
322
|
+
end
|
323
|
+
|
324
|
+
context "without search" do
|
325
|
+
it "should matching values" do
|
326
|
+
expect(ds.values_for_filter(album_name)).to eq(['album-name-1', 'album-name-2', 'album-name-3'])
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
context "with scope" do
|
331
|
+
before(:each) do
|
332
|
+
ds.scope do |items|
|
333
|
+
items.where("tracks.id % 2 = 0")
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
it "should matching values" do
|
338
|
+
expect(ds.values_for_filter(album_name)).to eq(['album-name-1', 'album-name-2', 'album-name-3'])
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
context "using array as path" do
|
346
|
+
let!(:album_name) { ds.build_field [:album, :name] }
|
347
|
+
let!(:album_id) { ds.build_field [:album, :id] }
|
348
|
+
|
349
|
+
it_behaves_like "listing with projected values"
|
350
|
+
end
|
351
|
+
|
352
|
+
context "hash as path" do
|
353
|
+
let!(:album_name) { ds.build_field album: :name }
|
354
|
+
let!(:album_id) { ds.build_field album: :id }
|
355
|
+
|
356
|
+
it_behaves_like "listing with projected values"
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|