acts_as_data_table 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. data/.gitignore +18 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +55 -0
  5. data/Rakefile +11 -0
  6. data/acts_as_data_table.gemspec +25 -0
  7. data/app/coffeescripts/acts_as_data_table.coffee +47 -0
  8. data/app/helpers/acts_as_data_table_helper.rb +170 -0
  9. data/config/locales/acts_as_data_table.en.yml +11 -0
  10. data/generators/acts_as_data_table/acts_as_data_table_generator.rb +13 -0
  11. data/generators/acts_as_data_table/templates/assets/js/acts_as_data_table.js +37 -0
  12. data/init.rb +1 -0
  13. data/lib/acts_as_data_table/multi_column_scopes.rb +116 -0
  14. data/lib/acts_as_data_table/scope_filters/action_controller.rb +139 -0
  15. data/lib/acts_as_data_table/scope_filters/active_record.rb +264 -0
  16. data/lib/acts_as_data_table/scope_filters/form_helper.rb +67 -0
  17. data/lib/acts_as_data_table/scope_filters/validator.rb +144 -0
  18. data/lib/acts_as_data_table/session_helper.rb +193 -0
  19. data/lib/acts_as_data_table/shared/action_controller.rb +25 -0
  20. data/lib/acts_as_data_table/shared/session.rb +312 -0
  21. data/lib/acts_as_data_table/sortable_columns/action_controller.rb +111 -0
  22. data/lib/acts_as_data_table/sortable_columns/active_record.rb +34 -0
  23. data/lib/acts_as_data_table/sortable_columns/renderers/bootstrap2.rb +17 -0
  24. data/lib/acts_as_data_table/sortable_columns/renderers/default.rb +82 -0
  25. data/lib/acts_as_data_table/version.rb +3 -0
  26. data/lib/acts_as_data_table.rb +71 -0
  27. data/lib/acts_as_data_table_helper.rb.bak +165 -0
  28. data/lib/named_scope_filters.rb +273 -0
  29. data/lib/tasks/acts_as_data_table.rake +4 -0
  30. data/rails/init.rb +4 -0
  31. data/test/acts_as_searchable_test.rb +8 -0
  32. data/test/test_helper.rb +4 -0
  33. metadata +142 -0
