recordselect_vho 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/init.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'recordselect'
2
+
3
+ begin
4
+ RecordSelectAssets.copy_to_public
5
+ rescue
6
+ raise $! unless Rails.env == 'production'
7
+ end
data/install.rb ADDED
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -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,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,67 @@
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
+ :include => [record_select_includes, record_select_config.include].flatten.compact,
12
+ :limit => per_page,
13
+ :conditions => conditions,
14
+ :order => record_select_config.order_by)
15
+ end
16
+ @page = pager.page(params[:page] || 1)
17
+
18
+ respond_to do |wants|
19
+ wants.html { render_record_select :partial => 'browse'}
20
+ wants.js {
21
+ if params[:update]
22
+ render_record_select :template => 'browse', :format => :js, :layout => false
23
+ else
24
+ render_record_select :partial => 'browse'
25
+ end
26
+ }
27
+ wants.yaml {}
28
+ wants.xml {}
29
+ wants.json {}
30
+ end
31
+ end
32
+
33
+ # :method => :post
34
+ # params => [:id]
35
+ def select
36
+ klass = record_select_config.model
37
+ record = klass.find(params[:id])
38
+ if record_select_config.notify.is_a? Proc
39
+ record_select_config.notify.call(record)
40
+ elsif record_select_config.notify
41
+ send(record_select_config.notify, record)
42
+ end
43
+ render :nothing => true
44
+ end
45
+
46
+ protected
47
+
48
+ def record_select_config #:nodoc:
49
+ self.class.record_select_config
50
+ end
51
+
52
+ def render_record_select(options = {}) #:nodoc:
53
+ [:template,:partial].each do |template_name|
54
+ if options[template_name] then
55
+ options[template_name] = File.join(record_select_views_path, options[template_name])
56
+ end
57
+ end
58
+ if block_given? then yield options else render options end
59
+ end
60
+
61
+ private
62
+
63
+ def record_select_views_path
64
+ "record_select"
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,86 @@
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
+ # generate conditions from params[:search]
28
+ # override this if you want to customize the search routine
29
+ def record_select_conditions_from_search
30
+ search_pattern = record_select_config.full_text_search? ? '%?%' : '?%'
31
+
32
+ if params[:search] and !params[:search].strip.empty?
33
+ tokens = params[:search].strip.split(' ')
34
+
35
+ where_clauses = record_select_config.search_on.collect { |sql| "#{sql} LIKE ?" }
36
+ phrase = "(#{where_clauses.join(' OR ')})"
37
+
38
+ sql = ([phrase] * tokens.length).join(' AND ')
39
+ tokens = tokens.collect{ |value| [search_pattern.sub('?', value.downcase)] * record_select_config.search_on.length }.flatten
40
+
41
+ conditions = [sql, *tokens]
42
+ end
43
+ end
44
+
45
+ # instead of a shotgun approach, this assumes the user is
46
+ # searching vs some SQL field (possibly built with CONCAT())
47
+ # similar to the record labels.
48
+ # def record_select_simple_conditions_from_search
49
+ # return unless params[:search] and not params[:search].empty?
50
+ #
51
+ # search_pattern = record_select_config.full_text_search? ? '%?%' : '?%'
52
+ # search_string = search_pattern.sub('?', value.downcase)
53
+ #
54
+ # ["LOWER(#{record_select_config.search_on})", search_pattern.sub('?', value.downcase)]
55
+ # end
56
+
57
+ # generate conditions from the url parameters (e.g. users/browse?group_id=5)
58
+ def record_select_conditions_from_params
59
+ conditions = nil
60
+ params.each do |field, value|
61
+ next unless column = record_select_config.model.columns_hash[field]
62
+ conditions = merge_conditions(
63
+ conditions,
64
+ record_select_condition_for_column(column, value)
65
+ )
66
+ end
67
+ conditions
68
+ end
69
+
70
+ # generates an SQL condition for the given column/value
71
+ def record_select_condition_for_column(column, value)
72
+ if value.blank? and column.null
73
+ "#{column.name} IS NULL"
74
+ elsif column.text?
75
+ ["LOWER(#{column.name}) LIKE ?", value]
76
+ else
77
+ ["#{column.name} = ?", column.type_cast(value)]
78
+ end
79
+ end
80
+
81
+ def merge_conditions(*conditions) #:nodoc:
82
+ c = conditions.find_all {|c| not c.nil? and not c.empty? }
83
+ c.empty? ? nil : c.collect{|c| record_select_config.model.send(:sanitize_sql, c)}.join(' AND ')
84
+ end
85
+ end
86
+ 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,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,182 @@
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
+ current = current.inject([]) { |memo, record| memo.push({:id => record.id, :label => label_for_field(record, controller)}) }
98
+
99
+ url = url_for({:action => :browse, :controller => options[:controller], :escape => false}.merge(options[:params]))
100
+
101
+ html = text_field_tag("#{name}[]", nil, :autocomplete => 'off', :id => options[:id], :class => options[:class], :onfocus => "this.focused=true", :onblur => "this.focused=false")
102
+ html << content_tag('ul', '', :class => 'record-select-list');
103
+ html << javascript_tag("new RecordSelect.Multiple(#{options[:id].to_json}, #{url.to_json}, {current: #{current.to_json}});")
104
+
105
+ return html
106
+ end
107
+
108
+ # A helper to render RecordSelect partials
109
+ def render_record_select(options = {}) #:nodoc:
110
+ controller.send(:render_record_select, options) do |options|
111
+ render options
112
+ end
113
+ end
114
+
115
+ # Provides view access to the RecordSelect configuration
116
+ def record_select_config #:nodoc:
117
+ controller.send :record_select_config
118
+ end
119
+
120
+ # The id of the RecordSelect widget for the given controller.
121
+ def record_select_id(controller = nil) #:nodoc:
122
+ controller ||= params[:controller]
123
+ "record-select-#{controller.gsub('/', '_')}"
124
+ end
125
+
126
+ def record_select_search_id(controller = nil) #:nodoc:
127
+ "#{record_select_id(controller)}-search"
128
+ end
129
+
130
+ private
131
+ # render the record using the renderer and add a link to select the record
132
+ def render_record_in_list(record, controller_path)
133
+ text = render_record_from_config(record)
134
+ if record_select_config.link?
135
+ url_options = {:controller => controller_path, :action => :select, :id => record.id, :escape => false}
136
+ link_to text, url_options, :method => :post, :remote => true, :class => ''
137
+ else
138
+ text
139
+ end
140
+ end
141
+
142
+
143
+ # uses renderer (defaults to record_select_config.label) to determine how the given record renders.
144
+ def render_record_from_config(record, renderer = record_select_config.label)
145
+ case renderer
146
+ when Symbol, String
147
+ # return full-html from the named partial
148
+ render :partial => renderer.to_s, :locals => {:record => record}
149
+
150
+ when Proc
151
+ # return an html-cleaned descriptive string
152
+ h renderer.call(record)
153
+ end
154
+ end
155
+
156
+ # uses the result of render_record_from_config to snag an appropriate record label
157
+ # to display in a field.
158
+ #
159
+ # if given a controller, searches for a partial in its views path
160
+ def label_for_field(record, controller = self.controller)
161
+ renderer = controller.record_select_config.label
162
+ case renderer
163
+ when Symbol, String
164
+ # find the <label> element and grab its innerHTML
165
+ description = render_record_from_config(record, File.join(controller.controller_path, renderer.to_s))
166
+ description.match(/<label[^>]*>(.*)<\/label>/)[1]
167
+
168
+ when Proc
169
+ # just return the string
170
+ render_record_from_config(record, renderer)
171
+ end
172
+ end
173
+
174
+ def assert_controller_responds(controller_name)
175
+ controller_name = "#{controller_name.camelize}Controller"
176
+ controller = controller_name.constantize
177
+ unless controller.uses_record_select?
178
+ raise "#{controller_name} has not been configured to use RecordSelect."
179
+ end
180
+ controller
181
+ end
182
+ end