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,264 @@
1
+ module Acts
2
+ module DataTable
3
+ module ScopeFilters
4
+ module ActiveRecord
5
+ def self.included(base)
6
+ base.send :extend, ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ #
11
+ # Generates a scope filter in the given group, meaning that only one of the
12
+ # filters in the same group may be active at one time.
13
+ #
14
+ # @param [String, Symbol] group
15
+ # The group name the new filter should be generated in
16
+ #
17
+ # @param [String, Symbol] scope
18
+ # The scope name the filter depends on
19
+ #
20
+ # @param [Hash] options
21
+ # Additional options to customize the generated filter
22
+ #
23
+ # ----
24
+ #
25
+ # @option options [Array<String, Symbol>] :args ([])
26
+ # Arguments needed by the scope. This is the case if the scope consists
27
+ # of a lambda object.
28
+ # The argument names have to be given in the same order as the scope function's
29
+ # formal parameters.
30
+ #
31
+ # @option options [Symbol, String, Proc] :caption (nil)
32
+ # The caption displayed if the filter is used in the application
33
+ # The caption is either hardcoded or interpreted based on the given type:
34
+ #
35
+ # - String: The given string is used as is
36
+ # - Symbol: The system expects a function with the given name to be defined in the current model.
37
+ # It is called with 3 arguments:
38
+ # 1. The group name
39
+ # 2. The scope name
40
+ # 3. The arguments the scope was called with (if any)
41
+ # - Proc: The given proc is executed with a hash containing the given arguments (if any)
42
+ #
43
+ # If nothing is given, the default caption is used, meaining the system will use I18n.t
44
+ # to search for a defined scope name within the scope 'activerecord.scope_filters.scopes.model.SCOPE'
45
+ # Possible scope arguments are passed in as strings
46
+ #
47
+ # @option options [Proc, Symbol] :validate (nil)
48
+ # A validation method for the scope filter. The filter is only applied if the validation method returns +true+
49
+ # This is useful if e.g. a scope needs two dates to work correctly and should only be applied
50
+ # if the dates have the correct format.
51
+ # If a Symbol is given, the system expects a method with that name
52
+ # to be defined as class method in the model class.
53
+ #
54
+ # The validation method is supposed to return an Array<String>
55
+ # containing the error messages or a simple boolean value.
56
+ #
57
+ # ----
58
+ #
59
+ # @example A filter group for locked and not locked records, expecting the scopes :locked and :not_locked to be present.
60
+ #
61
+ # has_scope_filter :status, :locked
62
+ # has_scope_filter :status, :not_locked
63
+ #
64
+ # @example A full text search with one argument (scope: full_text, formal parameter: text)
65
+ #
66
+ # has_scope_filter :quick_search, :full_text, [:text]
67
+ #
68
+ # @example A date range filter
69
+ #
70
+ # has_scope_filter :date, :between, [:start_date, :end_date]
71
+ #
72
+ def has_scope_filter(group, scope, options = {})
73
+ #Load additional helper methods into the model class
74
+ extend Acts::DataTable::ScopeFilters::ActiveRecord::OnDemand
75
+
76
+ model = self
77
+
78
+ unless scopes.has_key?(:with_scope_filters)
79
+ # Generate the named scope which will handle the dynamically given filters
80
+ # A filter is only applied if a given validation method returns +true+
81
+ #
82
+ # Usually, the filters are automatically fetched from the current
83
+ # thread space, however, in some cases it might be necessary to pass
84
+ # them in manually (e.g. when using a delayed job to fetch records).
85
+ # Please only do this if it is really necessary.
86
+ named_scope :with_scope_filters, lambda {|*args|
87
+ filters = args.first
88
+ filters ||= Acts::DataTable::ScopeFilters::ActionController.get_request_filters
89
+
90
+ scope_chain = self
91
+ filters.each do |group_name, (scope, args)|
92
+
93
+ #Filters should already be validated when they are added through
94
+ #the controller, this check is to ensure that no invalid filter is
95
+ #ever applied to the model as final protection.
96
+ # TODO: Check if the filter is causing an exception (probably through a wrong valiation method)
97
+ # And remove it in this case, adding an error to the log or the module.
98
+ if Acts::DataTable::ScopeFilters::Validator.new(model, group_name, scope, args).valid?
99
+ actual_args = Acts::DataTable::ScopeFilters::ActiveRecord.actual_params(model, group_name, scope, args)
100
+ scope_chain = scope_chain.send(scope, *actual_args)
101
+ end
102
+ end
103
+
104
+ results = Acts::DataTable.lookup_nested_hash(scope_chain.current_scoped_methods, :find)
105
+ results || {}
106
+ }
107
+ end
108
+
109
+ Acts::DataTable::ScopeFilters::ActiveRecord.register_filter(self, group, scope, options)
110
+ end
111
+
112
+ #
113
+ # Registers multiple simple filters at once.
114
+ # Important: This does not allow setting validations, custom captions or any other customization options.
115
+ #
116
+ def has_scope_filters(group, *scopes)
117
+ scopes.each do |s|
118
+ has_scope_filter(group, s)
119
+ end
120
+ end
121
+ end
122
+
123
+
124
+ #
125
+ # This module is only included if at least one filter group was added to the model
126
+ # to avoid pollution in other models.
127
+ #
128
+ module OnDemand
129
+
130
+ end
131
+
132
+ def self.registered_filters
133
+ @@registered_filters ||= {}
134
+ end
135
+
136
+ #
137
+ # Registers a filter in the system which is then accessed when applying filters through a scope
138
+ # The filters are kept in this module as it can be accessed from everywhere,
139
+ # while a model class may not be known from within these methods.
140
+ #
141
+ # @param [ActiveRecord::Base] model
142
+ # The model class the scope to be added belongs to
143
+ #
144
+ # @param [String, Symbol] group
145
+ # The group name the scope belongs to within the +model+
146
+ #
147
+ # @param [String, Symbol] scope
148
+ # The scope name, it has to be a valid (named) scope within +model+
149
+ #
150
+ # @param [Hash] options
151
+ # Filter options, see #has_scope_filter
152
+ #
153
+ def self.register_filter(model, group, scope, options)
154
+ unless model.scopes.has_key?(scope.to_sym)
155
+ raise ArgumentError.new "The scope '#{scope}' in group '#{group}' does not exist in the model class '#{model.to_s}'"
156
+ end
157
+
158
+ Acts::DataTable.ensure_nested_hash!(self.registered_filters, model.to_s, group.to_s)
159
+ self.registered_filters[model.to_s][group.to_s][scope.to_s] = options
160
+ end
161
+
162
+ #
163
+ # @see #register_filter for arguments
164
+ #
165
+ # @return [Hash, NilClass] options given for the chosen filter if available
166
+ #
167
+ def self.filter_options(model, group, scope)
168
+ res = Acts::DataTable.lookup_nested_hash(self.registered_filters, model.to_s, group.to_s, scope.to_s)
169
+ unless res
170
+ raise ArgumentError.new("The scope '#{scope}' was expected to be defined in group '#{group}' of model '#{model}' but couldn't be found.")
171
+ end
172
+ res
173
+ end
174
+
175
+ #
176
+ # @return [Array<String, Symbol>] the args set up when registering the given filter
177
+ #
178
+ # @see #filter_options for parameters
179
+ #
180
+ def self.filter_args(*args)
181
+ self.filter_options(*args)[:args] || []
182
+ end
183
+
184
+ #
185
+ # @return [Fixnum] the amount of formal parameters the scope is defined with
186
+ # Note that this will return 0 for no formal parameters which differs to the way
187
+ # ruby 1.8 handles lambda expressions (returning -1)
188
+ #
189
+ # @see #filter_options for parameters
190
+ #
191
+ def self.filter_arity(*args)
192
+ self.filter_args(*args).size
193
+ end
194
+
195
+ #
196
+ # @return [TrueClass, FalseClass] +true+ if the given filter was actually registered before.
197
+ #
198
+ # @see #filter_options for parameters
199
+ #
200
+ def self.registered_filter?(*args)
201
+ !!(self.filter_options(*args))
202
+ end
203
+
204
+ #
205
+ # Checks whether the given argument count matches the given scope filter's formal
206
+ # parameter count.
207
+ #
208
+ # @return [TrueClass, FalseClass] +true+ if the given argument count is
209
+ # sufficient for the chosen filter.
210
+ #
211
+ def self.matching_arity?(model, group, scope, arg_count)
212
+ self.filter_arity(model, group, scope) == arg_count
213
+ end
214
+
215
+ #
216
+ # @return [String] The scope filter caption for the given scope
217
+ # @see #has_scope_filter for more information about how the caption is generated.
218
+ #
219
+ def self.scope_filter_caption(model, group, scope, args)
220
+ args ||= {}
221
+
222
+ case caption = self.filter_options(model, group, scope)[:caption]
223
+ when String
224
+ caption
225
+ when Symbol
226
+ if model.respond_to?(caption)
227
+ model.send(caption, group, scope, args)
228
+ else
229
+ raise ArgumentError.new "The method '#{caption}' was set up as scope filter caption method in model '#{model.name}', but doesn't exist."
230
+ end
231
+ when Proc
232
+ caption.call(args)
233
+ else
234
+ options = {:scope => "activerecord.scope_filters.scopes.#{model.name.underscore}"}
235
+ options.merge!(args.symbolize_keys)
236
+ I18n.t(scope, options)
237
+ end
238
+ end
239
+
240
+ #
241
+ # @return [Array<Object>] The actual parameters for a scope call
242
+ # based on the registered filter and the given arguments.
243
+ # This method should be used to ensure the correct order
244
+ # of arguments when applying a scope.
245
+ #
246
+ # @example Actual parameter generation for a date range scope
247
+ #
248
+ # #has_scope_filter :date, :between, [:start_date, :end_date]
249
+ # actual_params(model, :date, :between, {:end_date => '2015-04-01', :start_date => '2015-03-01'})
250
+ # #=> ['2015-03-01', '2015-04-01']
251
+ #
252
+ def self.actual_params(model, group, scope, args)
253
+ res = []
254
+ self.filter_args(model, group, scope).each do |arg_name|
255
+ res << args.stringify_keys[arg_name.to_s]
256
+ end
257
+ res
258
+ end
259
+
260
+
261
+ end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,67 @@
1
+ module Acts
2
+ module DataTable
3
+ module ScopeFilters
4
+ class FormHelper
5
+ def initialize(action_view, group, scope)
6
+ @action_view = action_view
7
+ @group = group.to_s
8
+ @scope = scope.to_s
9
+ end
10
+
11
+ def text_field(arg, options = {})
12
+ value = options.delete(:value) || current_arg(arg)
13
+ options[:id] = FormHelper.field_id(@group, @scope, arg)
14
+ @action_view.text_field_tag(FormHelper.field_name(arg), value, options)
15
+ end
16
+
17
+ #
18
+ # @return [TrueClass, FalseClass] +true+ if the current filter is currently active
19
+ #
20
+ def active?
21
+ @action_view.acts_as_data_table_session.active_filter(@group) == @scope
22
+ end
23
+
24
+ #
25
+ # @return [String] The URL to remove the current filter group from the active filters
26
+ #
27
+ def remove_url
28
+ @action_view.url_for({:scope_filters => {:action => 'remove', :group => @group}})
29
+ end
30
+
31
+ #
32
+ # @return [Array] Filter validation errors on the current group
33
+ #
34
+ def errors
35
+ @action_view.acts_as_data_table_session.errors_on(@group)
36
+ end
37
+
38
+ #
39
+ # @return [String] A generated field name for the given arg to be used in filter forms
40
+ #
41
+ def self.field_name(arg)
42
+ "scope_filters[args][#{arg}]"
43
+ end
44
+
45
+ #
46
+ # @return [String] A generated DOM id for the given group, scope and arg
47
+ #
48
+ def self.field_id(group, scope, arg)
49
+ [group, scope, arg].map(&:to_s).join('_')
50
+ end
51
+
52
+ private
53
+
54
+ #
55
+ # Retrieves the current value for the given arg name from the session.
56
+ # Also searches the current request's params if the arg couldn't be found in the session.
57
+ # This is useful to keep given values in case of validation errors.
58
+ #
59
+ def current_arg(arg)
60
+ Acts::DataTable.lookup_nested_hash(@action_view.acts_as_data_table_session.active_filters, @group, @scope.to_s, arg.to_s) ||
61
+ Acts::DataTable.lookup_nested_hash(@action_view.params, :scope_filters, :args, arg)
62
+ end
63
+
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,144 @@
1
+ module Acts
2
+ module DataTable
3
+ module ScopeFilters
4
+ class Validator
5
+
6
+ def initialize(model, group, scope, args)
7
+ @model = model
8
+ @group = group
9
+ @scope = scope
10
+ @args = args
11
+ end
12
+
13
+ #
14
+ # Validates the given scope filter and returns possible error messages
15
+ #
16
+ # @see {Acts::DataTable::ScopeFilters::ActiveRecord}#register_filter for arguments
17
+ #
18
+ def validate
19
+ validations = Acts::DataTable::ScopeFilters::ActiveRecord.filter_options(@model, @group, @scope)[:validate]
20
+
21
+ Array(validations).inject([]) do |res, proc|
22
+ res += run_validation(proc)
23
+ res
24
+ end.uniq.compact
25
+ end
26
+
27
+ def valid?
28
+ validate.empty?
29
+ end
30
+
31
+ private
32
+
33
+ #----------------------------------------------------------------
34
+ # Built-in Validations
35
+ #----------------------------------------------------------------
36
+
37
+ #
38
+ # @return [TrueClass, FalseClass] +true+ if the given validation
39
+ # is a built-in one.
40
+ #
41
+ def built_in_validation?(validation_name)
42
+ private_methods.include?("validate_#{validation_name}")
43
+ end
44
+
45
+ #
46
+ # Runs a built-in validation
47
+ #
48
+ def built_in_validation(validation_name)
49
+ send("validate_#{validation_name}")
50
+ end
51
+
52
+ #
53
+ # Each of the given arguments has to be parseable by Date.parse
54
+ #
55
+ def validate_all_dates
56
+ validate_all :invalid_date do |k, v|
57
+ Date.parse(v) rescue nil
58
+ end
59
+ end
60
+
61
+ #
62
+ # Each of the given arguments has to be present (!blank?)
63
+ #
64
+ def validate_all_present
65
+ validate_all :blank do |k, v|
66
+ v.present?
67
+ end
68
+ end
69
+
70
+ #
71
+ # Validates that all given arguments with names of the format /[a-z]+_id/
72
+ # actually refer to valid records in the system
73
+ #
74
+ def validate_record_existence
75
+ validate_all :invalid_record do |k, v|
76
+ m = /([a-z]+)_id/.match(k.to_s)
77
+ if m
78
+ model = m[1].camelize.constantize
79
+ !!model.find_by_id(v)
80
+ else
81
+ true
82
+ end
83
+ end
84
+ end
85
+
86
+ #----------------------------------------------------------------
87
+ # Helper Methods
88
+ #----------------------------------------------------------------
89
+
90
+ def run_validation(proc)
91
+ case proc
92
+ when Proc
93
+ result = proc.call(@args)
94
+ when Symbol
95
+ if built_in_validation?(proc)
96
+ result = built_in_validation(proc)
97
+ elsif @model.respond_to?(proc)
98
+ result = @model.send(proc, @args)
99
+ else
100
+ raise ArgumentError.new "The method '#{@proc}' was set up as scope filter validation method in model '#{@model.name}', but doesn't exist."
101
+ end
102
+ when NilClass
103
+ result = true
104
+ else
105
+ raise ArgumentError.new "An invalid validations method was given for the scope '#{@scope}' in group '#{@group}' of model '#{@model.name}'"
106
+ end
107
+
108
+ #If the result is already an array of error messages, we can simply return it.
109
+ #Otherwise, we have to generate an error array based on the boolean result
110
+ #the validation method produced.
111
+ if result.is_a?(Array)
112
+ result
113
+ else
114
+ result ? [] : [Acts::DataTable.t('scope_filters.validations.general_error')]
115
+ end
116
+ end
117
+
118
+ def validate_all(error, &proc)
119
+ @args.inject([]) do |res, (k, v)|
120
+ unless proc.call(k, v)
121
+ res << Acts::DataTable.t("scope_filters.validations.#{error}", :arg_name => localized_arg_name(k), :arg_value => v)
122
+ end
123
+ res
124
+ end
125
+ end
126
+
127
+ #
128
+ # Looks up the given argument name in activerecord.scope_filters.args.MODEL.
129
+ #
130
+ # If no translation was specified, it behaves like I18n.t in rails 4 and tries
131
+ # to make the best of the given given argument name
132
+ #
133
+ def localized_arg_name(arg_name)
134
+ l = I18n.t(arg_name, :scope => "activerecord.scope_filters.args.#{@model.to_s.underscore}.#{@scope}")
135
+ if l =~ /translation missing/
136
+ arg_name.to_s.split('_').map(&:camelize).join(' ')
137
+ else
138
+ l
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,193 @@
1
+ module Stex
2
+ module Acts
3
+ module DataTable
4
+ class SessionHelper
5
+ def initialize(session, controller_path, action, model_name)
6
+ @session = session
7
+ @controller_path = controller_path
8
+ @action = action
9
+ @model = model_name.to_s.classify.constantize
10
+ end
11
+
12
+ def controller_path
13
+ @controller_path
14
+ end
15
+
16
+ def action_name
17
+ @action
18
+ end
19
+
20
+ def model
21
+ @model
22
+ end
23
+
24
+ #----------------------------------------------------------------
25
+ # Filters
26
+ #----------------------------------------------------------------
27
+
28
+ # Adds a filter for a specific view
29
+ # If a filter for this group is already set, the new filter
30
+ # will override the existing one.
31
+ #--------------------------------------------------------------
32
+ def add_filter(group, scope, args)
33
+ controller_key = action_key(controller_path, action_name)
34
+ @session[:scope_filters] ||= {}
35
+ @session[:scope_filters][controller_key] ||= {}
36
+
37
+ args = hash_keys_to_strings(args) if args
38
+
39
+ @session[:scope_filters][controller_key][group.to_s] = {scope.to_s => args}
40
+ end
41
+
42
+ # Removes an active filter. As only one filter per group can
43
+ # be active at a time, we can simply delete the whole group.
44
+ # If all filters are deleted, we can also remove the namespaces
45
+ # in the session
46
+ #--------------------------------------------------------------
47
+ def remove_filter(group)
48
+ controller_key = action_key(controller_path, action_name)
49
+ return unless @session[:scope_filters][controller_key]
50
+ @session[:scope_filters][controller_key].delete(group.to_s)
51
+ @session[:scope_filters].delete(controller_key) if @session[:scope_filters][controller_key].empty?
52
+ @session.delete(:scope_filters) if @session[:scope_filters].empty?
53
+ end
54
+
55
+ # Removes all filters from the given controller + action
56
+ #--------------------------------------------------------------
57
+ def remove_all_filters
58
+ @session[:scope_filters].delete(action_key(controller_path, action_name))
59
+ end
60
+
61
+ # Returns all active filters for the given controller and action
62
+ #--------------------------------------------------------------
63
+ def active_filters
64
+ return {} unless @session[:scope_filters]
65
+ @session[:scope_filters][action_key(controller_path, action_name)] || {}
66
+ end
67
+
68
+ # Checks if the given filter is currently active
69
+ #--------------------------------------------------------------
70
+ def active_filter?(group, scope, args)
71
+ active = active_filters
72
+ args = hash_keys_to_strings(args) if args
73
+
74
+ active.any? &&
75
+ active[group.to_s] &&
76
+ active[group.to_s].has_key?(scope.to_s) &&
77
+ (active[group.to_s][scope.to_s].to_a - args.to_a).empty? #sometimes the session hash contains {:raise => true}, whereever it comes from...
78
+ end
79
+
80
+ #----------------------------------------------------------------
81
+ # Column Sorting
82
+ #----------------------------------------------------------------
83
+
84
+ # Replaces all current sorting columns with the new one
85
+ #--------------------------------------------------------------
86
+ def replace_sorting_column(model_name, column_name)
87
+ args = generate_sorting_arguments(model_name, column_name)
88
+ return unless args[:model].column_names.include?(column_name.to_s)
89
+
90
+ @session[:column_sorting] ||= {}
91
+ @session[:column_sorting][args[:key]] = [[args[:column], 'ASC']]
92
+ end
93
+
94
+ # Adds a new sorting column to the current controller and action
95
+ #--------------------------------------------------------------
96
+ def add_or_toggle_sorting_column(model_name, column_name)
97
+ args = generate_sorting_arguments(model_name, column_name)
98
+
99
+ return unless args[:model].column_names.include?(column_name.to_s)
100
+
101
+ @session[:column_sorting] ||= {}
102
+ @session[:column_sorting][args[:key]] ||= []
103
+
104
+ existing_entry = @session[:column_sorting][args[:key]].assoc(args[:column])
105
+
106
+ #Toggle the direction
107
+ if existing_entry
108
+ idx = @session[:column_sorting][args[:key]].index(existing_entry)
109
+
110
+ direction = existing_entry.last == 'ASC' ? 'DESC' : 'ASC'
111
+
112
+ @session[:column_sorting][args[:key]].delete(existing_entry)
113
+
114
+ @session[:column_sorting][args[:key]].insert(idx, [args[:column], direction])
115
+ else
116
+ #Simply append the new column to the sorting list
117
+ @session[:column_sorting][args[:key]] << [args[:column], 'ASC']
118
+ end
119
+ end
120
+
121
+ # Removes a sorting column from current controller and action
122
+ #--------------------------------------------------------------
123
+ def remove_sorting_column(model_name, column_name)
124
+ args = generate_sorting_arguments(model_name, column_name)
125
+ return if @session[:column_sorting].nil? || @session[:column_sorting].empty?
126
+ return unless @session[:column_sorting].has_key?(args[:key])
127
+
128
+ existing_entry = @session[:column_sorting][args[:key]].assoc(args[:column])
129
+ @session[:column_sorting][args[:key]].delete(existing_entry) if existing_entry
130
+
131
+ #Remove the controller namespace from the session if it's empty
132
+ @session[:column_sorting].delete(args[:key]) if @session[:column_sorting][args[:key]].empty?
133
+ end
134
+
135
+ # Returns all sorting columns as a string
136
+ #--------------------------------------------------------------
137
+ def sorting_columns_string
138
+ controller_key = action_key(controller_path, action_name)
139
+ return nil if @session[:column_sorting].nil? || @session[:column_sorting].empty?
140
+ return nil unless @session[:column_sorting].has_key?(controller_key)
141
+
142
+ @session[:column_sorting][controller_key].map {|column_and_direction| column_and_direction.join(' ')}.join(', ')
143
+ end
144
+
145
+ # Returns the current sorting direction for a given column
146
+ # or nil if this column is currently not active.
147
+ #--------------------------------------------------------------
148
+ def sorting_direction(model_name, column_name)
149
+ args = generate_sorting_arguments(model_name, column_name)
150
+
151
+ return nil if @session[:column_sorting].nil? || @session[:column_sorting].empty?
152
+ return nil unless @session[:column_sorting].has_key?(args[:key])
153
+ entry = @session[:column_sorting][args[:key]].assoc(args[:column])
154
+ entry ? entry.last : nil
155
+ end
156
+
157
+ # loads the sorting defaults for the current action
158
+ # Parameters==
159
+ # defaults:: [['users.last_name', 'ASC'], ['users.first_name', 'ASC']]
160
+ #--------------------------------------------------------------
161
+ def load_sorting_defaults(defaults = [])
162
+ return if defaults.empty?
163
+ controller_key = action_key(controller_path, action_name)
164
+ @session[:column_sorting] = {}
165
+ @session[:column_sorting][controller_key] = defaults
166
+ end
167
+
168
+ private
169
+
170
+ def generate_sorting_arguments(model_name, column_name)
171
+ model = model_name.to_s.classify.constantize
172
+ {
173
+ :model => model,
174
+ :key => action_key(controller_path, action_name),
175
+ :column => "#{model.table_name}.#{column_name}"
176
+ }
177
+ end
178
+
179
+ def action_key(controller, action)
180
+ [controller.gsub("/", "_"), action].join('_')
181
+ end
182
+
183
+ def hash_keys_to_strings(hash)
184
+ new_hash = {}
185
+ hash.each do |key, value|
186
+ new_hash[key.to_s] = value
187
+ end
188
+ new_hash
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,25 @@
1
+ module Acts
2
+ module DataTable
3
+ module Shared
4
+ module ActionController
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ end
12
+
13
+ module OnDemand
14
+ def self.included(base)
15
+ base.helper_method :acts_as_data_table_session
16
+ end
17
+
18
+ def acts_as_data_table_session
19
+ @acts_as_data_table_session ||= Acts::DataTable::Shared::Session.new(session, controller_path, action_name)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end