udongo 3.0.0 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/javascripts/backend/application.js +2 -0
- data/app/assets/javascripts/backend/search.js +26 -0
- data/app/assets/stylesheets/backend/application.scss +2 -0
- data/app/controllers/backend/search_controller.rb +5 -0
- data/app/controllers/backend/search_synonyms_controller.rb +49 -0
- data/app/models/concerns/content_type.rb +5 -1
- data/app/models/concerns/flexible_content.rb +6 -0
- data/app/models/concerns/publishable.rb +4 -0
- data/app/models/concerns/searchable.rb +101 -0
- data/app/models/content_column.rb +15 -0
- data/app/models/content_text.rb +8 -0
- data/app/models/page.rb +1 -0
- data/app/models/search_index.rb +5 -0
- data/app/models/search_module.rb +10 -0
- data/app/models/search_synonym.rb +5 -0
- data/app/views/backend/search/_page.html.erb +4 -0
- data/app/views/backend/search_synonyms/_form.html.erb +21 -0
- data/app/views/backend/search_synonyms/edit.html.erb +4 -0
- data/app/views/backend/search_synonyms/index.html.erb +36 -0
- data/app/views/backend/search_synonyms/new.html.erb +3 -0
- data/app/views/layouts/backend/_top_navigation.html.erb +14 -0
- data/changelog.md +12 -0
- data/config/locales/en_backend.yml +4 -0
- data/config/locales/en_forms.yml +4 -0
- data/config/locales/nl_backend.yml +4 -0
- data/config/locales/nl_forms.yml +4 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20170112151235_create_search_indices.rb +13 -0
- data/db/migrate/20170117143805_add_indices_to_search_indices.rb +6 -0
- data/db/migrate/20170118130428_create_search_synonyms.rb +11 -0
- data/db/migrate/20170118153840_create_search_modules.rb +13 -0
- data/db/migrate/20170202170310_rename_key_to_name_for_search_indices.rb +5 -0
- data/lib/udongo/search/backend.rb +22 -0
- data/lib/udongo/search/base.rb +75 -0
- data/lib/udongo/search/frontend.rb +23 -0
- data/lib/udongo/search/result_objects/backend/page.rb +22 -0
- data/lib/udongo/search/result_objects/base.rb +82 -0
- data/lib/udongo/search/result_objects/frontend/page.rb +22 -0
- data/lib/udongo/search/term.rb +26 -0
- data/lib/udongo/version.rb +1 -1
- data/readme.md +85 -3
- data/vendor/assets/javascripts/jquery-ui.autocomplete.html.js +41 -0
- data/vendor/assets/stylesheets/jquery-ui.theme.min.css +5 -0
- metadata +29 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3c5f900d7751fde5b3875bd0efe6ee7f56bf5488
|
4
|
+
data.tar.gz: 347e24cfe05e569e89178a2b3f4fae33325c933f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 849fbe000dd4bd06f352fed26e8ff1f2a5931f19a97ef08b0933ed11171d337c7bde6302ecc012e8863441b547a81a62c539f5ee579cc9044043862dd968a4c3
|
7
|
+
data.tar.gz: a73bbcad8f96b51c87a36788cb97b7e432d60be6e26924ed4d5389064a3676050be52b6c795786f55f229f7f61634ab4919b4254f6862a7369ae2c69296e856b
|
@@ -13,11 +13,13 @@
|
|
13
13
|
//= require jquery
|
14
14
|
//= require jquery_ujs
|
15
15
|
//= require jquery-ui/autocomplete
|
16
|
+
//= require jquery-ui.autocomplete.html.js
|
16
17
|
//= require jquery-ui/sortable
|
17
18
|
//= require backend/tether.min
|
18
19
|
//= require backend/bootstrap
|
19
20
|
//= require ckeditor/init
|
20
21
|
//= require backend/general
|
22
|
+
//= require backend/search
|
21
23
|
//= require backend/bootstrap-datepicker.min
|
22
24
|
//= require backend/datepickers
|
23
25
|
//= require backend/select2.min
|
@@ -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,49 @@
|
|
1
|
+
class Backend::SearchSynonymsController < Backend::BaseController
|
2
|
+
include Concerns::PaginationController
|
3
|
+
|
4
|
+
before_action -> { breadcrumb.add t('b.search_synonyms'), backend_search_synonyms_path }
|
5
|
+
before_action :find_model, only: [:edit, :update, :destroy]
|
6
|
+
|
7
|
+
def index
|
8
|
+
@search_synonyms = paginate(SearchSynonym.all)
|
9
|
+
end
|
10
|
+
|
11
|
+
def new
|
12
|
+
@search_synonym = SearchSynonym.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def create
|
16
|
+
@search_synonym = SearchSynonym.new(allowed_params)
|
17
|
+
|
18
|
+
if @search_synonym.save
|
19
|
+
redirect_to backend_search_synonyms_path, notice: translate_notice(:added, :search_synonym)
|
20
|
+
else
|
21
|
+
render :new
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def destroy
|
26
|
+
@search_synonym.destroy
|
27
|
+
redirect_to backend_search_synonyms_path, notice: translate_notice(:deleted, :search_synonym)
|
28
|
+
end
|
29
|
+
|
30
|
+
def update
|
31
|
+
if @search_synonym.update_attributes allowed_params
|
32
|
+
redirect_to backend_search_synonyms_path, notice: translate_notice(:edited, :search_synonym)
|
33
|
+
else
|
34
|
+
render :edit
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def allowed_params
|
41
|
+
params.require(:search_synonym).permit(
|
42
|
+
:locale, :term, :synonyms
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def find_model
|
47
|
+
@search_synonym = SearchSynonym.find(params[:id].to_i)
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Concerns
|
2
|
+
module Searchable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
has_many :search_indices, as: :searchable, dependent: :destroy
|
7
|
+
|
8
|
+
# The after_save block creates or saves indices for every indicated
|
9
|
+
# searchable field. Takes both translations and flexible content into
|
10
|
+
# account.
|
11
|
+
#
|
12
|
+
# Translation support was relatively painless, but FlexibleContent
|
13
|
+
# required more thought. See #save_flexible_content_indices!
|
14
|
+
after_save do
|
15
|
+
self.class.searchable_fields_list.each do |key|
|
16
|
+
if key == :flexible_content
|
17
|
+
save_flexible_content_search_indices! && next
|
18
|
+
end
|
19
|
+
|
20
|
+
if respond_to?(:translatable?) && self.class.translatable_fields_list.include?(key)
|
21
|
+
save_translatable_search_index!(key) && next
|
22
|
+
end
|
23
|
+
|
24
|
+
save_search_index!(key)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# I save all ContentText#content values as indices linked to the parent
|
29
|
+
# object. The Udongo::Search::Base#indices method already groups by
|
30
|
+
# searchable resource, so there are never any duplicates.
|
31
|
+
# Only additional matches for the searchable because it takes
|
32
|
+
# ContentText#content sources into account.
|
33
|
+
#
|
34
|
+
# ContentColumn and ContentText have after_{save,destroy} callbacks
|
35
|
+
# to help facilitate searchable management. Note that said code was
|
36
|
+
# initially present in this class, and it was such a mess that it became
|
37
|
+
# unpractical to maintain.
|
38
|
+
def save_flexible_content_search_indices!
|
39
|
+
content_rows.each do |row|
|
40
|
+
row.columns.each do |column|
|
41
|
+
next unless column.content.is_a?(ContentText)
|
42
|
+
key = "flexible_content:#{column.content_id}"
|
43
|
+
index = search_indices.find_or_create_by!(locale: row.locale, name: key)
|
44
|
+
index.value = column.content.content
|
45
|
+
index.save!
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def save_search_index!(key)
|
51
|
+
value = send(key)
|
52
|
+
return if value.blank?
|
53
|
+
|
54
|
+
index = search_indices.find_or_create_by!(locale: Udongo.config.i18n.app.default_locale, name: key)
|
55
|
+
index.value = value
|
56
|
+
index.save!
|
57
|
+
end
|
58
|
+
|
59
|
+
def save_translatable_search_index!(key)
|
60
|
+
Udongo.config.i18n.app.locales.each do |locale|
|
61
|
+
value = translation(locale.to_sym).send(key)
|
62
|
+
next if value.blank?
|
63
|
+
index = search_indices.find_or_create_by!(locale: locale, name: key)
|
64
|
+
index.value = value
|
65
|
+
index.save!
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def searchable?
|
70
|
+
true
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
module ClassMethods
|
75
|
+
def searchable_field(key)
|
76
|
+
unless searchable_fields_list.include?(key.to_sym)
|
77
|
+
searchable_fields_list << key.to_sym
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def searchable_fields(*args)
|
82
|
+
args.each { |key| searchable_field(key) }
|
83
|
+
end
|
84
|
+
|
85
|
+
def searchable_fields_list
|
86
|
+
@searchable_fields_list ||= default_searchable_fields
|
87
|
+
end
|
88
|
+
|
89
|
+
def default_searchable_fields
|
90
|
+
result = []
|
91
|
+
|
92
|
+
if respond_to?(:translatable_fields_list)
|
93
|
+
translatable_fields_list.each { |f| result << f }
|
94
|
+
end
|
95
|
+
|
96
|
+
result << :flexible_content if flexible_content?
|
97
|
+
result
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -2,6 +2,13 @@ class ContentColumn < ApplicationRecord
|
|
2
2
|
include Concerns::Sortable
|
3
3
|
sortable(scope: :row_id)
|
4
4
|
|
5
|
+
# Removing a column from a searchable object should remove the linked
|
6
|
+
# SearchIndex instance.
|
7
|
+
after_destroy do
|
8
|
+
next unless linked_to_searchable_parent?
|
9
|
+
parent.search_indices.where(name: "flexible_content:#{content_id}").destroy_all
|
10
|
+
end
|
11
|
+
|
5
12
|
belongs_to :row, class_name: 'ContentRow', touch: true
|
6
13
|
belongs_to :content, polymorphic: true, dependent: :destroy
|
7
14
|
|
@@ -11,4 +18,12 @@ class ContentColumn < ApplicationRecord
|
|
11
18
|
numericality: { greater_than: 0, less_than_or_equal_to: 12, only_integer: true }
|
12
19
|
|
13
20
|
default_scope -> { order(:position) }
|
21
|
+
|
22
|
+
def linked_to_searchable_parent?
|
23
|
+
parent.present? && parent.searchable?
|
24
|
+
end
|
25
|
+
|
26
|
+
def parent
|
27
|
+
row.rowable
|
28
|
+
end
|
14
29
|
end
|
data/app/models/content_text.rb
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
class ContentText < ApplicationRecord
|
2
2
|
include Concerns::ContentType
|
3
3
|
|
4
|
+
# This triggers the searchable instance's after_save callback, which
|
5
|
+
# in turn updates all search indices.
|
6
|
+
after_save { parent.save! if linked_to_searchable_parent? }
|
7
|
+
|
4
8
|
def content_type
|
5
9
|
:text
|
6
10
|
end
|
11
|
+
|
12
|
+
def linked_to_searchable_parent?
|
13
|
+
column.present? && parent.present? && parent.searchable?
|
14
|
+
end
|
7
15
|
end
|
data/app/models/page.rb
CHANGED
@@ -0,0 +1,10 @@
|
|
1
|
+
class SearchModule < ApplicationRecord
|
2
|
+
validates :name, presence: true
|
3
|
+
|
4
|
+
scope :weighted, -> { order('weight DESC') }
|
5
|
+
|
6
|
+
def indices
|
7
|
+
SearchIndex.joins('INNER JOIN search_modules ON search_indices.searchable_type = search_modules.name')
|
8
|
+
.where('search_modules.name = ?', name)
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<%= render 'backend/general_form_error', object: model %>
|
2
|
+
|
3
|
+
<%= simple_form_for [:backend, model] do |f| %>
|
4
|
+
<div class="row">
|
5
|
+
<div class="col-md-12">
|
6
|
+
<div class="card">
|
7
|
+
<div class="card-header">
|
8
|
+
<%= t 'b.general' %>
|
9
|
+
</div>
|
10
|
+
|
11
|
+
<div class="card-block">
|
12
|
+
<%= f.input :locale, collection: Udongo.config.i18n.cms.interface_locales, include_blank: false %>
|
13
|
+
<%= f.input :term %>
|
14
|
+
<%= f.input :synonyms, as: :string %>
|
15
|
+
</div>
|
16
|
+
</div>
|
17
|
+
</div>
|
18
|
+
</div>
|
19
|
+
|
20
|
+
<%= render 'backend/form_actions', cancel_url: backend_search_synonyms_path %>
|
21
|
+
<% end %>
|
@@ -0,0 +1,36 @@
|
|
1
|
+
<%= render 'backend/breadcrumbs' %>
|
2
|
+
|
3
|
+
<p class="text-xs-right">
|
4
|
+
<%= link_to icon(:plus, t('b.add')), new_backend_search_synonym_path, class: 'btn btn-primary btn-sm' %>
|
5
|
+
</p>
|
6
|
+
|
7
|
+
<% if @search_synonyms.any? %>
|
8
|
+
<table class="table table-striped table-hover">
|
9
|
+
<thead class="thead-inverse">
|
10
|
+
<tr>
|
11
|
+
<th><%= t 'b.locale' %></th>
|
12
|
+
<th><%= t 'b.search_term' %></th>
|
13
|
+
<th><%= t 'b.synonyms' %></th>
|
14
|
+
<th> </th>
|
15
|
+
</tr>
|
16
|
+
</thead>
|
17
|
+
|
18
|
+
<tbody>
|
19
|
+
<% @search_synonyms.each do |s| %>
|
20
|
+
<tr>
|
21
|
+
<td><%= s.locale %></td>
|
22
|
+
<td><%= s.term %></td>
|
23
|
+
<td><%= s.synonyms %></td>
|
24
|
+
<td class="text-xs-right">
|
25
|
+
<%= link_to icon(:pencil_square_o), edit_backend_search_synonym_path(s) %>
|
26
|
+
<%= link_to icon(:trash), backend_search_synonym_path(s), method: 'delete', data: { confirm: t('b.msg.confirm') } %>
|
27
|
+
</td>
|
28
|
+
</tr>
|
29
|
+
<% end %>
|
30
|
+
</tbody>
|
31
|
+
</table>
|
32
|
+
|
33
|
+
<%= udongo_paginate @search_synonyms %>
|
34
|
+
<% else %>
|
35
|
+
<p><%= t 'b.msg.no_items' %></p>
|
36
|
+
<% end %>
|
@@ -13,15 +13,29 @@
|
|
13
13
|
<%= link_to t('b.navigation'), backend_navigations_path, class: 'dropdown-item' %>
|
14
14
|
<%= link_to t('b.admins'), backend_admins_path, class: 'dropdown-item' %>
|
15
15
|
<%= link_to t('b.redirects'), backend_redirects_path, class: 'dropdown-item' %>
|
16
|
+
<div class="dropdown-divider"></div>
|
16
17
|
<%= link_to t('b.emails'), backend_emails_path, class: 'dropdown-item' %>
|
17
18
|
<%= link_to t('b.email_templates'), backend_email_templates_path, class: 'dropdown-item' %>
|
18
19
|
</div>
|
19
20
|
</li>
|
20
21
|
|
22
|
+
<li class="nav-item dropdown">
|
23
|
+
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"><%= icon(:search, t('b.search')) %></a>
|
24
|
+
<div class="dropdown-menu">
|
25
|
+
<%= link_to t('b.synonyms'), backend_search_synonyms_path, class: 'dropdown-item' %>
|
26
|
+
</div>
|
27
|
+
</li>
|
28
|
+
|
21
29
|
<li class="nav-item dropdown pull-xs-right">
|
22
30
|
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"><%= icon(:user, current_admin.full_name) %></a>
|
23
31
|
<div class="dropdown-menu">
|
24
32
|
<%= link_to icon(:sign_out, t('b.logout')), backend_session_path(id: 42), method: :delete, data: { confirm: t('b.msg.confirm') }, class: 'dropdown-item' %>
|
25
33
|
</div>
|
26
34
|
</li>
|
35
|
+
|
36
|
+
<li class="nav-item pull-xs-right">
|
37
|
+
<%= form_tag backend_search_path, method: :get, id: 'search', class: 'form-inline my-2 my-lg-0' do %>
|
38
|
+
<%= text_field_tag :term, params[:term], class: 'form-control mr-sm-2', placeholder: t('b.search') %>
|
39
|
+
<% end %>
|
40
|
+
</li>
|
27
41
|
</ul>
|
data/changelog.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
4.0.0 - 2017-02-12
|
2
|
+
--
|
3
|
+
* Added Concerns::Searchable. This lets model instances automatically save
|
4
|
+
SearchIndex records to the database as they are changed.
|
5
|
+
* Added functionality to Concerns::FlexibleContent so it can play nice with
|
6
|
+
Concerns::Searchable.
|
7
|
+
* Added a search input to the backend's top navigation bar. Autocompleted search
|
8
|
+
works through Udongo::Search::Backend.
|
9
|
+
* Provided a raw infrastructure to allow for Udongo::Search::Frontend or other
|
10
|
+
namespaced search classes.
|
11
|
+
|
12
|
+
|
1
13
|
3.0.0 - 2017-01-13
|
2
14
|
--
|
3
15
|
* Because of the complex structure and no actual necessity, the form models and
|
@@ -48,6 +48,9 @@ en:
|
|
48
48
|
redirects: Redirects
|
49
49
|
save: Save
|
50
50
|
search: Search
|
51
|
+
search_term: Search term
|
52
|
+
search_synonym: Search synonym
|
53
|
+
search_synonyms: Search synonyms
|
51
54
|
sender: Sender
|
52
55
|
sent_at: Sent at
|
53
56
|
settings: Settings
|
@@ -57,6 +60,7 @@ en:
|
|
57
60
|
status: Status
|
58
61
|
status_code: Status code
|
59
62
|
subject: Subject
|
63
|
+
synonyms: Synonyms
|
60
64
|
tags: Tags
|
61
65
|
telephone: Telephone
|
62
66
|
title: Title
|
data/config/locales/en_forms.yml
CHANGED
@@ -42,6 +42,7 @@ en:
|
|
42
42
|
subject: Subject
|
43
43
|
subtitle: Subtitle
|
44
44
|
summary: Summary
|
45
|
+
synonyms: Synonyms
|
45
46
|
title: Title
|
46
47
|
visible: Visible?
|
47
48
|
width: Width
|
@@ -69,3 +70,6 @@ en:
|
|
69
70
|
redirect:
|
70
71
|
destination_uri: Destination
|
71
72
|
source_uri: Source
|
73
|
+
|
74
|
+
search_synonym:
|
75
|
+
term: Search term
|
@@ -48,6 +48,9 @@ nl:
|
|
48
48
|
redirects: Redirects
|
49
49
|
save: Opslaan
|
50
50
|
search: Zoeken
|
51
|
+
search_term: Zoekterm
|
52
|
+
search_synonym: Zoekterm synoniem
|
53
|
+
search_synonyms: Zoekterm synoniemen
|
51
54
|
sender: Afzender
|
52
55
|
sent_at: Verzonden op
|
53
56
|
settings: Instellingen
|
@@ -57,6 +60,7 @@ nl:
|
|
57
60
|
status: Status
|
58
61
|
status_code: Statuscode
|
59
62
|
subject: Onderwerp
|
63
|
+
synonyms: Synoniemen
|
60
64
|
tags: Tags
|
61
65
|
telephone: Telefoonnummer
|
62
66
|
title: Titel
|
data/config/locales/nl_forms.yml
CHANGED
@@ -42,6 +42,7 @@ nl:
|
|
42
42
|
subject: Onderwerp
|
43
43
|
subtitle: Subtitel
|
44
44
|
summary: Korte inhoud
|
45
|
+
synonyms: Synoniemen
|
45
46
|
title: Titel
|
46
47
|
visible: Zichtbaar?
|
47
48
|
width: Breedte
|
@@ -69,3 +70,6 @@ nl:
|
|
69
70
|
redirect:
|
70
71
|
destination_uri: Bestemming
|
71
72
|
source_uri: Bron
|
73
|
+
|
74
|
+
search_synonym:
|
75
|
+
term: Zoekterm
|
data/config/routes.rb
CHANGED
@@ -14,6 +14,8 @@ Rails.application.routes.draw do
|
|
14
14
|
end
|
15
15
|
|
16
16
|
get '/' => 'dashboard#show'
|
17
|
+
get 'search' => 'search#query'
|
18
|
+
|
17
19
|
|
18
20
|
resources :sessions, only: [:new, :create, :destroy]
|
19
21
|
resources :admins
|
@@ -57,6 +59,8 @@ Rails.application.routes.draw do
|
|
57
59
|
|
58
60
|
resources :redirects, except: :show
|
59
61
|
|
62
|
+
resources :search_synonyms, except: :show
|
63
|
+
|
60
64
|
namespace :content do
|
61
65
|
resources :rows, only: [:index, :new, :destroy] do
|
62
66
|
concerns :positionable
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class CreateSearchModules < ActiveRecord::Migration[5.0]
|
2
|
+
def change
|
3
|
+
create_table :search_modules do |t|
|
4
|
+
t.string :name
|
5
|
+
t.boolean :searchable
|
6
|
+
t.integer :weight
|
7
|
+
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
|
11
|
+
add_index :search_modules, [:name, :searchable]
|
12
|
+
end
|
13
|
+
end
|
@@ -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 Udongo::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 Udongo::Search::Base for more information on how this
|
8
|
+
# search functionality is designed.
|
9
|
+
class Backend < Udongo::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
|
+
# Udongo::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,75 @@
|
|
1
|
+
module Udongo::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
|
+
# Udongo::Search::Backend - included in Udongo
|
8
|
+
# Udongo::Search::Frontend
|
9
|
+
# Udongo::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 = Udongo::Search::Term.new(term, controller: controller)
|
29
|
+
@controller = controller
|
30
|
+
@namespace = namespace
|
31
|
+
end
|
32
|
+
|
33
|
+
def class_exists?(class_name)
|
34
|
+
klass = Module.const_get(class_name)
|
35
|
+
return klass.is_a?(Class)
|
36
|
+
rescue NameError
|
37
|
+
return false
|
38
|
+
end
|
39
|
+
|
40
|
+
def indices
|
41
|
+
return [] unless term.present?
|
42
|
+
|
43
|
+
# Having the searchmodules sorted by weight returns indices in the
|
44
|
+
# correct order.
|
45
|
+
@indices ||= SearchModule.weighted.inject([]) do |stack, m|
|
46
|
+
# The group happens to make sure we end up with just 1 copy of
|
47
|
+
# a searchable result Otherwise matches from both an indexed
|
48
|
+
# Page#title and Page#description would be in the result set.
|
49
|
+
stack << m.indices.where('search_indices.value REGEXP ?', term.value)
|
50
|
+
.group([:searchable_type, :searchable_id])
|
51
|
+
end.flatten
|
52
|
+
end
|
53
|
+
|
54
|
+
def namespace
|
55
|
+
# This looks daft, but it gives us a foot in the door for when a frontend
|
56
|
+
# search is triggered in the backend.
|
57
|
+
return @namespace unless @namespace.nil?
|
58
|
+
return 'Frontend' if controller.nil?
|
59
|
+
controller.class.parent.to_s
|
60
|
+
end
|
61
|
+
|
62
|
+
# In order to provide a good result set in a search autocomplete, we have
|
63
|
+
# to translate the raw index to a class that makes an index adhere
|
64
|
+
# to a certain interface (that can include links).
|
65
|
+
def result_object(index)
|
66
|
+
klass = "Udongo::Search::ResultObjects::#{namespace}::#{index.searchable_type}"
|
67
|
+
klass = 'Udongo::Search::ResultObjects::Base' unless result_object_exists?(klass)
|
68
|
+
klass.constantize.new(index, search_context: self)
|
69
|
+
end
|
70
|
+
|
71
|
+
def result_object_exists?(name)
|
72
|
+
class_exists?(name) && name.constantize.method_defined?(:build_html)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Due to the load order of classes, Backend precedes the required Base class.
|
2
|
+
require_relative 'base'
|
3
|
+
|
4
|
+
module Udongo::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 Udongo::Search::Base for more information on how this
|
8
|
+
# search functionality is designed.
|
9
|
+
class Frontend < Udongo::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
|
+
# Udongo::Search::ResultObjects::Frontend::Page.
|
15
|
+
def search
|
16
|
+
indices.map do |index|
|
17
|
+
result = result_object(index)
|
18
|
+
next if result.hidden? || result.unpublished?
|
19
|
+
{ label: result.label, value: result.url }
|
20
|
+
end.select(&:present?)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative '../base'
|
2
|
+
|
3
|
+
module Udongo::Search::ResultObjects
|
4
|
+
module Backend
|
5
|
+
# This class should be used to further manipulate any of the data provided
|
6
|
+
# through #search_context or Udongo::Search::ResultObjects::Base.
|
7
|
+
#
|
8
|
+
# A search context class is accessible through #search_context. This
|
9
|
+
# gives you access to #search_context.controller, which can be used to
|
10
|
+
# call routes upon.
|
11
|
+
#
|
12
|
+
# Example of: If an autocomplete requires additional data to be rendered in
|
13
|
+
# the partial (think another model, or an API call), one could override
|
14
|
+
# the #locals method to include more variables. You could do this directly
|
15
|
+
# in the partial as well, but this way we have separation of concerns.
|
16
|
+
class Page < Udongo::Search::ResultObjects::Base
|
17
|
+
def url
|
18
|
+
search_context.controller.edit_backend_page_path(searchable)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Udongo
|
2
|
+
class InterfaceNotImplementedError < NoMethodError
|
3
|
+
end
|
4
|
+
|
5
|
+
module Search
|
6
|
+
module ResultObjects
|
7
|
+
class Base
|
8
|
+
attr_reader :index, :search_context
|
9
|
+
|
10
|
+
delegate :searchable, to: :index
|
11
|
+
|
12
|
+
def initialize(index, search_context: nil)
|
13
|
+
@index = index
|
14
|
+
@search_context = search_context
|
15
|
+
end
|
16
|
+
|
17
|
+
# Typically, an autocomplete requires 3 things:
|
18
|
+
#
|
19
|
+
# * A title indicating a resource name.
|
20
|
+
# Examples: Page#title, Product#name,...
|
21
|
+
# * A truncated summary providing a glimpse of the resource's contents.
|
22
|
+
# Examples: Page#subtitle, Product#description,...
|
23
|
+
# * A link to the resource.
|
24
|
+
# Examples: edit_backend_page_path(37), product_path(37),...
|
25
|
+
#
|
26
|
+
# However, this seems very restrictive to me. If I narrow down the data
|
27
|
+
# a dev can use in an autocomplete, it severely reduces options he/she has
|
28
|
+
# in how the autocomplete results look like. Think of autocompletes in a
|
29
|
+
# shop that require images or prices to be included in their result bodies.
|
30
|
+
#
|
31
|
+
# This is why I chose to let ApplicationController.render work around the
|
32
|
+
# problem by letting the dev decide how the row should look.
|
33
|
+
#
|
34
|
+
def build_html
|
35
|
+
unless File.exists?(full_partial)
|
36
|
+
raise(InterfaceNotImplementedError, "In order to display formatted HTML for search results, the build_html method expects for a partial to exist in #{full_partial}")
|
37
|
+
end
|
38
|
+
|
39
|
+
ApplicationController.render(partial: partial, locals: locals)
|
40
|
+
end
|
41
|
+
|
42
|
+
def full_partial
|
43
|
+
root = Rails.root.to_s.gsub('spec/dummy', '')
|
44
|
+
File.join(root, 'app/views', partial_path, "_#{partial_target}.html.erb")
|
45
|
+
end
|
46
|
+
|
47
|
+
def hidden?
|
48
|
+
searchable.respond_to?(:visible) && searchable.hidden?
|
49
|
+
end
|
50
|
+
|
51
|
+
def label
|
52
|
+
if index.name.include?('flexible_content')
|
53
|
+
id = index.name.split(':')[1].to_i
|
54
|
+
return ContentText.find(id).content
|
55
|
+
end
|
56
|
+
|
57
|
+
searchable.send(index.name)
|
58
|
+
end
|
59
|
+
|
60
|
+
def locals
|
61
|
+
{ "#{partial_target}": index.searchable, index: index }
|
62
|
+
end
|
63
|
+
|
64
|
+
def partial
|
65
|
+
"#{partial_path}/#{partial_target}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def partial_path
|
69
|
+
"#{search_context.namespace.to_s.underscore}/search"
|
70
|
+
end
|
71
|
+
|
72
|
+
def partial_target
|
73
|
+
index.searchable_type.underscore
|
74
|
+
end
|
75
|
+
|
76
|
+
def unpublished?
|
77
|
+
searchable.respond_to?(:published?) && searchable.unpublished?
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative '../base'
|
2
|
+
|
3
|
+
module Udongo::Search::ResultObjects
|
4
|
+
module Frontend
|
5
|
+
# This class should be used to further manipulate any of the data provided
|
6
|
+
# through #search_context or Udongo::Search::ResultObjects::Base.
|
7
|
+
#
|
8
|
+
# A search context class is accessible through #search_context. This
|
9
|
+
# gives you access to #search_context.controller, which can be used to
|
10
|
+
# call routes upon.
|
11
|
+
#
|
12
|
+
# Example of: If an autocomplete requires additional data to be rendered in
|
13
|
+
# the partial (think another model, or an API call), one could override
|
14
|
+
# the #locals method to include more variables. You could do this directly
|
15
|
+
# in the partial as well, but this way we have separation of concerns.
|
16
|
+
class Page < Udongo::Search::ResultObjects::Base
|
17
|
+
def url
|
18
|
+
# TODO: We don't have methods to build frontend URLs for pages yet?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Udongo::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
|
+
Udongo.config.i18n.app.default_locale.to_sym
|
13
|
+
end
|
14
|
+
|
15
|
+
def synonym
|
16
|
+
SearchSynonym.where(locale: locale)
|
17
|
+
.where('concat(",", synonyms, ",") LIKE ?', "%,#{string},%")
|
18
|
+
.take
|
19
|
+
end
|
20
|
+
|
21
|
+
def value
|
22
|
+
return synonym.term if synonym
|
23
|
+
string
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/udongo/version.rb
CHANGED
data/readme.md
CHANGED
@@ -72,7 +72,6 @@ Udongo.config.flexible_content.allowed_breakpoints = %w(xs sm md lg xl)
|
|
72
72
|
* Array
|
73
73
|
* Float
|
74
74
|
|
75
|
-
### Setup
|
76
75
|
```ruby
|
77
76
|
class User < ApplicationRecord
|
78
77
|
include Concerns::Storable
|
@@ -119,7 +118,6 @@ When you save the parent object (user), all the store collections will automatic
|
|
119
118
|
## Translatable concern
|
120
119
|
This concern is actually the storable concern with some predefined settings. In order to use this concern your model needs to have a database text field named ```locales```.
|
121
120
|
|
122
|
-
### Setup
|
123
121
|
```ruby
|
124
122
|
class Document < ApplicationRecord
|
125
123
|
include Concerns::Translatable
|
@@ -132,6 +130,21 @@ class Document < ApplicationRecord
|
|
132
130
|
end
|
133
131
|
```
|
134
132
|
|
133
|
+
## Searchable concern
|
134
|
+
Include this in your model if you want its records to appear in search autocompletes.
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
class Document < ApplicationRecord
|
138
|
+
include Concerns::Searchable
|
139
|
+
|
140
|
+
# One field
|
141
|
+
searchable_field :title
|
142
|
+
|
143
|
+
# Multiple fields
|
144
|
+
searchable_fields :title, :description, :summary
|
145
|
+
end
|
146
|
+
```
|
147
|
+
|
135
148
|
### Reading values
|
136
149
|
When reading values the current ```I18n.locale``` is used. If you want to specify the locale, you need to use the longer syntax.
|
137
150
|
|
@@ -175,7 +188,6 @@ documents = Document.by_locale(:nl)
|
|
175
188
|
## Addressable concern
|
176
189
|
This concern makes it easy to have multiple addresses with a category linked to a model.
|
177
190
|
|
178
|
-
### Setup
|
179
191
|
```ruby
|
180
192
|
class User < ApplicationRecord
|
181
193
|
include Concerns::Addressable
|
@@ -237,6 +249,76 @@ validates :email, email: true
|
|
237
249
|
validates :url, url: true
|
238
250
|
```
|
239
251
|
|
252
|
+
# Search engine
|
253
|
+
4.0 introduced a rough structure to build a search autocomplete upon through ```Concerns::Searchable```.
|
254
|
+
|
255
|
+
## How does it work?
|
256
|
+
Included in Udongo by default is the backend search, which makes Page records accessible through an autocomplete. In order to build search support for a model, we have to make it include the concern:
|
257
|
+
|
258
|
+
```ruby
|
259
|
+
# app/models/page.rb
|
260
|
+
class Page
|
261
|
+
include Concerns::Searchable
|
262
|
+
searchable_fields :title, :subtitle, :flexible_content
|
263
|
+
end
|
264
|
+
```
|
265
|
+
|
266
|
+
```Concerns::Searchable``` saves ```SearchIndex``` records to our database whenever a model gets saved. Support for both ```Concern::Translatable``` and ```Concern::FlexibleContent``` is built in, meaning that translatable fields can also be searchable fields.
|
267
|
+
|
268
|
+
By including ```:flexible_content``` as a searchable field, we flag it to build search indices for all flexible content of the ```ContentText``` type.
|
269
|
+
|
270
|
+
```Backend::SearchController#index``` contains a call to ```Udongo::Search::Backend```. That class is responsible for matching a search term against the available search indices:
|
271
|
+
|
272
|
+
```ruby
|
273
|
+
# app/controllers/backend/search_controller.rb
|
274
|
+
class Backend::SearchController < Backend::BaseController
|
275
|
+
def query
|
276
|
+
@results = Udongo::Search::Backend.new(params[:term], controller: self).search
|
277
|
+
render json: @results
|
278
|
+
end
|
279
|
+
end
|
280
|
+
```
|
281
|
+
|
282
|
+
```Udongo::Search::Backend#search``` in turn translates those indices in a format that jQueryUI's autocomplete understands: ```{ label: 'foo', value: 'bar' }```.
|
283
|
+
```ruby
|
284
|
+
# lib/udongo/search/backend.rb
|
285
|
+
module Udongo::Search
|
286
|
+
class Backend < Udongo::Search::Base
|
287
|
+
def search
|
288
|
+
indices.map do |index|
|
289
|
+
result = result_object(index)
|
290
|
+
{ label: result.build_html, value: result.url }
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
```
|
296
|
+
|
297
|
+
By default the ```#result_object``` is an instance of ```Udongo::Search::ResultObjects::Base```. You can define your own result object class, which in this example is done for the ```Page``` model:
|
298
|
+
```ruby
|
299
|
+
# lib/udongo/search/result_objects/page.rb
|
300
|
+
module Udongo::Search::ResultObjects
|
301
|
+
class Page < Udongo::Search::ResultObjects::Base
|
302
|
+
def url
|
303
|
+
if namespace == :backend
|
304
|
+
controller.edit_backend_page_path(index.searchable)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
````
|
310
|
+
This gives devs a way to extend the data for use in jQueryUI's autocomplete, or simply to mutate the index data. In the example above, we check what namespace we reside in in order to generate an edit link to the relevant page in the pages module. If one were to build a search for the frontend that includes pages, you could build the required URL for it here.
|
311
|
+
|
312
|
+
### HTML labels in autocomplete
|
313
|
+
Support for HTML labels is automatically included through ```vendor/assets/javascripts/jquery-ui.autocomplete.html.js`. The labels should reside in partial files and be rendered with ```Udongo::Search::ResultObjects::Base#build_html```. This provide support for funkier autocomplete result structures:
|
314
|
+
|
315
|
+
```erb
|
316
|
+
<!-- app/views/backend/search/_page.html.erb -->
|
317
|
+
<%= t('b.page') %> — <%= page.title %><br />
|
318
|
+
<small>
|
319
|
+
<%= truncate(page.description, length: 40) %>
|
320
|
+
</small>
|
321
|
+
```
|
240
322
|
|
241
323
|
# Cryptography
|
242
324
|
```Udongo::Cryptography``` is a module you can include in any class to provide you with functionality to encrypt and decrypt values. It is a wrapper that currently uses ```ActiveSupport::MessageEncryptor```, which in turns uses the Rails secret key to encrypt keys.
|
@@ -0,0 +1,41 @@
|
|
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 );
|
41
|
+
|
@@ -0,0 +1,5 @@
|
|
1
|
+
/*! jQuery UI - v1.12.1 - 2017-01-25
|
2
|
+
* http://jqueryui.com
|
3
|
+
* Copyright jQuery Foundation and other contributors; Licensed MIT */
|
4
|
+
|
5
|
+
.ui-widget{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget.ui-widget-content{border:1px solid #c5c5c5}.ui-widget-content{border:1px solid #ddd;background:#fff;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #ddd;background:#e9e9e9;color:#333;font-weight:bold}.ui-widget-header a{color:#333}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,.ui-button,html .ui-button.ui-state-disabled:hover,html .ui-button.ui-state-disabled:active{border:1px solid #c5c5c5;background:#f6f6f6;font-weight:normal;color:#454545}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited,a.ui-button,a:link.ui-button,a:visited.ui-button,.ui-button{color:#454545;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus,.ui-button:hover,.ui-button:focus{border:1px solid #ccc;background:#ededed;font-weight:normal;color:#2b2b2b}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,a.ui-button:hover,a.ui-button:focus{color:#2b2b2b;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px rgb(94,158,214)}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active,.ui-button:active,.ui-button.ui-state-active:hover{border:1px solid #003eff;background:#007fff;font-weight:normal;color:#fff}.ui-icon-background,.ui-state-active .ui-icon-background{border:#003eff;background-color:#fff}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#fff;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #dad55e;background:#fffa90;color:#777620}.ui-state-checked{border:1px solid #dad55e;background:#fffa90}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#777620}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #f1a899;background:#fddfdf;color:#5f3f3f}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#5f3f3f}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#5f3f3f}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon,.ui-button:hover .ui-icon,.ui-button:focus .ui-icon{background-image:url("images/ui-icons_555555_256x240.png")}.ui-state-active .ui-icon,.ui-button:active .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-highlight .ui-icon,.ui-button .ui-state-highlight.ui-icon{background-image:url("images/ui-icons_777620_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cc0000_256x240.png")}.ui-button .ui-icon{background-image:url("images/ui-icons_777777_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-65px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-65px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:1px -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:3px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:3px}.ui-widget-overlay{background:#aaa;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{-webkit-box-shadow:0 0 5px #666;box-shadow:0 0 5px #666}
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: udongo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 4.0.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: 2017-
|
12
|
+
date: 2017-02-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -431,6 +431,7 @@ files:
|
|
431
431
|
- app/assets/javascripts/backend/pages.js
|
432
432
|
- app/assets/javascripts/backend/plugins/autocomplete.js
|
433
433
|
- app/assets/javascripts/backend/plugins/tagbox.js
|
434
|
+
- app/assets/javascripts/backend/search.js
|
434
435
|
- app/assets/javascripts/backend/sortable.js
|
435
436
|
- app/assets/javascripts/backend/tags.js
|
436
437
|
- app/assets/javascripts/backend/tree.js
|
@@ -461,6 +462,8 @@ files:
|
|
461
462
|
- app/controllers/backend/navigations_controller.rb
|
462
463
|
- app/controllers/backend/pages_controller.rb
|
463
464
|
- app/controllers/backend/redirects_controller.rb
|
465
|
+
- app/controllers/backend/search_controller.rb
|
466
|
+
- app/controllers/backend/search_synonyms_controller.rb
|
464
467
|
- app/controllers/backend/seo_controller.rb
|
465
468
|
- app/controllers/backend/sessions_controller.rb
|
466
469
|
- app/controllers/backend/snippets_controller.rb
|
@@ -516,6 +519,7 @@ files:
|
|
516
519
|
- app/models/concerns/parentable.rb
|
517
520
|
- app/models/concerns/person.rb
|
518
521
|
- app/models/concerns/publishable.rb
|
522
|
+
- app/models/concerns/searchable.rb
|
519
523
|
- app/models/concerns/seo.rb
|
520
524
|
- app/models/concerns/sortable.rb
|
521
525
|
- app/models/concerns/spammable.rb
|
@@ -539,6 +543,9 @@ files:
|
|
539
543
|
- app/models/page.rb
|
540
544
|
- app/models/queued_task.rb
|
541
545
|
- app/models/redirect.rb
|
546
|
+
- app/models/search_index.rb
|
547
|
+
- app/models/search_module.rb
|
548
|
+
- app/models/search_synonym.rb
|
542
549
|
- app/models/setting.rb
|
543
550
|
- app/models/snippet.rb
|
544
551
|
- app/models/store.rb
|
@@ -595,6 +602,11 @@ files:
|
|
595
602
|
- app/views/backend/redirects/edit.html.erb
|
596
603
|
- app/views/backend/redirects/index.html.erb
|
597
604
|
- app/views/backend/redirects/new.html.erb
|
605
|
+
- app/views/backend/search/_page.html.erb
|
606
|
+
- app/views/backend/search_synonyms/_form.html.erb
|
607
|
+
- app/views/backend/search_synonyms/edit.html.erb
|
608
|
+
- app/views/backend/search_synonyms/index.html.erb
|
609
|
+
- app/views/backend/search_synonyms/new.html.erb
|
598
610
|
- app/views/backend/sessions/new.html.erb
|
599
611
|
- app/views/backend/snippets/_form.html.erb
|
600
612
|
- app/views/backend/snippets/_tabs.html.erb
|
@@ -690,6 +702,11 @@ files:
|
|
690
702
|
- db/migrate/20161029124558_add_locale_to_admin.rb
|
691
703
|
- db/migrate/20161029130557_add_bcc_and_cc_to_email_templates.rb
|
692
704
|
- db/migrate/20161029171056_add_ccc_and_bcc_to_email.rb
|
705
|
+
- db/migrate/20170112151235_create_search_indices.rb
|
706
|
+
- db/migrate/20170117143805_add_indices_to_search_indices.rb
|
707
|
+
- db/migrate/20170118130428_create_search_synonyms.rb
|
708
|
+
- db/migrate/20170118153840_create_search_modules.rb
|
709
|
+
- db/migrate/20170202170310_rename_key_to_name_for_search_indices.rb
|
693
710
|
- lib/tasks/task_extras.rb
|
694
711
|
- lib/tasks/udongo_tasks.rake
|
695
712
|
- lib/udongo.rb
|
@@ -716,6 +733,13 @@ files:
|
|
716
733
|
- lib/udongo/object_path.rb
|
717
734
|
- lib/udongo/pages/tree.rb
|
718
735
|
- lib/udongo/pages/tree_node.rb
|
736
|
+
- lib/udongo/search/backend.rb
|
737
|
+
- lib/udongo/search/base.rb
|
738
|
+
- lib/udongo/search/frontend.rb
|
739
|
+
- lib/udongo/search/result_objects/backend/page.rb
|
740
|
+
- lib/udongo/search/result_objects/base.rb
|
741
|
+
- lib/udongo/search/result_objects/frontend/page.rb
|
742
|
+
- lib/udongo/search/term.rb
|
719
743
|
- lib/udongo/version.rb
|
720
744
|
- lib/udongo/will_paginate/options.rb
|
721
745
|
- lib/udongo/will_paginate/renderer.rb
|
@@ -729,6 +753,7 @@ files:
|
|
729
753
|
- vendor/assets/javascripts/backend/select2.min.js
|
730
754
|
- vendor/assets/javascripts/backend/tagit.min.js
|
731
755
|
- vendor/assets/javascripts/backend/tether.min.js
|
756
|
+
- vendor/assets/javascripts/jquery-ui.autocomplete.html.js
|
732
757
|
- vendor/assets/javascripts/modernizr.js
|
733
758
|
- vendor/assets/stylesheets/backend/bootstrap-datepicker.scss
|
734
759
|
- vendor/assets/stylesheets/backend/jstree/default/style.min.scss
|
@@ -736,6 +761,7 @@ files:
|
|
736
761
|
- vendor/assets/stylesheets/backend/select2.min.scss
|
737
762
|
- vendor/assets/stylesheets/backend/tagit/tagit-autocomplete.css
|
738
763
|
- vendor/assets/stylesheets/backend/tagit/tagit.css
|
764
|
+
- vendor/assets/stylesheets/jquery-ui.theme.min.css
|
739
765
|
homepage: http://udongo.be
|
740
766
|
licenses:
|
741
767
|
- MIT
|
@@ -756,7 +782,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
756
782
|
version: '0'
|
757
783
|
requirements: []
|
758
784
|
rubyforge_project:
|
759
|
-
rubygems_version: 2.
|
785
|
+
rubygems_version: 2.5.1
|
760
786
|
signing_key:
|
761
787
|
specification_version: 4
|
762
788
|
summary: Blimp CMS
|