plok 1.0.3 → 1.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d547235d31f52b56077f2329d5e654fb84bab1bcde461161d922f89b745ebd9
4
- data.tar.gz: 25e975e1bbe1e10d5c574e0ca0a86f8ccf22b7f1085be5bc42ad35b80b55a120
3
+ metadata.gz: d4281765768b325decfff165db5df09cf406e2f6d9e211f6ecdfedbfa0dfd0a9
4
+ data.tar.gz: b654970a986c50e67112464b33649650f9fb0e43f858d8aade49330ae1cbad60
5
5
  SHA512:
6
- metadata.gz: '045079c93e3c9f510fc29831a908c3de4c29a2a803e62fa3fd6288f3ed030efc6f91de118c36bc9b7be29504a8edbb3c0f67d4cd95666c3c76d7848b720815cc'
7
- data.tar.gz: 1c1ff0f3a2fa04de54c55c6a409bfb1da55cebea2ce56dd3ad9b1206ac01cef359da4220d71f897e749904353cebf62836a65da6ea20747ce2ed7cef8f30bcea
6
+ metadata.gz: 866911090a04393d7d92c618daac7dd6bb02330e939d23a654375b014c363d44c574f2948cf3cd6f7bf536c44c2ccac24086774685436054a8557cb400b05ba2
7
+ data.tar.gz: 89fa51203f3af3018585810f1631ef15a5f71f53dd9a0ac27a8141c929b22a57f8890d53581dcf53e1aec26af9a7f7dca7c5c730fab7bb71dc2c1a7e3c939113
@@ -19,7 +19,7 @@ var search = search || {
19
19
  },
20
20
 
21
21
  target: function() {
22
- return $('#plok-search input');
22
+ return $('[data-plok-searchable] input');
23
23
  }
24
24
  };
25
25
 
@@ -1,8 +1,9 @@
1
+ # TODO: Find a way to spec this properly.
1
2
  module Plok::Searchable
2
3
  extend ActiveSupport::Concern
3
4
 
4
5
  included do
5
- has_many :search_indices, as: :searchable, dependent: :destroy
6
+ has_many :search_indices, as: :searchable
6
7
 
7
8
  # The after_save block creates or saves indices for every indicated
8
9
  # searchable field. Takes both translations and flexible content into
@@ -15,16 +16,18 @@ module Plok::Searchable
15
16
  end
16
17
 
17
18
  def trigger_indices_save!
18
- self.class.searchable_fields_list.each do |key|
19
- if key == :flexible_content
20
- save_flexible_content_search_indices! && next
21
- end
19
+ self.class.searchable_fields_list.each do |namespace, keys|
20
+ keys.each do |key|
21
+ if key == :flexible_content
22
+ save_flexible_content_search_indices!(namespace) && next
23
+ end
22
24
 
23
- if respond_to?(:translatable?) && self.class.translatable_fields_list.include?(key)
24
- save_translatable_search_index!(key) && next
25
- end
25
+ if respond_to?(:translatable?) && self.class.translatable_fields_list.include?(key)
26
+ save_translatable_search_index!(namespace, key) && next
27
+ end
26
28
 
27
- save_search_index!(key)
29
+ save_search_index!(namespace, key)
30
+ end
28
31
  end
29
32
  end
30
33
 
@@ -38,38 +41,37 @@ module Plok::Searchable
38
41
  # to help facilitate searchable management. Note that said code was
39
42
  # initially present in this class, and it was such a mess that it became
40
43
  # unpractical to maintain.
41
- def save_flexible_content_search_indices!
44
+ def save_flexible_content_search_indices!(namespace)
42
45
  content_rows.each do |row|
43
46
  row.columns.each do |column|
44
47
  next unless column.content.is_a?(ContentText)
45
48
  key = "flexible_content:#{column.content_id}"
46
- save_index!(key, value: column.content.content, locale: row.locale)
49
+ save_search_index!(namespace, key, value: column.content.content, locale: row.locale)
47
50
  end
48
51
  end
49
52
  end
50
53
 
