table_me 0.0.1

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 (63) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +3 -0
  3. data/Rakefile +40 -0
  4. data/app/assets/images/table_me/orderable.png +0 -0
  5. data/app/assets/stylesheets/table_me/table_me.sass +171 -0
  6. data/config/routes.rb +2 -0
  7. data/lib/table_me/builder.rb +36 -0
  8. data/lib/table_me/column.rb +19 -0
  9. data/lib/table_me/engine.rb +28 -0
  10. data/lib/table_me/filter.rb +81 -0
  11. data/lib/table_me/table_for_helper/table_for_helper.rb +112 -0
  12. data/lib/table_me/table_for_presenter.rb +229 -0
  13. data/lib/table_me/table_me_helper/table_me_helper.rb +28 -0
  14. data/lib/table_me/table_me_presenter.rb +105 -0
  15. data/lib/table_me/table_pagination.rb +110 -0
  16. data/lib/table_me/table_vo.rb +48 -0
  17. data/lib/table_me/url_builder.rb +33 -0
  18. data/lib/table_me/url_parser.rb +38 -0
  19. data/lib/table_me/version.rb +3 -0
  20. data/lib/table_me.rb +15 -0
  21. data/lib/tasks/table_me_tasks.rake +4 -0
  22. data/test/dummy/README.rdoc +261 -0
  23. data/test/dummy/Rakefile +7 -0
  24. data/test/dummy/app/assets/javascripts/application.js +15 -0
  25. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  26. data/test/dummy/app/controllers/application_controller.rb +3 -0
  27. data/test/dummy/app/helpers/application_helper.rb +2 -0
  28. data/test/dummy/app/models/users.rb +2 -0
  29. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  30. data/test/dummy/config/application.rb +56 -0
  31. data/test/dummy/config/boot.rb +10 -0
  32. data/test/dummy/config/database.yml +25 -0
  33. data/test/dummy/config/environment.rb +5 -0
  34. data/test/dummy/config/environments/development.rb +46 -0
  35. data/test/dummy/config/environments/production.rb +67 -0
  36. data/test/dummy/config/environments/test.rb +37 -0
  37. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  38. data/test/dummy/config/initializers/inflections.rb +15 -0
  39. data/test/dummy/config/initializers/mime_types.rb +5 -0
  40. data/test/dummy/config/initializers/secret_token.rb +7 -0
  41. data/test/dummy/config/initializers/session_store.rb +8 -0
  42. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  43. data/test/dummy/config/locales/en.yml +5 -0
  44. data/test/dummy/config/routes.rb +58 -0
  45. data/test/dummy/config.ru +4 -0
  46. data/test/dummy/db/development.sqlite3 +0 -0
  47. data/test/dummy/db/migrate/20120223225814_create_users.rb +9 -0
  48. data/test/dummy/db/schema.rb +23 -0
  49. data/test/dummy/db/test.sqlite3 +0 -0
  50. data/test/dummy/log/development.log +52 -0
  51. data/test/dummy/log/test.log +6 -0
  52. data/test/dummy/public/404.html +26 -0
  53. data/test/dummy/public/422.html +26 -0
  54. data/test/dummy/public/500.html +25 -0
  55. data/test/dummy/public/favicon.ico +0 -0
  56. data/test/dummy/script/rails +6 -0
  57. data/test/fixtures/users.yml +9 -0
  58. data/test/integration/navigation_test.rb +10 -0
  59. data/test/integration/table_for_spec.rb +97 -0
  60. data/test/table_me_test.rb +7 -0
  61. data/test/test_helper.rb +10 -0
  62. data/test/unit/table_me_presenter_test.rb +7 -0
  63. metadata +225 -0
