listings 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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