recordselect 3.2.5 → 3.2.6
Sign up to get free protection for your applications and to get access to all the features.
- data/app/assets/images/record_select/cross.gif +0 -0
- data/app/assets/images/record_select/next.gif +0 -0
- data/app/assets/images/record_select/previous.gif +0 -0
- data/app/assets/javascripts/jquery/record_select.js +544 -0
- data/app/assets/javascripts/prototype/record_select.js +419 -0
- data/app/assets/javascripts/record_select.js.erb +5 -0
- data/app/assets/stylesheets/record_select.css.erb +133 -0
- data/app/views/record_select/_browse.html.erb +8 -0
- data/app/views/record_select/_list.html.erb +31 -0
- data/app/views/record_select/_search.html.erb +20 -0
- data/app/views/record_select/browse.js.erb +1 -0
- data/config/locales/en.yml +9 -0
- data/config/locales/es.yml +13 -0
- data/lib/record_select.rb +36 -0
- data/lib/record_select/actions.rb +68 -0
- data/lib/record_select/conditions.rb +100 -0
- data/lib/record_select/config.rb +103 -0
- data/lib/record_select/engine.rb +4 -0
- data/lib/record_select/extensions/active_record.rb +9 -0
- data/lib/record_select/extensions/localization.rb +13 -0
- data/lib/record_select/extensions/routing_mapper.rb +20 -0
- data/lib/record_select/form_builder.rb +25 -0
- data/lib/record_select/helpers/record_select_helper.rb +217 -0
- data/lib/record_select/version.rb +9 -0
- data/lib/recordselect.rb +14 -0
- metadata +39 -28
@@ -0,0 +1,8 @@
|
|
1
|
+
<%
|
2
|
+
controller ||= params[:controller]
|
3
|
+
record_select_id = record_select_id(controller)
|
4
|
+
-%>
|
5
|
+
<div class="record-select" id="<%= record_select_id -%>">
|
6
|
+
<%= render_record_select :partial => 'search', :locals => {:controller => controller, :record_select_id => record_select_id} %>
|
7
|
+
<%= render_record_select :partial => 'list', :locals => {:controller => controller, :page => @page, :record_select_id => record_select_id} %>
|
8
|
+
</div>
|
@@ -0,0 +1,31 @@
|
|
1
|
+
<%
|
2
|
+
controller ||= params[:controller]
|
3
|
+
|
4
|
+
pagination_url_params = params.merge(:controller => controller, :action => :browse, :search => params[:search], :update => 1)
|
5
|
+
prev_url = url_for(pagination_url_params.merge(:page => page.prev.number, :escape => false)) if page.prev?
|
6
|
+
next_url = url_for(pagination_url_params.merge(:page => page.next.number, :escape => false)) if page.next?
|
7
|
+
-%>
|
8
|
+
<ol>
|
9
|
+
<li class="found"><%= rs_(:records_found, :count => page.pager.count,
|
10
|
+
:model => record_select_config.model.model_name.human(:count => page.pager.count).downcase) %></li>
|
11
|
+
<% if page.prev? -%>
|
12
|
+
<li class="pagination previous">
|
13
|
+
<%= link_to image_tag('record_select/previous.gif', :alt => rs_(:previous)) + " " + rs_(:previous_items,
|
14
|
+
:count => page.pager.per_page),
|
15
|
+
{:url => prev_url},
|
16
|
+
{:href => prev_url, :method => :get, :remote => true} %>
|
17
|
+
</li>
|
18
|
+
<% end -%>
|
19
|
+
<% page.items.each do |record| -%>
|
20
|
+
<li class="record <%= cycle 'odd', 'even' %>" id="rs<%= record.id -%>">
|
21
|
+
<%= render_record_in_list(record, controller) %>
|
22
|
+
</li>
|
23
|
+
<% end -%>
|
24
|
+
<% if page.next? -%>
|
25
|
+
<li class="pagination next">
|
26
|
+
<%= link_to (rs_(:next_items, :count => page.pager.per_page) + " " + image_tag('record_select/next.gif', :alt => rs_(:next))).html_safe,
|
27
|
+
{:url => next_url},
|
28
|
+
{:href => next_url, :method => :get, :remote => true} %>
|
29
|
+
</li>
|
30
|
+
<% end -%>
|
31
|
+
</ol>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<% url_options = params.merge(:controller => controller, :action => :browse, :page => 1, :update => 1, :escape => false) -%>
|
2
|
+
<%= form_tag url_options, {:method => :get, :remote => true, :id => record_select_search_id} -%>
|
3
|
+
<%= text_field_tag 'search', params[:search], :autocomplete => 'off', :class => 'text-input' %>
|
4
|
+
<%= submit_tag 'search', :class => "search_submit" %>
|
5
|
+
</form>
|
6
|
+
|
7
|
+
<script type="text/javascript">
|
8
|
+
//<![CDATA[
|
9
|
+
<% if RecordSelect::Config.js_framework == :prototype %>
|
10
|
+
var i = $(<%= record_select_search_id.to_json.html_safe %>).down('input.text-input');
|
11
|
+
Form.Element.AfterActivity(i, function() {
|
12
|
+
$(<%= record_select_search_id.to_json.html_safe -%>).down('input.search_submit').click();
|
13
|
+
}, 0.35);
|
14
|
+
<% elsif RecordSelect::Config.js_framework == :jquery %>
|
15
|
+
jQuery(<%= "##{record_select_search_id}".to_json.html_safe %>).find('input.text-input').delayedObserver(0.35, function() {
|
16
|
+
jQuery(<%= "##{record_select_search_id}".to_json.html_safe %>).trigger("submit");});
|
17
|
+
<% end %>
|
18
|
+
//]]>
|
19
|
+
</script>
|
20
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
RecordSelect.render_page('<%= record_select_id %>', '<%= escape_javascript(render_record_select(:partial => 'list', :locals => {:page => @page})) %>');
|
@@ -0,0 +1,13 @@
|
|
1
|
+
es:
|
2
|
+
record_select:
|
3
|
+
next: "Siguiente"
|
4
|
+
next_items:
|
5
|
+
one: "Siguente"
|
6
|
+
other: "%{count} siguentes"
|
7
|
+
previous: "Anterior"
|
8
|
+
previous_items:
|
9
|
+
one: "Anterior"
|
10
|
+
other: "%{count} anteriores"
|
11
|
+
records_found:
|
12
|
+
one: "1 %{model} encontrado"
|
13
|
+
other: "%{count} %{model} encontrados"
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module RecordSelect
|
2
|
+
def self.included(base)
|
3
|
+
base.send :extend, ClassMethods
|
4
|
+
end
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
# Enables and configures RecordSelect on your controller.
|
8
|
+
#
|
9
|
+
# *Options*
|
10
|
+
# +model+:: defaults based on the name of the controller
|
11
|
+
# +per_page+:: records to show per page when browsing
|
12
|
+
# +notify+:: a method name to invoke when a record has been selected.
|
13
|
+
# +order_by+:: a SQL string to order the search results
|
14
|
+
# +search_on+:: an array of searchable fields
|
15
|
+
# +full_text_search+:: a boolean for whether to use a %?% search pattern or not. default is false.
|
16
|
+
# +label+:: a proc that accepts a record as argument and returns an option label. default is to call record.to_label instead.
|
17
|
+
# +include+:: as for ActiveRecord::Base#find. can help with search conditions or just help optimize rendering the results.
|
18
|
+
# +link+:: a boolean for whether wrap the text returned by label in a link or not. default is true. set to false when
|
19
|
+
# label returns html code which can't be inside a tag. You can use record_select_link_to_select in your proc
|
20
|
+
# or partial to add a link to select action
|
21
|
+
#
|
22
|
+
# You may also pass a block, which will be used as options[:notify].
|
23
|
+
def record_select(options = {})
|
24
|
+
options[:model] ||= self.to_s.split('::').last.sub(/Controller$/, '').pluralize.singularize.underscore
|
25
|
+
@record_select_config = RecordSelect::Config.new(options.delete(:model), options)
|
26
|
+
self.send :include, RecordSelect::Actions
|
27
|
+
self.send :include, RecordSelect::Conditions
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :record_select_config
|
31
|
+
|
32
|
+
def uses_record_select?
|
33
|
+
!record_select_config.nil?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
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_model.where(conditions).includes(record_select_includes)
|
8
|
+
@count = klass.count
|
9
|
+
@count = @count.length if @count.is_a? ActiveSupport::OrderedHash
|
10
|
+
pager = ::Paginator.new(@count, record_select_config.per_page) do |offset, per_page|
|
11
|
+
klass.select(record_select_select).includes(record_select_config.include).order(record_select_config.order_by).limit(per_page).offset(offset).all
|
12
|
+
end
|
13
|
+
@page = pager.page(params[:page] || 1)
|
14
|
+
|
15
|
+
respond_to do |wants|
|
16
|
+
wants.html { render_record_select :partial => 'browse'}
|
17
|
+
wants.js {
|
18
|
+
if params[:update]
|
19
|
+
render_record_select :template => 'browse.js', :layout => false
|
20
|
+
else
|
21
|
+
render_record_select :partial => 'browse'
|
22
|
+
end
|
23
|
+
}
|
24
|
+
wants.yaml {}
|
25
|
+
wants.xml {}
|
26
|
+
wants.json {}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# :method => :post
|
31
|
+
# params => [:id]
|
32
|
+
def select
|
33
|
+
klass = record_select_model
|
34
|
+
record = klass.find(params[:id])
|
35
|
+
if record_select_config.notify.is_a? Proc
|
36
|
+
record_select_config.notify.call(record)
|
37
|
+
elsif record_select_config.notify
|
38
|
+
send(record_select_config.notify, record)
|
39
|
+
end
|
40
|
+
render :nothing => true
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
def record_select_config #:nodoc:
|
46
|
+
self.class.record_select_config
|
47
|
+
end
|
48
|
+
|
49
|
+
def render_record_select(options = {}) #:nodoc:
|
50
|
+
[:template,:partial].each do |template_name|
|
51
|
+
if options[template_name] then
|
52
|
+
options[template_name] = File.join(record_select_views_path, options[template_name])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
if block_given? then yield options else render options end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def record_select_views_path
|
61
|
+
"record_select"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def record_select_model
|
66
|
+
record_select_config.model
|
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,103 @@
|
|
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 ||= if defined? Jquery
|
30
|
+
:jquery
|
31
|
+
elsif defined? PrototypeRails
|
32
|
+
:prototype
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# The model object we're browsing
|
37
|
+
def model
|
38
|
+
@model ||= klass.to_s.camelcase.constantize
|
39
|
+
end
|
40
|
+
|
41
|
+
# Records to show on a page
|
42
|
+
def per_page
|
43
|
+
@per_page ||= 10
|
44
|
+
end
|
45
|
+
|
46
|
+
# The method name or proc to notify of a selection event.
|
47
|
+
# May not matter if the selection event is intercepted client-side.
|
48
|
+
def notify
|
49
|
+
@notify
|
50
|
+
end
|
51
|
+
|
52
|
+
# 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.
|
53
|
+
# NOTE: this does *NO* default transforms (such as LOWER()), that's left entirely up to you.
|
54
|
+
def search_on
|
55
|
+
@search_on ||= self.model.columns.collect{|c| c.name if [:text, :string].include? c.type}.compact
|
56
|
+
end
|
57
|
+
|
58
|
+
def order_by
|
59
|
+
@order_by ||= "#{model.table_name}.#{model.primary_key} ASC" unless @order_by == false
|
60
|
+
end
|
61
|
+
|
62
|
+
def full_text_search?
|
63
|
+
@full_text_search ? true : false
|
64
|
+
end
|
65
|
+
|
66
|
+
def include
|
67
|
+
@include
|
68
|
+
end
|
69
|
+
|
70
|
+
# If a proc, must accept the record as an argument and return a descriptive string.
|
71
|
+
#
|
72
|
+
# If a symbol or string, must name a partial that renders a representation of the
|
73
|
+
# record. The partial should assume a local "record" variable, and should include a
|
74
|
+
# <label> tag, even if it's not visible. The contents of the <label> tag will be used
|
75
|
+
# to represent the record once it has been selected. For example:
|
76
|
+
#
|
77
|
+
# record_select_config.label = :user_description
|
78
|
+
#
|
79
|
+
# > app/views/users/_user_description.erb
|
80
|
+
#
|
81
|
+
# <div class="user_description">
|
82
|
+
# <%= image_tag url_for_file_column(record, 'avatar') %>
|
83
|
+
# <label><%= record.username %></label>
|
84
|
+
# <p><%= record.quote %></p>
|
85
|
+
# </div>
|
86
|
+
#
|
87
|
+
def label
|
88
|
+
@label ||= proc {|r| r.to_label}
|
89
|
+
end
|
90
|
+
|
91
|
+
# whether wrap the text returned by label in a link or not
|
92
|
+
def link?
|
93
|
+
@link.nil? ? true : @link
|
94
|
+
end
|
95
|
+
|
96
|
+
protected
|
97
|
+
|
98
|
+
# A singularized underscored version of the model we're browsing
|
99
|
+
def klass
|
100
|
+
@klass
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,13 @@
|
|
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_(key, options = {})
|
6
|
+
unless key.blank?
|
7
|
+
text = I18n.translate "#{key}", {:scope => [:record_select], :default => key.is_a?(String) ? key : key.to_s.titleize}.merge(options)
|
8
|
+
# text = nil if text.include?('translation missing:')
|
9
|
+
end
|
10
|
+
text ||= key
|
11
|
+
text
|
12
|
+
end
|
13
|
+
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
|