active_scaffold 3.0.23 → 3.0.24

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 (62) hide show
  1. data/frontends/default/views/_action_group.html.erb +1 -1
  2. data/frontends/default/views/_action_group.html.erb~ +24 -0
  3. data/frontends/default/views/_form.html.erb~ +26 -0
  4. data/frontends/default/views/_form_association.html.erb~ +19 -0
  5. data/frontends/default/views/_form_association_footer.html.erb~ +16 -6
  6. data/frontends/default/views/_horizontal_subform.html.erb~ +29 -0
  7. data/frontends/default/views/_horizontal_subform_header.html.erb~ +3 -2
  8. data/frontends/default/views/_list_actions.html.erb~ +15 -0
  9. data/frontends/default/views/_list_inline_adapter.html.erb~ +10 -0
  10. data/frontends/default/views/_list_messages.html.erb~ +30 -0
  11. data/frontends/default/views/_list_pagination.html.erb~ +11 -0
  12. data/frontends/default/views/_list_pagination_links.html.erb~ +0 -0
  13. data/frontends/default/views/_render_field.js.erb~ +23 -0
  14. data/frontends/default/views/_row.html.erb~ +6 -0
  15. data/frontends/default/views/_vertical_subform.html.erb~ +12 -0
  16. data/frontends/default/views/edit_associated.js.erb~ +13 -0
  17. data/frontends/default/views/on_create.js.rjs +2 -2
  18. data/frontends/default/views/render_field.js.erb~ +1 -0
  19. data/lib/active_scaffold/actions/core.rb~ +13 -5
  20. data/lib/active_scaffold/actions/create.rb~ +149 -0
  21. data/lib/active_scaffold/actions/list.rb~ +196 -0
  22. data/lib/active_scaffold/actions/nested.rb +6 -2
  23. data/lib/active_scaffold/actions/nested.rb~ +252 -0
  24. data/lib/active_scaffold/actions/search.rb~ +49 -0
  25. data/lib/active_scaffold/actions/subform.rb~ +27 -0
  26. data/lib/active_scaffold/actions/update.rb~ +149 -0
  27. data/lib/active_scaffold/attribute_params.rb~ +202 -0
  28. data/lib/active_scaffold/bridges/record_select/{lib/record_select_bridge.rb~ → helpers.rb~} +7 -16
  29. data/lib/active_scaffold/bridges/shared/date_bridge.rb~ +209 -0
  30. data/lib/active_scaffold/config/create.rb +4 -4
  31. data/lib/active_scaffold/config/nested.rb +1 -0
  32. data/lib/active_scaffold/config/nested.rb~ +41 -0
  33. data/lib/active_scaffold/config/search.rb~ +74 -0
  34. data/lib/active_scaffold/constraints.rb~ +186 -0
  35. data/lib/active_scaffold/data_structures/action_columns.rb~ +140 -0
  36. data/lib/active_scaffold/data_structures/action_link.rb +4 -4
  37. data/lib/active_scaffold/data_structures/action_link.rb~ +179 -0
  38. data/lib/active_scaffold/data_structures/nested_info.rb~ +124 -0
  39. data/lib/active_scaffold/extensions/action_controller_rendering.rb~ +22 -0
  40. data/lib/active_scaffold/extensions/action_view_rendering.rb~ +108 -0
  41. data/lib/active_scaffold/extensions/cache_association.rb~ +12 -0
  42. data/lib/active_scaffold/extensions/reverse_associations.rb~ +64 -0
  43. data/lib/active_scaffold/extensions/routing_mapper.rb~ +34 -0
  44. data/lib/active_scaffold/extensions/unsaved_associated.rb~ +62 -0
  45. data/lib/active_scaffold/finder.rb~ +370 -0
  46. data/lib/active_scaffold/helpers/controller_helpers.rb~ +101 -0
  47. data/lib/active_scaffold/helpers/form_column_helpers.rb~ +321 -0
  48. data/lib/active_scaffold/helpers/id_helpers.rb~ +123 -0
  49. data/lib/active_scaffold/helpers/list_column_helpers.rb +9 -6
  50. data/lib/active_scaffold/helpers/list_column_helpers.rb~ +368 -0
  51. data/lib/active_scaffold/helpers/search_column_helpers.rb~ +94 -46
  52. data/lib/active_scaffold/helpers/view_helpers.rb +1 -1
  53. data/lib/active_scaffold/helpers/view_helpers.rb~ +353 -0
  54. data/lib/active_scaffold/version.rb +1 -1
  55. data/lib/active_scaffold.rb +1 -1
  56. data/lib/active_scaffold.rb~ +362 -0
  57. metadata +110 -76
  58. data/lib/active_scaffold/bridges/dragonfly/bridge.rb~ +0 -12
  59. data/lib/active_scaffold/bridges/dragonfly/lib/dragonfly_bridge.rb~ +0 -36
  60. data/lib/active_scaffold/bridges/dragonfly/lib/dragonfly_bridge_helpers.rb~ +0 -12
  61. data/lib/active_scaffold/bridges/dragonfly/lib/form_ui.rb~ +0 -27
  62. data/lib/active_scaffold/bridges/dragonfly/lib/list_ui.rb~ +0 -16
