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.
- data/frontends/default/views/_action_group.html.erb +1 -1
- data/frontends/default/views/_action_group.html.erb~ +24 -0
- data/frontends/default/views/_form.html.erb~ +26 -0
- data/frontends/default/views/_form_association.html.erb~ +19 -0
- data/frontends/default/views/_form_association_footer.html.erb~ +16 -6
- data/frontends/default/views/_horizontal_subform.html.erb~ +29 -0
- data/frontends/default/views/_horizontal_subform_header.html.erb~ +3 -2
- data/frontends/default/views/_list_actions.html.erb~ +15 -0
- data/frontends/default/views/_list_inline_adapter.html.erb~ +10 -0
- data/frontends/default/views/_list_messages.html.erb~ +30 -0
- data/frontends/default/views/_list_pagination.html.erb~ +11 -0
- data/frontends/default/views/_list_pagination_links.html.erb~ +0 -0
- data/frontends/default/views/_render_field.js.erb~ +23 -0
- data/frontends/default/views/_row.html.erb~ +6 -0
- data/frontends/default/views/_vertical_subform.html.erb~ +12 -0
- data/frontends/default/views/edit_associated.js.erb~ +13 -0
- data/frontends/default/views/on_create.js.rjs +2 -2
- data/frontends/default/views/render_field.js.erb~ +1 -0
- data/lib/active_scaffold/actions/core.rb~ +13 -5
- data/lib/active_scaffold/actions/create.rb~ +149 -0
- data/lib/active_scaffold/actions/list.rb~ +196 -0
- data/lib/active_scaffold/actions/nested.rb +6 -2
- data/lib/active_scaffold/actions/nested.rb~ +252 -0
- data/lib/active_scaffold/actions/search.rb~ +49 -0
- data/lib/active_scaffold/actions/subform.rb~ +27 -0
- data/lib/active_scaffold/actions/update.rb~ +149 -0
- data/lib/active_scaffold/attribute_params.rb~ +202 -0
- data/lib/active_scaffold/bridges/record_select/{lib/record_select_bridge.rb~ → helpers.rb~} +7 -16
- data/lib/active_scaffold/bridges/shared/date_bridge.rb~ +209 -0
- data/lib/active_scaffold/config/create.rb +4 -4
- data/lib/active_scaffold/config/nested.rb +1 -0
- data/lib/active_scaffold/config/nested.rb~ +41 -0
- data/lib/active_scaffold/config/search.rb~ +74 -0
- data/lib/active_scaffold/constraints.rb~ +186 -0
- data/lib/active_scaffold/data_structures/action_columns.rb~ +140 -0
- data/lib/active_scaffold/data_structures/action_link.rb +4 -4
- data/lib/active_scaffold/data_structures/action_link.rb~ +179 -0
- data/lib/active_scaffold/data_structures/nested_info.rb~ +124 -0
- data/lib/active_scaffold/extensions/action_controller_rendering.rb~ +22 -0
- data/lib/active_scaffold/extensions/action_view_rendering.rb~ +108 -0
- data/lib/active_scaffold/extensions/cache_association.rb~ +12 -0
- data/lib/active_scaffold/extensions/reverse_associations.rb~ +64 -0
- data/lib/active_scaffold/extensions/routing_mapper.rb~ +34 -0
- data/lib/active_scaffold/extensions/unsaved_associated.rb~ +62 -0
- data/lib/active_scaffold/finder.rb~ +370 -0
- data/lib/active_scaffold/helpers/controller_helpers.rb~ +101 -0
- data/lib/active_scaffold/helpers/form_column_helpers.rb~ +321 -0
- data/lib/active_scaffold/helpers/id_helpers.rb~ +123 -0
- data/lib/active_scaffold/helpers/list_column_helpers.rb +9 -6
- data/lib/active_scaffold/helpers/list_column_helpers.rb~ +368 -0
- data/lib/active_scaffold/helpers/search_column_helpers.rb~ +94 -46
- data/lib/active_scaffold/helpers/view_helpers.rb +1 -1
- data/lib/active_scaffold/helpers/view_helpers.rb~ +353 -0
- data/lib/active_scaffold/version.rb +1 -1
- data/lib/active_scaffold.rb +1 -1
- data/lib/active_scaffold.rb~ +362 -0
- metadata +110 -76
- data/lib/active_scaffold/bridges/dragonfly/bridge.rb~ +0 -12
- data/lib/active_scaffold/bridges/dragonfly/lib/dragonfly_bridge.rb~ +0 -36
- data/lib/active_scaffold/bridges/dragonfly/lib/dragonfly_bridge_helpers.rb~ +0 -12
- data/lib/active_scaffold/bridges/dragonfly/lib/form_ui.rb~ +0 -27
- 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,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
|