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