udongo 3.0.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/backend/application.js +2 -0
  3. data/app/assets/javascripts/backend/search.js +26 -0
  4. data/app/assets/stylesheets/backend/application.scss +2 -0
  5. data/app/controllers/backend/search_controller.rb +5 -0
  6. data/app/controllers/backend/search_synonyms_controller.rb +49 -0
  7. data/app/models/concerns/content_type.rb +5 -1
  8. data/app/models/concerns/flexible_content.rb +6 -0
  9. data/app/models/concerns/publishable.rb +4 -0
  10. data/app/models/concerns/searchable.rb +101 -0
  11. data/app/models/content_column.rb +15 -0
  12. data/app/models/content_text.rb +8 -0
  13. data/app/models/page.rb +1 -0
  14. data/app/models/search_index.rb +5 -0
  15. data/app/models/search_module.rb +10 -0
  16. data/app/models/search_synonym.rb +5 -0
  17. data/app/views/backend/search/_page.html.erb +4 -0
  18. data/app/views/backend/search_synonyms/_form.html.erb +21 -0
  19. data/app/views/backend/search_synonyms/edit.html.erb +4 -0
  20. data/app/views/backend/search_synonyms/index.html.erb +36 -0
  21. data/app/views/backend/search_synonyms/new.html.erb +3 -0
  22. data/app/views/layouts/backend/_top_navigation.html.erb +14 -0
  23. data/changelog.md +12 -0
  24. data/config/locales/en_backend.yml +4 -0
  25. data/config/locales/en_forms.yml +4 -0
  26. data/config/locales/nl_backend.yml +4 -0
  27. data/config/locales/nl_forms.yml +4 -0
  28. data/config/routes.rb +4 -0
  29. data/db/migrate/20170112151235_create_search_indices.rb +13 -0
  30. data/db/migrate/20170117143805_add_indices_to_search_indices.rb +6 -0
  31. data/db/migrate/20170118130428_create_search_synonyms.rb +11 -0
  32. data/db/migrate/20170118153840_create_search_modules.rb +13 -0
  33. data/db/migrate/20170202170310_rename_key_to_name_for_search_indices.rb +5 -0
  34. data/lib/udongo/search/backend.rb +22 -0
  35. data/lib/udongo/search/base.rb +75 -0
  36. data/lib/udongo/search/frontend.rb +23 -0
  37. data/lib/udongo/search/result_objects/backend/page.rb +22 -0
  38. data/lib/udongo/search/result_objects/base.rb +82 -0
  39. data/lib/udongo/search/result_objects/frontend/page.rb +22 -0
  40. data/lib/udongo/search/term.rb +26 -0
  41. data/lib/udongo/version.rb +1 -1
  42. data/readme.md +85 -3
  43. data/vendor/assets/javascripts/jquery-ui.autocomplete.html.js +41 -0
  44. data/vendor/assets/stylesheets/jquery-ui.theme.min.css +5 -0
  45. metadata +29 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 564894abf43269f506001373a3cdc9de4abfd9d1
4
- data.tar.gz: ff827b9591c732706c3b5f19302bc333e059be6d
3
+ metadata.gz: 3c5f900d7751fde5b3875bd0efe6ee7f56bf5488
4
+ data.tar.gz: 347e24cfe05e569e89178a2b3f4fae33325c933f
5
5
  SHA512:
6
- metadata.gz: 5628ca7034fb5091138b1f9646d64ea2dc2c312a6e28714046227ba676e1506b1e04a4c46caf5b6d9975c8451ebf0d3c6e95eb3727e04e63f098355f5e15a454
7
- data.tar.gz: be04a30e21475a52088f149a7bd3316ba15be895070626687f4e8fc2b98bbf8323888d1d9a4e59b3e6a1401cb0998aeddbcc4bacf3bd92e0dd23ea11038b365e
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(); });
@@ -11,6 +11,8 @@
11
11
  * file per style scope.
12
12
  *
13
13
  *= require_self
14
+ *= require jquery-ui/autocomplete
15
+ *= require jquery-ui.theme.min
14
16
  */
15
17
 
16
18
  @import 'bootstrap';
