acts_as_data_table 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 (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