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,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
|