plok 1.0.0 → 1.0.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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/jquery-ui.autocomplete.html.js +40 -0
- data/app/assets/javascripts/plok/searchable.js +26 -0
- data/app/assets/stylesheets/plok/_autocomplete.scss +43 -0
- data/app/models/concerns/plok/searchable.rb +96 -0
- data/app/models/queued_task.rb +7 -0
- data/app/models/search_index.rb +5 -0
- data/app/models/search_module.rb +11 -0
- data/config/initializers/module.rb +1 -7
- data/db/migrate/20220923162300_create_search_indices.rb +19 -0
- data/db/migrate/20220923164100_create_search_modules.rb +16 -0
- data/lib/generators/plok/sidebar/templates/_menu_item.html.erb +3 -3
- data/lib/plok/engine.rb +2 -2
- data/lib/plok/search/backend.rb +22 -0
- data/lib/plok/search/base.rb +71 -0
- data/lib/plok/search/result_objects/base.rb +74 -0
- data/lib/plok/search/term.rb +23 -0
- data/lib/plok/version.rb +1 -1
- data/lib/tasks/plok_tasks.rake +11 -4
- data/spec/factories/search_indices.rb +6 -0
- data/spec/factories/search_modules.rb +5 -0
- data/spec/support/fake_ar_model.rb +95 -0
- data/spec/support/plok/searchable.rb +100 -0
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: caeb408fa4077fd24c19a153f273eb83f21d1da02a601035838e054080b80f85
|
4
|
+
data.tar.gz: e9ebfd53e89fdb4adddfb9498e2c7411ce994caf53039e0136459e90dd4a838e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 728e58a7153d8bb7bc4e19fde5b676ebec49d671b93d724b0c895d261e07e77bd7c6c347bf97c0dbf6dbc80e9c510e0c3f0bdf161713cf38b407a758f3da6ad0
|
7
|
+
data.tar.gz: d14e3d051253c34406c8736d4785b2ea7a1fba11f9e1dc46302b4ace6965d56f599b847ea604838aef90822595b7a526bcf18958f1e86a395e6a39a0c186cb18
|
@@ -0,0 +1,40 @@
|
|
1
|
+
/*
|
2
|
+
* jQuery UI Autocomplete HTML Extension
|
3
|
+
*
|
4
|
+
* Copyright 2010, Scott González (http://scottgonzalez.com)
|
5
|
+
* Dual licensed under the MIT or GPL Version 2 licenses.
|
6
|
+
*
|
7
|
+
* http://github.com/scottgonzalez/jquery-ui-extensions
|
8
|
+
*/
|
9
|
+
(function( $ ) {
|
10
|
+
|
11
|
+
var proto = $.ui.autocomplete.prototype,
|
12
|
+
initSource = proto._initSource;
|
13
|
+
|
14
|
+
function filter( array, term ) {
|
15
|
+
var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
|
16
|
+
return $.grep( array, function(value) {
|
17
|
+
return matcher.test( $( "<div>" ).html( value.label || value.value || value ).text() );
|
18
|
+
});
|
19
|
+
}
|
20
|
+
|
21
|
+
$.extend( proto, {
|
22
|
+
_initSource: function() {
|
23
|
+
if ( this.options.html && $.isArray(this.options.source) ) {
|
24
|
+
this.source = function( request, response ) {
|
25
|
+
response( filter( this.options.source, request.term ) );
|
26
|
+
};
|
27
|
+
} else {
|
28
|
+
initSource.call( this );
|
29
|
+
}
|
30
|
+
},
|
31
|
+
|
32
|
+
_renderItem: function( ul, item) {
|
33
|
+
return $( "<li></li>" )
|
34
|
+
.data( "item.autocomplete", item )
|
35
|
+
.append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
|
36
|
+
.appendTo( ul );
|
37
|
+
}
|
38
|
+
});
|
39
|
+
|
40
|
+
})( jQuery );
|
@@ -0,0 +1,26 @@
|
|
1
|
+
var search = search || {
|
2
|
+
init: function() {
|
3
|
+
this.target().autocomplete({
|
4
|
+
minLength: 2,
|
5
|
+
source: search.target().parents('form').attr('action'),
|
6
|
+
select: search.select,
|
7
|
+
html: true
|
8
|
+
}).on('keypress', this.keypress_listener);
|
9
|
+
},
|
10
|
+
|
11
|
+
keypress_listener: function(e) {
|
12
|
+
var code = (e.keyCode ? e.keyCode : e.which);
|
13
|
+
if(code == 13) return false;
|
14
|
+
},
|
15
|
+
|
16
|
+
select: function(event, ui) {
|
17
|
+
window.location = ui.item.value;
|
18
|
+
return false;
|
19
|
+
},
|
20
|
+
|
21
|
+
target: function() {
|
22
|
+
return $('#search input');
|
23
|
+
}
|
24
|
+
};
|
25
|
+
|
26
|
+
$(function(){ search.init(); });
|
@@ -0,0 +1,43 @@
|
|
1
|
+
.ui-autocomplete {
|
2
|
+
width: 24rem !important;
|
3
|
+
padding: 0;
|
4
|
+
z-index: 9999;
|
5
|
+
filter: drop-shadow(5px 5px 5px #666);
|
6
|
+
}
|
7
|
+
|
8
|
+
// Shows up during autocompletion sometimes.
|
9
|
+
.ui-helper-hidden-accessible {
|
10
|
+
display: none;
|
11
|
+
}
|
12
|
+
|
13
|
+
li.ui-menu-item {
|
14
|
+
list-style: none;
|
15
|
+
cursor: pointer;
|
16
|
+
border-bottom: 1px solid #ccc;
|
17
|
+
padding: 5px;
|
18
|
+
|
19
|
+
&:last-child {
|
20
|
+
border: none;
|
21
|
+
margin-bottom: 0;
|
22
|
+
padding-bottom: 0;
|
23
|
+
}
|
24
|
+
|
25
|
+
&:hover {
|
26
|
+
background-color: $primary;
|
27
|
+
color: white !important;
|
28
|
+
}
|
29
|
+
|
30
|
+
a.ui-menu-item-wrapper {
|
31
|
+
border: none;
|
32
|
+
|
33
|
+
&.ui-state-active {
|
34
|
+
background: none;
|
35
|
+
color: inherit;
|
36
|
+
text-decoration: none;
|
37
|
+
|
38
|
+
.text-muted {
|
39
|
+
color: white !important;
|
40
|
+
}
|
41
|
+
}
|
42
|
+
}
|
43
|
+
}
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Plok::Searchable
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
has_many :search_indices, as: :searchable, dependent: :destroy
|
6
|
+
|
7
|
+
# The after_save block creates or saves indices for every indicated
|
8
|
+
# searchable field. Takes both translations and flexible content into
|
9
|
+
# account.
|
10
|
+
#
|
11
|
+
# Translation support was relatively painless, but FlexibleContent
|
12
|
+
# required more thought. See #save_flexible_content_indices!
|
13
|
+
after_save do
|
14
|
+
trigger_indices_save!
|
15
|
+
end
|
16
|
+
|
17
|
+
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
|
22
|
+
|
23
|
+
if respond_to?(:translatable?) && self.class.translatable_fields_list.include?(key)
|
24
|
+
save_translatable_search_index!(key) && next
|
25
|
+
end
|
26
|
+
|
27
|
+
save_search_index!(key)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# I save all ContentText#content values as indices linked to the parent
|
32
|
+
# object. The Plok::Search::Base#indices method already groups by
|
33
|
+
# searchable resource, so there are never any duplicates.
|
34
|
+
# Only additional matches for the searchable because it takes
|
35
|
+
# ContentText#content sources into account.
|
36
|
+
#
|
37
|
+
# ContentColumn and ContentText have after_{save,destroy} callbacks
|
38
|
+
# to help facilitate searchable management. Note that said code was
|
39
|
+
# initially present in this class, and it was such a mess that it became
|
40
|
+
# unpractical to maintain.
|
41
|
+
def save_flexible_content_search_indices!
|
42
|
+
content_rows.each do |row|
|
43
|
+
row.columns.each do |column|
|
44
|
+
next unless column.content.is_a?(ContentText)
|
45
|
+
key = "flexible_content:#{column.content_id}"
|
46
|
+
save_index!(key, value: column.content.content, locale: row.locale)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def save_index!(key, value: nil, locale: nil)
|
52
|
+
value = read_attribute(key) if value.blank? && respond_to?(key)
|
53
|
+
return if value.blank?
|
54
|
+
|
55
|
+
search_indices
|
56
|
+
.find_or_create_by!(name: key, locale: locale)
|
57
|
+
.update_column(value: value)
|
58
|
+
end
|
59
|
+
|
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)
|
69
|
+
# TODO: locales can't be hardcoded
|
70
|
+
%w(nl fr).each do |locale|
|
71
|
+
value = translation(locale.to_sym).send(key)
|
72
|
+
save_index!(key, value: value, locale: locale)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def searchable?
|
77
|
+
true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
module ClassMethods
|
82
|
+
def searchable_field(key)
|
83
|
+
unless searchable_fields_list.include?(key.to_sym)
|
84
|
+
searchable_fields_list << key.to_sym
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def searchable_fields(*args)
|
89
|
+
args.each { |key| searchable_field(key) }
|
90
|
+
end
|
91
|
+
|
92
|
+
def searchable_fields_list
|
93
|
+
@searchable_fields_list ||= []
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/app/models/queued_task.rb
CHANGED
@@ -55,6 +55,13 @@ class QueuedTask < ActiveRecord::Base
|
|
55
55
|
destroy
|
56
56
|
end
|
57
57
|
|
58
|
+
# You want this separate from #unlock so the ensure block in #process! does
|
59
|
+
# not keep resetting attempts when it shouldn't. This should be called
|
60
|
+
# from the controller where you manually unlock tasks in your backend.
|
61
|
+
def reset_attempts!
|
62
|
+
update_column(:attempts, 0)
|
63
|
+
end
|
64
|
+
|
58
65
|
def increase_attempts!
|
59
66
|
update_column(:attempts, attempts + 1)
|
60
67
|
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class SearchModule < ActiveRecord::Base
|
2
|
+
scope :weighted, -> { order('weight DESC') }
|
3
|
+
|
4
|
+
validates :name, presence: true
|
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)
|
10
|
+
end
|
11
|
+
end
|
@@ -1,10 +1,3 @@
|
|
1
|
-
def class_exists?(class_name)
|
2
|
-
klass = Module.const_get(class_name)
|
3
|
-
return klass.is_a?(Class)
|
4
|
-
rescue NameError
|
5
|
-
return false
|
6
|
-
end
|
7
|
-
|
8
1
|
class Module
|
9
2
|
def takes(*arg_names)
|
10
3
|
define_method(:initialize) do |*arg_values|
|
@@ -15,6 +8,7 @@ class Module
|
|
15
8
|
end
|
16
9
|
|
17
10
|
instance_variable_set(:"@#{name}", value)
|
11
|
+
singleton_class.instance_eval { attr_reader name.to_sym }
|
18
12
|
end
|
19
13
|
end
|
20
14
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# This migration originally comes from udongo_engine
|
2
|
+
class CreateSearchIndices < ActiveRecord::Migration[6.1]
|
3
|
+
def change
|
4
|
+
return if table_exists?(:search_indices)
|
5
|
+
|
6
|
+
create_table :search_indices do |t|
|
7
|
+
t.string :searchable_type
|
8
|
+
t.integer :searchable_id
|
9
|
+
t.string :locale, index: true
|
10
|
+
t.string :name
|
11
|
+
t.text :value
|
12
|
+
|
13
|
+
t.timestamps
|
14
|
+
end
|
15
|
+
|
16
|
+
add_index :search_indices, [:searchable_type, :searchable_id]
|
17
|
+
add_index :search_indices, [:locale, :name]
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# This migration originally comes from udongo_engine
|
2
|
+
class CreateSearchModules < ActiveRecord::Migration[6.1]
|
3
|
+
def change
|
4
|
+
return if table_exists?(:search_modules)
|
5
|
+
|
6
|
+
create_table :search_modules do |t|
|
7
|
+
t.string :name
|
8
|
+
t.boolean :searchable
|
9
|
+
t.integer :weight
|
10
|
+
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
|
14
|
+
add_index :search_modules, [:name, :searchable]
|
15
|
+
end
|
16
|
+
end
|
@@ -2,13 +2,13 @@
|
|
2
2
|
<% href ||= defined?(path) ? path : "#nav-#{name}" %>
|
3
3
|
|
4
4
|
<%= content_tag :li, class: 'top-level' do %>
|
5
|
-
<%= link_to href, class: 'top-level-anchor link-light', data: { bs_toggle: ('
|
5
|
+
<%= link_to href, class: 'top-level-anchor link-light', data: { bs_toggle: (defined?(href) ? 'tooltip' : 'collapse') }, title: lbl do %>
|
6
6
|
<%= fa_icon icon, class: 'fa-fw' %>
|
7
7
|
<%= content_tag :span, lbl, class: 'text' %>
|
8
8
|
<% end %>
|
9
9
|
|
10
|
-
<%
|
11
|
-
<%= content_tag :div, id:
|
10
|
+
<% if defined?(path) %>
|
11
|
+
<%= content_tag :div, id: path, class: 'collapse' do %>
|
12
12
|
<h4><%= lbl %></h4>
|
13
13
|
<%= yield %>
|
14
14
|
<% end %>
|
data/lib/plok/engine.rb
CHANGED
@@ -22,14 +22,14 @@ module Plok
|
|
22
22
|
# Autoload classes from the lib dir
|
23
23
|
config.autoload_paths << File.expand_path('../..', __FILE__)
|
24
24
|
|
25
|
-
def class_exists?(class_name)
|
25
|
+
def self.class_exists?(class_name)
|
26
26
|
klass = Module.const_get(class_name.to_s)
|
27
27
|
klass.is_a?(Class)
|
28
28
|
rescue NameError
|
29
29
|
return false
|
30
30
|
end
|
31
31
|
|
32
|
-
def module_exists?(module_name)
|
32
|
+
def self.module_exists?(module_name)
|
33
33
|
# By casting to a string and making a constant, we can assume module_name
|
34
34
|
# can be either one without it being a problem.
|
35
35
|
module_name.to_s.constantize.is_a?(Module)
|
@@ -0,0 +1,22 @@
|
|
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
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Plok::Search
|
2
|
+
# The goal of the Base class is to filter our indices on the given search
|
3
|
+
# term. Further manipulation of individual index data into a meaningful
|
4
|
+
# result set (think autocomplete results) is done by extending this class.
|
5
|
+
#
|
6
|
+
# Examples of class extensions could be:
|
7
|
+
# Plok::Search::Backend - included in Plok
|
8
|
+
# Plok::Search::Frontend
|
9
|
+
# Plok::Search::Api
|
10
|
+
#
|
11
|
+
# The primary benefit in having these namespaced search interfaces is to
|
12
|
+
# provide a way for the developer to have different result objects for
|
13
|
+
# each resource.
|
14
|
+
#
|
15
|
+
# Example #1: A search request for a specific Page instance in the frontend
|
16
|
+
# will typically return a link to said page. However, in search requests made
|
17
|
+
# in the backend for the same Page instance, you'd expect a link to a form in
|
18
|
+
# the backend Page module where you can edit the page's contents.
|
19
|
+
#
|
20
|
+
# Example #2: Some autocompletes in a frontend namespace might require an
|
21
|
+
# image or a price to be included in its body.
|
22
|
+
#
|
23
|
+
# However these result objects are structured are also up to the developer.
|
24
|
+
class Base
|
25
|
+
attr_reader :term, :controller
|
26
|
+
|
27
|
+
def initialize(term, controller: nil, namespace: nil)
|
28
|
+
@term = Plok::Search::Term.new(term, controller: controller)
|
29
|
+
@controller = controller
|
30
|
+
@namespace = namespace
|
31
|
+
end
|
32
|
+
|
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
|
49
|
+
|
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
|
56
|
+
end
|
57
|
+
|
58
|
+
# In order to provide a good result set in a search autocomplete, we have
|
59
|
+
# to translate the raw index to a class that makes an index adhere
|
60
|
+
# to a certain interface (that can include links).
|
61
|
+
def result_object(index)
|
62
|
+
klass = "Plok::Search::ResultObjects::#{namespace}::#{index.searchable_type}"
|
63
|
+
klass = 'Plok::Search::ResultObjects::Base' unless result_object_exists?(klass)
|
64
|
+
klass.constantize.new(index, search_context: self)
|
65
|
+
end
|
66
|
+
|
67
|
+
def result_object_exists?(name)
|
68
|
+
Plok::Engine.class_exists?(name) && name.constantize.method_defined?(:build_html)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Plok
|
2
|
+
class InterfaceNotImplementedError < NoMethodError
|
3
|
+
end
|
4
|
+
|
5
|
+
class Search::ResultObjects::Base
|
6
|
+
attr_reader :index, :search_context
|
7
|
+
|
8
|
+
delegate :searchable, to: :index
|
9
|
+
|
10
|
+
def initialize(index, search_context: nil)
|
11
|
+
@index = index
|
12
|
+
@search_context = search_context
|
13
|
+
end
|
14
|
+
|
15
|
+
# Typically, an autocomplete requires 3 things:
|
16
|
+
#
|
17
|
+
# * A title indicating a resource name.
|
18
|
+
# Examples: Page#title, Product#name,...
|
19
|
+
# * A truncated summary providing a glimpse of the resource's contents.
|
20
|
+
# Examples: Page#subtitle, Product#description,...
|
21
|
+
# * A link to the resource.
|
22
|
+
# Examples: edit_backend_page_path(37), product_path(37),...
|
23
|
+
#
|
24
|
+
# However, this seems very restrictive to me. If I narrow down the data
|
25
|
+
# a dev can use in an autocomplete, it severely reduces options he/she has
|
26
|
+
# in how the autocomplete results look like. Think of autocompletes in a
|
27
|
+
# shop that require images or prices to be included in their result bodies.
|
28
|
+
#
|
29
|
+
# This is why I chose to let ApplicationController.render work around the
|
30
|
+
# problem by letting the dev decide how the row should look.
|
31
|
+
#
|
32
|
+
def build_html
|
33
|
+
ApplicationController.render(partial: partial, locals: locals)
|
34
|
+
end
|
35
|
+
|
36
|
+
def hidden?
|
37
|
+
searchable.respond_to?(:visible?) && searchable.hidden?
|
38
|
+
end
|
39
|
+
|
40
|
+
def label
|
41
|
+
# We want to grab the name of the index from ContentText whenever
|
42
|
+
# we're dealing with FlexibleContent stuff.
|
43
|
+
if index.name.include?('flexible_content')
|
44
|
+
id = index.name.split(':')[1].to_i
|
45
|
+
return ContentText.find(id).content
|
46
|
+
end
|
47
|
+
|
48
|
+
searchable.send(index.name)
|
49
|
+
end
|
50
|
+
|
51
|
+
def locals
|
52
|
+
{ "#{partial_target}": index.searchable, index: index }
|
53
|
+
end
|
54
|
+
|
55
|
+
def partial
|
56
|
+
"#{partial_path}/#{partial_target}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def partial_path
|
60
|
+
"#{search_context.namespace.to_s.underscore}/search"
|
61
|
+
end
|
62
|
+
|
63
|
+
def partial_target
|
64
|
+
index.searchable_type.underscore
|
65
|
+
end
|
66
|
+
|
67
|
+
def unpublished?
|
68
|
+
searchable.respond_to?(:published?) && searchable.unpublished?
|
69
|
+
end
|
70
|
+
|
71
|
+
def url
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Plok::Search
|
2
|
+
class Term
|
3
|
+
attr_reader :controller, :string
|
4
|
+
|
5
|
+
def initialize(string, controller: nil)
|
6
|
+
@string = string
|
7
|
+
@controller = controller
|
8
|
+
end
|
9
|
+
|
10
|
+
def locale
|
11
|
+
return controller.locale if controller.present?
|
12
|
+
:nl
|
13
|
+
end
|
14
|
+
|
15
|
+
def valid?
|
16
|
+
@string.present?
|
17
|
+
end
|
18
|
+
|
19
|
+
def value
|
20
|
+
string
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/plok/version.rb
CHANGED
data/lib/tasks/plok_tasks.rake
CHANGED
@@ -1,4 +1,11 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
namespace 'plok:search' do
|
2
|
+
desc 'Rebuild all search indices'
|
3
|
+
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!)
|
8
|
+
end
|
9
|
+
puts "Done."
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# This exists so one is able to mock actual ActiveRecord model classes that are
|
2
|
+
# not tied to a database. This is useful if you want to write tests for concerns
|
3
|
+
# without tying them to project-specific models.
|
4
|
+
#
|
5
|
+
# * It allows you to not mock simple ActiveRecord DSL like find/find_by/where
|
6
|
+
# calls.
|
7
|
+
# * Projects using Plok can also use this, provided they call
|
8
|
+
# Plok::Engine.load_spec_supports in their spec/rails_helper.rb file.
|
9
|
+
# * Even if you're required to mock additional business logic onto your classes,
|
10
|
+
# you can just extend from Plok::FakeArModel to fill in gaps.
|
11
|
+
#
|
12
|
+
# Example:
|
13
|
+
#
|
14
|
+
# class Product < Plok::FakeArModel
|
15
|
+
# def visible?
|
16
|
+
# true
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# def hidden?
|
20
|
+
# !visible?
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
class Plok::FakeArModel
|
25
|
+
extend ActiveModel::Naming
|
26
|
+
extend ActiveModel::Translation
|
27
|
+
include ActiveModel::Validations
|
28
|
+
include ActiveModel::Conversion
|
29
|
+
include ActiveModel::Dirty
|
30
|
+
|
31
|
+
def initialize(attributes = {})
|
32
|
+
attributes.each do |key, value|
|
33
|
+
instance_variable_set(:"@#{key}", value)
|
34
|
+
self.class.send(:attr_accessor, key)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.collection(list)
|
39
|
+
@collection = list.to_a.map { |values| new(values) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.primary_key
|
43
|
+
:id
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.polymorphic_name
|
47
|
+
:fake_ar_model
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.current_scope
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.find(id)
|
54
|
+
@collection.to_a.detect { |o| o.id == id }
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.find_by(arguments)
|
58
|
+
where(arguments)&.first
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.where(arguments = nil)
|
62
|
+
@collection.select do |o|
|
63
|
+
if arguments.is_a?(Hash)
|
64
|
+
!!arguments.detect { |k, v| o.send(k) == v }
|
65
|
+
elsif(arguments.is_a?(String))
|
66
|
+
# NOTE: This will require intensive tinkering if we ever need it,
|
67
|
+
# so lets leave it for now.
|
68
|
+
[]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def destroyed?
|
74
|
+
false
|
75
|
+
end
|
76
|
+
|
77
|
+
def new_record?
|
78
|
+
true
|
79
|
+
end
|
80
|
+
|
81
|
+
def save(options = {})
|
82
|
+
true
|
83
|
+
end
|
84
|
+
|
85
|
+
def _read_attribute(a)
|
86
|
+
a
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def collection
|
92
|
+
self.class.instance_variable_get(:@collection)&.to_a || []
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
shared_examples_for :searchable do
|
4
|
+
let(:klass) { described_class.to_s.underscore.to_sym }
|
5
|
+
|
6
|
+
describe 'after_save' do
|
7
|
+
context 'non-translatable model' do
|
8
|
+
before do
|
9
|
+
# Because this is a spec/support included through it_behaves_like,
|
10
|
+
# we have to mock the entry points of the data used in
|
11
|
+
# Plok::Searchable.
|
12
|
+
allow(described_class).to receive(:searchable_fields_list) { [:foo] }
|
13
|
+
if described_class.respond_to?(:translatable_fields_list)
|
14
|
+
allow(described_class).to receive(:translatable_fields_list) { [] }
|
15
|
+
end
|
16
|
+
described_class.define_method(:foo) { 'bar' }
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'default' do
|
20
|
+
subject = build(klass)
|
21
|
+
allow(subject).to receive(:foo) { 'bar' }
|
22
|
+
expect(subject.search_indices).to eq []
|
23
|
+
end
|
24
|
+
|
25
|
+
# NOTE: See if you can dynamically test the creation of indices on save.
|
26
|
+
end
|
27
|
+
|
28
|
+
if described_class.respond_to?(:translatable_fields)
|
29
|
+
before do
|
30
|
+
allow(described_class).to receive(:searchable_fields_list) { [:foo] }
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'translatable model' do
|
34
|
+
before do
|
35
|
+
described_class.translatable_fields :foo, :bar
|
36
|
+
end
|
37
|
+
|
38
|
+
let(:create_subject!) do
|
39
|
+
subject = build(klass)
|
40
|
+
%w(nl fr).each do |l|
|
41
|
+
t = subject.translation(l.to_sym)
|
42
|
+
t.foo = "baz #{l}"
|
43
|
+
t.bar = 'bak'
|
44
|
+
end
|
45
|
+
subject.save
|
46
|
+
subject
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'does not copy translatable fields that are not in searchable fields' do
|
50
|
+
subject = create_subject!
|
51
|
+
expect(subject.search_indices.find_by(locale: :nl, name: 'bar')).to be nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
if described_class.respond_to?(:content_rows)
|
57
|
+
context 'model with flexible_content' do
|
58
|
+
let(:subject) { create(klass) }
|
59
|
+
let(:content) { create(:content_text, content: 'Lorem ipsum') }
|
60
|
+
|
61
|
+
before do
|
62
|
+
# Because this is a spec/support included through it_behaves_like,
|
63
|
+
# we have to mock the entry points of the data used in
|
64
|
+
# Plok::Searchable.
|
65
|
+
allow(described_class).to receive(:searchable_fields_list) { [:foo] }
|
66
|
+
allow_any_subject_of(described_class).to receive(:foo) { 'bar' }
|
67
|
+
allow_any_subject_of(Concerns::Storable::Collection).to receive(:foo) { 'bar' }
|
68
|
+
|
69
|
+
row = create(:content_row, locale: 'nl', rowable: subject)
|
70
|
+
row.columns << create(:content_column, content: content)
|
71
|
+
subject.content_rows << row
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'saves index when updating searchable subject' do
|
75
|
+
subject.save!
|
76
|
+
key = "flexible_content:#{content.id}"
|
77
|
+
expect(subject.search_indices.find_by(locale: :nl, name: key).value).to eq 'Lorem ipsum'
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'saves index when updating flexible content' do
|
81
|
+
subject.save!
|
82
|
+
content.content = 'Dolor sit amet'
|
83
|
+
content.save!
|
84
|
+
key = "flexible_content:#{content.id}"
|
85
|
+
expect(subject.search_indices.find_by(locale: :nl, name: key).value).to eq 'Dolor sit amet'
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
it '.respond_to?' do
|
92
|
+
expect(described_class).to respond_to(
|
93
|
+
:searchable_field, :searchable_fields, :searchable_fields_list
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
it '#respond_to?' do
|
98
|
+
expect(subject).to respond_to(:search_indices)
|
99
|
+
end
|
100
|
+
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.
|
4
|
+
version: 1.0.1
|
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-
|
12
|
+
date: 2022-10-14 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -78,14 +78,20 @@ files:
|
|
78
78
|
- MIT-LICENSE
|
79
79
|
- Rakefile
|
80
80
|
- app/assets/config/plok_manifest.js
|
81
|
+
- app/assets/javascripts/jquery-ui.autocomplete.html.js
|
82
|
+
- app/assets/javascripts/plok/searchable.js
|
81
83
|
- app/assets/javascripts/plok/sidebar.js
|
84
|
+
- app/assets/stylesheets/plok/_autocomplete.scss
|
82
85
|
- app/assets/stylesheets/plok/_sidebar.scss
|
83
86
|
- app/assets/stylesheets/plok/_sidebar_compact.scss
|
84
87
|
- app/controllers/catch_all_controller.rb
|
85
88
|
- app/controllers/plok/version_controller.rb
|
86
89
|
- app/models/concerns/plok/loggable.rb
|
90
|
+
- app/models/concerns/plok/searchable.rb
|
87
91
|
- app/models/log.rb
|
88
92
|
- app/models/queued_task.rb
|
93
|
+
- app/models/search_index.rb
|
94
|
+
- app/models/search_module.rb
|
89
95
|
- config/initializers/module.rb
|
90
96
|
- config/routes.rb
|
91
97
|
- db/migrate/20211008130809_create_plok_logs.rb
|
@@ -96,6 +102,8 @@ files:
|
|
96
102
|
- db/migrate/20211203103118_add_file_to_logs.rb
|
97
103
|
- db/migrate/20220512141814_add_log_indexes_to_category_type_and_id.rb
|
98
104
|
- db/migrate/20220512142356_add_index_to_queued_task_weight.rb
|
105
|
+
- db/migrate/20220923162300_create_search_indices.rb
|
106
|
+
- db/migrate/20220923164100_create_search_modules.rb
|
99
107
|
- lib/generators/plok/sidebar/USAGE
|
100
108
|
- lib/generators/plok/sidebar/sidebar_generator.rb
|
101
109
|
- lib/generators/plok/sidebar/templates/_menu_item.html.erb
|
@@ -104,11 +112,19 @@ files:
|
|
104
112
|
- lib/generators/plok/sidebar/templates/_wrapper.html.erb
|
105
113
|
- lib/plok.rb
|
106
114
|
- lib/plok/engine.rb
|
115
|
+
- lib/plok/search/backend.rb
|
116
|
+
- lib/plok/search/base.rb
|
117
|
+
- lib/plok/search/result_objects/base.rb
|
118
|
+
- lib/plok/search/term.rb
|
107
119
|
- lib/plok/version.rb
|
108
120
|
- lib/tasks/plok_tasks.rake
|
109
121
|
- spec/factories/logs.rb
|
110
122
|
- spec/factories/queued_tasks.rb
|
123
|
+
- spec/factories/search_indices.rb
|
124
|
+
- spec/factories/search_modules.rb
|
125
|
+
- spec/support/fake_ar_model.rb
|
111
126
|
- spec/support/plok/loggable.rb
|
127
|
+
- spec/support/plok/searchable.rb
|
112
128
|
homepage: https://plok.blimp.be
|
113
129
|
licenses:
|
114
130
|
- MIT
|