51
- def save_index!(key, value: nil, locale: nil)
52
- value = read_attribute(key) if value.blank? && respond_to?(key)
54
+ def save_search_index!(namespace, key, value: nil, locale: nil)
55
+ value = if value.present?
56
+ value
57
+ elsif ActiveRecord::Base.connection.column_exists?(self.class.table_name, key)
58
+ read_attribute(key)
59
+ elsif respond_to?(key)
60
+ send(key)
61
+ end
62
+
53
63
  return if value.blank?
54
64
 
55
65
  search_indices
56
- .find_or_create_by!(name: key, locale: locale)
66
+ .find_or_create_by!(namespace: namespace, name: key, locale: locale)
57
67
  .update_column(:value, value)
58
68
  end
59
69
 
60
- # This exists so we can use #save_search_index! as the main method
61
- # to override, and then be able to call #save_index! in the overridden
62
- # method to accommodate further defaults.
63
- def save_search_index!(key)
64
- value = read_attribute(key)
65
- save_index!(key, value: value)
66
- end
67
-
68
- def save_translatable_search_index!(key)
70
+ def save_translatable_search_index!(namespace, key)
69
71
  # TODO: locales can't be hardcoded
70
72
  %w(nl fr).each do |locale|
71
73
  value = translation(locale.to_sym).send(key)
72
- save_index!(key, value: value, locale: locale)
74
+ save_search_index!(namespace, key, value: value, locale: locale)
73
75
  end
74
76
  end
75
77
 
@@ -79,18 +81,22 @@ module Plok::Searchable
79
81
  end
80
82
 
81
83
  module ClassMethods
82
- def searchable_field(key)
83
- unless searchable_fields_list.include?(key.to_sym)
84
- searchable_fields_list << key.to_sym
84
+ def searchable_field(namespace, key)
85
+ return if searchable_fields_list.dig(namespace.to_sym)&.include?(key.to_sym)
86
+
87
+ if searchable_fields_list[namespace.to_sym].blank?
88
+ searchable_fields_list[namespace.to_sym] = []
85
89
  end
90
+
91
+ searchable_fields_list[namespace.to_sym] << key.to_sym
86
92
  end
87
93
 
88
- def searchable_fields(*args)
89
- args.each { |key| searchable_field(key) }
94
+ def plok_searchable(namespace: nil, fields: [], conditions: [])
95
+ fields.each { |key| searchable_field(namespace, key) }
90
96
  end
91
97
 
92
98
  def searchable_fields_list
93
- @searchable_fields_list ||= []
99
+ @searchable_fields_list ||= {}
94
100
  end
95
101
  end
96
102
  end
@@ -1,11 +1,12 @@
1
1
  class SearchModule < ActiveRecord::Base
2
- scope :weighted, -> { order('weight DESC') }
3
2
 
4
- validates :name, presence: true
3
+ scope :searchable, -> { where('search_modules.searchable': true) }
4
+ scope :weighted, -> { order('search_modules.weight DESC') }
5
5
 
6
- def indices
7
- SearchIndex
8
- .joins('INNER JOIN search_modules ON search_indices.searchable_type = search_modules.name')
9
- .where('search_modules.name = ?', name)
6
+ validates :klass, presence: true
7
+
8
+ def search_indices
9
+ SearchIndex.where(searchable_type: klass)
10
10
  end
11
+
11
12
  end
