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.
Files changed (48) hide show
  1. data/app/assets/javascripts/listings.js +1 -1
  2. data/app/views/listings/_filters.html.haml +5 -5
  3. data/app/views/listings/_table_content.html.haml +1 -1
  4. data/lib/listings.rb +1 -0
  5. data/lib/listings/base.rb +34 -35
  6. data/lib/listings/base_field_descriptor.rb +21 -0
  7. data/lib/listings/base_field_view.rb +39 -0
  8. data/lib/listings/column_descriptor.rb +7 -47
  9. data/lib/listings/column_view.rb +18 -27
  10. data/lib/listings/configuration_methods.rb +27 -10
  11. data/lib/listings/filter_descriptor.rb +7 -0
  12. data/lib/listings/filter_view.rb +19 -0
  13. data/lib/listings/sources.rb +7 -0
  14. data/lib/listings/sources/active_record_data_source.rb +159 -0
  15. data/lib/listings/sources/data_source.rb +76 -0
  16. data/lib/listings/sources/object_data_source.rb +91 -0
  17. data/lib/listings/version.rb +1 -1
  18. data/lib/rspec/listings_helpers.rb +27 -5
  19. data/spec/dummy/app/controllers/welcome_controller.rb +3 -0
  20. data/spec/dummy/app/listings/array_listing.rb +1 -0
  21. data/spec/dummy/app/listings/tracks_fixed_order_listing.rb +13 -0
  22. data/spec/dummy/app/listings/tracks_listing.rb +18 -0
  23. data/spec/dummy/app/models/album.rb +4 -0
  24. data/spec/dummy/app/models/object_album.rb +8 -0
  25. data/spec/dummy/app/models/object_track.rb +8 -0
  26. data/spec/dummy/app/models/track.rb +7 -0
  27. data/spec/dummy/app/views/welcome/index.html.haml +7 -1
  28. data/spec/dummy/app/views/welcome/tracks.html.haml +3 -0
  29. data/spec/dummy/config/routes.rb +1 -0
  30. data/spec/dummy/db/migrate/20150611185824_create_albums.rb +9 -0
  31. data/spec/dummy/db/migrate/20150611185922_create_tracks.rb +12 -0
  32. data/spec/dummy/db/schema.rb +17 -1
  33. data/spec/dummy/db/seeds.rb +6 -0
  34. data/spec/factories/albums.rb +25 -0
  35. data/spec/factories/post.rb +1 -0
  36. data/spec/factories/tracks.rb +14 -0
  37. data/spec/factories/traits.rb +9 -0
  38. data/spec/lib/filter_parser_spec.rb +5 -0
  39. data/spec/lib/sources/active_record_data_source_spec.rb +359 -0
  40. data/spec/lib/sources/object_data_source_spec.rb +231 -0
  41. data/spec/listings/tracks_fixed_order_listing_spec.rb +19 -0
  42. data/spec/listings/tracks_listing_spec.rb +27 -0
  43. data/spec/models/album_spec.rb +9 -0
  44. data/spec/models/post_spec.rb +2 -2
  45. data/spec/models/track_spec.rb +8 -0
  46. data/spec/spec_helper.rb +3 -0
  47. data/spec/support/query_counter.rb +29 -0
  48. 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
@@ -0,0 +1,4 @@
1
+ class Album < ActiveRecord::Base
2
+ attr_accessible :name if Rails::VERSION::MAJOR == 3
3
+ has_many :tracks
4
+ end
@@ -0,0 +1,8 @@
1
+ class ObjectAlbum
2
+ attr_accessor :name
3
+ attr_accessor :tracks
4
+
5
+ def to_h
6
+ { name: name }
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ class ObjectTrack
2
+ attr_accessor :album
3
+ attr_accessor :order, :title
4
+
5
+ def to_h
6
+ { title: title, album: album.try(&:to_h) }
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ class Track < ActiveRecord::Base
2
+ belongs_to :album
3
+ attr_accessible :order, :title if Rails::VERSION::MAJOR == 3
4
+
5
+ scope :even, -> { where("#{table_name}.id % 2 = 0") }
6
+
7
+ 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', hash_path
29
+ = link_to 'Array', array_path
24
30
  %dd
25
31
  Array based listing
@@ -0,0 +1,3 @@
1
+ %h1 Welcome#tracks
2
+
3
+ = render_listing :tracks
@@ -8,6 +8,7 @@ Rails.application.routes.draw do
8
8
 
9
9
  get 'array', to: 'welcome#array'
10
10
  get 'hash', to: 'welcome#hash'
11
+ get 'tracks', to: 'welcome#tracks'
11
12
 
12
13
  root to: 'welcome#index'
13
14
 
@@ -0,0 +1,9 @@
1
+ class CreateAlbums < ActiveRecord::Migration
2
+ def change
3
+ create_table :albums do |t|
4
+ t.string :name
5
+
6
+ t.timestamps
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ class CreateTracks < ActiveRecord::Migration
2
+ def change
3
+ create_table :tracks do |t|
4
+ t.string :title
5
+ t.integer :order
6
+ t.references :album
7
+
8
+ t.timestamps
9
+ end
10
+ add_index :tracks, :album_id
11
+ end
12
+ end
@@ -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 => 20140728151246) do
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
@@ -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
@@ -1,4 +1,5 @@
1
1
  FactoryGirl.define do
2
2
  factory :post do
3
+ title
3
4
  end
4
5
  end
@@ -0,0 +1,14 @@
1
+ FactoryGirl.define do
2
+ factory :track do
3
+ title
4
+ order 1
5
+ album nil
6
+ end
7
+
8
+ factory :object_track do
9
+ title
10
+ order 1
11
+ album nil
12
+ end
13
+
14
+ end
@@ -0,0 +1,9 @@
1
+ FactoryGirl.define do
2
+ sequence :title do |n|
3
+ "title #{rand(1000)} #{n}"
4
+ end
5
+
6
+ sequence :name do |n|
7
+ "name #{rand(1000)} #{n}"
8
+ end
9
+ end
@@ -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