alchemy-pg_search 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.travis.yml +10 -0
  4. data/CHANGELOG.md +4 -0
  5. data/Gemfile +14 -0
  6. data/LICENSE +27 -0
  7. data/README.md +136 -0
  8. data/Rakefile +46 -0
  9. data/alchemy-pg_search.gemspec +26 -0
  10. data/app/helpers/alchemy/pg_search/search_helper.rb +53 -0
  11. data/app/views/alchemy/search/_form.html.erb +12 -0
  12. data/app/views/alchemy/search/_result.html.erb +13 -0
  13. data/app/views/alchemy/search/_results.html.erb +26 -0
  14. data/bin/rails +12 -0
  15. data/config/locales/alchemy.search.de.yml +14 -0
  16. data/config/locales/alchemy.search.en.yml +14 -0
  17. data/db/migrate/20141211105526_add_searchable_to_alchemy_essence_texts.rb +5 -0
  18. data/db/migrate/20141211105942_add_searchable_to_alchemy_essence_richtexts.rb +5 -0
  19. data/db/migrate/20141211110126_add_searchable_to_alchemy_essence_pictures.rb +5 -0
  20. data/lib/alchemy-pg_search.rb +23 -0
  21. data/lib/alchemy/pg_search/config.rb +17 -0
  22. data/lib/alchemy/pg_search/content_extension.rb +30 -0
  23. data/lib/alchemy/pg_search/controller_methods.rb +92 -0
  24. data/lib/alchemy/pg_search/element_extension.rb +40 -0
  25. data/lib/alchemy/pg_search/engine.rb +26 -0
  26. data/lib/alchemy/pg_search/page_extension.rb +45 -0
  27. data/lib/alchemy/pg_search/page_search_scope.rb +9 -0
  28. data/lib/alchemy/pg_search/searchable.rb +27 -0
  29. data/lib/alchemy/pg_search/version.rb +5 -0
  30. data/lib/generators/alchemy/pg_search/upgrade/templates/migration.rb.tt +31 -0
  31. data/lib/generators/alchemy/pg_search/upgrade/upgrade_generator.rb +20 -0
  32. data/lib/generators/alchemy/pg_search/views/views_generator.rb +15 -0
  33. data/lib/tasks/alchemy/pg_search_tasks.rake +4 -0
  34. metadata +166 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 49d88251b2d43c33f9512645a9d2a47f99370b91392dff0752bfa7827dab9484