@@ -0,0 +1,312 @@
1
+ module Acts
2
+ module DataTable
3
+ module Shared
4
+ class Session
5
+
6
+ def initialize(session, controller_path, action_name)
7
+ @session = session
8
+ @controller_path = controller_path
9
+ @action_name = action_name
10
+ end
11
+
12
+ def sf_session
13
+ @session[:scope_filters] ||= {}
14
+ end
15
+
16
+ def sc_session
17
+ @session[:sortable_columns] ||= {}
18
+ end
19
+
20
+ def model
21
+ Acts::DataTable::ScopeFilters::ActionController.get_request_model
22
+ end
23
+
24
+ def errors
25
+ @errors ||= {}
26
+ end
27
+
28
+ def errors_on(context)
29
+ errors[context.to_s] || []
30
+ end
31
+
32
+ #----------------------------------------------------------------
33
+ # Filter Management
34
+ #----------------------------------------------------------------
35
+
36
+ #
37
+ # @return [Hash] all active filters for the current controller action
38
+ # by the group they are registered in.
39
+ #
40
+ def active_filters
41
+ sf_session[current_action_key] || {}
42
+ end
43
+
44
+ #
45
+ # Checks whether the given filter is currently active
46
+ # Note that it also checks if it is active with exactly the given arguments.
47
+ #
48
+ # @return [TrueClass, FalseClass] +true+ if the filter is active AND
49
+ # the given +args+ match the ones used in the filter.
50
+ #
51
+ def active_filter?(group, scope, args)
52
+ args ||= {}
53
+ used_args = Acts::DataTable.lookup_nested_hash(active_filters, group.to_s, scope.to_s)
54
+ used_args && (args.stringify_keys.to_a - used_args.to_a).empty?
55
+ end
56
+
57
+ #
58
+ # @return [String, NilClass] The name of the scope filter which is
59
+ # currently active in the given group or +nil+ if no filter from this group is
60
+ # currently active
61
+ #
62
+ def active_filter(group)
63
+ Acts::DataTable.lookup_nested_hash(active_filters, group.to_s).try(:keys).try(:first)
64
+ end
65
+
66
+ #
67
+ # Adds a new filter to the current controller action
68
+ # Before the filter is added, the following things are checked to ensure
69
+ # that no invalid filter is added (which could cause reoccurring errors in the application)
70
+ #
71
+ # 1. The given +scope+ has to be registered in the currently used model
72
+ # 2. The given arguments have to be sufficient for the given +scope+
73
+ # 3. The +scope+ has to pass the set up validation check
74
+ #
75
+ # @param [String] group
76
+ # The group the given +scope+ is part of in the current #model
77
+ #
78
+ # @param [String] scope
79
+ # The scope name within +group+ and #model
80
+ #
81
+ # @param [Hash] args
82
+ # Arguments to be passed to the scope. They have to be in the format
83
+ # {arg_name => arg_value} as set up in the model. This is necessary
84
+ # to make validations as easy as possible.
85
+ #
86
+ def add_filter(group, scope, args)
87
+ reset_errors!
88
+
89
+ #Ensure that the argument hash is set properly. The following methods
90
+ #might fail for filters which do not require arguments otherwise.
91
+ args = {} unless args.is_a?(Hash)
92
+
93
+ #Check whether the given filter was registered properly in the model
94
+ unless Acts::DataTable::ScopeFilters::ActiveRecord.registered_filter?(model, group, scope)
95
+ add_error group, Acts::DataTable.t('scope_filters.add_filter.filter_not_registered', :model => model.name, :group => group, :scope_name => scope)
96
+ return false
97
+ end
98
+
99
+ #Check whether the given arguments are sufficient for the given filter
100
+ unless Acts::DataTable::ScopeFilters::ActiveRecord.matching_arity?(model, group, scope, args.size)
101
+ add_error group, Acts::DataTable.t('scope_filters.add_filter.non_matching_arity', :model => model.name, :group => group, :scope_name => scope)
102
+ return false
103
+ end
104
+
105
+ #Run possible validation methods on the given filter and add generated error messages
106
+ if (errors = Acts::DataTable::ScopeFilters::Validator.new(model, group, scope, args).validate).any?
107
+ errors.each {|e| add_error(group, e)}
108
+ return false
109
+ end
110
+
111
+ #Add the new filter to the session
112
+ current_action_session[group.to_s] = {scope.to_s => args.stringify_keys}
113
+ true
114
+ end
115
+
116
+ #
117
+ # Removes the given filter group from the current controller action
118
+ # It is sufficient to only specify the group here as only one filter in a group
119
+ # may be active at a time.
120
+ #
121
+ def remove_filter!(group)
122
+ current_action_session.delete(group.to_s)
123
+ end
124
+
125
+ #
126
+ # Resets all filters for the current controller action
127
+ #
128
+ def remove_all_filters!
129
+ sf_session[current_action_key] = {}
130
+ end
131
+
132
+ #----------------------------------------------------------------
133
+ # Sortable Columns
134
+ #----------------------------------------------------------------
135
+
136
+ #
137
+ # @return [Array] The sorting columns for the current request
138
+ # (controller + action) in the format [['col1', 'dir1'], ['col2', 'dir2'], ...]
139
+ #
140
+ # If no columns are set, the given default ones are used.
141
+ #
142
+ def active_columns
143
+ current_action_session(sc_session, [])
144
+ end
145
+
146
+ #
147
+ # Adds or removes a column from the current sorting columns
148
+ # This happens whenever the user decides to sort a table by multiple columns
149
+ #
150
+ def toggle_column!(model_name, column_name)
151
+ if active_column?(model_name, column_name)
152
+ remove_column!(model_name, column_name)
153
+ else
154
+ add_column!(model_name, column_name)
155
+ end
156
+ end
157
+
158
+ #
159
+ # Changes the sorting direction for the given column
160
+ # If no direction is given, it will change it to the opposite of the current direction
161
+ #
162
+ def change_direction!(model_name, column_name, direction = nil)
163
+ if active_column?(model_name, column_name)
164
+ ca = column_array(model_name, column_name)
165
+ ca[1] = (direction || opposite_direction(ca.last)).to_s.upcase
166
+ end
167
+ end
168
+
169
+ #
170
+ # Replaces all current sorting columns with the given one
171
+ #
172
+ def set_base_column!(model, column, direction = nil)
173
+ reset_columns!
174
+ add_column!(model, column, direction)
175
+ end
176
+
177
+ #
178
+ # Sets all sorting columns for the current controller action at once.
179
+ # This can be used when supplying the user with a form to choose
180
+ # the sorting in a separate area of the page instead of clicking
181
+ # on table column headers and adding one column after the other
182
+ #
183
+ # @param [Array<String>] columns
184
+ # A 2D array of the form [['model_name', 'column_name', 'direction'], ...]
185
+ #
186
+ def set_columns!(columns)
187
+ reset_columns!
188
+ columns.each do |model, column, direction|
189
+ add_column!(model, column, direction)
190
+ end
191
+ end
192
+
193
+ #
194
+ # Retrieves the current sorting direction for a given column.
195
+ #
196
+ # @return [String, NilClass] 'ASC' or 'DESC' if the given column
197
+ # is currently active, +nil+ otherwise
198
+ #
199
+ def sorting_direction(model_name, column_name)
200
+ column_array(model_name, column_name).try(:last)
201
+ end
202
+
203
+ #
204
+ # @return [TrueClass, FalseClass] +true+ if the given column is currently
205
+ # used for sorting
206
+ #
207
+ def active_column?(model_name, column_name)
208
+ !!column_array(model_name, column_name)
209
+ end
210
+
211
+ private
212
+
213
+ #
214
+ # Removes all current sorting columns
215
+ #
216
+ def reset_columns!
217
+ sc_session[current_action_key] = []
218
+ end
219
+
220
+ #
221
+ # @return [String] the opposite direction to the given one
222
+ #
223
+ def opposite_direction(direction)
224
+ direction.downcase == 'asc' ? 'DESC' : 'ASC'
225
+ end
226
+
227
+ #
228
+ # Removes the given column from the current sorting
229
+ #
230
+ def remove_column!(model_name, column_name)
231
+ current_action_session(sc_session, []).delete(column_array(model_name, column_name))
232
+ end
233
+
234
+ #
235
+ # Adds the given column to the current sorting
236
+ #
237
+ def add_column!(model_name, column_name, direction = nil)
238
+ direction ||= 'ASC'
239
+ d = column_data(model_name, column_name)
240
+ current_action_session(sc_session, []).push([d[:column], direction.to_s.upcase])
241
+ end
242
+
243
+ #
244
+ # Retrieves the array consisting of column name and direction from the session
245
+ #
246
+ # @return [Array, NilClass] Either an array of the form ['table.column', 'direction']
247
+ # or +nil+ if the given column is not part of the current sorting
248
+ #
249
+ def column_array(model_name, column_name)
250
+ current_action_session(sc_session, []).assoc(column_data(model_name, column_name)[:column])
251
+ end
252
+
253
+ #
254
+ # @return [ActiveRecord::Base] The constantized model
255
+ #
256
+ def get_model(model_name)
257
+ model_name.to_s.camelize.constantize
258
+ end
259
+
260
+ #
261
+ # @return [Hash] The constantized model and column name with table prefix
262
+ #
263
+ def column_data(model_name, column_name)
264
+ m = get_model(model_name)
265
+ column = "#{m.table_name}.#{column_name}"
266
+ {:model => m, :column => column}
267
+ end
268
+
269
+ def current_action_session(s = sf_session, default = {})
270
+ s[current_action_key] ||= default
271
+ end
272
+
273
+ #
274
+ # Clears old error messages. This is useful whenever only error messages from
275
+ # a current action should be retrieved.
276
+ #
277
+ def reset_errors!
278
+ @errors = {}
279
+ end
280
+
281
+ #
282
+ # Adds an error message to the errors array
283
+ #
284
+ # @param [String] context
285
+ # Context the error occurred in, e.g. a scope filter group
286
+ #
287
+ # @param [String] message
288
+ # The error message to be added.
289
+ #
290
+ def add_error(context, message)
291
+ errors[context.to_s] ||= []
292
+ errors[context.to_s] << message
293
+ end
294
+
295
+ #
296
+ # Generates a key from the given controller and action name
297
+ #
298
+ def action_key(controller, action)
299
+ [controller.gsub('/', '_'), action].join('_')
300
+ end
301
+
302
+ #
303
+ # @see #action_key, uses the current controller path and action name
304
+ #
305
+ def current_action_key
306
+ action_key(@controller_path, @action_name)
307
+ end
308
+
309
+ end
310
+ end
311
+ end
312
+ end
@@ -0,0 +1,111 @@
1
+ module Acts
2
+ module DataTable
3
+ module SortableColumns
4
+ module ActionController
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ #
12
+ # Sets up automatic column sorting for this controller
13
+ #
14
+ # @param [Hash] options
15
+ # Options to specify which controller actions should have column sorting
16
+ # and to customize the filter's behaviour.
17
+ # Options are everything that #around_filter would accept
18
+ #
19
+ # @option options [Hash] :default
20
+ # Default sorting columns for different actions.
21
+ # If none are specified for an action, the default order (usually 'id ASC') is used
22
+ #
23
+ # @example Set up automatic column sorting only for the index action
24
+ # with the default ordering "deleted_at ASC, name ASC"
25
+ #
26
+ # sortable_columns :only => [:index], :default => {:index => [['deleted_at', 'ASC'], ['name', 'ASC']]}
27
+ #
28
+ def sortable_columns(options = {})
29
+ #Include on-demand methods
30
+ include Acts::DataTable::Shared::ActionController::OnDemand
31
+
32
+ defaults = (options.delete(:default) || {}).stringify_keys
33
+
34
+ around_filter(options) do |controller, block|
35
+
36
+ af_params = controller.request.params[:sortable_columns]
37
+ request_defaults = defaults[controller.action_name.to_s] || []
38
+
39
+ begin
40
+ #Ensure that the given action is valid
41
+ if af_params.present? && %w(toggle change_direction set_base set).include?(af_params[:action].to_s)
42
+ case af_action = af_params[:action].to_s
43
+ when 'toggle'
44
+ controller.acts_as_data_table_session.toggle_column!(af_params[:model], af_params[:column])
45
+ when 'change_direction'
46
+ controller.acts_as_data_table_session.change_direction!(af_params[:model], af_params[:column])
47
+ when 'set_base'
48
+ controller.acts_as_data_table_session.set_base_column!(af_params[:model], af_params[:column])
49
+ when 'set'
50
+ controller.acts_as_data_table_session.set_columns!(af_params[:columns])
51
+ else
52
+ raise ArgumentError.new "Invalid scope filter action '#{af_action}' was given."
53
+ end
54
+ end
55
+
56
+ #Set the defaults as sorting columns none were set by the user
57
+ if controller.acts_as_data_table_session.active_columns.empty?
58
+ controller.acts_as_data_table_session.set_columns!(request_defaults)
59
+ end
60
+
61
+ #Set the updated filters
62
+ Acts::DataTable::SortableColumns::ActionController.set_request_sort_columns!(controller.acts_as_data_table_session.active_columns)
63
+ block.call
64
+ ensure
65
+ Acts::DataTable::SortableColumns::ActionController.clear_request_sort_columns!
66
+ end
67
+
68
+ end
69
+ end
70
+ end
71
+
72
+ #
73
+ # Returns the currently active sortable columns.
74
+ # This function should only be used when the automatic scope +with_sortable_columns+
75
+ # is not working due to a different execution time or thread, e.g. a background worker.
76
+ #
77
+ def current_sortable_columns
78
+ Acts::DataTable::SortableColumns::ActionController.get_request_sort_columns
79
+ end
80
+
81
+ #
82
+ # Retrieves the columns to order by for the current request from the thread space. This is used in the
83
+ # model's scope, so no string has to be supplied in the controller action manually.
84
+ #
85
+ # @return [Array<String>] the columns and their sorting directions in the format
86
+ # [["col1", "dir1"], ["col2", "dir2"], ...]
87
+ #
88
+ def self.get_request_sort_columns
89
+ Thread.current[:sortable_columns] || []
90
+ end
91
+
92
+ #
93
+ # Sets the columns to order by for the current request
94
+ # in the thread space
95
+ #
96
+ # @param [Array<String>] columns
97
+ #
98
+ def self.set_request_sort_columns!(columns)
99
+ Thread.current[:sortable_columns] = columns
100
+ end
101
+
102
+ #
103
+ # Deletes the current sort columns from the thread space
104
+ #
105
+ def self.clear_request_sort_columns!
106
+ Thread.current[:sortable_columns] = nil
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,34 @@
1
+ module Acts
2
+ module DataTable
3
+ module SortableColumns
4
+ module ActiveRecord
5
+ def self.included(base)
6
+
7
+ #
8
+ # Scope which applies the currently active sorting directions.
9
+ # The sorting columns are automatically fetched from the current thread space,
10
+ # however, it is also possible to pass these values in as first argument.
11
+ # This should only be done if absolutely necessary, e.g. if
12
+ # the calculation happens in a different time or thread as it would in a
13
+ # background calculation.
14
+ #
15
+ base.named_scope :with_sortable_columns, lambda {|*args|
16
+ sort_columns = args.first
17
+ sort_columns ||= Acts::DataTable::SortableColumns::ActionController.get_request_sort_columns
18
+
19
+ if sort_columns.any?
20
+ sort_string = sort_columns.map {|col, dir| "#{col} #{dir}"}.join(', ')
21
+ {:order => sort_string}
22
+ else
23
+ {}
24
+ end
25
+ }
26
+
27
+ end
28
+
29
+
30
+
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,17 @@
1
+ module Acts
2
+ module DataTable
3
+ module SortableColumns
4
+ module Renderers
5
+ class Bootstrap2 < Default
6
+ def direction_indicator
7
+ if @sortable.direction == 'ASC'
8
+ @action_view.content_tag(:i, nil, :class => 'icon-sort-by-alphabet')
9
+ else
10
+ @action_view.content_tag(:i, nil, :class => 'icon-sort-by-alphabet-alt')
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,82 @@
1
+ #
2
+ # Default renderer for ActsAsDataTable sortable column links.
3
+ # It uses the javascript library shipped with the gem and does not depend on
4
+ # external sources like javascripts or additional stylesheets.
5
+ #
6
+ module Acts
7
+ module DataTable
8
+ module SortableColumns
9
+ module Renderers
10
+ def self.default_renderer
11
+ @@default_renderer ||= 'Acts::DataTable::SortableColumns::Renderers::Default'
12
+ end
13
+
14
+ def self.default_renderer=(renderer)
15
+ @@default_renderer = renderer.to_s
16
+ end
17
+
18
+ class Default
19
+ def initialize(sortable, action_view)
20
+ @action_view = action_view
21
+ @sortable = sortable
22
+ end
23
+
24
+ #
25
+ # @return [String] an indicator about the sorting direction for the current column.
26
+ # The direction is either 'ASC' or 'DESC'
27
+ #
28
+ def direction_indicator
29
+ @sortable.direction == 'ASC' ? '&Delta;' : '&nabla;'
30
+ end
31
+
32
+ #
33
+ # @return [String] The column header's caption
34
+ #
35
+ def caption
36
+ @sortable.caption
37
+ end
38
+
39
+ #
40
+ # @return [String] a link to change the sorting direction for an already active column
41
+ #
42
+ def direction_link
43
+ link_options = @sortable.html_options.clone
44
+ link_options['data-init'] = 'sortable-column-direction'
45
+ link_options['data-remote'] = @sortable.remote
46
+ link_options['data-url-change-direction'] = @sortable.urls.change_direction
47
+ @action_view.link_to(direction_indicator, '#', link_options)
48
+ end
49
+
50
+ #
51
+ # @return [String] a link to toggle a column
52
+ #
53
+ def caption_link
54
+ link_options = @sortable.html_options.clone
55
+ link_options['data-init'] = 'sortable-column'
56
+ link_options['data-remote'] = @sortable.remote
57
+ link_options['data-url-toggle'] = @sortable.urls.toggle
58
+ link_options['data-url-set-base'] = @sortable.urls.set_base
59
+ link_options['data-url-change-direction'] = @sortable.urls.change_direction
60
+ link_options['data-active'] = 'true' if @sortable.active
61
+
62
+ @action_view.link_to(@sortable.caption, '#', link_options)
63
+ end
64
+
65
+ #
66
+ # Generates the actual HTML (= caption and direction links)
67
+ # to be embedded into the view
68
+ #
69
+ # @return [String] the generated HTML code
70
+ #
71
+ def to_html
72
+ if @sortable.active
73
+ caption_link + ' ' + direction_link
74
+ else
75
+ caption_link
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,3 @@
1
+ module ActsAsDataTable
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,71 @@
1
+ require 'acts_as_data_table/version'
2
+
3
+ require 'acts_as_data_table/multi_column_scopes'
4
+
5
+ require 'acts_as_data_table/shared/session'
6
+ require 'acts_as_data_table/shared/action_controller'
7
+
8
+ require 'acts_as_data_table/scope_filters/action_controller'
9
+ require 'acts_as_data_table/scope_filters/active_record'
10
+ require 'acts_as_data_table/scope_filters/validator'
11
+
12
+ require 'acts_as_data_table/sortable_columns/action_controller'
13
+ require 'acts_as_data_table/sortable_columns/active_record'
14
+ require 'acts_as_data_table/scope_filters/form_helper'
15
+
16
+ #Sortable Column Renderers
17
+ require 'acts_as_data_table/sortable_columns/renderers/default'
18
+ require 'acts_as_data_table/sortable_columns/renderers/bootstrap2'
19
+
20
+ module ActsAsDataTable
21
+ end
22
+
23
+ module Acts
24
+ module DataTable
25
+ I18n_LOCALES = %w(en)
26
+
27
+ def self.log(level, message)
28
+ Rails.logger.send(level, "Acts::DataTable [#{level}] -- #{message}")
29
+ end
30
+
31
+ def self.ensure_nested_hash!(hash, *keys)
32
+ h = hash
33
+ keys.each do |key|
34
+ h[key] ||= {}
35
+ h = h[key]
36
+ end
37
+ end
38
+
39
+ def self.lookup_nested_hash(hash, *keys)
40
+ return nil if hash.nil?
41
+
42
+ h = hash
43
+ keys.each do |key|
44
+ return nil if h[key].nil?
45
+ h = h[key]
46
+ end
47
+ h
48
+ end
49
+
50
+ #
51
+ # Retrieves a value from the gem's locale namespace.
52
+ # If there are no translations for the application's locale, the
53
+ # english versions are used.
54
+ #
55
+ def self.t(key, options = {})
56
+ locale = I18n_LOCALES.include?(I18n.locale.to_s) ? I18n.locale : 'en'
57
+ I18n.t(key, options.merge({:scope => 'acts_as_data_table', :locale => locale}))
58
+ end
59
+ end
60
+ end
61
+
62
+ ActiveRecord::Base.class_eval do
63
+ include Acts::DataTable::MultiColumnScopes
64
+ include Acts::DataTable::ScopeFilters::ActiveRecord
65
+ include Acts::DataTable::SortableColumns::ActiveRecord
66
+ end
67
+
68
+ ActionController::Base.class_eval do
69
+ include Acts::DataTable::ScopeFilters::ActionController
70
+ include Acts::DataTable::SortableColumns::ActionController
71
+ end