@@ -0,0 +1,5 @@
1
+ class Backend::SearchController < Backend::BaseController
2
+ def query
3
+ render json: Udongo::Search::Backend.new(params[:term], controller: self).search
4
+ end
5
+ end
@@ -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
@@ -7,7 +7,11 @@ module Concerns
7
7
  end
8
8
 
9
9
  def column
10
- ::ContentColumn.where(content_type: self.class.name, content_id: id).take
10
+ ContentColumn.find_by(content: self)
11
+ end
12
+
13
+ def parent
14
+ column.row.rowable
11
15
  end
12
16
  end
13
17
  end
@@ -5,5 +5,11 @@ module Concerns
5
5
  included do
6
6
  has_many :content_rows, as: :rowable, dependent: :destroy
7
7
  end
8
+
9
+ module ClassMethods
10
+ def flexible_content?
11
+ true
12
+ end
13
+ end
8
14
  end
9
15
  end
@@ -18,5 +18,9 @@ module Concerns
18
18
  def published?
19
19
  published_at && published_at.past?
20
20
  end
21
+
22
+ def unpublished?
23
+ !published?
24
+ end
21
25
  end
22
26
  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
@@ -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
@@ -6,6 +6,7 @@ class Page < ApplicationRecord
6
6
  include Concerns::Deletable
7
7
  include Concerns::Draggable
8
8
  include Concerns::FlexibleContent
9
+ include Concerns::Searchable
9
10
 
10
11
  include Concerns::Sortable
11
12
  sortable scope: [:parent_id]
@@ -0,0 +1,5 @@
1
+ class SearchIndex < ApplicationRecord
2
+ belongs_to :searchable, polymorphic: true
3
+
4
+ validates :locale, :name, presence: true
5
+ end
@@ -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,5 @@
1
+ class SearchSynonym < ApplicationRecord
2
+ validates :locale, :term, :synonyms, presence: true
3
+
4
+ validates :term, uniqueness: { case_sensitive: false, scope: :locale }
5
+ end
@@ -0,0 +1,4 @@
1
+ <%= t('b.page') %> — <%= page.title %><br />
2
+ <small>
3
+ <%= truncate(page.description, length: 40) %>
4
+ </small>
@@ -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,4 @@
1
+ <% breadcrumb.add @search_synonym.term %>
2
+ <% breadcrumb.add t('b.edit') %>
3
+ <%= render 'backend/breadcrumbs' %>
4
+ <%= render 'backend/search_synonyms/form', model: @search_synonym %>
@@ -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>&nbsp;</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 %>
@@ -0,0 +1,3 @@
1
+ <% breadcrumb.add t('b.new') %>
2
+ <%= render 'backend/breadcrumbs' %>
3
+ <%= render 'backend/search_synonyms/form', model: @search_synonym %>
@@ -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>
@@ -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
@@ -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
@@ -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
@@ -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 CreateSearchIndices < ActiveRecord::Migration[5.0]
2
+ def change
3
+ create_table :search_indices do |t|
4
+ t.string :searchable_type
5
+ t.integer :searchable_id
6
+ t.string :locale
7
+ t.string :key
8
+ t.text :value
9
+
10
+ t.timestamps
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ class AddIndicesToSearchIndices < ActiveRecord::Migration[5.0]
2
+ def change
3
+ add_index :search_indices, [:searchable_type, :searchable_id]
4
+ add_index :search_indices, [:locale, :key]
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ class CreateSearchSynonyms < ActiveRecord::Migration[5.0]
2
+ def change
3
+ create_table :search_synonyms do |t|
4
+ t.string :locale, index: true
5
+ t.string :term, index: true
6
+ t.text :synonyms
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -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,5 @@
1
+ class RenameKeyToNameForSearchIndices < ActiveRecord::Migration[5.0]
2
+ def change
3
+ rename_column :search_indices, :key, :name
4
+ end
5
+ 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
@@ -1,3 +1,3 @@
1
1
  module Udongo
2
- VERSION = '3.0.0'
2
+ VERSION = '4.0.0'
3
3
  end
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: 3.0.0
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-01-13 00:00:00.000000000 Z
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.6.6
785
+ rubygems_version: 2.5.1
760
786
  signing_key:
761
787
  specification_version: 4
762
788
  summary: Blimp CMS