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
@@ -0,0 +1,7 @@
1
+ module Listings
2
+ class FilterDescriptor < BaseFieldDescriptor
3
+ def initialize(path, props, proc)
4
+ super
5
+ end
6
+ end
7
+ end
@@ -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,7 @@
1
+ module Listings::Sources
2
+ end
3
+
4
+ require 'listings/sources/data_source'
5
+ require 'listings/sources/object_data_source'
6
+ require 'listings/sources/active_record_data_source'
7
+
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Listings
2
- VERSION = "0.0.3"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -1,15 +1,37 @@
1
1
  module RSpec::ListingsHelpers
2
- class FakeViewContext
3
- include ::Listings::ActionViewExtensions
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 listings
6
- nil
11
+ def listing_content_url(*options)
12
+ "/"
7
13
  end
8
14
  end
9
15
 
10
16
  def query_listing(name)
11
- context = FakeViewContext.new
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
@@ -7,4 +7,7 @@ class WelcomeController < ApplicationController
7
7
 
8
8
  def hash
9
9
  end
10
+
11
+ def tracks
12
+ end
10
13
  end
@@ -10,6 +10,7 @@ class ArrayListing < Listings::Base
10
10
  end
11
11
  end
12
12
 
13
+ # paginates_per :none
13
14
  paginates_per 10
14
15
 
15
16
  column 'Num' do |n|
@@ -0,0 +1,13 @@
1
+ class TracksFixedOrderListing < Listings::Base
2
+
3
+ model Track
4
+
5
+ sortable false
6
+
7
+ filter album: :name
8
+
9
+ column :order
10
+ column :title
11
+ column album: :name
12
+
13
+ end