radiant-sphinx_search-extension 0.9.1
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.
- data/.gitmodules +0 -0
- data/README.md +87 -0
- data/Rakefile +134 -0
- data/VERSION +1 -0
- data/app/models/search_page.rb +8 -0
- data/app/views/admin/pages/_search_toggle.haml +15 -0
- data/db/migrate/20100808155751_add_delta_to_pages.rb +9 -0
- data/lib/radiant-sphinx_search-extension.rb +1 -0
- data/lib/sphinx_search.rb +6 -0
- data/lib/sphinx_search/link_renderer.rb +16 -0
- data/lib/sphinx_search/page_context_extensions.rb +16 -0
- data/lib/sphinx_search/page_extensions.rb +24 -0
- data/lib/sphinx_search/radius_tags.rb +169 -0
- data/lib/tasks/sphinx_search_extension_tasks.rake +67 -0
- data/lib/templates/initializer.rb +10 -0
- data/lib/templates/sphinx.yml +6 -0
- data/radiant-sphinx_search-extension.gemspec +71 -0
- data/spec/datasets/search_pages_dataset.rb +13 -0
- data/spec/lib/search_radius_tags_spec.rb +73 -0
- data/spec/models/page_spec.rb +25 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +37 -0
- data/sphinx_search_extension.rb +14 -0
- metadata +114 -0
data/.gitmodules
ADDED
File without changes
|
data/README.md
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# Sphinx Search
|
2
|
+
|
3
|
+
Adds ThinkingSphinx (<http://ts.freelancing-gods.com>) support to Radiant,
|
4
|
+
including a paginated search results page.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
First you'll need to install Sphinx, available via your preferred package
|
9
|
+
manager or from <http://sphinxsearch.com>.
|
10
|
+
|
11
|
+
Installing the extension itself as a gem is preferred. Add to your
|
12
|
+
`config/environment.rb:`
|
13
|
+
|
14
|
+
config.gem 'radiant-sphinx_search-extension'
|
15
|
+
|
16
|
+
After the gem is configured, run `rake db:migrate:extensions` and `rake
|
17
|
+
radiant:extensions:update_all`. You should at least read the Thinking Sphinx
|
18
|
+
quickstart guide, but these commands should be enough to get you started:
|
19
|
+
|
20
|
+
rake ts:in
|
21
|
+
rake ts:start
|
22
|
+
|
23
|
+
## Indexing
|
24
|
+
|
25
|
+
SphinxSearch indexes your pages on title and content (parts.) Sphinx
|
26
|
+
attributes are created for `status_id`, `updated_at`, and `virtual`.
|
27
|
+
|
28
|
+
## Building a Search Page
|
29
|
+
|
30
|
+
SphinxSearch adds a new Page subclass, SearchPage. This page serves as both
|
31
|
+
the search form and the search results display.
|
32
|
+
|
33
|
+
There are a number of Radius tags available to the Search page, most of which
|
34
|
+
are customizable to some extent. You can find the full documentation for these
|
35
|
+
tags and their options in the tag reference, but a simple search page might
|
36
|
+
look like this:
|
37
|
+
|
38
|
+
<r:search>
|
39
|
+
<r:form />
|
40
|
+
<p>Your search for <strong><r:query /></strong> returned <r:count />.</p>
|
41
|
+
<r:results paginated="true">
|
42
|
+
<r:each>
|
43
|
+
<h3><r:link /></h3>
|
44
|
+
<p><r:excerpt /></p>
|
45
|
+
</r:each>
|
46
|
+
<r:pagination />
|
47
|
+
</r:results>
|
48
|
+
</r:search>
|
49
|
+
|
50
|
+
## NoMethodError
|
51
|
+
|
52
|
+
If you're using some versions of Radiant (generally <= 0.9.1) or certain
|
53
|
+
3rd-party extensions, you may see this error:
|
54
|
+
|
55
|
+
NoMethodError
|
56
|
+
|
57
|
+
You have a nil object when you didn't expect it!
|
58
|
+
You might have expected an instance of Array.
|
59
|
+
The error occurred while evaluating nil.<<
|
60
|
+
|
61
|
+
This is a load-order problem and it means another extension created a Page
|
62
|
+
subclass before SphinxSearch had a chance to extend the base class. The
|
63
|
+
easiest way around this is to simply load SphinxSearch first:
|
64
|
+
|
65
|
+
config.extensions = [:sphinx_search, :all]
|
66
|
+
|
67
|
+
------------------------------------------------------------------------------
|
68
|
+
|
69
|
+
Copyright (c) 2008 Digital Pulp, Inc.
|
70
|
+
|
71
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
72
|
+
of this software and associated documentation files (the "Software"), to deal
|
73
|
+
in the Software without restriction, including without limitation the rights
|
74
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
75
|
+
copies of the Software, and to permit persons to whom the Software is
|
76
|
+
furnished to do so, subject to the following conditions:
|
77
|
+
|
78
|
+
The above copyright notice and this permission notice shall be included in
|
79
|
+
all copies or substantial portions of the Software.
|
80
|
+
|
81
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
82
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
83
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
84
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
85
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
86
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
87
|
+
THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
begin
|
2
|
+
require 'jeweler'
|
3
|
+
Jeweler::Tasks.new do |gem|
|
4
|
+
gem.name = "radiant-sphinx_search-extension"
|
5
|
+
gem.summary = %Q{Sphinx Search Extension for Radiant CMS}
|
6
|
+
gem.description = %Q{Adds fulltext search capability to Radiant content via Thinking Sphinx.}
|
7
|
+
gem.email = "git@digitalpulp.com"
|
8
|
+
gem.homepage = "http://ext.radiantcms.org/extensions/15-sphinx-search"
|
9
|
+
gem.authors = ["Josh French", "Kunal Shah", "Justin Blecher"]
|
10
|
+
gem.add_dependency 'radiant'
|
11
|
+
gem.add_dependency 'thinking-sphinx', '>= 1.3.3'
|
12
|
+
end
|
13
|
+
rescue LoadError
|
14
|
+
puts "Jeweler (or a dependency) not available. This is only required if you plan to package page-factory as a gem."
|
15
|
+
end
|
16
|
+
|
17
|
+
# In rails 1.2, plugins aren't available in the path until they're loaded.
|
18
|
+
# Check to see if the rspec plugin is installed first and require
|
19
|
+
# it if it is. If not, use the gem version.
|
20
|
+
|
21
|
+
# Determine where the RSpec plugin is by loading the boot
|
22
|
+
unless defined? RADIANT_ROOT
|
23
|
+
ENV["RAILS_ENV"] = "test"
|
24
|
+
case
|
25
|
+
when ENV["RADIANT_ENV_FILE"]
|
26
|
+
require File.dirname(ENV["RADIANT_ENV_FILE"]) + "/boot"
|
27
|
+
when File.dirname(__FILE__) =~ %r{vendor/radiant/vendor/extensions}
|
28
|
+
require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../../")}/config/boot"
|
29
|
+
else
|
30
|
+
require "#{File.expand_path(File.dirname(__FILE__) + "/../../../")}/config/boot"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
require 'rake'
|
35
|
+
require 'rake/rdoctask'
|
36
|
+
require 'rake/testtask'
|
37
|
+
|
38
|
+
rspec_base = File.expand_path(RADIANT_ROOT + '/vendor/plugins/rspec/lib')
|
39
|
+
$LOAD_PATH.unshift(rspec_base) if File.exist?(rspec_base)
|
40
|
+
require 'spec/rake/spectask'
|
41
|
+
# require 'spec/translator'
|
42
|
+
|
43
|
+
# Cleanup the RADIANT_ROOT constant so specs will load the environment
|
44
|
+
Object.send(:remove_const, :RADIANT_ROOT)
|
45
|
+
|
46
|
+
extension_root = File.expand_path(File.dirname(__FILE__))
|
47
|
+
|
48
|
+
task :default => :spec
|
49
|
+
task :stats => "spec:statsetup"
|
50
|
+
|
51
|
+
desc "Run all specs in spec directory"
|
52
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
53
|
+
t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
|
54
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
55
|
+
end
|
56
|
+
|
57
|
+
namespace :spec do
|
58
|
+
desc "Run all specs in spec directory with RCov"
|
59
|
+
Spec::Rake::SpecTask.new(:rcov) do |t|
|
60
|
+
t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
|
61
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
62
|
+
t.rcov = true
|
63
|
+
t.rcov_opts = ['--exclude', 'spec', '--rails']
|
64
|
+
end
|
65
|
+
|
66
|
+
desc "Print Specdoc for all specs"
|
67
|
+
Spec::Rake::SpecTask.new(:doc) do |t|
|
68
|
+
t.spec_opts = ["--format", "specdoc", "--dry-run"]
|
69
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
70
|
+
end
|
71
|
+
|
72
|
+
[:models, :controllers, :views, :helpers].each do |sub|
|
73
|
+
desc "Run the specs under spec/#{sub}"
|
74
|
+
Spec::Rake::SpecTask.new(sub) do |t|
|
75
|
+
t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
|
76
|
+
t.spec_files = FileList["spec/#{sub}/**/*_spec.rb"]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Hopefully no one has written their extensions in pre-0.9 style
|
81
|
+
# desc "Translate specs from pre-0.9 to 0.9 style"
|
82
|
+
# task :translate do
|
83
|
+
# translator = ::Spec::Translator.new
|
84
|
+
# dir = RAILS_ROOT + '/spec'
|
85
|
+
# translator.translate(dir, dir)
|
86
|
+
# end
|
87
|
+
|
88
|
+
# Setup specs for stats
|
89
|
+
task :statsetup do
|
90
|
+
require 'code_statistics'
|
91
|
+
::STATS_DIRECTORIES << %w(Model\ specs spec/models)
|
92
|
+
::STATS_DIRECTORIES << %w(View\ specs spec/views)
|
93
|
+
::STATS_DIRECTORIES << %w(Controller\ specs spec/controllers)
|
94
|
+
::STATS_DIRECTORIES << %w(Helper\ specs spec/views)
|
95
|
+
::CodeStatistics::TEST_TYPES << "Model specs"
|
96
|
+
::CodeStatistics::TEST_TYPES << "View specs"
|
97
|
+
::CodeStatistics::TEST_TYPES << "Controller specs"
|
98
|
+
::CodeStatistics::TEST_TYPES << "Helper specs"
|
99
|
+
::STATS_DIRECTORIES.delete_if {|a| a[0] =~ /test/}
|
100
|
+
end
|
101
|
+
|
102
|
+
namespace :db do
|
103
|
+
namespace :fixtures do
|
104
|
+
desc "Load fixtures (from spec/fixtures) into the current environment's database. Load specific fixtures using FIXTURES=x,y"
|
105
|
+
task :load => :environment do
|
106
|
+
require 'active_record/fixtures'
|
107
|
+
ActiveRecord::Base.establish_connection(RAILS_ENV.to_sym)
|
108
|
+
(ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir.glob(File.join(RAILS_ROOT, 'spec', 'fixtures', '*.{yml,csv}'))).each do |fixture_file|
|
109
|
+
Fixtures.create_fixtures('spec/fixtures', File.basename(fixture_file, '.*'))
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
desc 'Generate documentation for the sphinx_search extension.'
|
117
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
118
|
+
rdoc.rdoc_dir = 'rdoc'
|
119
|
+
rdoc.title = 'SphinxSearchExtension'
|
120
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
121
|
+
rdoc.rdoc_files.include('README')
|
122
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
123
|
+
end
|
124
|
+
|
125
|
+
# For extensions that are in transition
|
126
|
+
desc 'Test the sphinx_search extension.'
|
127
|
+
Rake::TestTask.new(:test) do |t|
|
128
|
+
t.libs << 'lib'
|
129
|
+
t.pattern = 'test/**/*_test.rb'
|
130
|
+
t.verbose = true
|
131
|
+
end
|
132
|
+
|
133
|
+
# Load any custom rakefiles for extension
|
134
|
+
Dir[File.dirname(__FILE__) + '/tasks/*.rake'].sort.each { |f| require f }
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.9.1
|
@@ -0,0 +1,15 @@
|
|
1
|
+
- content_for :page_css do
|
2
|
+
:plain
|
3
|
+
#page-search-status .BoxFix { padding: 4px 0; }
|
4
|
+
|
5
|
+
%tr#page-search-status
|
6
|
+
%th.label Search
|
7
|
+
%td.field
|
8
|
+
.BoxFix
|
9
|
+
%ul.Inputs.HorizList
|
10
|
+
%li
|
11
|
+
= radio_button :page, :searchable, true, :class => 'RadioInput'
|
12
|
+
%label{:for => 'page_searchable_true' } Searchable
|
13
|
+
%li
|
14
|
+
= radio_button :page, :searchable, false, :class => 'RadioInput'
|
15
|
+
%label{:for => 'page_searchable_false' } Invisible to Search
|
@@ -0,0 +1 @@
|
|
1
|
+
# Nothing to see here, move along.
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module SphinxSearch
|
2
|
+
class LinkRenderer < Radiant::Pagination::LinkRenderer
|
3
|
+
def initialize(url_stem, query)
|
4
|
+
@url_stem, @query = url_stem, query
|
5
|
+
end
|
6
|
+
|
7
|
+
def page_link(page, text, attributes = {})
|
8
|
+
linkclass = %{ class="#{attributes[:class]}"} if attributes[:class]
|
9
|
+
linkrel = %{ rel="#{attributes[:rel]}"} if attributes[:rel]
|
10
|
+
page_param_name = WillPaginate::ViewHelpers.pagination_options[:param_name]
|
11
|
+
search_param_name = SphinxSearch.param_name || 'q'
|
12
|
+
%Q{<a href="#{@url_stem}?#{search_param_name}=#{@query}&#{page_param_name}=#{page}"#{linkrel}#{linkclass}>#{text}</a>}
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module SphinxSearch
|
2
|
+
module PageContextExtensions
|
3
|
+
def self.included(base)
|
4
|
+
base.alias_method_chain :set_process_variables, :sphinx
|
5
|
+
end
|
6
|
+
|
7
|
+
def set_process_variables_with_sphinx(result)
|
8
|
+
quacks_like_page = lambda do |object|
|
9
|
+
[:request, :response, :request=, :response=].all? { |method| object.respond_to?(method) }
|
10
|
+
end
|
11
|
+
set_process_variables_without_sphinx(result) if quacks_like_page.call(result)
|
12
|
+
end
|
13
|
+
private :set_process_variables_with_sphinx
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'thinking_sphinx'
|
2
|
+
|
3
|
+
module SphinxSearch
|
4
|
+
module PageExtensions
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.define_index do
|
8
|
+
set_property :delta => true, :group_concat_max_len => SphinxSearch.content_length || 8.kilobytes
|
9
|
+
set_property :field_weights => { 'title' => 100 }
|
10
|
+
indexes title, parts.content
|
11
|
+
has updated_at, status_id, virtual
|
12
|
+
end
|
13
|
+
|
14
|
+
base.extend ClassMethods
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
def searchable(search=true)
|
19
|
+
search ? SphinxSearch.hidden_classes.delete(self.name) : SphinxSearch.hidden_classes.push(self.name)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
module SphinxSearch
|
2
|
+
module RadiusTags
|
3
|
+
include Radiant::Taggable
|
4
|
+
include ActionView::Helpers::TextHelper
|
5
|
+
|
6
|
+
desc %{
|
7
|
+
Namespace for all search tags.
|
8
|
+
}
|
9
|
+
tag 'search' do |tag|
|
10
|
+
param = (SphinxSearch.param_name || 'q').to_sym
|
11
|
+
tag.locals.query = tag.globals.page.request[param]
|
12
|
+
tag.expand
|
13
|
+
end
|
14
|
+
|
15
|
+
desc %{
|
16
|
+
Renders a basic search form. Takes optional @id@ and @class@ values if
|
17
|
+
you need to target the form with specific CSS or JS; also takes an optional
|
18
|
+
@value@ tag as the label on the input. If for some reason you don't want
|
19
|
+
to use the default search term paramater @q@, you can override this by
|
20
|
+
defining @SphinxSearch.param_name@ in @/config/initializers/sphinx_search.rb@.
|
21
|
+
|
22
|
+
*Usage:*
|
23
|
+
|
24
|
+
<pre><code><r:search:form [id="form-id"] [class="form-class"] [value="Go"] /></code></pre>
|
25
|
+
}
|
26
|
+
tag 'search:form' do |tag|
|
27
|
+
form_id = tag.attr['id'] || 'search-form'
|
28
|
+
form_class = tag.attr['class'] || 'search-form'
|
29
|
+
form_value = tag.attr['value'] || 'Search'
|
30
|
+
form_input = SphinxSearch.param_name || 'q'
|
31
|
+
return <<-HTML
|
32
|
+
<form action="#{tag.globals.page.url}" method="get" id="#{form_id}" class="#{form_class}">
|
33
|
+
<input type="text" name="#{form_input}" value="#{tag.locals.query}">
|
34
|
+
<input type="submit" value="#{form_value}">
|
35
|
+
</form>
|
36
|
+
HTML
|
37
|
+
end
|
38
|
+
|
39
|
+
desc %{
|
40
|
+
Namespace for search results. Expands if a query was passed and at least
|
41
|
+
one result was found. Takes a @paginated@ attribute and all the standard
|
42
|
+
pagination attributes.
|
43
|
+
|
44
|
+
*Usage:*
|
45
|
+
|
46
|
+
<pre><code><r:search:results [paginated="false|true"] [per_page="..."] [...other pagination attributes]>...</r:search:results></code></pre>
|
47
|
+
}
|
48
|
+
tag 'search:results' do |tag|
|
49
|
+
options = thinking_sphinx_options(tag)
|
50
|
+
paging = pagination_find_options(tag)
|
51
|
+
if paging
|
52
|
+
options.merge!(paging)
|
53
|
+
tag.locals.pagination_opts = will_paginate_options(tag)
|
54
|
+
end
|
55
|
+
tag.globals.results ||= ThinkingSphinx.search(tag.locals.query, options)
|
56
|
+
tag.expand if tag.globals.results.any? and not tag.locals.query.blank?
|
57
|
+
end
|
58
|
+
|
59
|
+
desc %{
|
60
|
+
Expands if a query was run but no results were returned.
|
61
|
+
}
|
62
|
+
tag 'search:no_results' do |tag|
|
63
|
+
tag.expand if tag.globals.results.blank? and not tag.locals.query.blank?
|
64
|
+
end
|
65
|
+
|
66
|
+
desc %{
|
67
|
+
Displays the total (unpaginated) number of search results, in the format
|
68
|
+
*X results.* If you would like a different label than "result", pass
|
69
|
+
the optional @label@ attribute. The label will be pluralized as necessary.
|
70
|
+
|
71
|
+
*Usage:*
|
72
|
+
|
73
|
+
<pre><code><r:search:results:count [label="..."] /></code></pre>
|
74
|
+
}
|
75
|
+
tag 'search:results:count' do |tag|
|
76
|
+
label = tag.attr['label'] || 'result'
|
77
|
+
pluralize tag.globals.results.total_entries, label
|
78
|
+
end
|
79
|
+
|
80
|
+
desc %{
|
81
|
+
Returns the current page of search results.
|
82
|
+
}
|
83
|
+
tag 'search:results:current_page' do |tag|
|
84
|
+
tag.globals.results.current_page
|
85
|
+
end
|
86
|
+
|
87
|
+
desc %{
|
88
|
+
Returns the total number of pages of search results.
|
89
|
+
}
|
90
|
+
tag 'search:results:total_pages' do |tag|
|
91
|
+
tag.globals.results.total_pages
|
92
|
+
end
|
93
|
+
|
94
|
+
desc %{
|
95
|
+
Displays the original search term, sanitized for display.
|
96
|
+
}
|
97
|
+
tag 'search:query' do |tag|
|
98
|
+
ActionController::Base.helpers.strip_tags tag.locals.query
|
99
|
+
end
|
100
|
+
|
101
|
+
desc %{
|
102
|
+
Iterates over each search result. Sets @tag.locals.page@ to the current result.
|
103
|
+
|
104
|
+
*Usage:*
|
105
|
+
|
106
|
+
<pre><code><r:search:results:each>...</r:search:results:each></code></pre>
|
107
|
+
}
|
108
|
+
tag 'search:results:each' do |tag|
|
109
|
+
tag.globals.results.collect do |result|
|
110
|
+
tag.locals.page = result
|
111
|
+
tag.expand
|
112
|
+
end.join("\n")
|
113
|
+
end
|
114
|
+
|
115
|
+
desc %{
|
116
|
+
Returns the associated excerpt for each search result. If you want to
|
117
|
+
take the excerpt from the page title or just one specific page part, use
|
118
|
+
the optional @for@ attribute.
|
119
|
+
|
120
|
+
*Usage:*
|
121
|
+
|
122
|
+
<pre><code><r:search:results:each:excerpt [for="title|part_name"]/></code></pre>
|
123
|
+
}
|
124
|
+
tag 'search:results:each:excerpt' do |tag|
|
125
|
+
content = case tag.attr['for']
|
126
|
+
when 'title' : tag.locals.page.title
|
127
|
+
when nil : tag.locals.page.parts.map(&:content).join(' ')
|
128
|
+
else tag.locals.page.part(tag.attr['for']).try(:content) || ''
|
129
|
+
end
|
130
|
+
tag.globals.results.excerpt_for(content, tag.locals.page.class)
|
131
|
+
end
|
132
|
+
|
133
|
+
desc %{
|
134
|
+
Renders pagination links for the results.
|
135
|
+
|
136
|
+
*Usage:*
|
137
|
+
<pre><code><r:search:results paginated="true" [pagination options]>...<r:search:results:pagination /></r:search:results></code></pre>
|
138
|
+
}
|
139
|
+
tag 'search:results:pagination' do |tag|
|
140
|
+
if tag.globals.results
|
141
|
+
will_paginate(tag.globals.results, tag.locals.pagination_opts)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
desc %{
|
146
|
+
Renders if no a query was run without a term, e.g. someone hit the search
|
147
|
+
button without entering anything.
|
148
|
+
}
|
149
|
+
tag 'search:empty_query' do |tag|
|
150
|
+
tag.expand if tag.locals.query.try(:empty?)
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
def will_paginate_options(tag)
|
156
|
+
options = super
|
157
|
+
options[:renderer] &&= SphinxSearch::LinkRenderer.new(tag.globals.page.url, tag.locals.query)
|
158
|
+
options
|
159
|
+
end
|
160
|
+
|
161
|
+
def thinking_sphinx_options(tag)
|
162
|
+
{
|
163
|
+
:with => { :status_id => 100, :virtual => false },
|
164
|
+
:without => { :class_crc => SphinxSearch.hidden_classes.map(&:to_crc32) },
|
165
|
+
:retry_stale => true
|
166
|
+
}
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'thinking_sphinx/tasks'
|
2
|
+
|
3
|
+
namespace :radiant do
|
4
|
+
namespace :extensions do
|
5
|
+
namespace :sphinx_search do
|
6
|
+
|
7
|
+
desc "Runs the migration of the Sphinx Search extension"
|
8
|
+
task :migrate => :environment do
|
9
|
+
require 'radiant/extension_migrator'
|
10
|
+
if ENV["VERSION"]
|
11
|
+
SphinxSearchExtension.migrator.migrate(ENV["VERSION"].to_i)
|
12
|
+
else
|
13
|
+
SphinxSearchExtension.migrator.migrate
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Turns off indexing for subsequent tasks"
|
18
|
+
task :disable_deltas do
|
19
|
+
ThinkingSphinx.deltas_enabled = false
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "Copies public assets of the Sphinx Search to the instance public/ directory."
|
23
|
+
task :update => :environment do
|
24
|
+
is_svn_or_dir = proc {|path| path =~ /\.svn/ || File.directory?(path) }
|
25
|
+
puts "Copying assets from SphinxSearchExtension"
|
26
|
+
Dir[SphinxSearchExtension.root + "/public/**/*"].reject(&is_svn_or_dir).each do |file|
|
27
|
+
path = file.sub(SphinxSearchExtension.root, '')
|
28
|
+
directory = File.dirname(path)
|
29
|
+
mkdir_p RAILS_ROOT + directory, :verbose => false
|
30
|
+
cp file, RAILS_ROOT + path, :verbose => false
|
31
|
+
end
|
32
|
+
unless SphinxSearchExtension.root.starts_with? RAILS_ROOT # don't need to copy vendored tasks
|
33
|
+
puts "Copying rake tasks from SphinxSearchExtension"
|
34
|
+
local_tasks_path = File.join(RAILS_ROOT, %w(lib tasks))
|
35
|
+
mkdir_p local_tasks_path, :verbose => false
|
36
|
+
Dir[File.join SphinxSearchExtension.root, %w(lib tasks *.rake)].each do |file|
|
37
|
+
cp file, local_tasks_path, :verbose => false
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
desc "Copies the sphinx.yml config to the project root"
|
43
|
+
task :config => :environment do
|
44
|
+
config_temp = File.join(SphinxSearchExtension.root, %w(lib templates sphinx.yml))
|
45
|
+
config_dest = File.join(RAILS_ROOT, %w(config sphinx.yml))
|
46
|
+
cp(config_temp, config_dest, :verbose => false) unless File.exists?(config_dest)
|
47
|
+
|
48
|
+
init_temp = File.join(SphinxSearchExtension.root, %w(lib templates initializer.rb))
|
49
|
+
init_dest = File.join(RAILS_ROOT, %w(config initializers sphinx_search.rb))
|
50
|
+
cp(init_temp, init_dest, :verbose => false) unless File.exists?(init_dest)
|
51
|
+
end
|
52
|
+
task :update => :config
|
53
|
+
|
54
|
+
desc 'Run specs with coverage'
|
55
|
+
task :coverage do
|
56
|
+
Spec::Rake::SpecTask.new('radiant:extensions:sphinx_search:coverage') do |t|
|
57
|
+
t.spec_opts = ['--format profile', '--loadby mtime', '--reverse']
|
58
|
+
t.spec_files = FileList['vendor/extensions/sphinx_search/spec/**/*_spec.rb']
|
59
|
+
t.rcov = true
|
60
|
+
t.rcov_opts = ['--exclude', 'gems,spec,/usr/lib/ruby,config,vendor/radiant', '--include-file', 'vendor/extensions/sphinx_search/app,vendor/extensions/sphinx_search/lib', '--sort', 'coverage']
|
61
|
+
t.rcov_dir = 'coverage'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# The parameter passed in your search URLs.
|
2
|
+
SphinxSearch.param_name = 'q'
|
3
|
+
|
4
|
+
# Max length of content to index, per page. Bump this up if you have more than
|
5
|
+
# 8k of text per page (as calculated by combining all page parts.)
|
6
|
+
SphinxSearch.content_length = 8.kilobytes
|
7
|
+
|
8
|
+
# Don't include these page subclasses in the search results. This can also be
|
9
|
+
# accessed by declaring `self.searchable [true|false]` in your Page subclasses.
|
10
|
+
SphinxSearch.hidden_classes = %w(SearchPage JavascriptPage StylesheetPage)
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{radiant-sphinx_search-extension}
|
8
|
+
s.version = "0.9.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Josh French", "Kunal Shah", "Justin Blecher"]
|
12
|
+
s.date = %q{2010-08-08}
|
13
|
+
s.description = %q{Adds fulltext search capability to Radiant content via Thinking Sphinx.}
|
14
|
+
s.email = %q{git@digitalpulp.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README.md"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".gitmodules",
|
20
|
+
"README.md",
|
21
|
+
"Rakefile",
|
22
|
+
"VERSION",
|
23
|
+
"app/models/search_page.rb",
|
24
|
+
"app/views/admin/pages/_search_toggle.haml",
|
25
|
+
"db/migrate/20100808155751_add_delta_to_pages.rb",
|
26
|
+
"lib/radiant-sphinx_search-extension.rb",
|
27
|
+
"lib/sphinx_search.rb",
|
28
|
+
"lib/sphinx_search/link_renderer.rb",
|
29
|
+
"lib/sphinx_search/page_context_extensions.rb",
|
30
|
+
"lib/sphinx_search/page_extensions.rb",
|
31
|
+
"lib/sphinx_search/radius_tags.rb",
|
32
|
+
"lib/tasks/sphinx_search_extension_tasks.rake",
|
33
|
+
"lib/templates/initializer.rb",
|
34
|
+
"lib/templates/sphinx.yml",
|
35
|
+
"radiant-sphinx_search-extension.gemspec",
|
36
|
+
"spec/datasets/search_pages_dataset.rb",
|
37
|
+
"spec/lib/search_radius_tags_spec.rb",
|
38
|
+
"spec/models/page_spec.rb",
|
39
|
+
"spec/spec.opts",
|
40
|
+
"spec/spec_helper.rb",
|
41
|
+
"sphinx_search_extension.rb"
|
42
|
+
]
|
43
|
+
s.homepage = %q{http://ext.radiantcms.org/extensions/15-sphinx-search}
|
44
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
45
|
+
s.require_paths = ["lib"]
|
46
|
+
s.rubygems_version = %q{1.3.6}
|
47
|
+
s.summary = %q{Sphinx Search Extension for Radiant CMS}
|
48
|
+
s.test_files = [
|
49
|
+
"spec/datasets/search_pages_dataset.rb",
|
50
|
+
"spec/lib/search_radius_tags_spec.rb",
|
51
|
+
"spec/models/page_spec.rb",
|
52
|
+
"spec/spec_helper.rb"
|
53
|
+
]
|
54
|
+
|
55
|
+
if s.respond_to? :specification_version then
|
56
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
57
|
+
s.specification_version = 3
|
58
|
+
|
59
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
60
|
+
s.add_runtime_dependency(%q<radiant>, [">= 0"])
|
61
|
+
s.add_runtime_dependency(%q<thinking-sphinx>, [">= 1.3.3"])
|
62
|
+
else
|
63
|
+
s.add_dependency(%q<radiant>, [">= 0"])
|
64
|
+
s.add_dependency(%q<thinking-sphinx>, [">= 1.3.3"])
|
65
|
+
end
|
66
|
+
else
|
67
|
+
s.add_dependency(%q<radiant>, [">= 0"])
|
68
|
+
s.add_dependency(%q<thinking-sphinx>, [">= 1.3.3"])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class SearchPagesDataset < Dataset::Base
|
2
|
+
uses :home_page
|
3
|
+
|
4
|
+
def load
|
5
|
+
create_page 'searchable', :slug => 'searchable', :parent_id => page_id(:home),
|
6
|
+
:description => 'a searchable page' do
|
7
|
+
create_page_part "body", :content => "Hello world!", :id => 1
|
8
|
+
create_page_part "extended", :content => "sweet harmonious biscuits", :id => 2
|
9
|
+
end
|
10
|
+
|
11
|
+
create_page 'search_results', :slug => 'search_results', :parent_id => page_id(:home), :class_name => "SearchPage"
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
class Array
|
4
|
+
alias_method :total_entries, :size # make arrays quack like TS result sets
|
5
|
+
end
|
6
|
+
|
7
|
+
describe SphinxSearch::RadiusTags do
|
8
|
+
dataset :search_pages
|
9
|
+
|
10
|
+
before do
|
11
|
+
@page = pages(:search_results)
|
12
|
+
@request = ActionController::TestRequest.new
|
13
|
+
@request.params[:q] = 'query'
|
14
|
+
ActionController::TestRequest.stub!(:new).and_return(@request)
|
15
|
+
ThinkingSphinx.stub!(:search).and_return([pages(:searchable)])
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'r:search:form' do
|
19
|
+
it "should render a form input" do
|
20
|
+
form = @page.should render('<r:search:form id="test" class="test" value="go" />').matching(/action="#{@page.url}"/)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "r:search:results" do
|
25
|
+
it "should expand if there are results" do
|
26
|
+
@page.should render('<r:search:results>hi</r:search:results>').as('hi')
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should not expand if no query was run" do
|
30
|
+
@request.params[:q] = ''
|
31
|
+
@page.should render('<r:search:results>hi</r:search:results>').as('')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "r:search:results:each" do
|
36
|
+
it "should iterate over results" do
|
37
|
+
@page.should render('<r:search:results:each><r:title /></r:search:results:each>').as(pages(:searchable).title)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "r:search:results:count" do
|
42
|
+
it "should return result count" do
|
43
|
+
@page.should render('<r:search:results:count />').as("1 result")
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should pluralize any label" do
|
47
|
+
ThinkingSphinx.stub!(:search).and_return([pages(:searchable), pages(:search_results)])
|
48
|
+
@page.should render('<r:search:results:count label="item" />').as("2 items")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "r:search:query" do
|
53
|
+
it "should sanitize query" do
|
54
|
+
@request.params[:q] = '<script>query'
|
55
|
+
@page.should render('<r:search:query />').as('query')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "r:search:empty_query" do
|
60
|
+
it "should render if query was blank" do
|
61
|
+
@request.params[:q] = ''
|
62
|
+
@page.should render('<r:search:empty_query>empty</r:search:empty_query>').as('empty')
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "r:search:no_results" do
|
68
|
+
it "should render if results were empty" do
|
69
|
+
ThinkingSphinx.stub!(:search).and_return([])
|
70
|
+
@page.should render('<r:search:no_results>none</r:search:no_results>').as('none')
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe Page do
|
4
|
+
describe "Page.searchable" do
|
5
|
+
it "should add itself to the hidden classes array" do
|
6
|
+
SphinxSearch.hidden_classes = []
|
7
|
+
FileNotFoundPage.searchable false
|
8
|
+
SphinxSearch.hidden_classes.should include('FileNotFoundPage')
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should remove itself from the hidden classes array" do
|
12
|
+
SphinxSearch.hidden_classes = %w(FileNotFoundPage)
|
13
|
+
FileNotFoundPage.searchable true
|
14
|
+
SphinxSearch.hidden_classes.should_not include("FileNotFoundPage")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "#thinking_sphinx_options" do
|
19
|
+
it "should description" do
|
20
|
+
SphinxSearch.hidden_classes = %w(FileNotFoundPage)
|
21
|
+
opts = SearchPage.new.send :thinking_sphinx_options, nil
|
22
|
+
opts[:without][:class_crc].should include('FileNotFoundPage'.to_crc32)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
unless defined? RADIANT_ROOT
|
2
|
+
ENV["RAILS_ENV"] = "test"
|
3
|
+
case
|
4
|
+
when ENV["RADIANT_ENV_FILE"]
|
5
|
+
require ENV["RADIANT_ENV_FILE"]
|
6
|
+
when File.dirname(__FILE__) =~ %r{vendor/radiant/vendor/extensions}
|
7
|
+
require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../../../")}/config/environment"
|
8
|
+
else
|
9
|
+
require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../")}/config/environment"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
require "#{RADIANT_ROOT}/spec/spec_helper"
|
13
|
+
|
14
|
+
Dataset::Resolver.default << (File.dirname(__FILE__) + "/datasets")
|
15
|
+
|
16
|
+
if File.directory?(File.dirname(__FILE__) + "/matchers")
|
17
|
+
Dir[File.dirname(__FILE__) + "/matchers/*.rb"].each {|file| require file }
|
18
|
+
end
|
19
|
+
|
20
|
+
Spec::Runner.configure do |config|
|
21
|
+
SphinxSearch.hidden_classes ||= %w(SearchPage JavascriptPage StylesheetPage)
|
22
|
+
# config.use_transactional_fixtures = true
|
23
|
+
# config.use_instantiated_fixtures = false
|
24
|
+
# config.fixture_path = RAILS_ROOT + '/spec/fixtures'
|
25
|
+
|
26
|
+
# You can declare fixtures for each behaviour like this:
|
27
|
+
# describe "...." do
|
28
|
+
# fixtures :table_a, :table_b
|
29
|
+
#
|
30
|
+
# Alternatively, if you prefer to declare them only once, you can
|
31
|
+
# do so here, like so ...
|
32
|
+
#
|
33
|
+
# config.global_fixtures = :table_a, :table_b
|
34
|
+
#
|
35
|
+
# If you declare global fixtures, be aware that they will be declared
|
36
|
+
# for all of your examples, even those that don't use them.
|
37
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class SphinxSearchExtension < Radiant::Extension
|
2
|
+
version YAML::load_file(File.join(File.dirname(__FILE__), 'VERSION'))
|
3
|
+
description "Search Pages with ThinkingSphinx"
|
4
|
+
url "http://digitalpulp.com"
|
5
|
+
|
6
|
+
def activate
|
7
|
+
Page.send(:include, SphinxSearch::PageExtensions)
|
8
|
+
PageContext.send(:include, SphinxSearch::PageContextExtensions)
|
9
|
+
end
|
10
|
+
|
11
|
+
def deactivate
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: radiant-sphinx_search-extension
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 9
|
8
|
+
- 1
|
9
|
+
version: 0.9.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Josh French
|
13
|
+
- Kunal Shah
|
14
|
+
- Justin Blecher
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2010-08-08 00:00:00 -04:00
|
20
|
+
default_executable:
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
name: radiant
|
24
|
+
prerelease: false
|
25
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: thinking-sphinx
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 1
|
43
|
+
- 3
|
44
|
+
- 3
|
45
|
+
version: 1.3.3
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id002
|
48
|
+
description: Adds fulltext search capability to Radiant content via Thinking Sphinx.
|
49
|
+
email: git@digitalpulp.com
|
50
|
+
executables: []
|
51
|
+
|
52
|
+
extensions: []
|
53
|
+
|
54
|
+
extra_rdoc_files:
|
55
|
+
- README.md
|
56
|
+
files:
|
57
|
+
- .gitmodules
|
58
|
+
- README.md
|
59
|
+
- Rakefile
|
60
|
+
- VERSION
|
61
|
+
- app/models/search_page.rb
|
62
|
+
- app/views/admin/pages/_search_toggle.haml
|
63
|
+
- db/migrate/20100808155751_add_delta_to_pages.rb
|
64
|
+
- lib/radiant-sphinx_search-extension.rb
|
65
|
+
- lib/sphinx_search.rb
|
66
|
+
- lib/sphinx_search/link_renderer.rb
|
67
|
+
- lib/sphinx_search/page_context_extensions.rb
|
68
|
+
- lib/sphinx_search/page_extensions.rb
|
69
|
+
- lib/sphinx_search/radius_tags.rb
|
70
|
+
- lib/tasks/sphinx_search_extension_tasks.rake
|
71
|
+
- lib/templates/initializer.rb
|
72
|
+
- lib/templates/sphinx.yml
|
73
|
+
- radiant-sphinx_search-extension.gemspec
|
74
|
+
- spec/datasets/search_pages_dataset.rb
|
75
|
+
- spec/lib/search_radius_tags_spec.rb
|
76
|
+
- spec/models/page_spec.rb
|
77
|
+
- spec/spec.opts
|
78
|
+
- spec/spec_helper.rb
|
79
|
+
- sphinx_search_extension.rb
|
80
|
+
has_rdoc: true
|
81
|
+
homepage: http://ext.radiantcms.org/extensions/15-sphinx-search
|
82
|
+
licenses: []
|
83
|
+
|
84
|
+
post_install_message:
|
85
|
+
rdoc_options:
|
86
|
+
- --charset=UTF-8
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
segments:
|
94
|
+
- 0
|
95
|
+
version: "0"
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
segments:
|
101
|
+
- 0
|
102
|
+
version: "0"
|
103
|
+
requirements: []
|
104
|
+
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 1.3.6
|
107
|
+
signing_key:
|
108
|
+
specification_version: 3
|
109
|
+
summary: Sphinx Search Extension for Radiant CMS
|
110
|
+
test_files:
|
111
|
+
- spec/datasets/search_pages_dataset.rb
|
112
|
+
- spec/lib/search_radius_tags_spec.rb
|
113
|
+
- spec/models/page_spec.rb
|
114
|
+
- spec/spec_helper.rb
|