@@ -0,0 +1,108 @@
1
+ module ActionView
2
+ class LookupContext
3
+ module ViewPaths
4
+ def find_all_templates(name, partial = false, locals = {})
5
+ prefixes.collect do |prefix|
6
+ view_paths.collect do |resolver|
7
+ temp_args = *args_for_lookup(name, [prefix], partial, locals)
8
+ temp_args[1] = temp_args[1][0]
9
+ resolver.find_all(*temp_args)
10
+ end
11
+ end.flatten!
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ # wrap the action rendering for ActiveScaffold views
18
+ module ActionView::Helpers #:nodoc:
19
+ module RenderingHelper
20
+ #
21
+ # Adds two rendering options.
22
+ #
23
+ # ==render :super
24
+ #
25
+ # This syntax skips all template overrides and goes directly to the provided ActiveScaffold templates.
26
+ # Useful if you want to wrap an existing template. Just call super!
27
+ #
28
+ # ==render :active_scaffold => #{controller.to_s}, options = {}+
29
+ #
30
+ # Lets you embed an ActiveScaffold by referencing the controller where it's configured.
31
+ #
32
+ # You may specify options[:constraints] for the embedded scaffold. These constraints have three effects:
33
+ # * the scaffold's only displays records matching the constraint
34
+ # * all new records created will be assigned the constrained values
35
+ # * constrained columns will be hidden (they're pretty boring at this point)
36
+ #
37
+ # You may also specify options[:conditions] for the embedded scaffold. These only do 1/3 of what
38
+ # constraints do (they only limit search results). Any format accepted by ActiveRecord::Base.find is valid.
39
+ #
40
+ # Defining options[:label] lets you completely customize the list title for the embedded scaffold.
41
+ #
42
+ def render_with_active_scaffold(*args, &block)
43
+ if args.first == :super
44
+ last_view = view_stack.last || {:view => instance_variable_get(:@virtual_path).split('/').last}
45
+ options = args[1] || {}
46
+ options[:locals] ||= {}
47
+ options[:locals].reverse_merge!(last_view[:locals] || {})
48
+ if last_view[:templates].nil?
49
+ last_view[:templates] = lookup_context.find_all_templates(last_view[:view], last_view[:partial], options[:locals].keys)
50
+ last_view[:templates].shift
51
+ end
52
+ options[:file] = last_view[:templates].shift
53
+ view_stack << last_view
54
+ result = render_without_active_scaffold options
55
+ view_stack.pop
56
+ result
57
+ elsif args.first.is_a? Hash and args.first[:active_scaffold]
58
+ require 'digest/md5'
59
+ options = args.first
60
+
61
+ remote_controller = options[:active_scaffold]
62
+ constraints = options[:constraints]
63
+ conditions = options[:conditions]
64
+ eid = Digest::MD5.hexdigest(params[:controller] + remote_controller.to_s + constraints.to_s + conditions.to_s)
65
+ session["as:#{eid}"] = {:constraints => constraints, :conditions => conditions, :list => {:label => args.first[:label]}}
66
+ options[:params] ||= {}
67
+ options[:params].merge! :eid => eid, :embedded => true
68
+
69
+ id = "as_#{eid}-embedded"
70
+ url_options = {:controller => remote_controller.to_s, :action => 'index'}.merge(options[:params])
71
+
72
+ if controller.respond_to?(:render_component_into_view)
73
+ controller.send(:render_component_into_view, url_options)
74
+ else
75
+ content_tag(:div, :id => id, :class => 'active-scaffold-component') do
76
+ url = url_for(url_options)
77
+ content_tag(:div, :class => 'active-scaffold-header') do
78
+ content_tag :h2, link_to(args.first[:label] || active_scaffold_config_for(remote_controller.to_s.singularize).list.label, url, :remote => true)
79
+ end <<
80
+ if ActiveScaffold.js_framework == :prototype
81
+ javascript_tag("new Ajax.Updater('#{id}', '#{url}', {method: 'get', evalScripts: true});")
82
+ elsif ActiveScaffold.js_framework == :jquery
83
+ javascript_tag("$('##{id}').load('#{url}');")
84
+ end
85
+ end
86
+ end
87
+
88
+ else
89
+ options = args.first
90
+ if options.is_a?(Hash)
91
+ current_view = {:view => options[:partial], :partial => true} if options[:partial]
92
+ current_view = {:view => options[:template], :partial => false} if current_view.nil? && options[:template]
93
+ current_view[:locals] = options[:locals] if !current_view.nil? && options[:locals]
94
+ view_stack << current_view if current_view.present?
95
+ end
96
+ result = render_without_active_scaffold(*args, &block)
97
+ view_stack.pop if current_view.present?
98
+ result
99
+ end
100
+ end
101
+ alias_method_chain :render, :active_scaffold
102
+
103
+ def view_stack
104
+ @_view_stack ||= []
105
+ end
106
+
107
+ end
108
+ end
@@ -0,0 +1,12 @@
1
+ module ActiveRecord
2
+ class Relation
3
+ def target=(records)
4
+ debugger
5
+ @loaded = true
6
+ @records = records
7
+ puts loaded?.inspect
8
+ puts self.object_id
9
+ @records
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,64 @@
1
+ module ActiveRecord
2
+ module Reflection
3
+ class AssociationReflection #:nodoc:
4
+ def inverse_for?(klass)
5
+ inverse_class = self.klass unless options[:polymorphic]
6
+ inverse_class.present? && (inverse_class == klass || klass < inverse_class)
7
+ end
8
+
9
+ attr_writer :reverse
10
+ def reverse
11
+ @reverse ||= inverse_of.try(:name)
12
+ end
13
+
14
+ def inverse_of_with_autodetect
15
+ inverse_of_without_autodetect || autodetect_inverse
16
+ end
17
+ alias_method_chain :inverse_of, :autodetect
18
+
19
+ protected
20
+
21
+ def autodetect_inverse
22
+ return nil if options[:polymorphic]
23
+ reverse_matches = []
24
+
25
+ # stage 1 filter: collect associations that point back to this model and use the same foreign_key
26
+ klass.reflect_on_all_associations.each do |assoc|
27
+ if self.options[:through]
28
+ # only iterate has_many :through associations
29
+ next unless assoc.options[:through]
30
+ next unless assoc.through_reflection.klass == self.through_reflection.klass
31
+ else
32
+ # skip over has_many :through associations
33
+ next if assoc.options[:through]
34
+ next unless assoc.options[:polymorphic] or assoc.class_name.constantize == self.active_record
35
+
36
+ case [assoc.macro, self.macro].find_all{|m| m == :has_and_belongs_to_many}.length
37
+ # if both are a habtm, then match them based on the join table
38
+ when 2
39
+ next unless assoc.options[:join_table] == self.options[:join_table]
40
+
41
+ # if only one is a habtm, they do not match
42
+ when 1
43
+ next
44
+
45
+ # otherwise, match them based on the foreign_key
46
+ when 0
47
+ next unless assoc.foreign_key.to_sym == self.foreign_key.to_sym
48
+ end
49
+ end
50
+
51
+ reverse_matches << assoc
52
+ end
53
+
54
+ # stage 2 filter: name-based matching (association name vs self.active_record.to_s)
55
+ reverse_matches.find_all do |assoc|
56
+ self.active_record.to_s.underscore.include? assoc.name.to_s.pluralize.singularize
57
+ end if reverse_matches.length > 1
58
+
59
+ reverse_matches.first
60
+ end
61
+
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,34 @@
1
+ module ActionDispatch
2
+ module Routing
3
+ ACTIVE_SCAFFOLD_CORE_ROUTING = {
4
+ :collection => {:show_search => :get, :render_field => :get},
5
+ :member => {:row => :get, :update_column => :post, :render_field => :get}
6
+ }
7
+ ACTIVE_SCAFFOLD_ASSOCIATION_ROUTING = {
8
+ :collection => {:edit_associated => :get, :new_existing => :get, :add_existing => :post},
9
+ :member => {:edit_associated => :get, :add_association => :get, :destroy_existing => :delete}
10
+ }
11
+ class Mapper
12
+ module Base
13
+ def as_routes(options = {:association => true})
14
+ collection do
15
+ ActionDispatch::Routing::ACTIVE_SCAFFOLD_CORE_ROUTING[:collection].each {|name, type| send(type, name)}
16
+ end
17
+ member do
18
+ ActionDispatch::Routing::ACTIVE_SCAFFOLD_CORE_ROUTING[:member].each {|name, type| send(type, name)}
19
+ end
20
+ as_association_routes if options[:association]
21
+ end
22
+
23
+ def as_association_routes
24
+ collection do
25
+ ActionDispatch::Routing::ACTIVE_SCAFFOLD_ASSOCIATION_ROUTING[:collection].each {|name, type| send(type, name)}
26
+ end
27
+ member do
28
+ ActionDispatch::Routing::ACTIVE_SCAFFOLD_ASSOCIATION_ROUTING[:member].each {|name, type| send(type, name)}
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,62 @@
1
+ # save and validation support for associations.
2
+ class ActiveRecord::Base
3
+ def associated_valid?(path = [])
4
+ return true if path.include?(self) # prevent recursion (if associated and parent are new records)
5
+ path << self
6
+ # using [].all? syntax to avoid a short-circuit
7
+ with_unsaved_associated { |a| debugger; [a.valid?, a.associated_valid?(path)].all? {|v| v == true} }
8
+ end
9
+
10
+ def save_associated
11
+ with_unsaved_associated { |a| a.save and a.save_associated }
12
+ end
13
+
14
+ def save_associated!
15
+ save_associated or raise(ActiveRecord::RecordNotSaved)
16
+ end
17
+
18
+ def no_errors_in_associated?
19
+ with_unsaved_associated {|a| a.errors.count == 0 and a.no_errors_in_associated?}
20
+ end
21
+
22
+ protected
23
+
24
+ # Provide an override to allow the model to restrict which associations are considered
25
+ # by ActiveScaffolds update mechanism. This allows the model to restrict things like
26
+ # Acts-As-Versioned versions associations being traversed.
27
+ #
28
+ # By defining the method :scaffold_update_nofollow returning an array of associations
29
+ # these associations will not be traversed.
30
+ # By defining the method :scaffold_update_follow returning an array of associations,
31
+ # only those associations will be traversed.
32
+ #
33
+ # Otherwise the default behaviour of traversing all associations will be preserved.
34
+ def associations_for_update
35
+ if self.respond_to?( :scaffold_update_nofollow )
36
+ self.class.reflect_on_all_associations.reject { |association| self.scaffold_update_nofollow.include?( association.name ) }
37
+ elsif self.respond_to?( :scaffold_update_follow )
38
+ self.class.reflect_on_all_associations.select { |association| self.scaffold_update_follow.include?( association.name ) }
39
+ else
40
+ self.class.reflect_on_all_associations
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ # yields every associated object that has been instantiated and is flagged as unsaved.
47
+ # returns false if any yield returns false.
48
+ # returns true otherwise, even when none of the associations have been instantiated. build wrapper methods accordingly.
49
+ def with_unsaved_associated
50
+ associations_for_update.all? do |association|
51
+ association_proxy = send(association.name)
52
+ if association_proxy
53
+ records = association_proxy
54
+ records = [records] unless records.is_a? Array # convert singular associations into collections for ease of use
55
+ debugger
56
+ records.select {|r| r.unsaved? and not r.readonly?}.all? {|r| yield r} # must use select instead of find_all, which Rails overrides on association proxies for db access
57
+ else
58
+ true
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,370 @@
1
+ module ActiveScaffold
2
+ module Finder
3
+ def self.like_operator
4
+ @@like_operator ||= ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL" ? "ILIKE" : "LIKE"
5
+ end
6
+
7
+ module ClassMethods
8
+ # Takes a collection of search terms (the tokens) and creates SQL that
9
+ # searches all specified ActiveScaffold columns. A row will match if each
10
+ # token is found in at least one of the columns.
11
+ def create_conditions_for_columns(tokens, columns, text_search = :full)
12
+ # if there aren't any columns, then just return a nil condition
13
+ return unless columns.length > 0
14
+ like_pattern = like_pattern(text_search)
15
+
16
+ tokens = [tokens] if tokens.is_a? String
17
+
18
+ where_clauses = []
19
+ columns.each do |column|
20
+ where_clauses << ((column.column.nil? || column.column.text?) ? "#{column.search_sql} #{ActiveScaffold::Finder.like_operator} ?" : "#{column.search_sql} = ?")
21
+ end
22
+ phrase = "(#{where_clauses.join(' OR ')})"
23
+
24
+ sql = ([phrase] * tokens.length).join(' AND ')
25
+ tokens = tokens.collect do |value|
26
+ columns.collect {|column| (column.column.nil? || column.column.text?) ? like_pattern.sub('?', value) : column.column.type_cast(value)}
27
+ end.flatten
28
+
29
+ [sql, *tokens]
30
+ end
31
+
32
+ # Generates an SQL condition for the given ActiveScaffold column based on
33
+ # that column's database type (or form_ui ... for virtual columns?).
34
+ # TODO: this should reside on the column, not the controller
35
+ def condition_for_column(column, value, text_search = :full)
36
+ like_pattern = like_pattern(text_search)
37
+ if self.respond_to?("condition_for_#{column.name}_column")
38
+ return self.send("condition_for_#{column.name}_column", column, value, like_pattern)
39
+ end
40
+ return unless column and column.search_sql and not value.blank?
41
+ search_ui = column.search_ui || column.column.try(:type)
42
+ begin
43
+ if search_ui && self.respond_to?("condition_for_#{search_ui}_type")
44
+ self.send("condition_for_#{search_ui}_type", column, value, like_pattern)
45
+ else
46
+ unless column.search_sql.instance_of? Proc
47
+ case search_ui
48
+ when :boolean, :checkbox
49
+ ["#{column.search_sql} = ?", column.column.type_cast(value)]
50
+ when :integer, :decimal, :float
51
+ condition_for_numeric(column, value)
52
+ when :string, :range
53
+ condition_for_range(column, value, like_pattern)
54
+ when :date, :time, :datetime, :timestamp
55
+ condition_for_datetime(column, value)
56
+ when :select, :multi_select, :country, :usa_state
57
+ ["#{column.search_sql} in (?)", Array(value)]
58
+ else
59
+ if column.column.nil? || column.column.text?
60
+ ["#{column.search_sql} #{ActiveScaffold::Finder.like_operator} ?", like_pattern.sub('?', value)]
61
+ else
62
+ ["#{column.search_sql} = ?", column.column.type_cast(value)]
63
+ end
64
+ end
65
+ else
66
+ column.search_sql.call(value)
67
+ end
68
+ end
69
+ rescue Exception => e
70
+ logger.error Time.now.to_s + "#{e.inspect} -- on the ActiveScaffold column :#{column.name}, search_ui = #{search_ui} in #{self.name}"
71
+ raise e
72
+ end
73
+ end
74
+
75
+ def condition_for_numeric(column, value)
76
+ if !value.is_a?(Hash)
77
+ ["#{column.search_sql} = ?", condition_value_for_numeric(column, value)]
78
+ elsif value[:from].blank? or not ActiveScaffold::Finder::NumericComparators.include?(value[:opt])
79
+ nil
80
+ elsif value[:opt] == 'BETWEEN'
81
+ ["#{column.search_sql} BETWEEN ? AND ?", condition_value_for_numeric(column, value[:from]), condition_value_for_numeric(column, value[:to])]
82
+ else
83
+ ["#{column.search_sql} #{value[:opt]} ?", condition_value_for_numeric(column, value[:from])]
84
+ end
85
+ end
86
+
87
+ def condition_for_range(column, value, like_pattern = nil)
88
+ if !value.is_a?(Hash)
89
+ if column.column.nil? || column.column.text?
90
+ ["#{column.search_sql} #{ActiveScaffold::Finder.like_operator} ?", like_pattern.sub('?', value)]
91
+ else
92
+ ["#{column.search_sql} = ?", column.column.type_cast(value)]
93
+ end
94
+ elsif ActiveScaffold::Finder::NullComparators.include?(value[:opt])
95
+ condition_for_null_type(column, value[:opt], like_pattern)
96
+ elsif value[:from].blank?
97
+ nil
98
+ elsif ActiveScaffold::Finder::StringComparators.values.include?(value[:opt])
99
+ ["#{column.search_sql} LIKE ?", value[:opt].sub('?', value[:from])]
100
+ elsif value[:opt] == 'BETWEEN'
101
+ ["#{column.search_sql} BETWEEN ? AND ?", value[:from], value[:to]]
102
+ elsif ActiveScaffold::Finder::NumericComparators.include?(value[:opt])
103
+ ["#{column.search_sql} #{value[:opt]} ?", value[:from]]
104
+ else
105
+ nil
106
+ end
107
+ end
108
+
109
+ def condition_value_for_datetime(value, conversion = :to_time)
110
+ if value.is_a? Hash
111
+ Time.zone.local(*[:year, :month, :day, :hour, :minute, :second].collect {|part| value[part].to_i}) rescue nil
112
+ elsif value.respond_to?(:strftime)
113
+ value.send(conversion)
114
+ elsif conversion == :to_date
115
+ Date.strptime(value, I18n.t('date.formats.default')) rescue nil
116
+ else
117
+ parts = Date._parse(value)
118
+ time_parts = [[:hour, '%H'], [:min, '%M'], [:sec, '%S']].collect {|part, format_part| format_part if parts[part].present?}.compact
119
+ format = "#{I18n.t('date.formats.default')} #{time_parts.join(':')} #{'%z' if parts[:offset].present?}"
120
+ time = DateTime.strptime(value, format)
121
+ time = Time.zone.local_to_utc(time) unless parts[:offset]
122
+ time.in_time_zone.send(conversion) rescue nil
123
+ end unless value.nil? || value.blank?
124
+ end
125
+
126
+ def condition_value_for_numeric(column, value)
127
+ return value if value.nil?
128
+ value = i18n_number_to_native_format(value) if [:i18n_number, :currency].include?(column.options[:format])
129
+ case (column.search_ui || column.column.type)
130
+ when :integer then value.to_i rescue value ? 1 : 0
131
+ when :float then value.to_f
132
+ when :decimal then ActiveRecord::ConnectionAdapters::Column.value_to_decimal(value)
133
+ else
134
+ value
135
+ end
136
+ end
137
+
138
+ def i18n_number_to_native_format(value)
139
+ native = '.'
140
+ delimiter = I18n.t('number.format.delimiter')
141
+ separator = I18n.t('number.format.separator')
142
+ return value if value.blank? || !value.is_a?(String)
143
+ unless delimiter == native && !value.include?(separator) && value !~ /\.\d{3}$/
144
+ value.gsub(/[^0-9\-#{I18n.t('number.format.separator')}]/, '').gsub(I18n.t('number.format.separator'), native)
145
+ else
146
+ value
147
+ end
148
+ end
149
+
150
+ def condition_for_datetime(column, value, like_pattern = nil)
151
+ conversion = column.column.type == :date ? :to_date : :to_time
152
+ from_value = condition_value_for_datetime(value[:from], conversion)
153
+ to_value = condition_value_for_datetime(value[:to], conversion)
154
+
155
+ if from_value.nil? and to_value.nil?
156
+ nil
157
+ elsif !from_value
158
+ ["#{column.search_sql} <= ?", to_value.to_s(:db)]
159
+ elsif !to_value
160
+ ["#{column.search_sql} >= ?", from_value.to_s(:db)]
161
+ else
162
+ ["#{column.search_sql} BETWEEN ? AND ?", from_value.to_s(:db), to_value.to_s(:db)]
163
+ end
164
+ end
165
+
166
+ def condition_for_record_select_type(column, value, like_pattern = nil)
167
+ if value.is_a?(Array)
168
+ ["#{column.search_sql} IN (?)", value]
169
+ else
170
+ ["#{column.search_sql} = ?", value]
171
+ end
172
+ end
173
+
174
+ def condition_for_null_type(column, value, like_pattern = nil)
175
+ case value.to_sym
176
+ when :null
177
+ ["#{column.search_sql} is null"]
178
+ when :not_null
179
+ ["#{column.search_sql} is not null"]
180
+ else
181
+ nil
182
+ end
183
+ end
184
+
185
+ def like_pattern(text_search)
186
+ case text_search
187
+ when :full then '%?%'
188
+ when :start then '?%'
189
+ when :end then '%?'
190
+ else '?'
191
+ end
192
+ end
193
+ end
194
+
195
+ NumericComparators = [
196
+ '=',
197
+ '>=',
198
+ '<=',
199
+ '>',
200
+ '<',
201
+ '!=',
202
+ 'BETWEEN'
203
+ ]
204
+ StringComparators = {
205
+ :contains => '%?%',
206
+ :begins_with => '?%',
207
+ :ends_with => '%?'
208
+ }
209
+ NullComparators = [
210
+ :null,
211
+ :not_null
212
+ ]
213
+
214
+
215
+
216
+ def self.included(klass)
217
+ klass.extend ClassMethods
218
+ end
219
+
220
+ protected
221
+
222
+ attr_writer :active_scaffold_conditions
223
+ def active_scaffold_conditions
224
+ @active_scaffold_conditions ||= []
225
+ end
226
+
227
+ attr_writer :active_scaffold_includes
228
+ def active_scaffold_includes
229
+ @active_scaffold_includes ||= []
230
+ end
231
+
232
+ attr_writer :active_scaffold_habtm_joins
233
+ def active_scaffold_habtm_joins
234
+ @active_scaffold_habtm_joins ||= []
235
+ end
236
+
237
+ def all_conditions
238
+ merge_conditions(
239
+ active_scaffold_conditions, # from the search modules
240
+ conditions_for_collection, # from the dev
241
+ conditions_from_params, # from the parameters (e.g. /users/list?first_name=Fred)
242
+ conditions_from_constraints, # from any constraints (embedded scaffolds)
243
+ active_scaffold_session_storage[:conditions] # embedding conditions (weaker constraints)
244
+ )
245
+ end
246
+
247
+ # returns a single record (the given id) but only if it's allowed for the specified action.
248
+ # accomplishes this by checking model.#{action}_authorized?
249
+ # TODO: this should reside on the model, not the controller
250
+ def find_if_allowed(id, crud_type, klass = beginning_of_chain)
251
+ record = klass.find(id)
252
+ raise ActiveScaffold::RecordNotAllowed, "#{klass} with id = #{id}" unless record.authorized_for?(:crud_type => crud_type.to_sym)
253
+ return record
254
+ end
255
+
256
+ # returns a hash with options to find records
257
+ # valid options may include:
258
+ # * :sorting - a Sorting DataStructure (basically an array of hashes of field => direction, e.g. [{:field1 => 'asc'}, {:field2 => 'desc'}]). please note that multi-column sorting has some limitations: if any column in a multi-field sort uses method-based sorting, it will be ignored. method sorting only works for single-column sorting.
259
+ # * :per_page
260
+ # * :page
261
+ def finder_options(options = {})
262
+ options.assert_valid_keys :sorting, :per_page, :page, :count_includes, :pagination, :select
263
+
264
+ search_conditions = all_conditions
265
+ full_includes = (active_scaffold_includes.blank? ? nil : active_scaffold_includes)
266
+
267
+ # create a general-use options array that's compatible with Rails finders
268
+ finder_options = { :order => options[:sorting].try(:clause),
269
+ :where => search_conditions,
270
+ :joins => joins_for_finder,
271
+ :includes => full_includes}
272
+
273
+ finder_options.merge! custom_finder_options
274
+ finder_options
275
+ end
276
+
277
+ # Returns a hash with options to count records, rejecting select and order options
278
+ # See finder_options for valid options
279
+ def count_options(find_options = {}, count_includes = nil)
280
+ count_includes ||= find_options[:includes] unless find_options[:where].nil?
281
+ options = find_options.reject{|k,v| [:select, :order].include? k}
282
+ options[:includes] = count_includes
283
+ options
284
+ end
285
+
286
+ # returns a Paginator::Page (not from ActiveRecord::Paginator) for the given parameters
287
+ # See finder_options for valid options
288
+ # TODO: this should reside on the model, not the controller
289
+ def find_page(options = {})
290
+ options[:per_page] ||= 999999999
291
+ options[:page] ||= 1
292
+
293
+ find_options = finder_options(options)
294
+ klass = beginning_of_chain
295
+
296
+ # NOTE: we must use :include in the count query, because some conditions may reference other tables
297
+ if options[:pagination] && options[:pagination] != :infinite
298
+ count_query = append_to_query(klass, count_options(find_options, options[:count_includes]))
299
+ debugger
300
+ count = count_query.count unless options[:pagination] == :infinite
301
+ end
302
+
303
+ # Converts count to an integer if ActiveRecord returned an OrderedHash
304
+ # that happens when find_options contains a :group key
305
+ count = count.length if count.is_a? ActiveSupport::OrderedHash
306
+
307
+ # we build the paginator differently for method- and sql-based sorting
308
+ if options[:sorting] and options[:sorting].sorts_by_method?
309
+ pager = ::Paginator.new(count, options[:per_page]) do |offset, per_page|
310
+ sorted_collection = sort_collection_by_column(append_to_query(klass, find_options).all, *options[:sorting].first)
311
+ sorted_collection = sorted_collection.slice(offset, per_page) if options[:pagination]
312
+ sorted_collection
313
+ end
314
+ else
315
+ pager = ::Paginator.new(count, options[:per_page]) do |offset, per_page|
316
+ find_options.merge!(:offset => offset, :limit => per_page) if options[:pagination]
317
+ append_to_query(klass, find_options).all
318
+ end
319
+ end
320
+ pager.page(options[:page])
321
+ end
322
+
323
+ def append_to_query(query, options)
324
+ options.assert_valid_keys :where, :select, :group, :order, :limit, :offset, :joins, :includes, :lock, :readonly, :from
325
+ options.reject{|k, v| v.blank?}.inject(query) do |query, (k, v)|
326
+ # default ordering of model has a higher priority than current queries ordering
327
+ # fix this by removing existing ordering from arel
328
+ if k.to_sym == :order
329
+ query = query.where('1=1') unless query.is_a?(ActiveRecord::Relation)
330
+ query = query.except(:order)
331
+ end
332
+ query.send((k.to_sym), v)
333
+ end
334
+ end
335
+
336
+ def joins_for_finder
337
+ case joins_for_collection
338
+ when String
339
+ [ joins_for_collection ]
340
+ when Array
341
+ joins_for_collection
342
+ else
343
+ []
344
+ end + active_scaffold_habtm_joins
345
+ end
346
+
347
+ def merge_conditions(*conditions)
348
+ segments = []
349
+ conditions.each do |condition|
350
+ unless condition.blank?
351
+ sql = active_scaffold_config.model.send(:sanitize_sql, condition)
352
+ segments << sql unless sql.blank?
353
+ end
354
+ end
355
+ "(#{segments.join(') AND (')})" unless segments.empty?
356
+ end
357
+
358
+ # TODO: this should reside on the column, not the controller
359
+ def sort_collection_by_column(collection, column, order)
360
+ sorter = column.sort[:method]
361
+ collection = collection.sort_by { |record|
362
+ value = (sorter.is_a? Proc) ? record.instance_eval(&sorter) : record.instance_eval(sorter)
363
+ value = '' if value.nil?
364
+ value
365
+ }
366
+ collection.reverse! if order.downcase == 'desc'
367
+ collection
368
+ end
369
+ end
370
+ end