listings 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,19 @@
|
|
1
|
+
module Listings
|
2
|
+
class FilterView < BaseFieldView
|
3
|
+
def initialize(listing, filter_description)
|
4
|
+
super
|
5
|
+
end
|
6
|
+
|
7
|
+
def values
|
8
|
+
@values ||= listing.data_source.values_for_filter(field)
|
9
|
+
end
|
10
|
+
|
11
|
+
def value_for(value)
|
12
|
+
if @field_description.proc
|
13
|
+
listing.instance_exec value, &@field_description.proc
|
14
|
+
else
|
15
|
+
value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module Listings::Sources
|
2
|
+
class ActiveRecordDataSource < DataSource
|
3
|
+
attr_reader :model_instance
|
4
|
+
|
5
|
+
def initialize(model)
|
6
|
+
@items = model
|
7
|
+
if model.is_a?(ActiveRecord::Relation)
|
8
|
+
@items_for_filter = model.clone
|
9
|
+
@model_instance = model.klass.new
|
10
|
+
else
|
11
|
+
@items_for_filter = model
|
12
|
+
@model_instance = model.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def connection
|
17
|
+
model_instance.class.connection
|
18
|
+
end
|
19
|
+
|
20
|
+
def items
|
21
|
+
if @items.is_a?(Class)
|
22
|
+
if Rails::VERSION::MAJOR == 3
|
23
|
+
@items.scoped
|
24
|
+
else
|
25
|
+
@items.all
|
26
|
+
end
|
27
|
+
else
|
28
|
+
@items
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def scope
|
33
|
+
@items = yield @items
|
34
|
+
@items_for_filter = yield @items_for_filter
|
35
|
+
end
|
36
|
+
|
37
|
+
def sort_with_direction(field, direction)
|
38
|
+
@items = field.sort @items, direction
|
39
|
+
end
|
40
|
+
|
41
|
+
def values_for_filter(field)
|
42
|
+
@items_for_filter.reorder(field.query_column).pluck("distinct #{field.query_column}").reject(&:nil?)
|
43
|
+
end
|
44
|
+
|
45
|
+
def search(fields, value)
|
46
|
+
criteria = []
|
47
|
+
values = []
|
48
|
+
fields.each do |field|
|
49
|
+
criteria << "#{field.query_column} like ?"
|
50
|
+
values << "%#{value}%"
|
51
|
+
end
|
52
|
+
@items = @items.where(criteria.join(' or '), *values)
|
53
|
+
end
|
54
|
+
|
55
|
+
def filter(field, value)
|
56
|
+
@items = @items.where("#{field.query_column} = ?", value)
|
57
|
+
end
|
58
|
+
|
59
|
+
def paginate(page, page_size)
|
60
|
+
@items = @items.page(page).per(page_size)
|
61
|
+
end
|
62
|
+
|
63
|
+
def joins(relation)
|
64
|
+
@items = @items.eager_load(relation)
|
65
|
+
@items_for_filter = @items_for_filter.joins(relation)
|
66
|
+
end
|
67
|
+
|
68
|
+
def build_field(path)
|
69
|
+
path = self.class.sanitaize_path(path)
|
70
|
+
if path.is_a?(Array)
|
71
|
+
ActiveRecordAssociationField.new(path, self)
|
72
|
+
else
|
73
|
+
ActiveRecordField.new(path, self)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class DataSource
|
79
|
+
class << self
|
80
|
+
def for_with_active_record(model)
|
81
|
+
if (model.is_a?(Class) && model.ancestors.include?(ActiveRecord::Base)) || model.is_a?(ActiveRecord::Relation)
|
82
|
+
ActiveRecordDataSource.new(model)
|
83
|
+
else
|
84
|
+
for_without_active_record(model)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
alias_method_chain :for, :active_record
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class ActiveRecordField < Field
|
93
|
+
delegate :connection, to: :data_source
|
94
|
+
delegate :quote_table_name, :quote_column_name, to: :connection
|
95
|
+
|
96
|
+
def initialize(attribute_name, data_source)
|
97
|
+
super(data_source)
|
98
|
+
@attribute_name = attribute_name
|
99
|
+
end
|
100
|
+
|
101
|
+
def value_for(item)
|
102
|
+
item.send @attribute_name
|
103
|
+
end
|
104
|
+
|
105
|
+
def query_column
|
106
|
+
"#{quote_table_name(data_source.items.table_name)}.#{quote_column_name(@attribute_name)}"
|
107
|
+
end
|
108
|
+
|
109
|
+
def sort(items, direction)
|
110
|
+
items.reorder("#{query_column} #{direction}")
|
111
|
+
end
|
112
|
+
|
113
|
+
def key
|
114
|
+
@attribute_name.to_s
|
115
|
+
end
|
116
|
+
|
117
|
+
def human_name
|
118
|
+
data_source.model_instance.class.human_attribute_name(@attribute_name)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class ActiveRecordAssociationField < Field
|
123
|
+
delegate :connection, to: :data_source
|
124
|
+
delegate :quote_table_name, :quote_column_name, to: :connection
|
125
|
+
|
126
|
+
def initialize(path, data_source)
|
127
|
+
super(data_source)
|
128
|
+
@path = path
|
129
|
+
|
130
|
+
data_source.joins(path[0])
|
131
|
+
end
|
132
|
+
|
133
|
+
def value_for(item)
|
134
|
+
result = item
|
135
|
+
@path.each do |attribute_name|
|
136
|
+
result = result.try { |o| o.send(attribute_name) }
|
137
|
+
end
|
138
|
+
|
139
|
+
result
|
140
|
+
end
|
141
|
+
|
142
|
+
def query_column
|
143
|
+
association = data_source.model_instance.association(@path[0])
|
144
|
+
"#{quote_table_name(association.reflection.table_name)}.#{quote_column_name(@path[1])}"
|
145
|
+
end
|
146
|
+
|
147
|
+
def sort(items, direction)
|
148
|
+
items.reorder("#{query_column} #{direction}")
|
149
|
+
end
|
150
|
+
|
151
|
+
def key
|
152
|
+
@path.join('_')
|
153
|
+
end
|
154
|
+
|
155
|
+
def human_name
|
156
|
+
@path.join(' ').titleize
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Listings::Sources
|
2
|
+
class DataSource
|
3
|
+
DESC = 'desc'
|
4
|
+
ASC = 'asc'
|
5
|
+
|
6
|
+
# returns items for the data source
|
7
|
+
# if +paginate+ is called, items will return just the current page
|
8
|
+
def items
|
9
|
+
end
|
10
|
+
|
11
|
+
# applies filter to the items
|
12
|
+
# scope will be called with a block with the ongoing items
|
13
|
+
# the result of the block is used as the narrowed items
|
14
|
+
def scope
|
15
|
+
end
|
16
|
+
|
17
|
+
# applies a human friendly search to items among multiple fields
|
18
|
+
def search(fields, value)
|
19
|
+
end
|
20
|
+
|
21
|
+
# applies exact match filtering among specified field
|
22
|
+
def filter(field, value)
|
23
|
+
end
|
24
|
+
|
25
|
+
# applies sorting with specified direction to items
|
26
|
+
# subclasses should implement +sort_with_direction+ in order to leave
|
27
|
+
# default direction logic in +DataSource+
|
28
|
+
def sort(field, direction = ASC)
|
29
|
+
sort_with_direction(field, direction)
|
30
|
+
end
|
31
|
+
|
32
|
+
# returns all values of field
|
33
|
+
# usually calling +search+/+filter+/+sort+/+paginate+ should not affect the results.
|
34
|
+
# calling +scope+ should affect the results.
|
35
|
+
def values_for_filter(field)
|
36
|
+
end
|
37
|
+
|
38
|
+
# apply pagination filter to +items+
|
39
|
+
# items of the selected page can be obtained through +items+
|
40
|
+
def paginate(page, page_size)
|
41
|
+
end
|
42
|
+
|
43
|
+
# returns a +Field+ for the specified options
|
44
|
+
def build_field(path)
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.for(model)
|
48
|
+
raise "Unable to create datasource for #{model}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.sanitaize_path(path)
|
52
|
+
if path.is_a?(Array)
|
53
|
+
path
|
54
|
+
elsif path.is_a?(Hash) && path.size == 1
|
55
|
+
path.to_a.first
|
56
|
+
else
|
57
|
+
path
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class Field
|
63
|
+
attr_reader :data_source
|
64
|
+
|
65
|
+
def initialize(data_source)
|
66
|
+
@data_source = data_source
|
67
|
+
end
|
68
|
+
|
69
|
+
# returns this field over the item
|
70
|
+
def value_for(item)
|
71
|
+
end
|
72
|
+
|
73
|
+
def key
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Listings::Sources
|
2
|
+
class ObjectDataSource < DataSource
|
3
|
+
def initialize(items)
|
4
|
+
@items = items
|
5
|
+
@items_for_filter = items
|
6
|
+
end
|
7
|
+
|
8
|
+
def items
|
9
|
+
@items
|
10
|
+
end
|
11
|
+
|
12
|
+
def paginate(page, page_size)
|
13
|
+
@items = Kaminari.paginate_array(@items).page(page).per(page_size)
|
14
|
+
end
|
15
|
+
|
16
|
+
def scope
|
17
|
+
@items = yield @items
|
18
|
+
@items_for_filter = yield @items_for_filter
|
19
|
+
end
|
20
|
+
|
21
|
+
def sort_with_direction(field, direction)
|
22
|
+
@items = @items.sort do |a, b|
|
23
|
+
b, a = a, b if direction == DESC
|
24
|
+
field.value_for(a) <=> field.value_for(b)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def values_for_filter(field)
|
29
|
+
@items_for_filter.map { |o| field.value_for(o) }.uniq.reject(&:nil?).sort
|
30
|
+
end
|
31
|
+
|
32
|
+
def search(fields, value)
|
33
|
+
@items = @items.select do |item|
|
34
|
+
fields.any? do |field|
|
35
|
+
field.value_for(item).try { |o| o.include?(value) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def filter(field, value)
|
41
|
+
@items = @items.select do |item|
|
42
|
+
field.value_for(item) == value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_field(path)
|
47
|
+
path = self.class.sanitaize_path(path)
|
48
|
+
unless path.is_a?(Array)
|
49
|
+
path = [path]
|
50
|
+
end
|
51
|
+
ObjectField.new(path, self)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class DataSource
|
56
|
+
class << self
|
57
|
+
def for_with_object(model)
|
58
|
+
ObjectDataSource.new(model)
|
59
|
+
end
|
60
|
+
|
61
|
+
alias_method_chain :for, :object
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class ObjectField < Field
|
66
|
+
def initialize(path, data_source)
|
67
|
+
super(data_source)
|
68
|
+
@path = path
|
69
|
+
end
|
70
|
+
|
71
|
+
def value_for(item)
|
72
|
+
@path.inject(item) do |obj, attribute|
|
73
|
+
if obj.nil?
|
74
|
+
nil
|
75
|
+
elsif obj.is_a?(Hash) && obj.key?(attribute)
|
76
|
+
obj[attribute]
|
77
|
+
else
|
78
|
+
obj.send attribute
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def key
|
84
|
+
@path.join('_')
|
85
|
+
end
|
86
|
+
|
87
|
+
def human_name
|
88
|
+
@path.join(' ')
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/listings/version.rb
CHANGED
@@ -1,15 +1,37 @@
|
|
1
1
|
module RSpec::ListingsHelpers
|
2
|
-
class
|
3
|
-
|
2
|
+
class FakeRoutes
|
3
|
+
def listing_full_path(*options)
|
4
|
+
"/"
|
5
|
+
end
|
6
|
+
|
7
|
+
def listing_full_url(*options)
|
8
|
+
"/"
|
9
|
+
end
|
4
10
|
|
5
|
-
def
|
6
|
-
|
11
|
+
def listing_content_url(*options)
|
12
|
+
"/"
|
7
13
|
end
|
8
14
|
end
|
9
15
|
|
10
16
|
def query_listing(name)
|
11
|
-
context =
|
17
|
+
context = fake_context
|
12
18
|
context.prepare_listing({:listing => name}, context)
|
13
19
|
end
|
14
20
|
|
21
|
+
def render_listing(name)
|
22
|
+
fake_context.render_listing(name)
|
23
|
+
end
|
24
|
+
|
25
|
+
def fake_context
|
26
|
+
controller = ActionController::Base.new
|
27
|
+
controller.request = ActionController::TestRequest.new(:host => "http://test.com")
|
28
|
+
context = controller.view_context
|
29
|
+
|
30
|
+
context.class.send(:define_method, 'listings') do
|
31
|
+
FakeRoutes.new
|
32
|
+
end
|
33
|
+
|
34
|
+
context
|
35
|
+
end
|
36
|
+
|
15
37
|
end
|