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