jakewendt-active_record_sunspotter 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []