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
@@ -147,7 +147,7 @@ $(function(){
147
147
  });
148
148
 
149
149
  function searchEscape(value) {
150
- if (value == /\w+/) {
150
+ if (value.toString().indexOf(" ") == -1) {
151
151
  return value;
152
152
  } else if (value.indexOf("'") > -1) {
153
153
  return '"' + value + '"';
@@ -1,11 +1,11 @@
1
1
  .filters
2
- - listing.filter_values.each do |k,v|
2
+ - listing.filters.each do |filter_view|
3
3
  %ul.nav.nav-list.well
4
4
  %li.nav-header
5
- = "Filter by #{k}"
6
- - v.each do |x|
5
+ = "Filter by #{filter_view.human_name}"
6
+ - filter_view.values.each do |value|
7
7
  %li.filter
8
- %a{:href => '#', :'data-key' => k, :'data-value' => x}
9
- = x
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.name, col.next_sort_direction), remote: true
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
 
@@ -1,5 +1,6 @@
1
1
  require 'kaminari'
2
2
  require 'bootstrap-kaminari-views'
3
+ require 'listings/sources'
3
4
  require 'listings/configuration'
4
5
  require 'listings/kaminari_helpers_tag_patch'
5
6
  require 'listings/base'
@@ -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, items)
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
- items = paginatable(scope.apply(self, items)) unless scope.nil?
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
- criteria = []
76
- values = []
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
- # pluck filters values before applying filters/pagination/sorting
86
- self.filter_values = {}
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
- items = items.where("#{model_class.table_name}.#{key} = ?", filter_value)
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 = column_with_name(params[param_sort_by])
100
+ sort_col = column_with_key(params[param_sort_by])
98
101
  sort_col.sort = params[param_sort_direction]
99
- items = items.reorder("#{sort_col.sort_by} #{params[param_sort_direction]}")
102
+ data_source.sort(sort_col.field, params[param_sort_direction])
100
103
  end
101
104
 
102
105
  if paginated?
103
- items = items.page(page).per(page_size)
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
- items = self.model_class
114
+ @data_source = Sources::DataSource.for(self.model_class)
116
115
  @has_active_model_source = items.respond_to? :human_attribute_name
117
116
 
118
- self.items = filter_items(self.scoped_params, paginatable(items))
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 column_with_name(name)
156
- self.columns.find { |c| c.name.to_s == name.to_s }
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
- attr_reader :name
4
- attr_reader :props
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?(listing)
35
- @props[:searchable] && is_model_column?(listing)
8
+ def searchable?
9
+ @props[:searchable] && is_field?
36
10
  end
37
11
 
38
12
  def sortable?
39
- s = @props[:sortable]
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
@@ -1,50 +1,41 @@
1
1
  module Listings
2
- class ColumnView
2
+ class ColumnView < BaseFieldView
3
3
  def initialize(listing, column_description)
4
- @listing = listing
5
- @column_description = column_description
4
+ super
6
5
  end
7
6
 
8
- def value_for(model)
9
- @column_description.value_for(@listing, model)
7
+ def column_description
8
+ @field_description
10
9
  end
11
10
 
12
- def human_name
13
- @column_description.human_name(@listing)
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
- @column_description.searchable?(@listing)
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
- @listing.is_sortable? && @column_description.sortable? && (self.is_model_column? || @column_description.sortable_property_is_expression?)
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
- @column_description.props[:class]
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 == 'asc' ? 'desc' : 'asc'
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 is_sortable?
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 && !!opt.first == opt.first
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(name = '', props = {}, &proc)
102
- columns << ColumnDescriptor.new(self, name, props, proc)
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 name
141
- filters << name
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