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
@@ -1,11 +1,11 @@
|
|
1
1
|
.filters
|
2
|
-
- listing.
|
2
|
+
- listing.filters.each do |filter_view|
|
3
3
|
%ul.nav.nav-list.well
|
4
4
|
%li.nav-header
|
5
|
-
= "Filter by #{
|
6
|
-
-
|
5
|
+
= "Filter by #{filter_view.human_name}"
|
6
|
+
- filter_view.values.each do |value|
|
7
7
|
%li.filter
|
8
|
-
%a{:href => '#', :'data-key' =>
|
9
|
-
=
|
8
|
+
%a{:href => '#', :'data-key' => filter_view.key, :'data-value' => value}
|
9
|
+
= filter_view.value_for(value)
|
10
10
|
%i.icon-remove
|
11
11
|
|
@@ -9,7 +9,7 @@
|
|
9
9
|
- listing.columns.each do |col|
|
10
10
|
%th{'class' => "#{'sortable ' + (col.sort || '') if col.sortable?}" }
|
11
11
|
- if col.sortable?
|
12
|
-
= link_to col.human_name, listing.url_for_sort(col.
|
12
|
+
= link_to col.human_name, listing.url_for_sort(col.key, col.next_sort_direction), remote: true
|
13
13
|
- else
|
14
14
|
= col.human_name
|
15
15
|
|
data/lib/listings.rb
CHANGED
data/lib/listings/base.rb
CHANGED
@@ -15,6 +15,9 @@ module Listings
|
|
15
15
|
attr_accessor :search_criteria
|
16
16
|
attr_accessor :search_filters
|
17
17
|
|
18
|
+
attr_reader :data_source
|
19
|
+
delegate :items, to: :data_source
|
20
|
+
|
18
21
|
def initialize
|
19
22
|
@page_size = self.class.page_size
|
20
23
|
end
|
@@ -36,7 +39,7 @@ module Listings
|
|
36
39
|
self.search_filters = {}
|
37
40
|
else
|
38
41
|
# otherwise parse the search stripping out allowed filterable fields
|
39
|
-
self.search_filters, self.search_criteria = parse_filter(self.search, self.filters)
|
42
|
+
self.search_filters, self.search_criteria = parse_filter(self.search, self.filters.map(&:key))
|
40
43
|
end
|
41
44
|
end
|
42
45
|
|
@@ -63,67 +66,55 @@ module Listings
|
|
63
66
|
return text
|
64
67
|
end
|
65
68
|
|
66
|
-
def filter_items(params
|
69
|
+
def filter_items(params)
|
70
|
+
columns # prepare columns
|
71
|
+
filters # prepare filters
|
72
|
+
|
67
73
|
self.page = params[param_page] || 1
|
68
74
|
self.scope = scope_by_name(params[param_scope])
|
69
75
|
self.search = params[param_search]
|
70
76
|
parse_search
|
71
77
|
|
72
|
-
|
78
|
+
unless scope.nil?
|
79
|
+
data_source.scope do |items|
|
80
|
+
scope.apply(self, items)
|
81
|
+
end
|
82
|
+
end
|
73
83
|
|
74
84
|
if search_criteria.present? && self.searchable?
|
75
|
-
|
76
|
-
|
77
|
-
self.columns.select(&:searchable?).each do |col|
|
78
|
-
criteria << "#{model_class.table_name}.#{col.name} like ?"
|
79
|
-
values << "%#{search_criteria}%"
|
80
|
-
end
|
81
|
-
items = items.where(criteria.join(' or '), *values)
|
85
|
+
search_fields = self.columns.select(&:searchable?).map &:field
|
86
|
+
data_source.search(search_fields, search_criteria)
|
82
87
|
end
|
83
88
|
|
84
89
|
if filterable?
|
85
|
-
|
86
|
-
|
87
|
-
filters.each do |v|
|
88
|
-
self.filter_values[v] = items.pluck("distinct #{v}").reject(&:nil?)
|
90
|
+
filters.each do |filter_view|
|
91
|
+
filter_view.values # prepare values
|
89
92
|
end
|
90
93
|
|
91
94
|
self.search_filters.each do |key, filter_value|
|
92
|
-
|
95
|
+
data_source.filter(filter_with_key(key).field, filter_value)
|
93
96
|
end
|
94
97
|
end
|
95
98
|
|
96
99
|
if params.include?(param_sort_by)
|
97
|
-
sort_col =
|
100
|
+
sort_col = column_with_key(params[param_sort_by])
|
98
101
|
sort_col.sort = params[param_sort_direction]
|
99
|
-
|
102
|
+
data_source.sort(sort_col.field, params[param_sort_direction])
|
100
103
|
end
|
101
104
|
|
102
105
|
if paginated?
|
103
|
-
|
104
|
-
end
|
105
|
-
|
106
|
-
if items.is_a?(Class)
|
107
|
-
items = items.all
|
106
|
+
data_source.paginate(page, page_size)
|
108
107
|
end
|
109
108
|
|
110
|
-
items
|
109
|
+
self.items
|
111
110
|
end
|
112
111
|
|
113
112
|
def query_items(params)
|
114
113
|
@params = params
|
115
|
-
|
114
|
+
@data_source = Sources::DataSource.for(self.model_class)
|
116
115
|
@has_active_model_source = items.respond_to? :human_attribute_name
|
117
116
|
|
118
|
-
|
119
|
-
end
|
120
|
-
|
121
|
-
def paginatable(array_or_model)
|
122
|
-
if array_or_model.is_a?(Array) && paginated? && !array_or_model.respond_to?(:page)
|
123
|
-
Kaminari.paginate_array(array_or_model)
|
124
|
-
else
|
125
|
-
array_or_model
|
126
|
-
end
|
117
|
+
filter_items(self.scoped_params)
|
127
118
|
end
|
128
119
|
|
129
120
|
def has_active_model_source?
|
@@ -152,8 +143,16 @@ module Listings
|
|
152
143
|
column.value_for(self, item)
|
153
144
|
end
|
154
145
|
|
155
|
-
def
|
156
|
-
self.columns.find { |c| c.
|
146
|
+
def column_with_key(key)
|
147
|
+
self.columns.find { |c| c.key == key }
|
148
|
+
end
|
149
|
+
|
150
|
+
def filter_with_key(key)
|
151
|
+
self.filters.find { |c| c.key == key }
|
152
|
+
end
|
153
|
+
|
154
|
+
def human_name(field)
|
155
|
+
field.human_name
|
157
156
|
end
|
158
157
|
|
159
158
|
def searchable?
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Listings
|
2
|
+
class BaseFieldDescriptor
|
3
|
+
attr_reader :path
|
4
|
+
attr_reader :props
|
5
|
+
attr_reader :proc
|
6
|
+
|
7
|
+
def initialize(path, props, proc)
|
8
|
+
@path = path
|
9
|
+
@props = props
|
10
|
+
@proc = proc
|
11
|
+
end
|
12
|
+
|
13
|
+
def build_field(listing)
|
14
|
+
listing.data_source.build_field(path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def is_field?
|
18
|
+
!path.nil? && !path.is_a?(String)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Listings
|
2
|
+
class BaseFieldView
|
3
|
+
attr_reader :field
|
4
|
+
attr_reader :listing
|
5
|
+
|
6
|
+
def initialize(listing, field_description)
|
7
|
+
@listing = listing
|
8
|
+
@field_description = field_description
|
9
|
+
@field = if @field_description.is_field?
|
10
|
+
@field_description.build_field(listing)
|
11
|
+
else
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def path
|
17
|
+
@field_description.path
|
18
|
+
end
|
19
|
+
|
20
|
+
def human_name
|
21
|
+
return @field_description.props[:title] if @field_description.props[:title]
|
22
|
+
return path if path.is_a?(String)
|
23
|
+
|
24
|
+
I18n.t("listings.headers.#{listing.name}.#{key}", default: listing.human_name(field))
|
25
|
+
end
|
26
|
+
|
27
|
+
def key
|
28
|
+
if @field
|
29
|
+
@field.key
|
30
|
+
else
|
31
|
+
path
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def is_field?
|
36
|
+
@field_description.is_field?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -1,56 +1,16 @@
|
|
1
1
|
module Listings
|
2
|
-
class ColumnDescriptor
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
def initialize(listing_class, name, props = {}, proc)
|
7
|
-
@listing_class = listing_class
|
8
|
-
@name = name
|
9
|
-
@props = props.reverse_merge! searchable: false, sortable: true
|
10
|
-
@proc = proc
|
11
|
-
end
|
12
|
-
|
13
|
-
def value_for(listing, model)
|
14
|
-
if @proc
|
15
|
-
listing.instance_exec model, &@proc
|
16
|
-
elsif model.is_a?(Hash)
|
17
|
-
model[name]
|
18
|
-
else
|
19
|
-
model.send(name)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
def human_name(listing)
|
24
|
-
return '' if name.blank?
|
25
|
-
|
26
|
-
fallback = if is_model_column?(listing)
|
27
|
-
listing.model_class.human_attribute_name(name)
|
28
|
-
else
|
29
|
-
name
|
30
|
-
end
|
31
|
-
I18n.t("listings.headers.#{listing.name}.#{name}", default: fallback)
|
2
|
+
class ColumnDescriptor < BaseFieldDescriptor
|
3
|
+
def initialize(path, props, proc)
|
4
|
+
props = props.reverse_merge! searchable: false, sortable: true
|
5
|
+
super(path, props, proc)
|
32
6
|
end
|
33
7
|
|
34
|
-
def searchable?
|
35
|
-
@props[:searchable] &&
|
8
|
+
def searchable?
|
9
|
+
@props[:searchable] && is_field?
|
36
10
|
end
|
37
11
|
|
38
12
|
def sortable?
|
39
|
-
|
40
|
-
if sortable_property_is_expression?
|
41
|
-
true # s is the expression that should be used for sorting
|
42
|
-
else
|
43
|
-
s # s is Boolean
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
def sortable_property_is_expression?
|
48
|
-
s = @props[:sortable]
|
49
|
-
!(!!s == s)
|
50
|
-
end
|
51
|
-
|
52
|
-
def is_model_column?(listing)
|
53
|
-
name.is_a?(Symbol) && listing.has_active_model_source?
|
13
|
+
@props[:sortable] && is_field?
|
54
14
|
end
|
55
15
|
end
|
56
16
|
end
|
data/lib/listings/column_view.rb
CHANGED
@@ -1,50 +1,41 @@
|
|
1
1
|
module Listings
|
2
|
-
class ColumnView
|
2
|
+
class ColumnView < BaseFieldView
|
3
3
|
def initialize(listing, column_description)
|
4
|
-
|
5
|
-
@column_description = column_description
|
4
|
+
super
|
6
5
|
end
|
7
6
|
|
8
|
-
def
|
9
|
-
@
|
7
|
+
def column_description
|
8
|
+
@field_description
|
10
9
|
end
|
11
10
|
|
12
|
-
def
|
13
|
-
@
|
11
|
+
def value_for(model)
|
12
|
+
if @field_description.proc
|
13
|
+
if is_field?
|
14
|
+
listing.instance_exec model, field.value_for(model), &@field_description.proc
|
15
|
+
else
|
16
|
+
listing.instance_exec model, &@field_description.proc
|
17
|
+
end
|
18
|
+
else
|
19
|
+
field.value_for(model)
|
20
|
+
end
|
14
21
|
end
|
15
22
|
|
16
23
|
def searchable?
|
17
|
-
|
18
|
-
end
|
19
|
-
|
20
|
-
def is_model_column?
|
21
|
-
@column_description.is_model_column?(@listing)
|
22
|
-
end
|
23
|
-
|
24
|
-
def name
|
25
|
-
@column_description.name
|
24
|
+
column_description.searchable?
|
26
25
|
end
|
27
26
|
|
28
27
|
def sortable?
|
29
|
-
|
30
|
-
end
|
31
|
-
|
32
|
-
def sort_by
|
33
|
-
if @column_description.sortable_property_is_expression?
|
34
|
-
@column_description.props[:sortable]
|
35
|
-
else
|
36
|
-
name
|
37
|
-
end
|
28
|
+
listing.sortable? && column_description.sortable?
|
38
29
|
end
|
39
30
|
|
40
31
|
def cell_css_class
|
41
|
-
|
32
|
+
column_description.props[:class]
|
42
33
|
end
|
43
34
|
|
44
35
|
attr_accessor :sort
|
45
36
|
|
46
37
|
def next_sort_direction
|
47
|
-
sort ==
|
38
|
+
self.sort == Sources::DataSource::ASC ? Sources::DataSource::DESC : Sources::DataSource::ASC
|
48
39
|
end
|
49
40
|
end
|
50
41
|
end
|
@@ -1,6 +1,10 @@
|
|
1
1
|
require 'listings/scope_descriptor'
|
2
|
+
require 'listings/base_field_descriptor'
|
3
|
+
require 'listings/base_field_view'
|
2
4
|
require 'listings/column_descriptor'
|
3
5
|
require 'listings/column_view'
|
6
|
+
require 'listings/filter_descriptor'
|
7
|
+
require 'listings/filter_view'
|
4
8
|
|
5
9
|
module Listings
|
6
10
|
module ConfigurationMethods
|
@@ -9,10 +13,8 @@ module Listings
|
|
9
13
|
included do
|
10
14
|
attr_accessor :page
|
11
15
|
attr_accessor :scope
|
12
|
-
attr_accessor :items
|
13
16
|
attr_accessor :search
|
14
17
|
attr_accessor :page_size
|
15
|
-
attr_accessor :filter_values
|
16
18
|
|
17
19
|
def scopes
|
18
20
|
@scopes ||= self.class.process_scopes
|
@@ -28,12 +30,12 @@ module Listings
|
|
28
30
|
self.class.export_formats
|
29
31
|
end
|
30
32
|
|
31
|
-
def
|
33
|
+
def sortable?
|
32
34
|
opt = self.class.sortable_options
|
33
35
|
if opt.nil?
|
34
36
|
true
|
35
37
|
else
|
36
|
-
if opt.length == 1
|
38
|
+
if opt.length == 1
|
37
39
|
opt.first
|
38
40
|
else
|
39
41
|
true
|
@@ -50,7 +52,9 @@ module Listings
|
|
50
52
|
end
|
51
53
|
|
52
54
|
def filters
|
53
|
-
self.class.filters
|
55
|
+
@filters ||= self.class.filters.map do |fd|
|
56
|
+
FilterView.new(self, fd)
|
57
|
+
end
|
54
58
|
end
|
55
59
|
end
|
56
60
|
|
@@ -83,7 +87,6 @@ module Listings
|
|
83
87
|
@scopes = scopes.select{ |s| !s.deferred? }
|
84
88
|
end
|
85
89
|
|
86
|
-
|
87
90
|
def model(model_class = nil, &proc)
|
88
91
|
if !model_class.nil?
|
89
92
|
self.send(:define_method, 'model_class') do
|
@@ -98,8 +101,19 @@ module Listings
|
|
98
101
|
@columns ||= []
|
99
102
|
end
|
100
103
|
|
101
|
-
def column(
|
102
|
-
|
104
|
+
def column(path = '', props = {}, &proc)
|
105
|
+
path, props = fix_path_props(path, props)
|
106
|
+
columns << ColumnDescriptor.new(path, props, proc)
|
107
|
+
end
|
108
|
+
|
109
|
+
def fix_path_props(path, props)
|
110
|
+
if path.is_a?(Hash) && path.size > 1
|
111
|
+
props = props.merge(path)
|
112
|
+
path = Hash[[path.first]]
|
113
|
+
props.except!(path.first.first)
|
114
|
+
end
|
115
|
+
|
116
|
+
[path, props]
|
103
117
|
end
|
104
118
|
|
105
119
|
def selectable #(column = :id)
|
@@ -121,6 +135,8 @@ module Listings
|
|
121
135
|
end
|
122
136
|
end
|
123
137
|
|
138
|
+
# call `sortable false` make listing non sorted
|
139
|
+
# default is `sortable true`
|
124
140
|
def sortable(*options)
|
125
141
|
@sortable_options = options
|
126
142
|
end
|
@@ -137,8 +153,9 @@ module Listings
|
|
137
153
|
@filters ||= []
|
138
154
|
end
|
139
155
|
|
140
|
-
def filter
|
141
|
-
|
156
|
+
def filter(path = '', props = {}, &proc)
|
157
|
+
path, props = fix_path_props(path, props)
|
158
|
+
filters << FilterDescriptor.new(path, props, proc)
|
142
159
|
end
|
143
160
|
end
|
144
161
|
end
|