@@ -0,0 +1,9 @@
1
+ class RenameNameToKlassForSearchModules < ActiveRecord::Migration[6.1]
2
+ def up
3
+ rename_column :search_modules, :name, :klass
4
+ end
5
+
6
+ def down
7
+ rename_column :search_modules, :klass, :name
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ class AddNamespaceToSearchIndices < ActiveRecord::Migration[6.1]
2
+ def change
3
+ add_column :search_indices, :namespace, :string, after: :searchable_id, index: true
4
+ end
5
+ end
@@ -0,0 +1,10 @@
1
+ Description:
2
+ This generates a Plok::Search class for a given namespace, which you can
3
+ use to search for search_indices tagged with the same namespace.
4
+
5
+ Example:
6
+ bin/rails g plok/search/class --namespace backend
7
+
8
+ This will create:
9
+ lib/plok/search/backend.rb
10
+ spec/lib/plok/search/backend_spec.rb
@@ -0,0 +1,36 @@
1
+ require 'rails/generators/base'
2
+
3
+ class Plok::Search::ClassGenerator < Rails::Generators::Base
4
+ source_root File.expand_path('templates', __dir__)
5
+ class_option :namespace, type: :string
6
+
7
+ def create
8
+ copy_file "namespace_class.rb", namespace_class_path
9
+ copy_file "namespace_class_spec.rb", namespace_class_spec_path
10
+ gsub_file(namespace_class_path, '[namespace]', namespace_class)
11
+ gsub_file(namespace_class_spec_path, '[namespace]', namespace_class)
12
+ gsub_file(namespace_class_spec_path, '[snakecased_namespace]', options.namespace)
13
+ end
14
+
15
+ private
16
+
17
+ def app_name
18
+ Rails.application.class.name.split('::').first
19
+ end
20
+
21
+ def namespace_class_path
22
+ "lib/plok/search/#{options.namespace}.rb"
23
+ end
24
+
25
+ def namespace_class_spec_path
26
+ "spec/lib/plok/search/#{options.namespace}_spec.rb"
27
+ end
28
+
29
+ def file_contains?(file, content)
30
+ File.readlines(file).grep(content).any?
31
+ end
32
+
33
+ def namespace_class
34
+ options.namespace.to_s.camelcase
35
+ end
36
+ end
@@ -0,0 +1,25 @@
1
+ module Plok::Search
2
+ # The reason this class exists is mainly to provide a context layer to
3
+ # override Plok::Search::Base. It provides a manipulated version of the
4
+ # filtered index data that we can use in the result set of an
5
+ # autocomplete-triggered search query.
6
+ class [namespace] < Plok::Search::Base
7
+ # This translates the filtered indices into meaningful result objects.
8
+ #
9
+ # #search_indices will return an ActiveRecord::Relation of SearchIndex
10
+ # records. Because #lazy is chained after it, we still have a way to fiddle
11
+ # with the AR query when necessary.
12
+ #
13
+ # #format_search_results will return a list of jquery-ui friendly hashes:
14
+ # [
15
+ # { label: ... value: ... },
16
+ # { label: ... value: ... }
17
+ # ]
18
+ #
19
+ def search(modules: [])
20
+ format_search_results(
21
+ search_indices(modules: modules).lazy
22
+ )
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ require 'rails_helper'
2
+
3
+ describe Plok::Search::[namespace] do
4
+ subject { described_class.new('John', namespace: '[snakecased_namespace]') }
5
+
6
+ describe '#search' do
7
+ it 'defaults to an empty list' do
8
+ # Have to cast to an array because search lazy loads results.
9
+ expect(subject.search.to_a).to eq []
10
+ end
11
+
12
+ it 'returns relevant indices' do
13
+ # TODO:
14
+ end
15
+ end
16
+
17
+ it '#respond_to?' do
18
+ expect(subject).to respond_to(:format_search_results, :search_indices)
19
+ end
20
+ end
@@ -4,7 +4,7 @@ module Plok::Search
4
4
  # result set (think autocomplete results) is done by extending this class.
5
5
  #
6
6
  # Examples of class extensions could be:
7
- # Plok::Search::Backend - included in Plok
7
+ # Plok::Search::Backend
8
8
  # Plok::Search::Frontend
9
9
  # Plok::Search::Api
10
10
  #
@@ -22,44 +22,36 @@ module Plok::Search
22
22
  #
23
23
  # However these result objects are structured are also up to the developer.
24
24
  class Base
25
- attr_reader :term, :controller
26
25
 
27
- def initialize(term, controller: nil, namespace: nil)
26
+ attr_reader :term, :namespace, :controller
27
+
28
+ def initialize(term, namespace: nil, controller: nil)
28
29
  @term = Plok::Search::Term.new(term, controller: controller)
29
- @controller = controller
30
30
  @namespace = namespace
31
+ @controller = controller
31
32
  end
32
33
 
