active_scaffold 3.0.23 → 3.0.24

Sign up to get free protection for your applications and to get access to all the features.
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