recordselect 3.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,37 @@
1
+ module RecordSelect
2
+ def self.included(base)
3
+ base.send :extend, ClassMethods
4
+ #base.append_view_path "#{File.dirname(__FILE__)}/../app/views" if defined?(RECORD_SELECT_GEM)
5
+ end
6
+
7
+ module ClassMethods
8
+ # Enables and configures RecordSelect on your controller.
9
+ #
10
+ # *Options*
11
+ # +model+:: defaults based on the name of the controller
12
+ # +per_page+:: records to show per page when browsing
13
+ # +notify+:: a method name to invoke when a record has been selected.
14
+ # +order_by+:: a SQL string to order the search results
15
+ # +search_on+:: an array of searchable fields
16
+ # +full_text_search+:: a boolean for whether to use a %?% search pattern or not. default is false.
17
+ # +label+:: a proc that accepts a record as argument and returns an option label. default is to call record.to_label instead.
18
+ # +include+:: as for ActiveRecord::Base#find. can help with search conditions or just help optimize rendering the results.
19
+ # +link+:: a boolean for whether wrap the text returned by label in a link or not. default is true. set to false when
20
+ # label returns html code which can't be inside a tag. You can use record_select_link_to_select in your proc
21
+ # or partial to add a link to select action
22
+ #
23
+ # You may also pass a block, which will be used as options[:notify].
24
+ def record_select(options = {})
25
+ options[:model] ||= self.to_s.split('::').last.sub(/Controller$/, '').pluralize.singularize.underscore
26
+ @record_select_config = RecordSelect::Config.new(options.delete(:model), options)
27
+ self.send :include, RecordSelect::Actions
28
+ self.send :include, RecordSelect::Conditions
29
+ end
30
+
31
+ attr_reader :record_select_config
32
+
33
+ def uses_record_select?
34
+ !record_select_config.nil?
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,68 @@
1
+ module RecordSelect
2
+ module Actions
3
+ # :method => :get
4
+ # params => [:page, :search]
5
+ def browse
6
+ conditions = record_select_conditions
7
+ klass = record_select_config.model
8
+ @count = klass.count(:conditions => conditions, :include => record_select_includes)
9
+ pager = ::Paginator.new(@count, record_select_config.per_page) do |offset, per_page|
10
+ klass.find(:all, :offset => offset,
11
+ :select => record_select_select||"*",
12
+ :include => [record_select_includes, record_select_config.include].flatten.compact,
13
+ :limit => per_page,
14
+ :conditions => conditions,
15
+ :order => record_select_config.order_by)
16
+ end
17
+ @page = pager.page(params[:page] || 1)
18
+
19
+ respond_to do |wants|
20
+ wants.html { render_record_select :partial => 'browse'}
21
+ wants.js {
22
+ if params[:update]
23
+ render_record_select :template => 'browse.js', :layout => false
24
+ else
25
+ render_record_select :partial => 'browse'
26
+ end
27
+ }
28
+ wants.yaml {}
29
+ wants.xml {}
30
+ wants.json {}
31
+ end
32
+ end
33
+
34
+ # :method => :post
35
+ # params => [:id]
36
+ def select
37
+ klass = record_select_config.model
38
+ record = klass.find(params[:id])
39
+ if record_select_config.notify.is_a? Proc
40
+ record_select_config.notify.call(record)
41
+ elsif record_select_config.notify
42
+ send(record_select_config.notify, record)
43
+ end
44
+ render :nothing => true
45
+ end
46
+
47
+ protected
48
+
49
+ def record_select_config #:nodoc:
50
+ self.class.record_select_config
51
+ end
52
+
53
+ def render_record_select(options = {}) #:nodoc:
54
+ [:template,:partial].each do |template_name|
55
+ if options[template_name] then
56
+ options[template_name] = File.join(record_select_views_path, options[template_name])
57
+ end
58
+ end
59
+ if block_given? then yield options else render options end
60
+ end
61
+
62
+ private
63
+
64
+ def record_select_views_path
65
+ "record_select"
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,100 @@
1
+ module RecordSelect
2
+ module Conditions
3
+ protected
4
+ # returns the combination of all conditions.
5
+ # conditions come from:
6
+ # * current search (params[:search])
7
+ # * intelligent url params (e.g. params[:first_name] if first_name is a model column)
8
+ # * specific conditions supplied by the developer
9
+ def record_select_conditions
10
+ conditions = []
11
+
12
+ merge_conditions(
13
+ record_select_conditions_from_search,
14
+ record_select_conditions_from_params,
15
+ record_select_conditions_from_controller
16
+ )
17
+ end
18
+
19
+ # an override method.
20
+ # here you can provide custom conditions to define the selectable records. useful for situational restrictions.
21
+ def record_select_conditions_from_controller; end
22
+
23
+ # another override method.
24
+ # define any association includes you want for the finder search.
25
+ def record_select_includes; end
26
+
27
+ def record_select_like_operator
28
+ @like_operator ||= ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL" ? "ILIKE" : "LIKE"
29
+ end
30
+
31
+ # define special list of selected fields,
32
+ # mainly to define extra fields that can be used for
33
+ # specialized sorting.
34
+ def record_select_select; end
35
+
36
+ # generate conditions from params[:search]
37
+ # override this if you want to customize the search routine
38
+ def record_select_conditions_from_search
39
+ search_pattern = record_select_config.full_text_search? ? '%?%' : '?%'
40
+
41
+ if params[:search] and !params[:search].strip.empty?
42
+ if record_select_config.full_text_search?
43
+ tokens = params[:search].strip.split(' ')
44
+ else
45
+ tokens = []
46
+ tokens << params[:search].strip
47
+ end
48
+
49
+ where_clauses = record_select_config.search_on.collect { |sql| "#{sql} #{record_select_like_operator} ?" }
50
+ phrase = "(#{where_clauses.join(' OR ')})"
51
+
52
+ sql = ([phrase] * tokens.length).join(' AND ')
53
+ tokens = tokens.collect{ |value| [search_pattern.sub('?', value)] * record_select_config.search_on.length }.flatten
54
+
55
+ conditions = [sql, *tokens]
56
+ end
57
+ end
58
+
59
+ # instead of a shotgun approach, this assumes the user is
60
+ # searching vs some SQL field (possibly built with CONCAT())
61
+ # similar to the record labels.
62
+ # def record_select_simple_conditions_from_search
63
+ # return unless params[:search] and not params[:search].empty?
64
+ #
65
+ # search_pattern = record_select_config.full_text_search? ? '%?%' : '?%'
66
+ # search_string = search_pattern.sub('?', value.downcase)
67
+ #
68
+ # ["LOWER(#{record_select_config.search_on})", search_pattern.sub('?', value.downcase)]
69
+ # end
70
+
71
+ # generate conditions from the url parameters (e.g. users/browse?group_id=5)
72
+ def record_select_conditions_from_params
73
+ conditions = nil
74
+ params.each do |field, value|
75
+ next unless column = record_select_config.model.columns_hash[field]
76
+ conditions = merge_conditions(
77
+ conditions,
78
+ record_select_condition_for_column(column, value)
79
+ )
80
+ end
81
+ conditions
82
+ end
83
+
84
+ # generates an SQL condition for the given column/value
85
+ def record_select_condition_for_column(column, value)
86
+ if value.blank? and column.null
87
+ "#{column.name} IS NULL"
88
+ elsif column.text?
89
+ ["LOWER(#{column.name}) LIKE ?", value]
90
+ else
91
+ ["#{column.name} = ?", column.type_cast(value)]
92
+ end
93
+ end
94
+
95
+ def merge_conditions(*conditions) #:nodoc:
96
+ c = conditions.find_all {|c| not c.nil? and not c.empty? }
97
+ c.empty? ? nil : c.collect{|c| record_select_config.model.send(:sanitize_sql, c)}.join(' AND ')
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,99 @@
1
+ module RecordSelect
2
+ # a write-once configuration object
3
+ class Config
4
+ def initialize(klass, options = {})
5
+ @klass = klass
6
+
7
+ @notify = block_given? ? proc : options[:notify]
8
+
9
+ @per_page = options[:per_page]
10
+
11
+ @search_on = [options[:search_on]].flatten unless options[:search_on].nil?
12
+
13
+ @order_by = options[:order_by]
14
+
15
+ @full_text_search = options[:full_text_search]
16
+
17
+ @label = options[:label]
18
+
19
+ @include = options[:include]
20
+
21
+ @link = options[:link]
22
+ end
23
+
24
+ def self.js_framework=(framework)
25
+ @@js_framework = framework
26
+ end
27
+
28
+ def self.js_framework
29
+ @@js_framework ||= :prototype
30
+ end
31
+
32
+ # The model object we're browsing
33
+ def model
34
+ @model ||= klass.to_s.camelcase.constantize
35
+ end
36
+
37
+ # Records to show on a page
38
+ def per_page
39
+ @per_page ||= 10
40
+ end
41
+
42
+ # The method name or proc to notify of a selection event.
43
+ # May not matter if the selection event is intercepted client-side.
44
+ def notify
45
+ @notify
46
+ end
47
+
48
+ # A collection of fields to search. This is essentially raw SQL, so you could search on "CONCAT(first_name, ' ', last_name)" if you wanted to.
49
+ # NOTE: this does *NO* default transforms (such as LOWER()), that's left entirely up to you.
50
+ def search_on
51
+ @search_on ||= self.model.columns.collect{|c| c.name if [:text, :string].include? c.type}.compact
52
+ end
53
+
54
+ def order_by
55
+ @order_by ||= "#{model.table_name}.#{model.primary_key} ASC" unless @order_by == false
56
+ end
57
+
58
+ def full_text_search?
59
+ @full_text_search ? true : false
60
+ end
61
+
62
+ def include
63
+ @include
64
+ end
65
+
66
+ # If a proc, must accept the record as an argument and return a descriptive string.
67
+ #
68
+ # If a symbol or string, must name a partial that renders a representation of the
69
+ # record. The partial should assume a local "record" variable, and should include a
70
+ # <label> tag, even if it's not visible. The contents of the <label> tag will be used
71
+ # to represent the record once it has been selected. For example:
72
+ #
73
+ # record_select_config.label = :user_description
74
+ #
75
+ # > app/views/users/_user_description.erb
76
+ #
77
+ # <div class="user_description">
78
+ # <%= image_tag url_for_file_column(record, 'avatar') %>
79
+ # <label><%= record.username %></label>
80
+ # <p><%= record.quote %></p>
81
+ # </div>
82
+ #
83
+ def label
84
+ @label ||= proc {|r| r.to_label}
85
+ end
86
+
87
+ # whether wrap the text returned by label in a link or not
88
+ def link?
89
+ @link.nil? ? true : @link
90
+ end
91
+
92
+ protected
93
+
94
+ # A singularized underscored version of the model we're browsing
95
+ def klass
96
+ @klass
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,4 @@
1
+ module RecordSelect
2
+ class Engine < Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveRecord # :nodoc:
2
+ class Base # :nodoc:
3
+ unless method_defined? :to_label
4
+ def to_label
5
+ self.to_s
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ # Provides a simple pass-through localizer for RecordSelect. If you want
2
+ # to localize RS, you need to override this method and route it to your
3
+ # own system.
4
+ class Object
5
+ def rs_(string_to_localize, *args)
6
+ args.empty? ? string_to_localize : (sprintf string_to_localize, *args)
7
+ end
8
+ end
@@ -0,0 +1,20 @@
1
+ module ActionDispatch
2
+ module Routing
3
+ RECORD_SELECT_ROUTING = {
4
+ :collection => {:browse => :get},
5
+ :member => {:select => :post}
6
+ }
7
+ class Mapper
8
+ module Base
9
+ def record_select_routes
10
+ collection do
11
+ ActionDispatch::Routing::RECORD_SELECT_ROUTING[:collection].each {|name, type| send(type, name)}
12
+ end
13
+ member do
14
+ ActionDispatch::Routing::RECORD_SELECT_ROUTING[:member].each {|name, type| send(type, name)}
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ module RecordSelect
2
+ module FormBuilder
3
+ def record_select(association, options = {})
4
+ reflection = @object.class.reflect_on_association(association)
5
+ form_name = form_name_for_association(reflection)
6
+ current = @object.send(association)
7
+ options[:id] ||= "#{@object_name.gsub(/[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")}_#{association}"
8
+
9
+ if [:has_one, :belongs_to].include? reflection.macro
10
+ @template.record_select_field(form_name, current || reflection.klass.new, options)
11
+ else
12
+ options[:controller] ||= reflection.klass.to_s.pluralize.underscore
13
+ @template.record_multi_select_field(form_name, current, options)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def form_name_for_association(reflection)
20
+ key_name = (reflection.options[:foreign_key] || reflection.association_foreign_key)
21
+ key_name += "s" unless [:has_one, :belongs_to].include? reflection.macro
22
+ form_name = "#{@object_name}[#{key_name}]"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,185 @@
1
+ module RecordSelectHelper
2
+ # Print this from your layout to include everything necessary for RecordSelect to work.
3
+ # Well, not everything. You need Prototype too.
4
+ def record_select_includes
5
+ includes = ''
6
+ includes << stylesheet_link_tag('record_select/record_select')
7
+ includes << javascript_include_tag('record_select/record_select')
8
+ includes.html_safe
9
+ end
10
+
11
+ # Adds a link on the page that toggles a RecordSelect widget from the given controller.
12
+ #
13
+ # *Options*
14
+ # +onselect+:: JavaScript code to handle selections client-side. This code has access to two variables: id, label. If the code returns false, the dialog will *not* close automatically.
15
+ # +params+:: Extra URL parameters. If any parameter is a column name, the parameter will be used as a search term to filter the result set.
16
+ def link_to_record_select(name, controller, options = {})
17
+ options[:params] ||= {}
18
+ options[:params].merge!(:controller => controller, :action => :browse)
19
+ options[:onselect] = "function(id, label) {#{options[:onselect]}}" if options[:onselect]
20
+ options[:html] ||= {}
21
+ options[:html][:id] ||= "rs_#{rand(9999)}"
22
+
23
+ assert_controller_responds(options[:params][:controller])
24
+
25
+ html = link_to_function(name, '', options[:html])
26
+ html << javascript_tag("new RecordSelect.Dialog(#{options[:html][:id].to_json}, #{url_for(options[:params].merge(:escape => false)).to_json}, {onselect: #{options[:onselect] || ''}})")
27
+
28
+ return html
29
+ end
30
+
31
+ # Adds a RecordSelect-based form field. The field submits the record's id using a hidden input.
32
+ #
33
+ # *Arguments*
34
+ # +name+:: the input name that will be used to submit the selected record's id.
35
+ # +current+:: the currently selected object. provide a new record if there're none currently selected and you have not passed the optional :controller argument.
36
+ #
37
+ # *Options*
38
+ # +controller+:: The controller configured to provide the result set. Optional if you have standard resource controllers (e.g. UsersController for the User model), in which case the controller will be inferred from the class of +current+ (the second argument)
39
+ # +params+:: A hash of extra URL parameters
40
+ # +id+:: The id to use for the input. Defaults based on the input's name.
41
+ # +onchange+:: A JavaScript function that will be called whenever something new is selected. It should accept the new id as the first argument, and the new label as the second argument. For example, you could set onchange to be "function(id, label) {alert(id);}", or you could create a JavaScript function somewhere else and set onchange to be "my_function" (without the parantheses!).
42
+ def record_select_field(name, current, options = {})
43
+ options[:controller] ||= current.class.to_s.pluralize.underscore
44
+ options[:params] ||= {}
45
+ options[:id] ||= name.gsub(/[\[\]]/, '_')
46
+
47
+ controller = assert_controller_responds(options[:controller])
48
+
49
+ id = label = ''
50
+ if current and not current.new_record?
51
+ id = current.id
52
+ label = label_for_field(current, controller)
53
+ end
54
+
55
+ url = url_for({:action => :browse, :controller => options[:controller], :escape => false}.merge(options[:params]))
56
+
57
+ html = text_field_tag(name, nil, :autocomplete => 'off', :id => options[:id], :class => options[:class], :onfocus => "this.focused=true", :onblur => "this.focused=false")
58
+ html << javascript_tag("new RecordSelect.Single(#{options[:id].to_json}, #{url.to_json}, {id: #{id.to_json}, label: #{label.to_json}, onchange: #{options[:onchange] || ''.to_json}});")
59
+
60
+ return html
61
+ end
62
+
63
+ # Assists with the creation of an observer for the :onchange option of the record_select_field method.
64
+ # Currently only supports building an Ajax.Request based on the id of the selected record.
65
+ #
66
+ # options[:url] should be a hash with all the necessary options *except* :id. that parameter
67
+ # will be provided based on the selected record.
68
+ #
69
+ # Question: if selecting users, what's more likely?
70
+ # /users/5/categories
71
+ # /categories?user_id=5
72
+ def record_select_observer(options = {})
73
+ fn = ""
74
+ fn << "function(id, value) {"
75
+ fn << "var url = #{url_for(options[:url].merge(:id => ":id:")).to_json}.replace(/:id:/, id);"
76
+ fn << "new Ajax.Request(url);"
77
+ fn << "}"
78
+ end
79
+
80
+ # Adds a RecordSelect-based form field for multiple selections. The values submit using a list of hidden inputs.
81
+ #
82
+ # *Arguments*
83
+ # +name+:: the input name that will be used to submit the selected records' ids. empty brackets will be appended to the name.
84
+ # +current+:: pass a collection of existing associated records
85
+ #
86
+ # *Options*
87
+ # +controller+:: The controller configured to provide the result set.
88
+ # +params+:: A hash of extra URL parameters
89
+ # +id+:: The id to use for the input. Defaults based on the input's name.
90
+ def record_multi_select_field(name, current, options = {})
91
+ options[:controller] ||= current.first.class.to_s.pluralize.underscore
92
+ options[:params] ||= {}
93
+ options[:id] ||= name.gsub(/[\[\]]/, '_')
94
+
95
+ controller = assert_controller_responds(options[:controller])
96
+
97
+ # js identifier so we can talk to it.
98
+ widget = "rs_%s" % name.gsub(/[\[\]]/, '_').chomp('_')
99
+
100
+ current = current.inject([]) { |memo, record| memo.push({:id => record.id, :label => label_for_field(record, controller)}) }
101
+
102
+ url = url_for({:action => :browse, :controller => options[:controller], :escape => false}.merge(options[:params]))
103
+
104
+ html = text_field_tag("#{name}[]", nil, :autocomplete => 'off', :id => options[:id], :class => options[:class], :onfocus => "this.focused=true", :onblur => "this.focused=false")
105
+ html << content_tag('ul', '', :class => 'record-select-list');
106
+ html << javascript_tag("#{widget} = new RecordSelect.Multiple(#{options[:id].to_json}, #{url.to_json}, {current: #{current.to_json}});")
107
+
108
+ return html
109
+ end
110
+
111
+ # A helper to render RecordSelect partials
112
+ def render_record_select(options = {}) #:nodoc:
113
+ controller.send(:render_record_select, options) do |options|
114
+ render options
115
+ end
116
+ end
117
+
118
+ # Provides view access to the RecordSelect configuration
119
+ def record_select_config #:nodoc:
120
+ controller.send :record_select_config
121
+ end
122
+
123
+ # The id of the RecordSelect widget for the given controller.
124
+ def record_select_id(controller = nil) #:nodoc:
125
+ controller ||= params[:controller]
126
+ "record-select-#{controller.gsub('/', '_')}"
127
+ end
128
+
129
+ def record_select_search_id(controller = nil) #:nodoc:
130
+ "#{record_select_id(controller)}-search"
131
+ end
132
+
133
+ private
134
+ # render the record using the renderer and add a link to select the record
135
+ def render_record_in_list(record, controller_path)
136
+ text = render_record_from_config(record)
137
+ if record_select_config.link?
138
+ url_options = {:controller => controller_path, :action => :select, :id => record.id, :escape => false}
139
+ link_to text, url_options, :method => :post, :remote => true, :class => ''
140
+ else
141
+ text
142
+ end
143
+ end
144
+
145
+
146
+ # uses renderer (defaults to record_select_config.label) to determine how the given record renders.
147
+ def render_record_from_config(record, renderer = record_select_config.label)
148
+ case renderer
149
+ when Symbol, String
150
+ # return full-html from the named partial
151
+ render :partial => renderer.to_s, :locals => {:record => record}
152
+
153
+ when Proc
154
+ # return an html-cleaned descriptive string
155
+ h renderer.call(record)
156
+ end
157
+ end
158
+
159
+ # uses the result of render_record_from_config to snag an appropriate record label
160
+ # to display in a field.
161
+ #
162
+ # if given a controller, searches for a partial in its views path
163
+ def label_for_field(record, controller = self.controller)
164
+ renderer = controller.record_select_config.label
165
+ case renderer
166
+ when Symbol, String
167
+ # find the <label> element and grab its innerHTML
168
+ description = render_record_from_config(record, File.join(controller.controller_path, renderer.to_s))
169
+ description.match(/<label[^>]*>(.*)<\/label>/)[1]
170
+
171
+ when Proc
172
+ # just return the string
173
+ render_record_from_config(record, renderer)
174
+ end
175
+ end
176
+
177
+ def assert_controller_responds(controller_name)
178
+ controller_name = "#{controller_name.camelize}Controller"
179
+ controller = controller_name.constantize
180
+ unless controller.uses_record_select?
181
+ raise "#{controller_name} has not been configured to use RecordSelect."
182
+ end
183
+ controller
184
+ end
185
+ end