jakewendt-active_record_sunspotter 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: efc5d2892a72ebadab5312d46eb1bab1d30821be
4
+ data.tar.gz: f58e5becead798a846087b9ce8641ac8a705b90e
5
+ SHA512:
6
+ metadata.gz: 15fd738ed93098d20eb9e563c73f90f7d0818e1e8956fefd2ad5bf4b54de3f4f4752597a66a7053dee7e7ec0bf672bb1112337b4e16ea086c02f725c931c50a8
7
+ data.tar.gz: 3f6705aa87f2e0a77ea34f37b9f14bf43f90f179d41b019da958f61b2613eb8797066536e484cc5e2ea51ff65d456abd389275a55358e636362781c54a7d92e9
data/README.rdoc ADDED
@@ -0,0 +1,26 @@
1
+ = active_record_sunspotter
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+ == Gemified with Jeweler
13
+
14
+ vi Rakefile
15
+ rake version:write
16
+
17
+ rake version:bump:patch
18
+ rake version:bump:minor
19
+ rake version:bump:major
20
+
21
+ rake gemspec
22
+
23
+ rake install
24
+ rake release
25
+
26
+ Copyright (c) 2010 [Jake Wendt], released under the MIT license
@@ -0,0 +1,18 @@
1
+
2
+ # why do I have to explicitly require it here, but not when gem included in app's Gemfile?
3
+ require 'sunspot_rails'
4
+
5
+ module ActiveRecordSunspotter; end
6
+ require 'active_record_sunspotter/sunspot_rails_server'
7
+ require 'active_record_sunspotter/sunspot_column'
8
+ require 'active_record_sunspotter/sunspotability'
9
+ require 'active_record_sunspotter/search_sunspot_for'
10
+ require 'active_record_sunspotter/sunspot_helper'
11
+
12
+
13
+ if defined?(Rails)
14
+ require 'active_record_sunspotter/rails/engine'
15
+ require 'active_record_sunspotter/rails/railtie'
16
+
17
+ ActionController::Base.append_view_path( File.join(File.dirname(__FILE__), '../vendor/views'))
18
+ end
@@ -0,0 +1,9 @@
1
+ #
2
+ # really just to flag this to look for include javascripts and stylesheets
3
+ #
4
+ module ActiveRecordSunspotter
5
+ module Rails
6
+ class Engine < ::Rails::Engine
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ module ActiveRecordSunspotter
2
+ class Railtie < ::Rails::Railtie
3
+ initializer 'add_helpers_to_actionview' do |app|
4
+ ActiveSupport.on_load :action_view do
5
+ ActionView::Base.send(:include, ActiveRecordSunspotter::SunspotHelper)
6
+ end
7
+ end
8
+ # rake_tasks do
9
+ # Dir["#{File.dirname(__FILE__)}/../tasks/**/*.rake"].sort.each { |ext| load ext }
10
+ # end
11
+ end
12
+ end
@@ -0,0 +1,247 @@
1
+ module ActiveRecordSunspotter::SearchSunspotFor
2
+
3
+ def search_sunspot_for( search_class )
4
+ @sunspot_search_class = search_class
5
+
6
+ # Formerly a before_filter, but after being genericized,
7
+ # we don't know the search class until the search begins.
8
+ @sunspot_search_class.methods.include?(:solr_search) ||
9
+ access_denied("Sunspot server probably wasn't started first!", root_path)
10
+
11
+ #
12
+ # Something has changed that causes this now when :search is stubbed.
13
+ # Unstubbing makes testing raise and error and basically stop.
14
+ #
15
+ #/opt/local/lib/ruby2.0/gems/2.0.0/gems/mocha-0.13.3/lib/mocha/class_method.rb:80:in `public': undefined method `search' for class `Class' (NameError)
16
+ #
17
+ # changing this to use solr_search rather than search seems to make it ok.
18
+ # search is actually an alias to solr_search which may be the cause.
19
+ # new ruby, new rules? Perhaps a newer mocha would work, but I've had
20
+ # a number of problems with newer versions.
21
+ #
22
+
23
+ begin
24
+ @search = @sunspot_search_class.solr_search do
25
+
26
+ if params[:q].present?
27
+ fulltext params[:q]
28
+ end
29
+
30
+ self.instance_variable_get('@setup').clazz.sunspot_all_filters.each do |f| # don't use |facet|
31
+
32
+ p=f.name
33
+
34
+ if f.range
35
+
36
+ range_facet_and_filter_for(p,params.dup,f.range)
37
+
38
+ elsif f.ranges # YES, PLURAL for an array of fixed ranges
39
+
40
+ fixed_range_facet_and_filter_for(p,params.dup,f.ranges)
41
+
42
+ else
43
+
44
+ if params[p]
45
+ #
46
+ # 20130423 - be advised that false.blank? is true so the boolean attributes
47
+ # will not work correctly here. Need to find another way.
48
+ # I don't use boolean columns anymore
49
+ #
50
+ params[p] = [params[p].dup].flatten.reject{|x|x.blank?}
51
+
52
+ if params[p+'_op'] && params[p+'_op'].match(/AND/i).present?
53
+ unless params[p].blank? # empty? # blank? works for arrays too
54
+ with(p).all_of params[p]
55
+ else
56
+ params.delete(p) # remove the key so doesn't show in view
57
+ end
58
+
59
+ #
60
+ # NOTE This is an INTEGER SORT for the BETWEEN filter!
61
+ #
62
+ elsif params[p+'_op'] && params[p+'_op'].match(/BETWEEN/i).present?
63
+ unless params[p].blank? #empty? # blank? works for arrays too
64
+ # between is expecting an array with a first and last (can be array of 1 really)
65
+ with(p).between [params[p].sort_by(&:to_i)].flatten
66
+ else
67
+ params.delete(p) # remove the key so doesn't show in view
68
+ end
69
+
70
+ else # using 'OR'
71
+ unless params[p].blank? #empty? # blank? works for arrays too
72
+ with(p).any_of params[p]
73
+ else
74
+ params.delete(p) # remove the key so doesn't show in view
75
+ end
76
+ end # if params[p+'_op'] && params[p+'_op']=='AND'
77
+
78
+ end # if params[p]
79
+
80
+ # facet.sort
81
+ # This param determines the ordering of the facet field constraints.
82
+ # count - sort the constraints by count (highest count first)
83
+ # index - to return the constraints sorted in their index order
84
+ # (lexicographic by indexed term). For terms in the ascii range,
85
+ # this will be alphabetically sorted.
86
+ # The default is count if facet.limit is greater than 0, index otherwise.
87
+ # Prior to Solr1.4, one needed to use true instead of count and false instead of index.
88
+ # This parameter can be specified on a per field basis.
89
+ #
90
+ # put this inside the else condition as the if block is
91
+ # for ranges and it calls facet
92
+ facet p.to_sym, :sort => :index if f.facetable
93
+
94
+ end
95
+
96
+ end # @sunspot_search_class.sunspot_all_filters.each do |p|
97
+
98
+ order_by *search_order
99
+
100
+ if request.format.to_s.match(/csv|json/)
101
+ # don't paginate csv file. Only way seems to be to make BIG query
102
+ # rather than the arbitrarily big number, I could possibly
103
+ # use the @search.total from the previous search sent as param?
104
+ paginate :page => 1, :per_page => 1000000
105
+ else
106
+ paginate :page => params[:page], :per_page => params[:per_page]||=50
107
+ end
108
+ end # @search = @sunspot_search_class.solr_search do
109
+
110
+ rescue Errno::ECONNREFUSED
111
+ flash[:error] = "Solr seems to be down for the moment."
112
+ redirect_to root_path
113
+ end # begin
114
+
115
+ end
116
+
117
+ def search_order
118
+ if params[:order] and @sunspot_search_class.sunspot_orderable_column_names.include?(
119
+ params[:order].downcase )
120
+ order_string = params[:order]
121
+ dir = case params[:dir].try(:downcase)
122
+ when 'desc' then 'desc'
123
+ else 'asc'
124
+ end
125
+ return order_string.to_sym, dir.to_sym
126
+ else
127
+ return :id, :asc
128
+ end
129
+ end
130
+
131
+ ::Sunspot::DSL::Search.class_eval do
132
+
133
+ def range_filter_for(field,params={})
134
+ if params[field]
135
+ # "expect"=>["1e-5..1e0"]
136
+ any_of do
137
+ params[field].each do |pp|
138
+ # if pp =~ /^Under (\d+)$/
139
+ if pp =~ /^Under (.+)$/
140
+ with( field.to_sym ).less_than $1 # actually less than or equal to
141
+ # elsif pp =~ /^Over (\d+)$/
142
+ elsif pp =~ /^Over (.+)$/
143
+ with( field.to_sym ).greater_than $1 # actually greater than or equal to
144
+ # elsif pp =~ /^\d+\.\.\d+$/
145
+ elsif pp =~ /^.+\.\..+$/
146
+ with( field.to_sym, eval(pp) ) # NOTE could add parantheses then use Range.new( $1,$2 )???
147
+ elsif pp =~ /^\d+$/
148
+ with( field.to_sym, pp ) # primarily for testing? No range, just value
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ def fixed_range_facet_and_filter_for(field,params={},options={})
156
+ range_filter_for(field,params)
157
+ facet field.to_sym do
158
+ options.each do |h|
159
+ # row "#{h[:name]}" do
160
+ # row "#{h[:between].sort.first}..#{h[:between].sort.last}" do
161
+ # with( field.to_sym, Range.new(h[:between].sort.first,h[:between].sort.last) )
162
+ row "#{h[:range]}" do
163
+ with( field.to_sym, h[:range] )
164
+ end
165
+ end # options.each do |h|
166
+ end # facet field.to_sym do
167
+ end
168
+
169
+ #
170
+ # what's the difference between
171
+ # with( field, Range.new(range,range+step) )
172
+ # and
173
+ # with( field ).between( [range,range+step] )
174
+ # I don't think that there is any
175
+ # Note that
176
+ # with( field, [range,range+step] )
177
+ # IS NOT THE SAME. That would be an ANY check.
178
+ # And using Range.new probably only works with integers.
179
+ # Not floats, doubles, text, dates, etc.
180
+ # Actually the example uses 3.0..5.0 but I'm not sure
181
+ # how ruby would interpret that. Base on the number of given decimal places?
182
+ # Exponential notation probably would not work.
183
+ #
184
+
185
+ def range_facet_and_filter_for(field,params={},options={})
186
+ start = (options[:start] || 20) #.to_i
187
+ stop = (options[:stop] || 50) #.to_i
188
+ step = (options[:step] || 10) #.to_i
189
+ log = (options[:log] || false) #.to_i
190
+ range_filter_for(field,params)
191
+ # if params[field]
192
+ ## "expect"=>["1e-5..1e0"]
193
+ # any_of do
194
+ # params[field].each do |pp|
195
+ ## if pp =~ /^Under (\d+)$/
196
+ # if pp =~ /^Under (.+)$/
197
+ # with( field.to_sym ).less_than $1 # actually less than or equal to
198
+ ## elsif pp =~ /^Over (\d+)$/
199
+ # elsif pp =~ /^Over (.+)$/
200
+ # with( field.to_sym ).greater_than $1 # actually greater than or equal to
201
+ ## elsif pp =~ /^\d+\.\.\d+$/
202
+ # elsif pp =~ /^.+\.\..+$/
203
+ # with( field.to_sym, eval(pp) ) # NOTE could add parantheses then use Range.new( $1,$2 )???
204
+ # elsif pp =~ /^\d+$/
205
+ # with( field.to_sym, pp ) # primarily for testing? No range, just value
206
+ # end
207
+ # end
208
+ # end
209
+ # end
210
+ facet field.to_sym do
211
+ if log
212
+ row "Under 1e#{start}" do
213
+ with( field.to_sym ).less_than "1e#{start}".to_f
214
+ end
215
+ (start..(stop-step)).step(step).each do |range|
216
+ row "1e#{range}..1e#{range+step}" do
217
+ with( field.to_sym, Range.new("1e#{range}".to_f,"1e#{range+step}".to_f) )
218
+ end
219
+ end
220
+ row "Over 1e#{stop}" do
221
+ with( field.to_sym ).greater_than "1e#{stop}".to_f
222
+ end
223
+ else
224
+ # row "text label for facet in view", block for facet.query
225
+ row "Under #{start}" do
226
+ # Is less_than just less_than or does it also include equal_to?
227
+ # Results appear to include equal_to which makes it actually incorrect and misleading.
228
+ with( field.to_sym ).less_than start # facet query to pre-show count if selected (NOT A FILTER)
229
+ end
230
+ # this works when like 1-100 step 10
231
+ (start..(stop-step)).step(step).each do |range|
232
+ row "#{range}..#{range+step}" do
233
+ with( field.to_sym, Range.new(range,range+step) )
234
+ end
235
+ end
236
+ row "Over #{stop}" do
237
+ # Is greater_than just greater_than or does it also include equal_to?
238
+ # Results appear to include equal_to which makes it actually incorrect and misleading.
239
+ with( field.to_sym ).greater_than stop
240
+ end
241
+ end
242
+ end
243
+ end
244
+
245
+ end # Sunspot::DSL::Search.class_eval do
246
+
247
+ end
@@ -0,0 +1,51 @@
1
+ require 'ostruct'
2
+ class ActiveRecordSunspotter::SunspotColumn < OpenStruct
3
+
4
+ def initialize(*args)
5
+ # some sensible defaults
6
+ default_options = {
7
+ :type => :string,
8
+ :orderable => true,
9
+ :facetable => false,
10
+ :filterable => false,
11
+ :multiple => false,
12
+ :default => false
13
+ }
14
+ options = args.extract_options!.with_indifferent_access
15
+ if [String,Symbol].include?( args.first.class ) and !options.has_key?(:name)
16
+ options[:name] = args.first.to_s
17
+ end
18
+
19
+ # if( options[:type] == :null_yndk_string ) && options[:meth].blank?
20
+ # options[:meth] = ->(s){ YNDK[s.send(:name)]||'NULL' }
21
+ # options[:type] = :string
22
+ # end
23
+
24
+ default_options.update(options)
25
+ # default_options[:orderable] = false if options[:type] == :multistring
26
+ default_options[:orderable] = false if options[:multiple]
27
+ default_options[:filterable] = true if options[:facetable]
28
+ super default_options
29
+ end
30
+
31
+ def hash_table
32
+ instance_variable_get("@table")
33
+ end
34
+
35
+ def to_s
36
+ name
37
+ end
38
+
39
+ end
40
+
41
+
42
+ __END__
43
+
44
+
45
+ Add :index option. Default to true.
46
+ False would effectively mean that it is only useful as a column.
47
+ This may be the same as facetable.
48
+ WAIT! If aren't indexed, then can't be sorted on.
49
+
50
+ So could skip adding if weren't facetable, orderable or multiple.
51
+
@@ -0,0 +1,187 @@
1
+ module ActiveRecordSunspotter::SunspotHelper
2
+
3
+ # Rails helpers are already "html_safe".
4
+ # Manually creating the strings will require adding it.
5
+
6
+ def operator_radio_button_tag_and_label(name,operator,selected)
7
+ s = radio_button_tag( "#{name}_op", operator, selected == operator,
8
+ :id => "#{name}_op_#{operator.downcase}" )
9
+ s << label_tag( "#{name}_op_#{operator.downcase}", operator )
10
+ end
11
+
12
+ # originally from CLIC
13
+
14
+ def multi_select_operator_for(name)
15
+ content_tag(:div) do
16
+ selected = ( params["#{name}_op"] && params["#{name}_op"] == 'AND' ) ? 'AND' : 'OR'
17
+ s = content_tag(:span,"Multi-select operator")
18
+ s << operator_radio_button_tag_and_label(name,'AND',selected)
19
+ s << operator_radio_button_tag_and_label(name,'OR',selected)
20
+ end
21
+ end
22
+
23
+ def facet_toggle(facet,icon)
24
+ content_tag(:div,:class => 'facet_toggle') do
25
+ s = content_tag(:span,'&nbsp;'.html_safe,:class => "ui-icon #{icon}")
26
+ # Don't include the blank fields, so don't count them.
27
+ # May need to figure out how to deal with blanks in the future
28
+ # as occassionally they are what one would be searching for.
29
+ #
30
+ # 20130423 - false.blank? is true so boolean fields won't work here
31
+ #
32
+ # perhaps do r.value.to_s.blank? as 'false'.blank? is false
33
+ #
34
+ non_blank_row_count = facet.rows.reject{|r|r.value.blank?}.length
35
+ facet_label = facet.name.to_s
36
+ facet_label = if( facet_label.match(/^hex_/) )
37
+ # [facet_label.gsub(/^hex_/,'').split(/:/).first].pack('H*')
38
+ l = facet_label.gsub(/^hex_/,'').split(/:/)
39
+ l[0] = [l[0]].pack('H*')
40
+ l.join(' : ')
41
+ else
42
+ column = @sunspot_search_class.all_sunspot_columns.detect{|c|c.name == facet_label}
43
+
44
+ # Check if a translation exists. Use it if it does, otherwise ...
45
+ # http://stackoverflow.com/questions/12353416/rails-i18n-check-if-translation-exists
46
+ column.label || ( I18n.t("#{@sunspot_search_class.to_s.underscore}.#{facet.name}",
47
+ :scope => "activerecord.attributes",:raise => true ) rescue false ) || facet_label.titleize
48
+ end
49
+ s << link_to("#{facet_label}&nbsp;(#{non_blank_row_count})".html_safe, 'javascript:void()')
50
+ end
51
+ end
52
+
53
+ def facet_for(facet,options={})
54
+ return if facet.rows.empty?
55
+
56
+ # options include :multiselector, :facetcount
57
+ style, icon = if( params[facet.name] )
58
+ [" style='display:block;'", "ui-icon-triangle-1-s"]
59
+ else
60
+ [ nil, "ui-icon-triangle-1-e"]
61
+ end
62
+ s = facet_toggle(facet,icon)
63
+
64
+ # Wrap all the rest in a div which will be toggled
65
+ s << "\n<div id='#{facet.name}' class='facet_field'#{style}>\n".html_safe
66
+ s << multi_select_operator_for(facet.name) if options[:multiselector]
67
+
68
+
69
+
70
+ col = @sunspot_search_class.sunspot_columns.detect{|c|
71
+ c.name == facet.name.to_s }
72
+
73
+
74
+
75
+ s << "<ul class='facet_field_values'>\n".html_safe
76
+ facet.rows.each do |row|
77
+
78
+ #
79
+ # NOTE for now, if a blank field has made it into the index, IGNORE IT.
80
+ # Unfortunately, searching for a '' creates syntactically incorrect query.
81
+ #
82
+ # Of course, this mucks up the count. Errr!!!
83
+ # So I had to handle it yet again.
84
+ #
85
+
86
+ #
87
+ # 20130423 - false.blank? is true so boolean fields won't work here
88
+ #
89
+ next if row.value.blank?
90
+
91
+ label = if col.ranges
92
+ col.ranges.detect{|r|
93
+ r[:range].to_s == row.value.to_s }[:name] || 'RANGE NOT FOUND'
94
+ else
95
+ row.value
96
+ end
97
+
98
+ # TODO figure out how to facet on NULL and BLANK values
99
+ # I don't think that NULL gets faceted
100
+ # and blank creates a syntactically incorrect query
101
+
102
+ s << "<li>".html_safe
103
+ if options[:radio]
104
+ s << radio_button_tag( "#{facet.name}[]", row.value,
105
+ [params[facet.name]].flatten.include?(row.value.to_s),
106
+ { :id => "#{facet.name}_#{row.value.html_friendly}" } )
107
+ else
108
+ s << check_box_tag( "#{facet.name}[]", row.value,
109
+ [params[facet.name]].flatten.include?(row.value.to_s),
110
+ { :id => "#{facet.name}_#{row.value.html_friendly}" } )
111
+ end
112
+ s << "<label for='#{facet.name}_#{row.value.html_friendly}'>".html_safe
113
+ # s << "<span>#{row.value}</span>".html_safe
114
+ s << "<span>#{label}</span>".html_safe
115
+ s << "&nbsp;(&nbsp;#{row.count}&nbsp;)".html_safe if options[:facet_counts]
116
+ s << "</label></li>\n".html_safe
117
+ end
118
+ s << "</ul>\n".html_safe # "<ul class='facet_field_values'>"
119
+ s << "</div>\n".html_safe # "<div id='#{facet.name}' class='facet_field'#{style}>"
120
+ end
121
+
122
+ def columns
123
+ columns ||= if( params[:c].present? )
124
+ [params[:c]].flatten.uniq # sometimes this is needed and others it is not?
125
+ else
126
+ @sunspot_search_class.sunspot_default_column_names
127
+ end
128
+ end
129
+
130
+ def column_header(column)
131
+ if @sunspot_search_class.sunspot_orderable_column_names.include?(column.to_s)
132
+ sort_link(column,:image => false)
133
+ else
134
+ column
135
+ end
136
+ end
137
+
138
+ #
139
+ # NEVER use send on a tainted string!
140
+ #
141
+ def column_content(subject,column)
142
+ case column.to_s
143
+
144
+ #
145
+ # Just wanting to format any date columns
146
+ #
147
+ when *@sunspot_search_class.sunspot_date_columns.collect(&:name)
148
+ col = @sunspot_search_class.sunspot_columns.detect{|c|
149
+ c.name == column.to_s }
150
+ ( col.hash_table.has_key?(:meth) ) ?
151
+ [col.meth.call(subject)].flatten.join(',') :
152
+ ( subject.respond_to?(column) ) ?
153
+ subject.try(column).try(:strftime,'%m/%d/%Y') :
154
+ 'DATE COLUMN NOT FOUND?'
155
+
156
+ #
157
+ # All valid columns can use meth, so I don't need to define them.
158
+ #
159
+ when *@sunspot_search_class.sunspot_column_names
160
+ col = @sunspot_search_class.sunspot_columns.detect{|c|
161
+ c.name == column.to_s }
162
+ ( col.hash_table.has_key?(:meth) ) ?
163
+ [col.meth.call(subject)].flatten.join(',') :
164
+ ( subject.respond_to?(column) ) ?
165
+ subject.try(column) :
166
+ 'COLUMN NOT FOUND?'
167
+
168
+ else
169
+ 'UNKNOWN COLUMN'
170
+ end
171
+ end
172
+
173
+
174
+
175
+
176
+ def html_column_content(result,column)
177
+ content = column_content(result,column).to_s.gsub(/\s+/,'&nbsp;') || '&nbsp;'
178
+
179
+ col = @sunspot_search_class.sunspot_columns.detect{|c|
180
+ c.name == column.to_s }
181
+ html_content = ( col.hash_table.has_key?(:link_to) ) ?
182
+ link_to( content, col.link_to.call(result), :target => :new ) : content
183
+
184
+ html_content.html_safe
185
+ end
186
+
187
+ end
@@ -0,0 +1,15 @@
1
+ class Sunspot::Rails::Server
2
+ # http://opensoul.org/blog/archives/2010/04/07/cucumber-and-sunspot/
3
+ def running?
4
+ begin
5
+ open("http://localhost:#{self.port}/")
6
+ true
7
+ rescue Errno::ECONNREFUSED => e
8
+ # server not running yet
9
+ false
10
+ rescue OpenURI::HTTPError
11
+ # getting a response so the server is running
12
+ true
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,127 @@
1
+ module ActiveRecordSunspotter::Sunspotability
2
+ def self.included(base)
3
+ base.extend(ClassMethods)
4
+ base.class_eval do
5
+
6
+ #
7
+ # MUST delay execution of code until included as cattr_accessor is ActiveRecord specific
8
+ #
9
+ cattr_accessor :all_sunspot_columns
10
+ self.all_sunspot_columns = [] # order is only relevant to the facets
11
+ end
12
+ end
13
+
14
+ module ClassMethods
15
+
16
+ def add_sunspot_column(*args)
17
+ all_sunspot_columns.push( ActiveRecordSunspotter::SunspotColumn.new( *args ) )
18
+ end
19
+
20
+ def sunspot_orderable_columns
21
+ all_sunspot_columns.select{|c|c.orderable}
22
+ end
23
+
24
+ def sunspot_orderable_column_names
25
+ sunspot_orderable_columns.collect(&:name)
26
+ end
27
+
28
+ def sunspot_default_columns
29
+ all_sunspot_columns.select{|c|c.default}
30
+ end
31
+
32
+ def sunspot_default_column_names
33
+ sunspot_default_columns.collect(&:name)
34
+ end
35
+
36
+ def sunspot_all_filters
37
+ all_sunspot_columns.select{|c|c.filterable}
38
+ end
39
+
40
+ # in the order that they will appear on the page
41
+ def sunspot_all_facets
42
+ all_sunspot_columns.select{|c|c.facetable}
43
+ end
44
+ def sunspot_all_facet_names
45
+ sunspot_all_facets.collect(&:name)
46
+ end
47
+
48
+ def sunspot_columns
49
+ all_sunspot_columns
50
+ end
51
+
52
+ def sunspot_column_names
53
+ all_sunspot_columns.collect(&:name)
54
+ end
55
+
56
+ def sunspot_available_column_names
57
+ sunspot_column_names.sort
58
+ end
59
+
60
+ def sunspot_date_columns
61
+ all_sunspot_columns.select{|c|c.type == :date }
62
+ end
63
+
64
+ def searchable_plus(&block)
65
+ searchable do
66
+ #
67
+ # Trying to simplify. Simplify? Minimize?
68
+ # Will I need all of the above methods after this is done?
69
+ #
70
+ # .select{|c| c.facetable }
71
+ # or just use "sunspot_all_facets" instead of "all_sunspot_columns"
72
+ # WAIT! If aren't indexed, then can't be sorted on.
73
+ all_sunspot_columns.select{|c| ![:boolean,:nulled_string].include?(c.type) }.each{|c|
74
+ options = {}
75
+ options[:multiple] = true if( c.multiple )
76
+ #
77
+ # I don't think that trie works with :long or :double
78
+ # I got this when I tried a :double
79
+ # Trie fields are only valid for numeric and time types
80
+ #
81
+ # I found documentation that says that Lucene/Solr should work for longs and doubles?
82
+ #
83
+ #
84
+ # these are missing in sunspot 2.0. I added them in my taxonomy app
85
+ #
86
+ # module Sunspot::Type
87
+ # class TrieDoubleType < DoubleType
88
+ # def indexed_name(name)
89
+ # "#{super}t"
90
+ # end
91
+ # end
92
+ # class TrieLongType < LongType
93
+ # def indexed_name(name)
94
+ # "#{super}t"
95
+ # end
96
+ # end
97
+ # end
98
+ #
99
+ # options[:trie] = true if( [:integer,:long,:double,:float,:time].include?(c.type) )
100
+ options[:trie] = true if( [:integer,:float,:time].include?(c.type) )
101
+ send( c.type, c.name, options ){
102
+ c.hash_table.has_key?(:meth) ? c.meth.call(self) : send( c.name )
103
+ }
104
+ #
105
+ # booleans? nulled_strings?
106
+ #
107
+ }
108
+
109
+
110
+ # yield if block_given?
111
+ # yield block if block_given?
112
+ end
113
+
114
+ # this works, but why can't I just yield inside the block
115
+ searchable &block if block_given?
116
+
117
+ end
118
+
119
+ end # module ClassMethods
120
+
121
+ end
122
+ __END__
123
+
124
+ What's the point of adding a column to the index that isn't faceted?
125
+ It would basically just be useful as a column, which is why I was
126
+ considering creating an "index" option.
127
+ WAIT! If aren't indexed, then can't be sorted on.
@@ -0,0 +1 @@
1
+ require 'active_record_sunspotter'
@@ -0,0 +1,27 @@
1
+ jQuery(function(){
2
+
3
+ jQuery('div.facet_toggle a').click(function(){
4
+ // jQuery(this).parent().next().toggle(500);
5
+ // added 'blind' so doesn't resize stuff and just slides in.
6
+ // be advised that this effect temporarily wraps the target in a div until done.
7
+ jQuery(this).parent().next().toggle('blind',500);
8
+ jQuery(this).prev().toggleClass('ui-icon-triangle-1-e');
9
+ jQuery(this).prev().toggleClass('ui-icon-triangle-1-s');
10
+ return false;
11
+ });
12
+
13
+ jQuery( "#selected_columns, #unselected_columns" ).sortable({
14
+ connectWith: ".selectable_columns"
15
+ }).disableSelection();
16
+
17
+ jQuery('form').submit(function(){
18
+ jQuery('#selected_columns li').each(function(){
19
+ jQuery('<input>').attr({
20
+ name: 'c[]',
21
+ type: 'hidden',
22
+ value: $(this).text()
23
+ }).appendTo('form');
24
+ });
25
+ });
26
+
27
+ });
@@ -0,0 +1,207 @@
1
+ body {
2
+ min-width: 100%;
3
+ padding:0;
4
+ margin:0;
5
+ }
6
+ div#header {
7
+ margin:0;
8
+ padding:0;
9
+ background-color: #6A7780;
10
+ height: auto;
11
+ div#header_top {
12
+ height: 50px;
13
+ display:block;
14
+ padding: 0px;
15
+ margin: 0px;
16
+ border: 0px;
17
+ clear:both;
18
+ }
19
+ }
20
+ div#logo {
21
+ float:left;
22
+ padding-left: 10px;
23
+ font-family: Times New Roman,LeagueGothicRegular,helvetica,arial,sans-serif;
24
+ h2 {
25
+ font-size: 30px;
26
+ margin: 10px;
27
+ margin-top: 3px;
28
+ margin-bottom: 0px;
29
+ color:white;
30
+ text-shadow: #000 0 2px 3px;
31
+ display:inline-block;
32
+ }
33
+ a {
34
+ color:white;
35
+ text-decoration:none;
36
+ }
37
+ }
38
+ #container {
39
+ padding: 0;
40
+ margin: auto 0px;
41
+ display:table;
42
+ // width: 100%;
43
+ min-width: 100%;
44
+ }
45
+
46
+ #footer {
47
+ clear:both;
48
+ padding-top: 1ex;
49
+ padding-bottom: 1ex;
50
+ text-align:center;
51
+ font-size:8pt;
52
+ line-height:8pt;
53
+ background-color: #EEE;
54
+ color: gray;
55
+ }
56
+
57
+
58
+ .table {
59
+ /* this holds the table-row open, otherwise shrinks to whatever */
60
+ display:table;
61
+ width:100%;
62
+ .row {
63
+ display:table-row;
64
+ > div {
65
+ display:table-cell;
66
+ vertical-align: top;
67
+ text-align: center;
68
+ }
69
+ }
70
+ }
71
+ table {
72
+ width: 100%;
73
+ }
74
+
75
+ tr:nth-child(2n) { background-color: #FCF9EF; }
76
+ tr:nth-child(2n+1) { background-color: #F5F1E8; }
77
+
78
+
79
+
80
+
81
+
82
+
83
+
84
+
85
+
86
+
87
+
88
+ #refine {
89
+ min-width: 300px;
90
+ /* still don't understand why, but when min-width is used, the facet column flickers too wide for a second when toggling? */
91
+ /* interestingly, having both width and min-width seems to do exactly what I want without flicker. Yay */
92
+ width: 400px;
93
+ border-right: 2px solid silver;
94
+ padding: 0 10px;
95
+ form input[type=submit] {
96
+ font-size:14pt;
97
+ font-weight:bold;
98
+ border: 1px solid black;
99
+ width: 100%;
100
+ margin: 10px 0;
101
+
102
+ /* http://css-radius.heroku.com */
103
+ -webkit-border-radius: 8px;
104
+ -moz-border-radius: 8px;
105
+ border-radius: 8px;
106
+
107
+ background-color: #DDDDDD;
108
+ /* for webkit browsers */
109
+ background-image: -webkit-gradient(linear, left top, left bottom, from(#FFFFFF), to(#BBBBBB));
110
+ /* for firefox 3.6+ */
111
+ background-image: -moz-linear-gradient(top, #FFFFFF, #BBBBBB);
112
+ /* for ie , but doesn't seem to work as expected despite other examples working */
113
+ filter: progid:DXImageTransform.Microsoft.gradient(StartColorStr='#FFFFFF', EndColorStr='#BBBBBB');
114
+ }
115
+ }
116
+
117
+
118
+
119
+ .facets {
120
+ border: 1px solid silver;
121
+ border-top: 0;
122
+ div.facet_toggle {
123
+ display:block;
124
+ padding: 5px;
125
+ border-top: 1px solid silver;
126
+ background-color:#EEE;
127
+ text-align: left;
128
+ * {
129
+ vertical-align:middle;
130
+ }
131
+ span {
132
+ display: inline-block;
133
+ padding-right:5px;
134
+ }
135
+ a {
136
+ display:inline-block;
137
+ text-align: left;
138
+ }
139
+ }
140
+ div.facet_field {
141
+ display:none;
142
+ ul {
143
+ padding-left: 35px;
144
+ /* I want a 10px padding, but also want the wordwrap indented
145
+ further. so large padding and negative indent */
146
+ li {
147
+ list-style: none;
148
+ text-align: left;
149
+ text-indent: -30px;
150
+ label {
151
+ padding-left: 5px;
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+
158
+ #results {
159
+ padding: 0 10px;
160
+ background-color: #FFF;
161
+ }
162
+
163
+ #filters {
164
+ padding: 0 10px;
165
+ border: 1px solid gray;
166
+ h3 {
167
+ margin-top:0;
168
+ padding-top:10px;
169
+ }
170
+ }
171
+
172
+
173
+
174
+ ul.selectable_columns {
175
+ background-color: silver;
176
+ padding: 10px;
177
+ list-style: none;
178
+ text-align: left;
179
+ text-indent: 20px;
180
+ }
181
+
182
+
183
+
184
+
185
+ /* only used in ODMS so should probably move it there */
186
+ div#map_canvas_background {
187
+ position: absolute;
188
+ width: 100%;
189
+ height: 100%;
190
+ background-color: gray;
191
+ opacity: 0.6;
192
+ filter:alpha(opacity=60);
193
+ top: 0px;
194
+ }
195
+ div#map_canvas_wrapper {
196
+ position: absolute;
197
+ width: 100%;
198
+ height: 100%;
199
+ margin: auto;
200
+ text-align: center;
201
+ top: 0px;
202
+ }
203
+ div#map_canvas {
204
+ width: 90%;
205
+ height: 90%;
206
+ margin: 10px auto;
207
+ }
@@ -0,0 +1,5 @@
1
+ <div id='footer'>
2
+ <p>
3
+ <small>2010 California Childhood Leukemia Study, UC Berkeley School of Public Health</small>
4
+ </p>
5
+ </div><!-- footer -->
@@ -0,0 +1,14 @@
1
+ <head>
2
+ <meta name="ROBOTS" content="NOINDEX, NOFOLLOW" />
3
+ <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
4
+ <%# explicity adding root_url to href making an absolute path
5
+ actually allows the cal favicon to load %>
6
+ <link rel="shortcut icon" href="<%=root_url%><%=image_path('favicon.ico')%>" />
7
+ <title><%= @page_title || "Sunspotter" -%></title>
8
+ <link href="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.1/themes/base/jquery-ui.css" media="screen" rel="stylesheet" type="text/css" />
9
+ <%= stylesheet_link_tag 'sunspot' %>
10
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js" type="text/javascript"></script>
11
+ <script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js" type="text/javascript"></script>
12
+ <%= javascript_include_tag 'application','sunspot' %>
13
+ <%= yield :head %>
14
+ </head>
@@ -0,0 +1,5 @@
1
+ <div id='header'>
2
+ <div id='header_top'>
3
+ <div id='logo'><h2>Sunspotter<span>[beta]</span></h2></div>
4
+ </div><!-- id='header_top' -->
5
+ </div><!-- header -->
@@ -0,0 +1,22 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3
+ <%#= yield :doctype -%>
4
+ <%#
5
+ Could add "valid" tag attributes by ...
6
+ [
7
+ <!ATTLIST tag myAttri CDATA #IMPLIED>
8
+ ] %>
9
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
10
+ <%= render 'layouts/sunspot_head' %>
11
+ <body>
12
+ <div id='container'>
13
+ <%= render 'layouts/sunspot_header' %>
14
+ <div class='table'>
15
+ <div class='row'>
16
+ <%= yield %>
17
+ </div><!-- row -->
18
+ </div><!-- table -->
19
+ <%= render 'layouts/footer' %>
20
+ </div><!-- container -->
21
+ </body>
22
+ </html>
@@ -0,0 +1,5 @@
1
+ <% if @search.total < 5000 %>
2
+ <p><%= link_to 'Download ALL as CSV', params.merge(:format => :csv) %></p>
3
+ <% else %>
4
+ <p>More than 5000 samples.<br/>CSV download disabled.</p>
5
+ <% end %>
@@ -0,0 +1,15 @@
1
+ <div class='facets'>
2
+ <% @search.facets.each do |facet| -%>
3
+ <% next if facet.rows.empty? -%><%# don't show empty facets -%>
4
+ <%= facet_for(facet,
5
+ :multiselector => @sunspot_search_class.sunspot_columns.detect{|c|
6
+ c.name == facet.name.to_s }.try(:multiple),
7
+ :facet_counts => true ) %>
8
+ <%#
9
+ multiselector is for when a single subject could have many
10
+ Either as an array in itself or perhaps some implementation
11
+ of a has_many for enrollments or something
12
+ %>
13
+ <% end -%><%# @search.facets.each do |facet| -%>
14
+ <%= render 'select_columns' %>
15
+ </div><!-- class='facets' -->
@@ -0,0 +1,16 @@
1
+ <% @sunspot_search_class.sunspot_all_filters.each do |f| %>
2
+ <% unless f.facetable %>
3
+ <% if params["#{f.name}_op"].present? %>
4
+ <%= hidden_field_tag "#{f.name}_op", params["#{f.name}_op"], :id => nil %>
5
+ <% end %>
6
+ <% if params[f.name].present? %>
7
+ <% if params[f.name].is_a?(Array) %>
8
+ <% params[f.name].each do |value| %>
9
+ <%= hidden_field_tag "#{f.name}[]", value, :id => nil %>
10
+ <% end %>
11
+ <% else %>
12
+
13
+ <% end %>
14
+ <% end %><%# if params[f.name].present? %>
15
+ <% end %><%# unless f.facetable %>
16
+ <% end %><%# @sunspot_search_class.sunspot_all_filters.each do |f| %>
@@ -0,0 +1 @@
1
+ LINK
@@ -0,0 +1,4 @@
1
+ <div class='field_wrapper'>
2
+ <%= label_tag( :per_page, 'per page (html only) ' )%>
3
+ <%= select_tag( :per_page, options_for_select([25,50,100], params[:per_page]||50) )%>
4
+ </div><!-- class='field_wrapper' -->
@@ -0,0 +1,24 @@
1
+ <div id='results'>
2
+ <% unless @search.results.empty? %>
3
+ <table><thead><tr>
4
+ <th>&nbsp;</th>
5
+ <% columns.each do |column| %>
6
+ <th><%= column_header(column) %></th>
7
+ <% end %>
8
+ </tr></thead><tbody>
9
+ <% @search.results.each do |result| %>
10
+ <tr class='row'>
11
+ <td><%= render 'link', :result => result %></td>
12
+ <% columns.each do |column| %>
13
+ <td><%#= column_content(result,column).to_s.gsub(/\s+/,'&nbsp;').html_safe || '&nbsp;'.html_safe -%><%= html_column_content(result,column) %></td>
14
+ <% end %>
15
+ </tr>
16
+ <% end %>
17
+ </tbody></table>
18
+ <br/><%= will_paginate(@search.hits) %>
19
+ <p class='page_info'>Displaying page <%=@search.hits.current_page%> of
20
+ <%=@search.hits.total_pages%> out of <%=@search.total%> results</p>
21
+ <% else %>
22
+ <p>No results</p>
23
+ <% end %>
24
+ </div>
@@ -0,0 +1,21 @@
1
+ <div class="facet_toggle">
2
+ <span class="ui-icon ui-icon-triangle-1-e">&nbsp;</span><a href="javascript:void(0)">Column Selection</a>
3
+ </div>
4
+ <div id='columns' class='facet_field'>
5
+ <p>Used Columns</p>
6
+ <ul id="selected_columns" class="selectable_columns">
7
+ <% columns.each do |column| %>
8
+ <%# NEED to find a way to use the id, but already using id matching these
9
+ column names in the facet selectors %>
10
+ <%= content_tag(:li, column )%>
11
+ <% end %>
12
+ </ul>
13
+ <p>Unused Columns</p>
14
+ <ul id="unselected_columns" class="selectable_columns">
15
+ <% (@sunspot_search_class.sunspot_available_column_names - columns).each do |column| %>
16
+ <%# NEED to find a way to use the id, but already using id matching these
17
+ column names in the facet selectors %>
18
+ <%= content_tag(:li, column )%>
19
+ <% end %>
20
+ </ul>
21
+ </div>
@@ -0,0 +1,25 @@
1
+ <% require 'csv' -%>
2
+ <%#
3
+ adding :headers => true is supposed to set a flag in the file
4
+ somewhere marking it as having a header row, but I don't
5
+ think that it actually does.
6
+
7
+ also, use a - with the ruby closing tag to avoid extra lines in csv file.
8
+
9
+ Unfortunately, when Access imports a csv file it adds a type to columns, I think.
10
+
11
+ The hospital_no is treated like an integer. If it gets too big or contains
12
+ a non-digit character, it just leaves the field blank. This is an absolutely
13
+ awful idea. Thanks again Microsoft. Because of this, I force quotes.
14
+ I really only need to quote the hospital_no, but it is easier to just quote them all.
15
+
16
+ -%>
17
+ <%= columns.to_csv( :headers => true) -%>
18
+ <% @search.results.each do |result| %>
19
+ <%= columns.collect{|c| column_content(result,c) }.to_csv(:force_quotes => true).html_safe -%>
20
+ <% end -%>
21
+ <%#
22
+
23
+ I added html_safe to stop any html encoding just in case
24
+
25
+ -%>
@@ -0,0 +1,25 @@
1
+ <%# require 'csv' -%>
2
+ <%#
3
+ adding :headers => true is supposed to set a flag in the file
4
+ somewhere marking it as having a header row, but I don't
5
+ think that it actually does.
6
+
7
+ also, use a - with the ruby closing tag to avoid extra lines in csv file.
8
+
9
+ Unfortunately, when Access imports a csv file it adds a type to columns, I think.
10
+
11
+ The hospital_no is treated like an integer. If it gets too big or contains
12
+ a non-digit character, it just leaves the field blank. This is an absolutely
13
+ awful idea. Thanks again Microsoft. Because of this, I force quotes.
14
+ I really only need to quote the hospital_no, but it is easier to just quote them all.
15
+
16
+ -%>
17
+ <%#= columns.to_csv( :headers => true) -%>
18
+ <%= @search.results.collect do |result| -%>
19
+ <% columns.inject({}){|h,c| h[c] = column_content(result,c); h} -%>
20
+ <% end.to_json.html_safe -%>
21
+ <%#
22
+
23
+ I added html_safe to stop any html encoding just in case
24
+
25
+ -%>
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jakewendt-active_record_sunspotter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.12
5
+ platform: ruby
6
+ authors:
7
+ - George 'Jake' Wendt
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-03-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sunspot_rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 2.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 2.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: sunspot_solr
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 2.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 2.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: progress_bar
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: active_record_sunspotter
56
+ email: github@jakewendt.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files:
60
+ - README.rdoc
61
+ files:
62
+ - lib/active_record_sunspotter.rb
63
+ - lib/active_record_sunspotter/rails/engine.rb
64
+ - lib/active_record_sunspotter/rails/railtie.rb
65
+ - lib/active_record_sunspotter/search_sunspot_for.rb
66
+ - lib/active_record_sunspotter/sunspot_column.rb
67
+ - lib/active_record_sunspotter/sunspot_helper.rb
68
+ - lib/active_record_sunspotter/sunspot_rails_server.rb
69
+ - lib/active_record_sunspotter/sunspotability.rb
70
+ - lib/jakewendt-active_record_sunspotter.rb
71
+ - vendor/assets/javascripts/sunspot.js
72
+ - vendor/assets/stylesheets/sunspot.css.scss
73
+ - vendor/views/layouts/_footer.html.erb
74
+ - vendor/views/layouts/_sunspot_head.html.erb
75
+ - vendor/views/layouts/_sunspot_header.html.erb
76
+ - vendor/views/layouts/sunspot.html.erb
77
+ - vendor/views/sunspot/_download_csv.html.erb
78
+ - vendor/views/sunspot/_facets.html.erb
79
+ - vendor/views/sunspot/_filters.html.erb
80
+ - vendor/views/sunspot/_link.html.erb
81
+ - vendor/views/sunspot/_per_page.html.erb
82
+ - vendor/views/sunspot/_results.html.erb
83
+ - vendor/views/sunspot/_select_columns.html.erb
84
+ - vendor/views/sunspot/index.csv.erb
85
+ - vendor/views/sunspot/index.json.erb
86
+ - README.rdoc
87
+ homepage: http://github.com/jakewendt/active_record_sunspotter
88
+ licenses: []
89
+ metadata: {}
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubyforge_project:
106
+ rubygems_version: 2.0.14
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: active_record_sunspotter
110
+ test_files: []