alchemy-pg_search 1.2.0

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.
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: []