4
+ data.tar.gz: 2b402676acaf93b70bb00c900f509bdc3fb2afedf8682040410ff4d0e75187ef
5
+ SHA512:
6
+ metadata.gz: 5ea033157ed66c0ac4f198d1879f9a78102a237f33977b9f43a0165e4e988baef011f1e28fc1e456e747c1ed134275526293da84f6d181019f668afeedf323e9
7
+ data.tar.gz: c7bb6894fad22d584ce463e72f57b261d3aa38de6abcd33e80eea39c2e8c55ebad30a279cb96acf9cb07a63a66a10dcbc52e6bb4e93675aeaad588a47a1960b8
@@ -0,0 +1,9 @@
1
+ .bundle/
2
+ log/*.log
3
+ pkg/
4
+ Gemfile.lock
5
+ .rspec
6
+ spec/dummy/log/*.log
7
+ spec/dummy/db/*.sqlite3*
8
+ spec/dummy/tmp/*
9
+ .ruby-*
@@ -0,0 +1,10 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3
5
+ - 2.4
6
+ branches:
7
+ only:
8
+ - master
9
+ before_script: 'rake alchemy:spec:prepare'
10
+ script: rspec
@@ -0,0 +1,4 @@
1
+ ## 1.2.0 (2018-11-28)
2
+
3
+ - Rename the #search method into #full_text_search [#14](https://github.com/AlchemyCMS/alchemy-pg_search/pull/14) by [tvdeyen](https://github.com/tvdeyen)
4
+ - Add a page search scope class [#13](https://github.com/AlchemyCMS/alchemy-pg_search/pull/13) by [tvdeyen](https://github.com/tvdeyen)
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'alchemy_cms', github: 'AlchemyCMS/alchemy_cms', branch: '3.6-stable'
6
+ gem 'sassc-rails'
7
+ gem 'pg', '< 1.0'
8
+
9
+ group :test do
10
+ gem 'factory_girl_rails'
11
+ gem "capybara"
12
+ gem "pry"
13
+ gem 'launchy'
14
+ end
data/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2014, magic labs GmbH
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification,
5
+ are permitted provided that the following conditions are met:
6
+
7
+ Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ Redistributions in binary form must reproduce the above copyright notice, this
11
+ list of conditions and the following disclaimer in the documentation and/or
12
+ other materials provided with the distribution.
13
+
14
+ Neither the name of the magic labs GmbH nor the names of its
15
+ contributors may be used to endorse or promote products derived from
16
+ this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,136 @@
1
+ [![Build Status](https://travis-ci.org/AlchemyCMS/alchemy-pg_search.svg?branch=master)](https://travis-ci.org/AlchemyCMS/alchemy-pg_search)
2
+
3
+ # Alchemy CMS Postgresql Fulltext Search
4
+
5
+ This gem provides full text search for projects using postgresql databases to Alchemy CMS 3.3 and above.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's `Gemfile`:
10
+
11
+ ```ruby
12
+ gem 'alchemy-pg_search', github: 'AlchemyCMS/alchemy-pg_search', branch: 'master'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ ```shell
18
+ $ bundle install
19
+ ```
20
+
21
+ Add migrations:
22
+
23
+ ```shell
24
+ $ bin/rake alchemy_pg_search:install:migrations
25
+ $ bin/rake db:migrate
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ Every `EssenceText`, `EssenceRichtext` and `EssencePicture` will be indexed unless you tell Alchemy to not index a specific content.
31
+
32
+ ### Disable Indexing
33
+
34
+ Simply pass `searchable: false` to your content definitions and Alchemy will not index results from that particular content.
35
+
36
+ ```yaml
37
+ # elements.yml
38
+ - name: secrets
39
+ contents:
40
+ - name: passwords
41
+ type: EssenceText
42
+ searchable: false
43
+ default: 'This is my secret password.'
44
+ ```
45
+
46
+ ### Rendering search results.
47
+
48
+ In order to render the search results, you'll need a page layout that represents the search result page. Simply mark a page layout as `searchresults: true`. The search form will pick this page as result page.
49
+
50
+ #### Search Results Page
51
+
52
+ ```yaml
53
+ # page_layouts.yml
54
+ - name: search
55
+ searchresults: true
56
+ unique: true
57
+ ```
58
+
59
+ Tip: For maximum flexibility you could also add an element that represents the search results. This lets your editors to place additional elements (maybe a header image or additional text blocks) on the search result page.
60
+
61
+ ```yaml
62
+ # page_layouts.yml
63
+ - name: search
64
+ searchresults: true
65
+ unique: true
66
+ elements:
67
+ - searchresults
68
+ autogenerate:
69
+ - searchresults
70
+
71
+ # elements.yml
72
+ - name: searchresults
73
+ unique: true
74
+ ```
75
+
76
+ and then use the view helpers to render the search form on the page layout partial and the search results on the element view partial.
77
+
78
+ ### View Helpers
79
+
80
+ This gem provides some helper methods that let you render the form and the search results.
81
+
82
+ * Render the search form:
83
+ `render_search_form`
84
+
85
+ * Render the search results:
86
+ `render_search_results`
87
+
88
+ ### Customize Views
89
+
90
+ If you want to override the search form and search result views please use this generator.
91
+
92
+ ```shell
93
+ $ bin/rails g alchemy:pg_search:views
94
+ ```
95
+
96
+ ### Translating Views
97
+
98
+ The views are fully translatable. German and english translations are already provided with this gem.
99
+
100
+ If you want add your own translation, just place a locale file into your projects `config/locales` folder.
101
+
102
+ Here is the english example:
103
+
104
+ ```yaml
105
+ en:
106
+ alchemy:
107
+
108
+ search_form:
109
+ placeholder: 'Search query'
110
+ submit: 'Search'
111
+
112
+ search_result_page:
113
+ result_page: Page
114
+ no_results: "Your search for '%{query}' offers no result"
115
+ result_heading: "Your search for '%{query}'"
116
+ result_count:
117
+ one: 'Offers one result'
118
+ other: 'Offers %{count} results'
119
+ ```
120
+
121
+ ## Upgrading
122
+
123
+ If you are upgrading from an old Alchemy < 3.0 based project that uses the ferret based full text search, please run this handy generator:
124
+
125
+ ```shell
126
+ $ bin/rails g alchemy:pg_search:upgrade
127
+ $ bin/rake db:migrate
128
+ ```
129
+
130
+ ## Contributing
131
+
132
+ 1. Fork it
133
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
134
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
135
+ 4. Push to the branch (`git push origin my-new-feature`)
136
+ 5. Create new Pull Request
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ begin
9
+ require 'rdoc/task'
10
+ rescue LoadError
11
+ require 'rdoc/rdoc'
12
+ require 'rake/rdoctask'
13
+ RDoc::Task = Rake::RDocTask
14
+ end
15
+
16
+ desc 'Generate documentation for Alchemy CMS.'
17
+ RDoc::Task.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = 'Alchemy PgSearch'
20
+ rdoc.options << '--line-numbers' << '--inline-source'
21
+ rdoc.rdoc_files.include('README.md')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ rdoc.rdoc_files.include('app/**/*.rb')
24
+ end
25
+
26
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
27
+ load 'rails/tasks/engine.rake'
28
+
29
+ require 'rspec/core'
30
+ require 'rspec/core/rake_task'
31
+
32
+ RSpec::Core::RakeTask.new(:spec)
33
+
34
+ task :default => ['alchemy:spec:prepare', :spec]
35
+
36
+ Bundler::GemHelper.install_tasks
37
+
38
+ namespace :alchemy do
39
+ namespace :spec do
40
+
41
+ desc "Prepares database for testing"
42
+ task :prepare do
43
+ system 'cd spec/dummy; RAILS_ENV=test bin/rake db:setup db:seed; cd -'
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'alchemy/pg_search/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "alchemy-pg_search"
8
+ spec.version = Alchemy::PgSearch::VERSION
9
+ spec.authors = ["Thomas von Deyen"]
10
+ spec.email = ["alchemy@magiclabs.de"]
11
+ spec.description = %q{PostgreSQL search for Alchemy CMS 3.0}
12
+ spec.summary = %q{This gem provides PostgreSQL full text search to Alchemy 3.0}
13
+ spec.homepage = "http://alchemy-cms.com"
14
+ spec.license = "BSD"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^spec/}) }
17
+ spec.require_paths = ["lib"]
18
+
19
+ spec.add_runtime_dependency "alchemy_cms", ["> 3.2", "< 4.0"]
20
+ spec.add_runtime_dependency "pg_search", ["~> 0.7"]
21
+ spec.add_runtime_dependency "pg"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec-rails"
26
+ end
@@ -0,0 +1,53 @@
1
+ module Alchemy
2
+ module PgSearch
3
+ module SearchHelper
4
+
5
+ # Renders a search form
6
+ #
7
+ # It queries the controller and then redirects to the search result page.
8
+ #
9
+ # === Example search results page layout
10
+ #
11
+ # Only performs the search if ferret is enabled in your +config/alchemy/config.yml+ and
12
+ # a page is present that is flagged with +searchresults+ true.
13
+ #
14
+ # # config/alchemy/page_layouts.yml
15
+ # - name: search
16
+ # searchresults: true # Flag as search result page
17
+ #
18
+ # === Note
19
+ #
20
+ # The search result page will not be cached.
21
+ #
22
+ # @option options html5 [Boolean] (true) Should the search form be of type search or not?
23
+ # @option options class [String] (fulltext_search) The default css class of the form
24
+ # @option options id [String] (search) The default css id of the form
25
+ #
26
+ def render_search_form(options={})
27
+ default_options = {
28
+ html5: false,
29
+ class: 'fulltext_search',
30
+ id: 'search'
31
+ }
32
+ render 'alchemy/search/form', options: default_options.merge(options), search_result_page: search_result_page
33
+ end
34
+
35
+ # Renders the search results partial within +app/views/alchemy/search/_results.html+
36
+ #
37
+ # @option options show_result_count [Boolean] (true) Should the count of results be displayed or not?
38
+ # @option options show_heading [Boolean] (true) Should the heading be displayed or not?
39
+ #
40
+ def render_search_results(options={})
41
+ default_options = {
42
+ show_result_count: true,
43
+ show_heading: true
44
+ }
45
+ render 'alchemy/search/results', options: default_options.merge(options)
46
+ end
47
+
48
+ def highlighted_excerpt(text, phrase, radius = 50)
49
+ highlight(excerpt(text, phrase, radius: radius).to_s, phrase)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,12 @@
1
+ <% if search_result_page %>
2
+ <%= form_tag show_alchemy_page_path(search_result_page),
3
+ method: 'get', id: options[:id], class: options[:class] do %>
4
+ <% if options[:html5] %>
5
+ <%= search_field_tag :query, params[:query],
6
+ placeholder: Alchemy.t(:placeholder, scope: 'search_form') %>
7
+ <% else %>
8
+ <%= text_field_tag(:query, params[:query]) %>
9
+ <% end %>
10
+ <%= submit_tag Alchemy.t(:submit, scope: 'search_form'), name: nil %>
11
+ <% end %>
12
+ <% end %>
@@ -0,0 +1,13 @@
1
+ <li class="search_result">
2
+ <h3><%= link_to page.name, show_alchemy_page_path(page) %></h3>
3
+ <% if elements.any? %>
4
+ <% elements.each do |element| %>
5
+ <% element.searchable_contents.each do |content| %>
6
+ <p><%= highlighted_excerpt(content.searchable_ingredient.to_s, params[:query]) %></p>
7
+ <% end %>
8
+ <% end %>
9
+ <% else %>
10
+ <p><%= page.meta_description %></p>
11
+ <% end %>
12
+ <p><%= link_to page.urlname, show_alchemy_page_path(page) %></p>
13
+ </li>
@@ -0,0 +1,26 @@
1
+ <div class="search_results">
2
+ <% if @search_results.blank? %>
3
+ <h2 class="no_search_results">
4
+ <%= raw Alchemy.t('search_result_page.no_results', query: h(params[:query])) %>
5
+ </h2>
6
+ <% else %>
7
+ <h2 class="search_results_heading">
8
+ <%= raw Alchemy.t("search_result_page.result_heading", query: h(params[:query])) %>
9
+ <%= Alchemy.t("search_result_page.result_count",
10
+ count: @search_results.try(:total_count) || @search_results.size) %>
11
+ </h2>
12
+ <ul class="search_result_list">
13
+ <% @search_results.each do |page| %>
14
+ <%= render('alchemy/search/result',
15
+ elements: page.element_search_results(params[:query]),
16
+ options: options,
17
+ page: page
18
+ ) %>
19
+ <% end %>
20
+ </ul>
21
+ <% end %>
22
+ </div>
23
+
24
+ <% if @search_results.try(:total_pages) %>
25
+ <%= paginate @search_results %>
26
+ <% end %>
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application.
3
+
4
+ ENGINE_ROOT = File.expand_path('../..', __FILE__)
5
+ ENGINE_PATH = File.expand_path('../../lib/alchemy_pg_search/engine', __FILE__)
6
+
7
+ # Set up gems listed in the Gemfile.
8
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
9
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
10
+
11
+ require 'rails/all'
12
+ require 'rails/engine/commands'
@@ -0,0 +1,14 @@
1
+ de:
2
+ alchemy:
3
+
4
+ search_form:
5
+ placeholder: 'Suchbegriff'
6
+ submit: 'Suchen'
7
+
8
+ search_result_page:
9
+ result_page: Seite
10
+ no_results: "Ihre Suche nach '%{query}' ergab keine Treffer."
11
+ result_heading: "Ihre Suche nach '%{query}'"
12
+ result_count:
13
+ one: "ergab einen Treffer"
14
+ other: "ergab %{count} Treffer"
@@ -0,0 +1,14 @@
1
+ en:
2
+ alchemy:
3
+
4
+ search_form:
5
+ placeholder: 'Search query'
6
+ submit: 'Search'
7
+
8
+ search_result_page:
9
+ result_page: Page
10
+ no_results: "Your search for '%{query}' returned no result"
11
+ result_heading: "Your search for '%{query}'"
12
+ result_count:
13
+ one: 'returned one result'
14
+ other: 'returned %{count} results'
@@ -0,0 +1,5 @@
1
+ class AddSearchableToAlchemyEssenceTexts < ActiveRecord::Migration
2
+ def change
3
+ add_column :alchemy_essence_texts, :searchable, :boolean, default: true
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddSearchableToAlchemyEssenceRichtexts < ActiveRecord::Migration
2
+ def change
3
+ add_column :alchemy_essence_richtexts, :searchable, :boolean, default: true
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddSearchableToAlchemyEssencePictures < ActiveRecord::Migration
2
+ def change
3
+ add_column :alchemy_essence_pictures, :searchable, :boolean, default: true
4
+ end
5
+ end
@@ -0,0 +1,23 @@
1
+ require "alchemy/pg_search/engine"
2
+ require "alchemy/pg_search/config"
3
+ require "alchemy/pg_search/page_search_scope"
4
+
5
+ module Alchemy
6
+ module PgSearch
7
+ SEARCHABLE_ESSENCES = %w(EssenceText EssenceRichtext EssencePicture)
8
+ DEFAULT_CONFIG = {
9
+ page_search_scope: PageSearchScope.new
10
+ }
11
+
12
+ extend Config
13
+ self.config = DEFAULT_CONFIG
14
+
15
+ def self.is_searchable_essence?(essence_type)
16
+ SEARCHABLE_ESSENCES.include?(essence_type)
17
+ end
18
+
19
+ def self.searchable_essence_classes
20
+ SEARCHABLE_ESSENCES.map { |k| "Alchemy::#{k.classify}".constantize }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ module Alchemy
2
+ module PgSearch
3
+ module Config
4
+ @@config = {
5
+ paginate_per: 10
6
+ }
7
+
8
+ def config=(settings)
9
+ @@config.merge!(settings)
10
+ end
11
+
12
+ def config
13
+ @@config
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ Alchemy::Content.class_eval do
2
+
3
+ # Prepares the attributes for creating the essence.
4
+ #
5
+ # 1. It sets a default text if given in +elements.yml+
6
+ # 2. It sets searchable value for EssenceText, EssencePicture and EssenceRichtext essences
7
+ #
8
+ def prepared_attributes_for_essence
9
+ attributes = {
10
+ ingredient: default_text(definition['default'])
11
+ }
12
+ if Alchemy::PgSearch.is_searchable_essence?(definition['type'])
13
+ attributes.merge!(searchable: definition.fetch('searchable', true))
14
+ end
15
+ attributes
16
+ end
17
+
18
+ def searchable_ingredient
19
+ case essence_type
20
+ when 'Alchemy::EssencePicture'
21
+ then essence.caption
22
+ when 'Alchemy::EssenceRichtext'
23
+ then essence.stripped_body
24
+ when 'Alchemy::EssenceText'
25
+ then essence.body
26
+ else
27
+ ingredient
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,92 @@
1
+ module Alchemy
2
+ module PgSearch
3
+
4
+ # Provides full text search methods in your controller
5
+ #
6
+ module ControllerMethods
7
+
8
+ # Adds a +before_action+ to your controller
9
+ #
10
+ def self.included(controller)
11
+ controller.send(:before_action, :perform_search, only: :show)
12
+ controller.send(:helper_method, :search_result_page)
13
+ controller.send(:helper, Alchemy::PgSearch::SearchHelper)
14
+ end
15
+
16
+ private
17
+
18
+ # Performs a full text search with +PgSearch+.
19
+ #
20
+ # Gets invoked everytime 'query' is given in params.
21
+ #
22
+ # This method only sets the +@search_results+ instance variable.
23
+ #
24
+ # You have to redirect to the search result page within a search form.
25
+ #
26
+ # === Alchemy provides a handy helper for rendering the search form:
27
+ #
28
+ # render_search_form
29
+ #
30
+ # === Note
31
+ #
32
+ # If in preview mode a fake search value "lorem" will be set.
33
+ #
34
+ # @see Alchemy::PagesHelper#render_search_form
35
+ #
36
+ def perform_search
37
+ if self.class == Alchemy::Admin::PagesController && params[:query].blank?
38
+ params[:query] = 'lorem'
39
+ end
40
+ return if params[:query].blank?
41
+ @search_results = search_results
42
+ if paginate_per
43
+ @search_results = @search_results.page(params[:page]).per(paginate_per)
44
+ end
45
+ end
46
+
47
+ # Find Pages that have what is provided in "query" param with PgSearch
48
+ #
49
+ # @return [Array]
50
+ #
51
+ def search_results
52
+ pages = Alchemy::PgSearch.config[:page_search_scope].pages
53
+ # Since CanCan cannot (oh the irony) merge +accessible_by+ scope with pg_search scopes,
54
+ # we need to fake a page object here
55
+ if can? :show, Alchemy::Page.new(restricted: true, public_on: Date.current)
56
+ pages.full_text_search(params[:query])
57
+ else
58
+ pages.not_restricted.full_text_search(params[:query])
59
+ end
60
+ end
61
+
62
+ # A view helper that loads the search result page.
63
+ #
64
+ # @return [Alchemy::Page]
65
+ #
66
+ def search_result_page
67
+ @search_result_page ||= begin
68
+ page = Page.published.find_by(
69
+ page_layout: search_result_page_layout['name'],
70
+ language_id: Language.current.id
71
+ )
72
+ if page.nil?
73
+ logger.warn "\n++++++\nNo published search result page found. Please create one or publish your search result page.\n++++++\n"
74
+ end
75
+ page
76
+ end
77
+ end
78
+
79
+ def search_result_page_layout
80
+ page_layout = PageLayout.get_all_by_attributes(searchresults: true).first
81
+ if page_layout.nil?
82
+ raise "No searchresults page layout found. Please add page layout with `searchresults: true` into your `page_layouts.yml` file."
83
+ end
84
+ page_layout
85
+ end
86
+
87
+ def paginate_per
88
+ Alchemy::PgSearch.config[:paginate_per]
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,40 @@
1
+ Alchemy::Element.class_eval do
2
+ include PgSearch
3
+
4
+ pg_search_scope :full_text_search,
5
+ associated_against: {
6
+ searchable_essence_texts: :body,
7
+ searchable_essence_richtexts: :stripped_body,
8
+ searchable_essence_pictures: :caption
9
+ },
10
+ using: {
11
+ tsearch: {prefix: true}
12
+ }
13
+
14
+ has_many :searchable_essence_texts,
15
+ -> { joins(:element).where(searchable: true, alchemy_elements: {public: true}) },
16
+ class_name: 'Alchemy::EssenceText',
17
+ source_type: 'Alchemy::EssenceText',
18
+ through: :contents,
19
+ source: :essence
20
+
21
+ has_many :searchable_essence_richtexts,
22
+ -> { joins(:element).where(searchable: true, alchemy_elements: {public: true}) },
23
+ class_name: 'Alchemy::EssenceRichtext',
24
+ source_type: 'Alchemy::EssenceRichtext',
25
+ through: :contents,
26
+ source: :essence
27
+
28
+ has_many :searchable_essence_pictures,
29
+ -> { joins(:element).where(searchable: true, alchemy_elements: {public: true}) },
30
+ class_name: 'Alchemy::EssencePicture',
31
+ source_type: 'Alchemy::EssencePicture',
32
+ through: :contents,
33
+ source: :essence
34
+
35
+ has_many :searchable_contents,
36
+ -> { where(essence_type: ['Alchemy::EssenceText', 'Alchemy::EssenceRichtext', 'Alchemy::EssencePicture']) },
37
+ class_name: 'Alchemy::Content',
38
+ source: :contents
39
+
40
+ end
@@ -0,0 +1,26 @@
1
+ require 'alchemy_cms'
2
+ require 'pg_search'
3
+
4
+ module Alchemy
5
+ module PgSearch
6
+ class Engine < ::Rails::Engine
7
+ engine_name 'alchemy_pg_search'
8
+
9
+ config.to_prepare do
10
+ require_relative "./content_extension"
11
+ require_relative './controller_methods'
12
+ require_relative "./element_extension"
13
+ require_relative "./page_extension"
14
+ require_relative './searchable'
15
+
16
+ # We need to have the search methods present in all Alchemy controllers
17
+ Alchemy::BaseController.send(:include, Alchemy::PgSearch::ControllerMethods)
18
+
19
+ # Inject searchable attribute persistence into searchable essence classes
20
+ Alchemy::PgSearch.searchable_essence_classes.each do |klass|
21
+ klass.send(:include, Alchemy::PgSearch::Searchable)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,45 @@
1
+ Alchemy::Page.class_eval do
2
+ include PgSearch
3
+
4
+ # Enable Postgresql full text indexing.
5
+ #
6
+ pg_search_scope :full_text_search, against: {
7
+ meta_description: 'B',
8
+ meta_keywords: 'B',
9
+ title: 'B',
10
+ name: 'A'
11
+ },
12
+ associated_against: {
13
+ searchable_essence_texts: :body,
14
+ searchable_essence_richtexts: :stripped_body,
15
+ searchable_essence_pictures: :caption
16
+ },
17
+ using: {
18
+ tsearch: {prefix: true}
19
+ }
20
+
21
+ has_many :searchable_essence_texts,
22
+ -> { where(searchable: true, alchemy_elements: {public: true}) },
23
+ class_name: 'Alchemy::EssenceText',
24
+ source_type: 'Alchemy::EssenceText',
25
+ through: :descendent_contents,
26
+ source: :essence
27
+
28
+ has_many :searchable_essence_richtexts,
29
+ -> { where(searchable: true, alchemy_elements: {public: true}) },
30
+ class_name: 'Alchemy::EssenceRichtext',
31
+ source_type: 'Alchemy::EssenceRichtext',
32
+ through: :descendent_contents,
33
+ source: :essence
34
+
35
+ has_many :searchable_essence_pictures,
36
+ -> { where(searchable: true, alchemy_elements: {public: true}) },
37
+ class_name: 'Alchemy::EssencePicture',
38
+ source_type: 'Alchemy::EssencePicture',
39
+ through: :descendent_contents,
40
+ source: :essence
41
+
42
+ def element_search_results(query)
43
+ descendent_elements.full_text_search(query)
44
+ end
45
+ end
@@ -0,0 +1,9 @@
1
+ module Alchemy
2
+ module PgSearch
3
+ class PageSearchScope
4
+ def pages
5
+ Alchemy::Page.published.contentpages.with_language(Alchemy::Language.current.id)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,27 @@
1
+ module Alchemy
2
+ module PgSearch
3
+
4
+ # Ensures that the current content definition value for +searchable+ gets persisted.
5
+ #
6
+ # It is enabled per default, but you can disable indexing in your +elements.yml+ file.
7
+ #
8
+ # === Example
9
+ #
10
+ # name: secrets
11
+ # contents:
12
+ # - name: confidential
13
+ # type: EssenceRichtext
14
+ # searchable: false
15
+ #
16
+ module Searchable
17
+ extend ActiveSupport::Concern
18
+
19
+ included do
20
+ before_update do
21
+ write_attribute(:searchable, definition.fetch('searchable', true))
22
+ true
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ module Alchemy
2
+ module PgSearch
3
+ VERSION = "1.2.0"
4
+ end
5
+ end
@@ -0,0 +1,31 @@
1
+ require 'yaml'
2
+
3
+ class UpgradeFromAlchemyFerret < ActiveRecord::Migration
4
+ def up
5
+ not_searchable_elements.each do |element|
6
+ element['contents'].each do |content|
7
+ next unless Alchemy::PgSearch.is_searchable_essence?(content)
8
+ next if content['searchable'].nil?
9
+ "Alchemy::#{content['type'].classify}".constantize
10
+ .joins(content: :element)
11
+ .where(alchemy_contents: {name: content['name']})
12
+ .where(alchemy_elements: {name: element['name']})
13
+ .update_all(searchable: false)
14
+ say "Sets searchable to false for `#{content['name']}` contents of `#{element['name']}` elements."
15
+ end
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def elements_yml
22
+ @elements_yml ||= YAML.load_file Rails.root.join('config/alchemy/elements.yml')
23
+ end
24
+
25
+ def not_searchable_elements
26
+ elements_yml.select do |element|
27
+ next if !element['contents']
28
+ element['contents'].any? { |content| content['searchable'] == false }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ require 'rails'
2
+
3
+ module Alchemy
4
+ module PgSearch
5
+ class UpgradeGenerator < ::Rails::Generators::Base
6
+ desc "This generator upgrades your project from alchemy-ferret based projects."
7
+ source_root File.expand_path('templates', File.dirname(__FILE__))
8
+
9
+ def replace_element_config
10
+ gsub_file Rails.root.join('config/alchemy/elements.yml'),
11
+ 'do_not_index: true', 'searchable: false'
12
+ end
13
+
14
+ def copy_migration_file
15
+ timestamp = Time.now.strftime('%Y%m%d%H%M%S')
16
+ copy_file "migration.rb.tt", Rails.root.join("db/migrate/#{timestamp}_upgrade_from_alchemy_ferret.alchemy_pg_search.rb")
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ require 'rails'
2
+
3
+ module Alchemy
4
+ module PgSearch
5
+ class ViewsGenerator < ::Rails::Generators::Base
6
+ desc "This generator copies the search form and result views into your project."
7
+
8
+ source_root File.expand_path("../../../../../app/views/alchemy", File.dirname(__FILE__))
9
+
10
+ def copy_views
11
+ directory 'search', Rails.root.join('app/views/alchemy/search')
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :alchemy_pg_search do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,166 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: alchemy-pg_search
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Thomas von Deyen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-11-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: alchemy_cms
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.2'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '4.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">"
28
+ - !ruby/object:Gem::Version
29
+ version: '3.2'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '4.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: pg_search
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.7'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.7'
47
+ - !ruby/object:Gem::Dependency
48
+ name: pg
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: bundler
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.3'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.3'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rake
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: rspec-rails
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ description: PostgreSQL search for Alchemy CMS 3.0
104
+ email:
105
+ - alchemy@magiclabs.de
106
+ executables: []
107
+ extensions: []
108
+ extra_rdoc_files: []
109
+ files:
110
+ - ".gitignore"
111
+ - ".travis.yml"
112
+ - CHANGELOG.md
113
+ - Gemfile
114
+ - LICENSE
115
+ - README.md
116
+ - Rakefile
117
+ - alchemy-pg_search.gemspec
118
+ - app/helpers/alchemy/pg_search/search_helper.rb
119
+ - app/views/alchemy/search/_form.html.erb
120
+ - app/views/alchemy/search/_result.html.erb
121
+ - app/views/alchemy/search/_results.html.erb
122
+ - bin/rails
123
+ - config/locales/alchemy.search.de.yml
124
+ - config/locales/alchemy.search.en.yml
125
+ - db/migrate/20141211105526_add_searchable_to_alchemy_essence_texts.rb
126
+ - db/migrate/20141211105942_add_searchable_to_alchemy_essence_richtexts.rb
127
+ - db/migrate/20141211110126_add_searchable_to_alchemy_essence_pictures.rb
128
+ - lib/alchemy-pg_search.rb
129
+ - lib/alchemy/pg_search/config.rb
130
+ - lib/alchemy/pg_search/content_extension.rb
131
+ - lib/alchemy/pg_search/controller_methods.rb
132
+ - lib/alchemy/pg_search/element_extension.rb
133
+ - lib/alchemy/pg_search/engine.rb
134
+ - lib/alchemy/pg_search/page_extension.rb
135
+ - lib/alchemy/pg_search/page_search_scope.rb
136
+ - lib/alchemy/pg_search/searchable.rb
137
+ - lib/alchemy/pg_search/version.rb
138
+ - lib/generators/alchemy/pg_search/upgrade/templates/migration.rb.tt
139
+ - lib/generators/alchemy/pg_search/upgrade/upgrade_generator.rb
140
+ - lib/generators/alchemy/pg_search/views/views_generator.rb
141
+ - lib/tasks/alchemy/pg_search_tasks.rake
142
+ homepage: http://alchemy-cms.com
143
+ licenses:
144
+ - BSD
145
+ metadata: {}
146
+ post_install_message:
147
+ rdoc_options: []
148
+ require_paths:
149
+ - lib
150
+ required_ruby_version: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: '0'
155
+ required_rubygems_version: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ requirements: []
161
+ rubyforge_project:
162
+ rubygems_version: 2.7.6
163
+ signing_key:
164
+ specification_version: 4
165
+ summary: This gem provides PostgreSQL full text search to Alchemy 3.0
166
+ test_files: []