33
- def indices
34
- # Having the searchmodules sorted by weight returns indices in the
35
- # correct order.
36
- @indices ||= SearchModule.weighted.inject([]) do |stack, m|
37
- # The group happens to make sure we end up with just 1 copy of
38
- # a searchable result. Otherwise matches from both an indexed
39
- # Page#title and Page#description would be in the result set.
40
- stack << m.indices
41
- .where(
42
- '(searchable_id = ? OR search_indices.value LIKE ?)',
43
- term.value,
44
- "%#{term.value}%"
45
- )
46
- .group([:searchable_type, :searchable_id])
47
- end.flatten
48
- end
34
+ def format_search_results(indices, options = {})
35
+ options.reverse_merge!(
36
+ label_method: :build_html,
37
+ value_method: :url
38
+ )
39
+
40
+ indices.map do |index|
41
+ result = result_object(index)
49
42
 
50
- def namespace
51
- # This looks daft, but it gives us a foot in the door for when a frontend
52
- # search is triggered in the backend.
53
- return @namespace unless @namespace.nil?
54
- return 'Frontend' if controller.nil?
55
- controller.class.module_parent.to_s
43
+ {
44
+ label: result.send(options[:label_method]),
45
+ value: result.send(options[:value_method])
46
+ }
47
+ end
56
48
  end
57
49
 
58
50
  # In order to provide a good result set in a search autocomplete, we have
59
51
  # to translate the raw index to a class that makes an index adhere
60
52
  # to a certain interface (that can include links).
61
53
  def result_object(index)
62
- klass = "Plok::Search::ResultObjects::#{namespace}::#{index.searchable_type}"
54
+ klass = "Plok::Search::ResultObjects::#{@namespace.camelcase}::#{index.searchable_type}"
63
55
  klass = 'Plok::Search::ResultObjects::Base' unless result_object_exists?(klass)
64
56
  klass.constantize.new(index, search_context: self)
65
57
  end
@@ -67,5 +59,28 @@ module Plok::Search
67
59
  def result_object_exists?(name)
68
60
  Plok::Engine.class_exists?(name) && name.constantize.method_defined?(:build_html)
69
61
  end
62
+
63
+ # TODO: SearchIndexCollection
64
+ # TODO: What if records are hidden? Make this smart and have SearchIndex#visible?
65
+ # TODO: See if there's a way to pass weight through individual records.
66
+ def search_indices(modules: [])
67
+ modules = SearchModule.searchable.pluck(:klass) if modules.blank?
68
+
69
+ # Having the searchmodules sorted by weight returns indices in the
70
+ # correct order.
71
+ #
72
+ # The group happens to make sure we end up with just 1 copy of
73
+ # a searchable result. Otherwise matches from both an indexed
74
+ # Page#title and Page#description would be in the result set.
75
+ @search_indices ||= SearchIndex
76
+ .joins('INNER JOIN search_modules ON search_indices.searchable_type = search_modules.klass')
77
+ .where('search_modules.searchable': true)
78
+ .where('search_modules.klass in (?)', modules)
79
+ .where('search_indices.namespace = ?', @namespace)
80
+ .where('search_indices.value LIKE ?', "%#{term.value}%")
81
+ .group([:searchable_type, :searchable_id])
82
+ .preload(:searchable) # ".includes" for polymorphic relations
83
+ end
84
+
70
85
  end
71
86
  end
data/lib/plok/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Plok
2
- VERSION = '1.0.3'
2
+ VERSION = '1.1.0'
3
3
  end
@@ -1,11 +1,46 @@
1
1
  namespace 'plok:search' do
2
- desc 'Rebuild all search indices'
2
+ # The official Rails way to pass arguments to Rake tasks is this:
3
+ #
4
+ # task :rebuild_indices, [:modules] => :environment do |_t, args|
5
+ # args.with_defaults(modules: SearchModule.searchable.pluck(:klass))
6
+ # end
7
+ #
8
+ # The problem with that is that you can't comma-separate multiple modules
9
+ # unless you escape them. This is because commas denote multiple arguments.
10
+ #
11
+ # In short, this wouldn't work:
12
+ #
13
+ # bin/rails plok:search:rebuild_indices[EmailTemplate,Article]
14
+ #
15
+ # And you'd need to do this:
16
+ #
17
+ # bin/rails plok:search:rebuild_indices["EmailTemplate\,Article"]
18
+ #
19
+ # I don't agree with that notation, so instead I opted for ENV vars instead:
20
+ #
21
+ # bin/rails plok:search:rebuild_indices modules=EmailTemplate,Article
22
+ #
23
+ desc 'Rebuild select search indices (or all of them without additional arguments).'
3
24
  task rebuild_indices: :environment do