@@ -0,0 +1,229 @@
1
+ require_relative 'table_me_presenter'
2
+ require_relative 'table_pagination'
3
+ require_relative 'builder'
4
+ require_relative 'url_builder'
5
+
6
+ module TableMe
7
+
8
+ # the first parameter of table_for is the name set in table_me. By default the class
9
+ # name is used if a name isn't set.
10
+
11
+ # table_for :user
12
+ # Now, this will list all columns from the database in your table, which you
13
+ # may not always want to do. You can pass a block of columns to be more specific:
14
+
15
+ # table_for :user do |t|
16
+ # t.column :id
17
+ # t.column :email
18
+ # t.column :created_at
19
+ # end
20
+
21
+ # This will give you a user table with the columns id, email, and created_at.
22
+
23
+ # What if you want to customize the output of the column? Each column can also
24
+ # take a block of content:
25
+
26
+ # table_for :user do |t|
27
+ # t.column :id
28
+ # t.column :email do |c|
29
+ # "<h1>c.email</h1>"
30
+ # end
31
+ # t.column :created_at
32
+ # end
33
+
34
+ # Now, when a block is used to alter the content of a column, the sorting is lost,
35
+ # since the table can no longer assume what is in the column. You need to set a sort_on
36
+ # param to tell the column what to sort by. For example:
37
+
38
+ # table_for :user do |t|
39
+ # t.column :id
40
+ # t.column :email, sort_on: :email do |c|
41
+ # "<h1>c.email</h1>"
42
+ # end
43
+ # t.column :created_at
44
+ # end
45
+
46
+ # Filters
47
+ # You can add basic filter fields to the table by using the filter method. Right now,
48
+ # only one filter can be applied and the filters are search fields. I would like to
49
+ # eventually add different types for different types of data. I would like to eventually
50
+ # add in the ability for multiple filter types with a single search button, but the basic
51
+ # form is all I need at the moment. Ajax enabled filtering would be freaking great as well.
52
+
53
+ # Filter usage:
54
+
55
+ # table_for :user do |t|
56
+ # t.filter :email
57
+ # t.filter :name
58
+ # t.column :id
59
+ # t.column :email
60
+ # t.column :name
61
+ # end
62
+
63
+ # The build_table method will use the other public methods to build a full table. It's
64
+ # possible to construct your own build table method if you want a custom layout.
65
+
66
+
67
+ class TableForPresenter < ActionView::Base
68
+ include ActionView::Helpers::CaptureHelper
69
+ include Haml::Helpers if defined?(Haml)
70
+
71
+ attr_accessor :name, :options
72
+ attr_reader :data
73
+
74
+ def initialize table_name, options = {}, &block
75
+ # self.parent = parent
76
+ self.options = options
77
+ self.name = table_name
78
+ @block = block
79
+
80
+ # required to get capture to work with haml
81
+ init_haml_helpers if defined?(Haml)
82
+
83
+ process_data_attributes
84
+ end
85
+
86
+ # build the complete table with pagination and filters if set.
87
+
88
+ def build_table
89
+ <<-HTML.strip_heredoc.html_safe
90
+ <div class='table-me'>
91
+ #{table_filters}
92
+ <div class="table-me-table #{'with-filters' if table_builder.filters}">
93
+ #{table_pagination.pagination_info}
94
+ <table>
95
+ <thead>
96
+ <tr>#{create_header}</tr>
97
+ </thead>
98
+ <tbody>
99
+ #{table_rows}
100
+ </tbody>
101
+ </table>
102
+ #{table_pagination.pagination_controls}
103
+ </div>
104
+ </div>
105
+ HTML
106
+ end
107
+
108
+ # get data from the table_me_presenter in the controller. This breaks encapsulation and makes
109
+ # this class too tightly coupled to the TableMePresenter, I wanted to keep the table data out
110
+ # of the class variables in the controller and view, but there has to be a better way to do it.
111
+ # TODO decouple this and options below
112
+ def data
113
+ TableMePresenter.data[name.to_s]
114
+ end
115
+
116
+
117
+ # same as data above, only with table options. Ideally this needs to be a value object instead
118
+ # of just a hash. TODO use a value object instead of a hash, see table_vo.rb
119
+ def options
120
+ TableMePresenter.options[name.to_s]
121
+ end
122
+
123
+
124
+ private
125
+
126
+
127
+ # create table filters if they exist
128
+ def table_filters
129
+ <<-HTML if table_builder.filters
130
+ <div class='table-filters'>
131
+ <h3>Filters</h3>
132
+ #{table_builder.filters.map do |filter|
133
+ filter.display
134
+ end.join("\n")}
135
+ #{table_builder.clear_filter}
136
+ </div>
137
+ HTML
138
+ end
139
+
140
+ # create a table pagination object
141
+ def table_pagination
142
+ @table_pagination ||= TablePagination.new(options)
143
+ end
144
+
145
+ # find the class of the data passed in
146
+ def data_class
147
+ data.first.class
148
+ end
149
+
150
+ # create the sortable headers from columns given
151
+ def create_header
152
+ order = options[:order].split(' ')
153
+ table_columns.map do |column|
154
+ if column.sortable
155
+ if order[0] == column.sortable.to_s
156
+ url = TableMe::UrlBuilder.url_for(options, order: "#{column.sortable.to_s} #{order[1].downcase == 'asc' ? 'desc' : 'asc'}")
157
+ klass = order[1]
158
+ else
159
+ url = TableMe::UrlBuilder.url_for(options, order: "#{column.sortable.to_s} asc")
160
+ klass = nil
161
+ end
162
+ "<th><a #{"class='#{klass}'" if klass} href='#{url}'>#{column.name.to_s.split('_').join(' ').titleize}</a></th>"
163
+ else
164
+ "<th>#{column.name.to_s.split('_').join(' ').titleize}</th>"
165
+ end
166
+ end.join.html_safe
167
+ end
168
+
169
+ # create table rows based on data for each row using columns as a template
170
+ def table_rows
171
+ data.map do |d|
172
+ <<-HTML
173
+ <tr>
174
+ #{table_column_for(d)}
175
+ </tr>
176
+ HTML
177
+ end.join.html_safe
178
+ end
179
+
180
+ # it would ne nicer to encapsulate this into the column class, but then we have
181
+ # to set it up like a view so capture works. For now, I'll leave this here, I'm not
182
+ # sure if the encapsulation is worth the overhead
183
+ def table_column_for data
184
+ table_columns.map do |column|
185
+ if column.content
186
+ "<td>#{capture(data, &column.content)}</td>"
187
+ else
188
+ "<td>#{data[column.name]}</td>"
189
+ end
190
+ end.join
191
+ end
192
+
193
+ # create a table builder instance
194
+ def table_builder
195
+ @builder ||= TableMe::Builder.new(options)
196
+ end
197
+
198
+ # get column names from the table_builder
199
+ def col_names
200
+ table_builder.names
201
+ end
202
+
203
+ # get table columns from the table_builder
204
+ def table_columns
205
+ table_builder.columns
206
+ end
207
+
208
+ # pass in the block given to the table_for_presenter if it exists
209
+ # else just create a column for every column in the data object
210
+ def process_data_attributes
211
+ if @block
212
+ capture(table_builder, &@block)
213
+ if table_builder.columns.empty?
214
+ build_all_columns
215
+ end
216
+ else
217
+ build_all_columns
218
+ end
219
+ end
220
+
221
+ # create a column for every column in the data object
222
+ def build_all_columns
223
+ data.first.attribute_names.each do |attribute|
224
+ table_builder.column(attribute)
225
+ end
226
+ end
227
+
228
+ end
229
+ end
@@ -0,0 +1,28 @@
1
+ require_relative '../../../table_me/table_me_presenter'
2
+ module TableMe
3
+
4
+ # Controller
5
+ # A table must first be created in your controller using the table_me method.
6
+
7
+ # table_me(collection, options)
8
+ # The collection can be two things, first an ActiveRecord::Relation, which
9
+ # is the result of some sort of active record query ex:
10
+
11
+ # table_me( User.where(subscribed: true) )
12
+ # Keep in mind that doing User.all just returns an array of objects, not the a relation.
13
+
14
+ # In order to do the equivalent of the .all just pass in the ActiveRecord class:
15
+
16
+ # table_me( User )
17
+ # Possible options available for this method are:
18
+
19
+ # name - Label for the the table
20
+ # per_page - The amount of items per page of the table
21
+
22
+ module TableMeHelper
23
+ def table_me(model, options = {})
24
+ table_presenter = TableMePresenter.new(model, options,params)
25
+ table_presenter.name
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,105 @@
1
+ require_relative 'url_parser'
2
+ module TableMe
3
+
4
+ # table_me(collection, options)
5
+ # The collection can be two things, first an ActiveRecord::Relation, which
6
+ # is the result of some sort of active record query ex:
7
+
8
+ # table_me( User.where(subscribed: true) )
9
+ # Keep in mind that doing User.all just returns an array of objects, not the a relation.
10
+
11
+ # In order to do the equivalent of the .all just pass in the ActiveRecord class:
12
+
13
+ # table_me( User )
14
+ # Possible options available for this method are:
15
+
16
+ # name - Label for the the table
17
+ # per_page - The amount of items per page of the table
18
+ class TableMePresenter
19
+ attr_accessor :params, :name
20
+ attr_reader :data, :options
21
+
22
+ class << self
23
+ def data
24
+ @@data
25
+ end
26
+
27
+ def options
28
+ @@options
29
+ end
30
+ end
31
+
32
+ @@data = {}
33
+ @@options = {}
34
+
35
+ def initialize model, options = {}, params = {}
36
+ # this was more of a patch for the code. I need to go back through and normalize all
37
+ # the hash access so a normal has can be used with confidence
38
+ # TODO normalize hash access to strings or symbols
39
+ @options = ActiveSupport::HashWithIndifferentAccess.new(options)
40
+
41
+ set_defaults_for model
42
+ parse_params_for params
43
+ get_data_for model
44
+ @@options[self.name] = @options
45
+ end
46
+
47
+ # parse the params into an options hash that we can use
48
+ def parse_params_for params
49
+ options.merge! URLParser.parse_params_for(params, self.name)
50
+ end
51
+
52
+ # set defaults for options
53
+ def set_defaults_for model
54
+ options[:page] = 1
55
+ options[:per_page] ||= 10
56
+ options[:name] ||= model.to_s.downcase
57
+ options[:order] ||= 'created_at ASC'
58
+ self.name = options[:name]
59
+ end
60
+
61
+ # make the model queries to pull back the data based on pagination and search results if given
62
+ def get_data_for model
63
+ model = apply_search_to(model)
64
+
65
+ @@data[self.name] = @data = model.limit(options[:per_page])
66
+ .offset(start_item)
67
+ .order(options[:order])
68
+
69
+ options[:total_count] = model.count
70
+ options[:page_total] = (options[:total_count] / options[:per_page].to_f).ceil
71
+ end
72
+
73
+ # Apply the search query to the appropriate table columns. This is sort of ugly at the moment
74
+ # and not as reliable as it could be. It needs to be refactored to account for different column
75
+ # types and use appropriate search methods. Ex. LIKE doesn't work for integers
76
+ # TODO refactor this to be more reliable for all column types
77
+ def apply_search_to model
78
+ if options[:search]
79
+ if options[:new_search]
80
+ options[:page] = 1
81
+ options.delete(:new_search)
82
+ end
83
+
84
+ column_hash = model.columns_hash || model.class.columns_hash
85
+
86
+ if column_hash[options[:search][:column].to_s].sql_type.include?('char')
87
+ model.where(model.arel_table[options[:search][:column]].matches("%#{options[:search][:query]}%"))
88
+ else
89
+ model.where(options[:search][:column].to_sym => options[:search][:query])
90
+ end
91
+ else
92
+ model
93
+ end
94
+ end
95
+
96
+ # beginning item for the offset relation call
97
+ def start_item
98
+ (options[:page].to_i - 1) * options[:per_page].to_i
99
+ end
100
+
101
+ def name= value
102
+ self.options[:name] = @name = value
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,110 @@
1
+ require 'cgi'
2
+ require_relative 'url_builder'
3
+ module TableMe
4
+
5
+ # This handles the pagination elements of the table
6
+ class TablePagination
7
+ attr_accessor :options
8
+
9
+ def initialize table_options
10
+ self.options = table_options
11
+ end
12
+
13
+ # Information at the top of the table displaying the table name and
14
+ # position page/item wise out of a total.
15
+ def pagination_info
16
+ <<-HTML.strip_heredoc
17
+ <div class='table-me-pagination-info'>
18
+ <h3>#{options[:name].split('_').join(' ').titleize}</h3> <p><b>#{options[:page]}</b> of <b>#{options[:page_total]}</b> out of a total <b>#{options[:total_count]}</b></p>
19
+ </div>
20
+ HTML
21
+ end
22
+
23
+ # Adds controls at the bottom of the table for previous and next and a 5 number range
24
+ # TODO Refactor so controllers are hidden when pages on unavailable. IE, if your on page 1
25
+ # you shouldn't be able to see a previous button, or if your on the last page you shouldn't
26
+ # be able to see the next button
27
+ def pagination_controls
28
+ <<-HTML.strip_heredoc
29
+ <div class='table-me-pagination-controls'>
30
+ <a href="#{prev_page_url}" class='previous'>&laquo; Prev</a> #{pagination_number_list} <a href="#{next_page_url}" class='next'>Next &raquo;</a>
31
+ </div>
32
+ HTML
33
+ end
34
+
35
+ def next_page_url
36
+ page = if current_page == total_pages
37
+ total_pages
38
+ else
39
+ current_page + 1
40
+ end
41
+
42
+ link_for_page page
43
+ end
44
+
45
+ def prev_page_url
46
+ page = if current_page == 0
47
+ 0
48
+ else
49
+ current_page - 1
50
+ end
51
+
52
+ link_for_page page
53
+ end
54
+
55
+ # List of number links for the number range between next and previous
56
+ def pagination_number_list
57
+ (0...page_button_count).to_a.map do |n|
58
+ link_number = n + page_number_offset
59
+ number_span(link_number)
60
+ end.join(' ')
61
+ end
62
+
63
+ private
64
+
65
+ def number_span link_number
66
+ if current_page.to_s == link_number.to_s
67
+ <<-HTML.strip_heredoc
68
+ <span class='page current'>#{link_number}</span>
69
+ HTML
70
+ else
71
+ <<-HTML.strip_heredoc
72
+ <span class='page'><a href='#{link_for_page(link_number)}'>#{link_number}</a></span>
73
+ HTML
74
+ end
75
+ end
76
+
77
+ def link_for_page page
78
+ TableMe::UrlBuilder.url_for options, page: page
79
+ end
80
+
81
+ def current_page
82
+ options[:page].to_i
83
+ end
84
+
85
+ def total_pages
86
+ options[:page_total]
87
+ end
88
+
89
+ def page_number_offset
90
+ if current_page >= total_pages - 2
91
+ current_page - 4 + (total_pages - current_page)
92
+ elsif current_page <= 2
93
+ 1
94
+ else
95
+ current_page - 2
96
+ end
97
+ end
98
+
99
+ def page_button_count
100
+ if total_pages > 5
101
+ 5
102
+ elsif total_pages > 1
103
+ total_pages
104
+ else
105
+ 0
106
+ end
107
+ end
108
+
109
+ end
110
+ end
@@ -0,0 +1,48 @@
1
+ # I was going to use this instead of a hash which is passed around, but it started getting too complex
2
+ # for a generic value object, so I might do something else later, not worth it at this time.
3
+ module TableMe
4
+ class TableVO
5
+ def self.vo_attr_accessor *args
6
+ @@set_methods = *args
7
+ attr_accessor *args
8
+ end
9
+
10
+ vo_attr_accessor :page, :page_total, :search,
11
+ :order, :name, :per_page,
12
+ :total_count, :other_tables
13
+
14
+ def initialize params = {}
15
+ self.merge! params
16
+ end
17
+
18
+ def []= key, value
19
+ self.method("#{key}=").call(value)
20
+ end
21
+
22
+ def [] key
23
+ self.method(key).call
24
+ end
25
+
26
+ def merge! object
27
+ object.each do |k,v|
28
+ self.method("#{k}=").call(v) unless v.nil?
29
+ end
30
+ self
31
+ end
32
+
33
+ def each
34
+ @@set_methods.each do |method|
35
+ yield(method,self.method(method).call)
36
+ end
37
+ end
38
+
39
+ def to_hash
40
+ hash = {}
41
+ @@set_methods.each do |method|
42
+ hash[method] = method(method).call
43
+ end
44
+ hash
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,33 @@
1
+ require 'cgi'
2
+ module TableMe
3
+
4
+ # This class builds the url needed for the tables based on the table options.
5
+ # Some options are filtered out to make the url as short as possible.
6
+ class UrlBuilder
7
+ class << self
8
+
9
+ def url_for options, additional_options = {}
10
+ table_options = options.merge additional_options
11
+
12
+ url = []
13
+ filter_options(table_options).each do |option|
14
+ url << {"tm_#{option[:name]}".to_sym => option }.to_param
15
+ end
16
+ "?#{url.join('&')}"
17
+ end
18
+
19
+ # Filter out all options except name page search and order
20
+ # We need to dup the objects so we don't alter the options
21
+ # object through out the table_me module
22
+ def filter_options options
23
+ other_tables = options[:other_tables].dup || []
24
+ temp_options = options.dup
25
+ temp_options.keep_if do |k,v|
26
+ ['name','page','search','order'].include? k.to_s
27
+ end
28
+ other_tables << temp_options
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,38 @@
1
+ require 'cgi'
2
+ require_relative 'url_builder'
3
+ module TableMe
4
+ # Parse the url params into the hash needed for table_me to work. Take the tables
5
+ # which aren't the current one and save them as other_tables so we can persist
6
+ # their states in links.
7
+ class URLParser
8
+ def self.parse_params_for params, name
9
+ @@name = name
10
+ parse_table_me(params)
11
+ end
12
+
13
+
14
+ private
15
+
16
+
17
+ def self.parse_table_me params
18
+ table_options = {}
19
+ other_tables = []
20
+ params.each do |k,v|
21
+ if k.to_s.include? 'tm_'
22
+ if v[:name] == @@name.to_s
23
+ table_options = v
24
+ else
25
+ other_tables << v
26
+ end
27
+ end
28
+ end
29
+ table_options[:other_tables] = other_tables
30
+ table_options
31
+ end
32
+
33
+ def self.name
34
+ @@name
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,3 @@
1
+ module TableMe
2
+ VERSION = "0.0.1"
3
+ end
data/lib/table_me.rb ADDED
@@ -0,0 +1,15 @@
1
+ require "active_support/dependencies"
2
+
3
+ module TableMe
4
+
5
+ mattr_accessor :app_root
6
+
7
+ # Yield self on setup for nice config blocks
8
+ def self.setup
9
+ yield self
10
+ end
11
+
12
+ end
13
+
14
+ # Require our engine
15
+ require "table_me/engine"
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :table_me do
3
+ # # Task goes here
4
+ # end