4
- SearchIndex.destroy_all
5
- SearchModule.where(searchable: true).each do |m|
6
- puts "Rebuilding #{m.name} indices..."
7
- m.name.constantize.all.each(&:trigger_indices_save!)
25
+ # Default to all searchable modules
26
+ modules = ENV['modules']&.split(',') || SearchModule.searchable.pluck(:klass)
27
+
28
+ # A safeguard to prevent mishaps
29
+ modules.select! do |m|
30
+ SearchModule.exists?(klass: m) && # Only known modules
31
+ Plok::Engine.class_exists?(m.constantize) # Only existing classes
8
32
  end
33
+
34
+ # Faster than SearchIndex.where(searchable_type: modules).destroy_all
35
+ ActiveRecord::Base
36
+ .connection
37
+ .execute("DELETE FROM search_indices WHERE searchable_type in ('#{modules.join("','")}')")
38
+
39
+ SearchModule.where(klass: modules).each do |m|
40
+ puts "Rebuilding #{m.klass} indices..."
41
+ m.klass.constantize.all.each(&:trigger_indices_save!)
42
+ end
43
+
9
44
  puts "Done."
10
45
  end
11
46
  end
@@ -1,5 +1,5 @@
1
1
  FactoryBot.define do
2
2
  factory :search_module do
3
- name { 'Page' }
3
+ klass { 'Page' }
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plok
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Davy Hellemans
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-10-28 00:00:00.000000000 Z
12
+ date: 2022-11-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -104,6 +104,12 @@ files:
104
104
  - db/migrate/20220512142356_add_index_to_queued_task_weight.rb
105
105
  - db/migrate/20220923162300_create_search_indices.rb
106
106
  - db/migrate/20220923164100_create_search_modules.rb
107
+ - db/migrate/20221021133242_rename_name_to_klass_for_search_modules.rb
108
+ - db/migrate/20221031143932_add_namespace_to_search_indices.rb
109
+ - lib/generators/plok/search/USAGE
110
+ - lib/generators/plok/search/class_generator.rb
111
+ - lib/generators/plok/search/templates/namespace_class.rb
112
+ - lib/generators/plok/search/templates/namespace_class_spec.rb
107
113
  - lib/generators/plok/sidebar/USAGE
108
114
  - lib/generators/plok/sidebar/sidebar_generator.rb
109
115
  - lib/generators/plok/sidebar/templates/_menu_item.html.erb
@@ -112,7 +118,6 @@ files:
112
118
  - lib/generators/plok/sidebar/templates/_wrapper.html.erb
113
119
  - lib/plok.rb
114
120
  - lib/plok/engine.rb
115
- - lib/plok/search/backend.rb
116
121
  - lib/plok/search/base.rb
117
122
  - lib/plok/search/result_objects/base.rb
118
123
  - lib/plok/search/term.rb
@@ -1,22 +0,0 @@
1
- # Due to the load order of classes, Backend precedes the required Base class.
2
- require_relative 'base'
3
-
4
- module Plok::Search
5
- # The goal of this class is to provide a manipulated version of the filtered
6
- # index data that we can use in the result set of an autocomplete-triggered
7
- # search query. See Plok::Search::Base for more information on how this
8
- # search functionality is designed.
9
- class Backend < Plok::Search::Base
10
- # This translates the filtered indices into meaningful result objects.
11
- # These require a { label: ... value: ... } to accommodate jquery-ui.
12
- #
13
- # Note that the result_object#url method is defined in
14
- # Plok::Search::ResultObjects::Backend::Page.
15
- def search
16
- indices.map do |index|
17
- result = result_object(index)
18
- { label: result.build_html, value: result.url }
19
- end
20
- end
21
- end
22
- end