udongo 7.7.2 → 7.8.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/general.js +8 -0
- data/app/assets/javascripts/backend/redirects.js +61 -0
- data/app/assets/stylesheets/backend/components/_form.scss +4 -0
- data/app/controllers/backend/redirects/test_controller.rb +38 -0
- data/app/controllers/backend/redirects_controller.rb +25 -5
- data/app/controllers/redirects_controller.rb +1 -0
- data/app/decorators/redirect_decorator.rb +23 -0
- data/app/helpers/icon_helper.rb +4 -0
- data/app/helpers/link_helper.rb +7 -7
- data/app/models/redirect.rb +81 -0
- data/app/views/backend/redirects/_card.html.erb +41 -0
- data/app/views/backend/redirects/_filter.html.erb +7 -2
- data/app/views/backend/redirects/_form.html.erb +31 -1
- data/app/views/backend/redirects/_row_columns.html.erb +0 -0
- data/app/views/backend/redirects/_test_modal.html.erb +20 -0
- data/app/views/backend/redirects/index.html.erb +30 -10
- data/app/views/backend/redirects/show.html.erb +16 -0
- data/app/views/backend/redirects/test/resolve.js.erb +9 -0
- data/changelog.md +7 -0
- data/config/locales/en_backend.yml +16 -0
- data/config/locales/en_general.yml +15 -0
- data/config/locales/nl_backend.yml +16 -0
- data/config/locales/nl_general.yml +3 -0
- data/config/routes.rb +7 -1
- data/db/migrate/20181019161330_add_working_to_redirects.rb +5 -0
- data/db/migrate/20181129141539_add_depth_to_redirects.rb +5 -0
- data/db/migrate/20181130093908_rename_depth_to_jumps_for_redirects.rb +5 -0
- data/lib/udongo/redirects/jumps_cacher.rb +19 -0
- data/lib/udongo/redirects/response.rb +33 -0
- data/lib/udongo/redirects/status_badge.rb +33 -0
- data/lib/udongo/redirects/test.rb +21 -0
- data/lib/udongo/redirects/uri_sanitizer.rb +26 -0
- data/lib/udongo/version.rb +1 -1
- metadata +38 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e97f435ff6a84446f7ff5826ace9409c173a7cf2
|
4
|
+
data.tar.gz: 9e6c8e8ab81d2a9f9bd8a8341f8a8eeaaced1c17
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b64db4f43f38e0add637f3242966e148f41479f6b6ccab7fa58af211caf703255f1e98b8998a04ee46195eccd5d01df0725eb01ddd9d587ac561eee02d86b44f
|
7
|
+
data.tar.gz: cf9bfe2b2b2c77c9a34ab67d8a23b90cb6955acea8594a07d7c9baffd4385a6d615d39d4f07c25d9cf75332c6a32f854f2ead954330831a486a40ad76c723e59
|
@@ -3,6 +3,14 @@ var general = general || {
|
|
3
3
|
$('form:not(.no-focus) input:visible:not(.no-focus):first').focus();
|
4
4
|
$('[data-toggle="tooltip"]').tooltip();
|
5
5
|
$('[href="#lity-close"]').on('click', this.lity_close_click_listener);
|
6
|
+
this.bind_remote_content_for_bootstrap_modals();
|
7
|
+
},
|
8
|
+
|
9
|
+
bind_remote_content_for_bootstrap_modals: function() {
|
10
|
+
$('[data-toggle="modal"]').on('click', function() {
|
11
|
+
var anchor = $(this);
|
12
|
+
$(anchor.data('target') + ' .modal-body').load(anchor.data('remote'));
|
13
|
+
});
|
6
14
|
},
|
7
15
|
|
8
16
|
lity_close_click_listener: function(e) {
|
@@ -0,0 +1,61 @@
|
|
1
|
+
var redirects = redirects || {
|
2
|
+
trigger_button: null,
|
3
|
+
containers: null,
|
4
|
+
|
5
|
+
init: function() {
|
6
|
+
redirects.bind_test_button();
|
7
|
+
$('#test-redirect-modal').on('shown.bs.modal', redirects.bind_test_button);
|
8
|
+
},
|
9
|
+
|
10
|
+
bind_test_button: function() {
|
11
|
+
redirects.trigger_button = $('#test-redirects');
|
12
|
+
redirects.prepare_containers();
|
13
|
+
redirects.trigger_button.off('click').on('click', redirects.trigger_button_click_listener);
|
14
|
+
},
|
15
|
+
|
16
|
+
mark_card_as_processing: function(card) {
|
17
|
+
card.removeClass('alert-danger');
|
18
|
+
card.removeClass('alert-success');
|
19
|
+
card.removeClass('alert-info');
|
20
|
+
card.addClass('alert-warning');
|
21
|
+
card.find('.card-icon').attr('hidden', true);
|
22
|
+
card.find('.spinner-icon').attr('hidden', false);
|
23
|
+
},
|
24
|
+
|
25
|
+
prepare_containers: function() {
|
26
|
+
redirects.containers = $('article.redirect').toArray();
|
27
|
+
redirects.trigger_button.removeClass('disabled');
|
28
|
+
$('[data-toggle="tooltip"]').tooltip();
|
29
|
+
},
|
30
|
+
|
31
|
+
process_next_container: function() {
|
32
|
+
var container = redirects.containers.shift();
|
33
|
+
redirects.run_for_container($(container));
|
34
|
+
},
|
35
|
+
|
36
|
+
run_for_container: function(container) {
|
37
|
+
var card = container.find('.card');
|
38
|
+
redirects.mark_card_as_processing(card);
|
39
|
+
|
40
|
+
// This fixes an issue with code somehow calling this method a fourth time
|
41
|
+
// when there are only 3 items in this.containers.
|
42
|
+
if(container.length == 0) return;
|
43
|
+
|
44
|
+
$.ajax({
|
45
|
+
url: container.data('path'),
|
46
|
+
type: 'POST'
|
47
|
+
}).done(function(data){
|
48
|
+
redirects.process_next_container(container);
|
49
|
+
});
|
50
|
+
},
|
51
|
+
|
52
|
+
trigger_button_click_listener: function(e) {
|
53
|
+
e.preventDefault();
|
54
|
+
var anchor = $(this);
|
55
|
+
anchor.addClass('disabled');
|
56
|
+
|
57
|
+
redirects.process_next_container();
|
58
|
+
}
|
59
|
+
};
|
60
|
+
|
61
|
+
$(function(){ redirects.init(); });
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class Backend::Redirects::TestController < Backend::BaseController
|
2
|
+
before_action :find_model
|
3
|
+
|
4
|
+
# The follow_location param determines the difference between a redirect trace
|
5
|
+
# that runs through every nested redirect and one that stops at the first
|
6
|
+
# redirect in the trace.
|
7
|
+
# (A nested redirect meaning that the destination of one redirect is the
|
8
|
+
# source of another)
|
9
|
+
def resolve
|
10
|
+
respond_to do |format|
|
11
|
+
format.js do
|
12
|
+
# The last redirect in a chain should always follow its location through,
|
13
|
+
# because that's the only way afaik to retrieve 404/500 and other faulty
|
14
|
+
# status codes, instead of the default 301.
|
15
|
+
vars = { base_url: request.base_url, follow_location: @redirect.next_in_chain.blank? }
|
16
|
+
|
17
|
+
if @redirect.resolves?(vars)
|
18
|
+
@redirect.working!
|
19
|
+
else
|
20
|
+
@redirect.broken!
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def find_model
|
29
|
+
@redirect ||= Redirect.find(params[:id]).decorate
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_notice(key)
|
33
|
+
t("b.msg.redirects.#{key}",
|
34
|
+
source: @redirect.source_uri,
|
35
|
+
destination: @redirect.destination_uri
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
@@ -2,11 +2,12 @@ class Backend::RedirectsController < Backend::BaseController
|
|
2
2
|
include Concerns::PaginationController
|
3
3
|
|
4
4
|
before_action -> { breadcrumb.add t('b.redirects'), backend_redirects_path }
|
5
|
-
before_action :find_model,
|
5
|
+
before_action :find_model, except: [:index, :new, :create]
|
6
6
|
|
7
7
|
def index
|
8
8
|
@search = Redirect.ransack params[:q]
|
9
|
-
|
9
|
+
results = @search.result(distinct: true).order('times_used DESC')
|
10
|
+
@redirects = paginate(results).decorate
|
10
11
|
end
|
11
12
|
|
12
13
|
def new
|
@@ -17,6 +18,7 @@ class Backend::RedirectsController < Backend::BaseController
|
|
17
18
|
@redirect = Redirect.new(allowed_params).decorate
|
18
19
|
|
19
20
|
if @redirect.save
|
21
|
+
@redirect.jumps_cacher.cache!
|
20
22
|
redirect_to backend_redirects_path, notice: translate_notice(:added, :redirect)
|
21
23
|
else
|
22
24
|
render :new
|
@@ -25,17 +27,35 @@ class Backend::RedirectsController < Backend::BaseController
|
|
25
27
|
|
26
28
|
def destroy
|
27
29
|
@redirect.destroy
|
28
|
-
|
30
|
+
@redirect.jumps_cacher.cache! unless @redirect.jumps_cacher.already_on_top?
|
31
|
+
redirect_to backend_redirects_path(search_params), notice: translate_notice(:deleted, :redirect)
|
32
|
+
end
|
33
|
+
|
34
|
+
def show
|
35
|
+
render layout: false
|
36
|
+
end
|
37
|
+
|
38
|
+
def edit
|
39
|
+
session['redirect_search_params'] = search_params
|
29
40
|
end
|
30
41
|
|
31
42
|
def update
|
32
43
|
if @redirect.update_attributes allowed_params
|
33
|
-
|
44
|
+
@redirect.jumps_cacher.cache!
|
45
|
+
redirect_to backend_redirects_path(session[:redirect_search_params]), notice: translate_notice(:edited, :redirect)
|
34
46
|
else
|
35
47
|
render :edit
|
36
48
|
end
|
37
49
|
end
|
38
50
|
|
51
|
+
def search_params
|
52
|
+
hash = { q: nil }
|
53
|
+
hash[:q] = params[:q].to_hash if params[:q]
|
54
|
+
hash[:page] = params[:page] if params[:page]
|
55
|
+
hash
|
56
|
+
end
|
57
|
+
helper_method :search_params
|
58
|
+
|
39
59
|
private
|
40
60
|
|
41
61
|
def allowed_params
|
@@ -45,6 +65,6 @@ class Backend::RedirectsController < Backend::BaseController
|
|
45
65
|
end
|
46
66
|
|
47
67
|
def find_model
|
48
|
-
@redirect
|
68
|
+
@redirect ||= Redirect.find(params[:id]).decorate
|
49
69
|
end
|
50
70
|
end
|
@@ -1,6 +1,10 @@
|
|
1
1
|
class RedirectDecorator < ApplicationDecorator
|
2
2
|
delegate_all
|
3
3
|
|
4
|
+
def status_badge
|
5
|
+
@status_badge ||= Udongo::Redirects::StatusBadge.new(h, object)
|
6
|
+
end
|
7
|
+
|
4
8
|
def status_code_collection
|
5
9
|
%w(301 303 307).map do |code|
|
6
10
|
[I18n.t("b.msg.status_codes.#{code}"), code]
|
@@ -10,4 +14,23 @@ class RedirectDecorator < ApplicationDecorator
|
|
10
14
|
def summary
|
11
15
|
"#{I18n.t('b.from')} #{object.source_uri} #{I18n.t('b.to').downcase} #{object.destination_uri}"
|
12
16
|
end
|
17
|
+
|
18
|
+
def tooltip_identifier
|
19
|
+
return :untested if working.nil?
|
20
|
+
return :working if working?
|
21
|
+
:broken
|
22
|
+
end
|
23
|
+
|
24
|
+
def tooltip_text
|
25
|
+
h.t("b.msg.redirects.tooltips.#{tooltip_identifier}")
|
26
|
+
end
|
27
|
+
|
28
|
+
def truncated_uri(type, length: 50)
|
29
|
+
value = object.send(type)
|
30
|
+
|
31
|
+
return value if value.length <= length
|
32
|
+
h.content_tag :span, data: { toggle: 'tooltip' }, title: value do
|
33
|
+
h.truncate(value, length: length)
|
34
|
+
end
|
35
|
+
end
|
13
36
|
end
|
data/app/helpers/icon_helper.rb
CHANGED
data/app/helpers/link_helper.rb
CHANGED
@@ -13,15 +13,15 @@ module LinkHelper
|
|
13
13
|
def link_to_edit_with_label(value, locale)
|
14
14
|
link_to(
|
15
15
|
object_label(value, locale),
|
16
|
-
path_from_string_or_object(value, 'edit_'),
|
16
|
+
path_from_string_or_object(value, prefix: 'edit_'),
|
17
17
|
title: t('b.edit')
|
18
18
|
)
|
19
19
|
end
|
20
20
|
|
21
|
-
def link_to_edit(value)
|
21
|
+
def link_to_edit(value, parameters = {})
|
22
22
|
link_to(
|
23
23
|
icon(:pencil_square_o),
|
24
|
-
path_from_string_or_object(value, 'edit_'),
|
24
|
+
path_from_string_or_object(value, prefix: 'edit_', parameters: parameters),
|
25
25
|
title: t('b.edit')
|
26
26
|
)
|
27
27
|
end
|
@@ -33,21 +33,21 @@ module LinkHelper
|
|
33
33
|
link_to_edit(url)
|
34
34
|
end
|
35
35
|
|
36
|
-
def link_to_delete(value)
|
36
|
+
def link_to_delete(value, parameters = {})
|
37
37
|
link_to(
|
38
38
|
icon(:trash),
|
39
|
-
path_from_string_or_object(value),
|
39
|
+
path_from_string_or_object(value, parameters: parameters),
|
40
40
|
method: :delete,
|
41
41
|
data: { confirm: t('b.msg.confirm') },
|
42
42
|
title: t('b.delete')
|
43
43
|
)
|
44
44
|
end
|
45
45
|
|
46
|
-
def path_from_string_or_object(value, prefix
|
46
|
+
def path_from_string_or_object(value, prefix: nil, parameters: {})
|
47
47
|
return value if value.is_a?(String)
|
48
48
|
|
49
49
|
str = "#{prefix}#{Udongo::ObjectPath.find(value)}"
|
50
|
-
send(str, *Udongo::ObjectPath.remove_symbols(value))
|
50
|
+
send(str, *Udongo::ObjectPath.remove_symbols(value), parameters)
|
51
51
|
end
|
52
52
|
|
53
53
|
def object_label(value, locale)
|
data/app/models/redirect.rb
CHANGED
@@ -1,16 +1,97 @@
|
|
1
1
|
class Redirect < ApplicationRecord
|
2
|
+
scope :working, -> { where(working: true) }
|
3
|
+
scope :broken, -> { where('working IS NULL or working = 0') }
|
2
4
|
scope :disabled, -> { where(disabled: true) }
|
3
5
|
scope :enabled, -> { where('disabled IS NULL or disabled = 0') }
|
4
6
|
|
5
7
|
validates :source_uri, :destination_uri, :status_code, presence: true
|
6
8
|
validates :source_uri, uniqueness: { case_sensitive: false }
|
7
9
|
|
10
|
+
# Sanitizing at this point makes sure the uniqueness validation keeps working.
|
11
|
+
before_validation do
|
12
|
+
self.source_uri = convert_to_uri_path(sanitize_uri(source_uri))
|
13
|
+
self.destination_uri = sanitize_uri(destination_uri)
|
14
|
+
end
|
15
|
+
|
16
|
+
def broken?
|
17
|
+
!working?
|
18
|
+
end
|
19
|
+
|
20
|
+
def broken!
|
21
|
+
update_attribute(:working, false)
|
22
|
+
end
|
23
|
+
|
24
|
+
def cache_jumps!
|
25
|
+
update_attribute(:jumps, calculate_jumps)
|
26
|
+
end
|
27
|
+
|
28
|
+
def calculate_jumps(total = 0)
|
29
|
+
total += 1
|
30
|
+
return next_in_chain.calculate_jumps(total) if next_in_chain.present?
|
31
|
+
total
|
32
|
+
end
|
33
|
+
|
34
|
+
def jumps_cacher
|
35
|
+
@jumps_cacher ||= Udongo::Redirects::JumpsCacher.new(self)
|
36
|
+
end
|
37
|
+
|
8
38
|
def enabled?
|
9
39
|
!disabled?
|
10
40
|
end
|
11
41
|
|
42
|
+
def next_in_chain
|
43
|
+
self.class.enabled.find_by(source_uri: destination_uri)
|
44
|
+
end
|
45
|
+
|
46
|
+
def previous_in_chain
|
47
|
+
self.class.enabled.find_by(destination_uri: source_uri)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Tests the redirect including any redirects following this one.
|
51
|
+
def resolves?(base_url: Udongo.config.base.host, follow_location: true)
|
52
|
+
response = Udongo::Redirects::Test.new(self).perform!(
|
53
|
+
base_url: base_url,
|
54
|
+
follow_location: follow_location
|
55
|
+
)
|
56
|
+
|
57
|
+
response.success? && response.endpoint_matches?(base_url + destination_uri)
|
58
|
+
end
|
59
|
+
|
60
|
+
# This builds a list of all redirects following the current one in its
|
61
|
+
# progression path. Includes the current redirect as the first item.
|
62
|
+
# See #next_in_chain for the trace conditions.
|
63
|
+
def trace_down(stack = [])
|
64
|
+
stack << self
|
65
|
+
return next_in_chain.trace_down(stack) if next_in_chain.present?
|
66
|
+
stack
|
67
|
+
end
|
68
|
+
|
69
|
+
# This builds a list of all redirects prior to the current one in its
|
70
|
+
# progression path. Includes the current redirect as the first item.
|
71
|
+
# See #previous_in_chain for the trace conditions.
|
72
|
+
def trace_up(stack = [])
|
73
|
+
stack << self
|
74
|
+
return previous_in_chain.trace_up(stack) if previous_in_chain.present?
|
75
|
+
stack.reverse # Reversing because the top most one will be at the end.
|
76
|
+
end
|
77
|
+
|
12
78
|
def used!
|
13
79
|
count = self.times_used.nil? ? 1 : times_used + 1
|
14
80
|
update_attribute :times_used, count
|
15
81
|
end
|
82
|
+
|
83
|
+
def working!
|
84
|
+
update_attribute(:working, true)
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def convert_to_uri_path(value)
|
90
|
+
URI::parse(value.to_s).path
|
91
|
+
end
|
92
|
+
|
93
|
+
def sanitize_uri(value)
|
94
|
+
return if value.blank?
|
95
|
+
Udongo::Redirects::UriSanitizer.new(value).sanitize!
|
96
|
+
end
|
16
97
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
<div class="card p-3 alert-<%= r.status_badge.css_class %>">
|
2
|
+
<div class="card-body">
|
3
|
+
<div class="card-text">
|
4
|
+
|
5
|
+
<div class="row">
|
6
|
+
<div class="col-1">
|
7
|
+
<span class="card-icon">
|
8
|
+
<%= r.status_badge.icon %>
|
9
|
+
</span>
|
10
|
+
<span class="spinner-icon" hidden>
|
11
|
+
<%= spinner_icon %>
|
12
|
+
</span>
|
13
|
+
</div>
|
14
|
+
|
15
|
+
<div class="col-md-7 col-sm-11">
|
16
|
+
<p class="text-center">
|
17
|
+
<strong><%= r.source_uri %></strong><br>
|
18
|
+
<large><%= icon :long_arrow_down %></large><br>
|
19
|
+
<strong><%= r.destination_uri %></strong>
|
20
|
+
</p>
|
21
|
+
</div>
|
22
|
+
|
23
|
+
<div class="col-md-4 col-sm-11">
|
24
|
+
<ul class="list-unstyled m-0 text-sm-left text-md-right">
|
25
|
+
<li>
|
26
|
+
<%= link_to backend_redirect_path(r), class: 'btn btn-secondary btn-sm', method: 'DELETE', data: { confirm: t('b.msg.confirm') } do %>
|
27
|
+
<%= icon(:trash) %>
|
28
|
+
<%= t('b.delete') %>
|
29
|
+
<% end %>
|
30
|
+
|
31
|
+
<%= link_to edit_backend_redirect_path(r), class: 'btn btn-primary btn-sm' do %>
|
32
|
+
<%= icon(:edit) %>
|
33
|
+
<%= t('b.edit') %>
|
34
|
+
<% end %>
|
35
|
+
</li>
|
36
|
+
</ul>
|
37
|
+
</div>
|
38
|
+
</div>
|
39
|
+
</div>
|
40
|
+
</div>
|
41
|
+
</div>
|
@@ -2,18 +2,23 @@
|
|
2
2
|
<div class="card-block">
|
3
3
|
<%= search_form_for [:backend, @search] do |f| %>
|
4
4
|
<div class="row">
|
5
|
-
<div class="col-md-
|
5
|
+
<div class="col-md-4 form-group">
|
6
6
|
<%= f.search_field :source_uri_cont, placeholder: t('b.source'), class: 'form-control' %>
|
7
7
|
</div>
|
8
8
|
|
9
|
-
<div class="col-md-
|
9
|
+
<div class="col-md-4 form-group">
|
10
10
|
<%= f.search_field :destination_uri_cont, placeholder: t('b.destination'), class: 'form-control' %>
|
11
11
|
</div>
|
12
|
+
|
13
|
+
<div class="col-md-4 form-group">
|
14
|
+
<%= f.select :working_eq, options_for_collection(:working, [true, false]), { include_blank: t('b.status') }, class: 'form-control' %>
|
15
|
+
</div>
|
12
16
|
</div>
|
13
17
|
|
14
18
|
<div class="row">
|
15
19
|
<div class="col-md-6">
|
16
20
|
<%= f.submit class: 'btn btn-primary btn-sm' %>
|
21
|
+
<%= link_to t('b.reset'), backend_redirects_path, class: 'btn btn-link btn-sm' %>
|
17
22
|
</div>
|
18
23
|
</div>
|
19
24
|
<% end %>
|
@@ -1,5 +1,29 @@
|
|
1
|
+
<% javascript 'backend/redirects' %>
|
2
|
+
|
1
3
|
<%= render 'backend/general_form_error', object: model %>
|
2
4
|
|
5
|
+
<% unless @redirect.new_record? %>
|
6
|
+
<div class="card">
|
7
|
+
<div class="card-header">
|
8
|
+
<%= t('b.msg.redirects.test_header', source: @redirect.source_uri) %>
|
9
|
+
</div>
|
10
|
+
<div class="card-body p-3">
|
11
|
+
<p>
|
12
|
+
<%= link_to '#', class: 'btn btn-primary', id: 'test-redirects', role: 'button' do %>
|
13
|
+
<%= icon :sitemap %>
|
14
|
+
<%= t('b.msg.redirects.test') %>
|
15
|
+
<% end %>
|
16
|
+
</p>
|
17
|
+
|
18
|
+
<% @redirect.trace_down.each do |r| %>
|
19
|
+
<article class="redirect" data-id="<%= r.id %>" data-path="<%= resolve_backend_test_redirect_path(r) %>">
|
20
|
+
<%= render 'backend/redirects/card', r: r.decorate %>
|
21
|
+
</article>
|
22
|
+
<% end %>
|
23
|
+
</div>
|
24
|
+
</div>
|
25
|
+
<% end %>
|
26
|
+
|
3
27
|
<%= simple_form_for [:backend, model] do |f| %>
|
4
28
|
<div class="row">
|
5
29
|
<div class="col-md-6">
|
@@ -34,5 +58,11 @@
|
|
34
58
|
</div>
|
35
59
|
</div>
|
36
60
|
|
37
|
-
|
61
|
+
<div class="form-actions">
|
62
|
+
<button type="submit" class="btn btn-primary"><%= t 'b.save' %></button>
|
63
|
+
<% if model.id %>
|
64
|
+
<%= link_to t('b.delete'), backend_redirect_path(model), class: 'btn btn-secondary', method: 'DELETE', data: { confirm: t('b.msg.confirm') } %>
|
65
|
+
<% end %>
|
66
|
+
<%= link_to t('b.cancel'), backend_redirects_path, class: 'btn btn-link' %>
|
67
|
+
</div>
|
38
68
|
<% end %>
|
File without changes
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<div class="modal fade" id="test-redirect-modal" tabindex="-1" role="dialog">
|
2
|
+
<div class="modal-dialog modal-lg" role="document">
|
3
|
+
<div class="modal-content">
|
4
|
+
<div class="modal-header">
|
5
|
+
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
6
|
+
<span aria-hidden="true">×</span>
|
7
|
+
</button>
|
8
|
+
</div>
|
9
|
+
<div class="modal-body">
|
10
|
+
</div>
|
11
|
+
<div class="modal-footer">
|
12
|
+
<button type="button" class="btn btn-secondary" data-dismiss="modal"><%= t('b.close') %></button>
|
13
|
+
<%= link_to '#', class: 'btn btn-primary', id: 'test-redirects', role: 'button' do %>
|
14
|
+
<%= icon :sitemap %>
|
15
|
+
<%= t('b.msg.redirects.test') %>
|
16
|
+
<% end %>
|
17
|
+
</div>
|
18
|
+
</div>
|
19
|
+
</div>
|
20
|
+
</div>
|
@@ -1,34 +1,52 @@
|
|
1
|
+
<% javascript 'backend/redirects' %>
|
2
|
+
|
1
3
|
<%= render 'backend/breadcrumbs' %>
|
2
|
-
<%= render 'filter' %>
|
4
|
+
<%= render 'backend/redirects/filter' %>
|
3
5
|
|
4
6
|
<p class="text-right">
|
5
7
|
<%= link_to icon(:plus, t('b.add')), new_backend_redirect_path, class: 'btn btn-primary btn-sm' %>
|
6
8
|
</p>
|
7
9
|
|
8
10
|
<% if @redirects.any? %>
|
9
|
-
<table class="table table-
|
11
|
+
<table class="table table-hover">
|
10
12
|
<thead class="thead-inverse">
|
11
13
|
<tr>
|
14
|
+
<th><%= t 'b.status' %></th>
|
15
|
+
<th><%= t 'b.jumps' %></th>
|
12
16
|
<th><%= t 'b.source' %></th>
|
13
17
|
<th><%= t 'b.destination' %></th>
|
14
|
-
<th><%= t 'b.
|
15
|
-
<th><%= t 'b.enabled' %></th>
|
18
|
+
<th><%= t 'b.active' %></th>
|
16
19
|
<th><%= t 'b.used' %></th>
|
17
20
|
<th> </th>
|
18
21
|
</tr>
|
19
22
|
</thead>
|
20
23
|
|
24
|
+
<tfoot>
|
25
|
+
<tr>
|
26
|
+
<td colspan="7">
|
27
|
+
<%= t('b.msg.possible_statuses') %>:
|
28
|
+
<span class="badge badge-success">Werkend</span>
|
29
|
+
<span class="badge badge-danger">Stuk</span>
|
30
|
+
<span class="badge badge-info">Ongetest</span>
|
31
|
+
</td>
|
32
|
+
</tr>
|
33
|
+
</tfoot>
|
34
|
+
|
21
35
|
<tbody>
|
22
36
|
<% @redirects.each do |r| %>
|
23
37
|
<tr>
|
24
|
-
<td><%= r.
|
25
|
-
<td><%= r.
|
26
|
-
<td><%= r.
|
38
|
+
<td><%= r.status_badge.html(data: { toggle: 'tooltip' }, title: r.tooltip_text) %></td>
|
39
|
+
<td><%= r.jumps %></td>
|
40
|
+
<td><%= r.truncated_uri(:source_uri) %></td>
|
41
|
+
<td><%= r.truncated_uri(:destination_uri) %></td>
|
27
42
|
<td><%= t r.enabled?.to_s %></td>
|
28
|
-
<td
|
43
|
+
<td data-toggle="tooltip" title="<%= t('b.last_used_at', timestamp: l(r.updated_at, format: :short)) %>">
|
44
|
+
<%= r.times_used.to_i %>
|
45
|
+
</td>
|
29
46
|
<td class="text-right">
|
30
|
-
<%=
|
31
|
-
<%=
|
47
|
+
<%= link_to icon(:sitemap), '#', data: { toggle: 'modal', backdrop: 'static', target: '#test-redirect-modal', remote: backend_redirect_path(r) }, title: t('b.msg.redirects.title_tag') %>
|
48
|
+
<%= link_to_edit [:backend, r], request.query_parameters %>
|
49
|
+
<%= link_to_delete [:backend, r], request.query_parameters %>
|
32
50
|
</td>
|
33
51
|
</tr>
|
34
52
|
<% end %>
|
@@ -39,3 +57,5 @@
|
|
39
57
|
<% else %>
|
40
58
|
<p><%= t 'b.msg.no_items' %></p>
|
41
59
|
<% end %>
|
60
|
+
|
61
|
+
<%= render 'backend/redirects/test_modal' %>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<% javascript 'backend/redirects' %>
|
2
|
+
|
3
|
+
<h5 class="mb-3"><%= t('b.msg.redirects.test_header', source: @redirect.source_uri) %>:</h5>
|
4
|
+
|
5
|
+
<% @redirect.trace_down.each_with_index do |r, index| %>
|
6
|
+
<div class="row">
|
7
|
+
<div class="col-1">
|
8
|
+
<%= index + 1 %>.
|
9
|
+
</div>
|
10
|
+
<div class="col-11">
|
11
|
+
<article class="redirect" data-id="<%= r.id %>" data-path="<%= resolve_backend_test_redirect_path(r) %>">
|
12
|
+
<%= render 'backend/redirects/card', r: r.decorate %>
|
13
|
+
</article>
|
14
|
+
</div>
|
15
|
+
</div>
|
16
|
+
<% end %>
|
@@ -0,0 +1,9 @@
|
|
1
|
+
var container = $('[data-id=<%= @redirect.id %>]');
|
2
|
+
container.html('<%= j render('backend/redirects/card', r: @redirect) %>');
|
3
|
+
if(redirects.containers.length == 0) {
|
4
|
+
// Using plenty of timeout or the JS will refill the containers too quick,
|
5
|
+
// causing an infinite loop of redirect tests.
|
6
|
+
setTimeout(function(){
|
7
|
+
redirects.prepare_containers();
|
8
|
+
}, 2000);
|
9
|
+
}
|
data/changelog.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
7.8.0 - 2018-10-26
|
2
|
+
--
|
3
|
+
* Fixes issue with blank search terms giving an unwanted AR exception.
|
4
|
+
* Redesigned the Redirects module, with a way to test and resolve redirects.
|
5
|
+
* Adjusted ```LinkHelper#link_to_{edit,destroy}``` to use named parameters.
|
6
|
+
|
7
|
+
|
1
8
|
7.7.2 - 2018-10-04
|
2
9
|
--
|
3
10
|
* Fixes issue with blank search terms giving an unwanted AR exception.
|
@@ -20,6 +20,7 @@ en:
|
|
20
20
|
custom_title: Custom title
|
21
21
|
custom_url: Custom URL
|
22
22
|
delete: Delete
|
23
|
+
depth: Depth
|
23
24
|
description: Description
|
24
25
|
destination: Destination
|
25
26
|
down: Down
|
@@ -50,7 +51,9 @@ en:
|
|
50
51
|
html_content: HTML content
|
51
52
|
image: Image
|
52
53
|
images: Images
|
54
|
+
jumps: Jumps
|
53
55
|
last_changed_at: Last changed at
|
56
|
+
last_used_at: Last used at %{timestamp}
|
54
57
|
last_name: Last name
|
55
58
|
locale: Locale
|
56
59
|
login: Login
|
@@ -73,6 +76,7 @@ en:
|
|
73
76
|
recipient: Recipient
|
74
77
|
redirect: Redirect
|
75
78
|
redirects: Redirects
|
79
|
+
reset: Reset
|
76
80
|
save: Save
|
77
81
|
search: Search
|
78
82
|
search_term: Search term
|
@@ -95,6 +99,7 @@ en:
|
|
95
99
|
tag: Tag
|
96
100
|
tags: Tags
|
97
101
|
telephone: Telephone
|
102
|
+
test_redirect: Test redirect
|
98
103
|
times_used: Times used
|
99
104
|
title: Title
|
100
105
|
to: To
|
@@ -176,6 +181,17 @@ en:
|
|
176
181
|
pages:
|
177
182
|
invisible: Deze pagina is niet zichtbaar op de website.
|
178
183
|
please_select_a_valid_image: Please select a valid image and try again.
|
184
|
+
possible_statuses: Possible statuses
|
185
|
+
redirects:
|
186
|
+
test: Test these redirects
|
187
|
+
test_header: All redirects starting from %{source}
|
188
|
+
broken: The redirect from %{source} to %{destination} does not work.
|
189
|
+
title_tag: Test this redirect
|
190
|
+
tooltips:
|
191
|
+
untested: This redirect is untested.
|
192
|
+
broken: This redirect does not work.
|
193
|
+
working: This redirect works.
|
194
|
+
works: The redirect from %{source} to %{destination} works.
|
179
195
|
saved: '%{actor} was saved.'
|
180
196
|
search_terms:
|
181
197
|
deleted: The list with used search terms was cleared.
|
@@ -20,6 +20,7 @@ nl:
|
|
20
20
|
custom_title: Aangepaste titel
|
21
21
|
custom_url: Aangepaste URL
|
22
22
|
delete: Verwijder
|
23
|
+
depth: Diepte
|
23
24
|
description: Beschrijving
|
24
25
|
destination: Bestemming
|
25
26
|
down: Omlaag
|
@@ -52,7 +53,9 @@ nl:
|
|
52
53
|
image_collections: Afbeeldingsgroepen
|
53
54
|
image: Afbeelding
|
54
55
|
images: Afbeeldingen
|
56
|
+
jumps: Sprongen
|
55
57
|
last_changed_at: Laatst gewijzigd op
|
58
|
+
last_used_at: Laatst gebruikt op %{timestamp}
|
56
59
|
last_name: Achternaam
|
57
60
|
locale: Taal
|
58
61
|
login: Inloggen
|
@@ -75,6 +78,7 @@ nl:
|
|
75
78
|
recipient: Bestemmeling
|
76
79
|
redirect: Redirect
|
77
80
|
redirects: Redirects
|
81
|
+
reset: Resetten
|
78
82
|
save: Opslaan
|
79
83
|
search: Zoeken
|
80
84
|
search_term: Zoekterm
|
@@ -97,6 +101,7 @@ nl:
|
|
97
101
|
tag: Tag
|
98
102
|
tags: Tags
|
99
103
|
telephone: Telefoonnummer
|
104
|
+
test_redirect: Redirect testen
|
100
105
|
times_used: Aantal keer gebruikt
|
101
106
|
title: Titel
|
102
107
|
to: Naar
|
@@ -178,6 +183,17 @@ nl:
|
|
178
183
|
pages:
|
179
184
|
invisible: Deze pagina is niet zichtbaar op de website.
|
180
185
|
please_select_a_valid_image: Gelieve een geldige afbeelding te selecteren en opnieuw te proberen.
|
186
|
+
possible_statuses: Mogelijke statussen
|
187
|
+
redirects:
|
188
|
+
test: Test deze redirects
|
189
|
+
test_header: Alle redirect sprongen vertrekkende van %{source}
|
190
|
+
broken: De redirect van %{source} naar %{destination} werkt niet.
|
191
|
+
title_tag: Test deze redirect
|
192
|
+
tooltips:
|
193
|
+
untested: Deze redirect is nog niet getest.
|
194
|
+
broken: Deze redirect werkt niet.
|
195
|
+
working: Deze redirect werkt.
|
196
|
+
works: De redirect van %{source} naar %{destination} werkt.
|
181
197
|
saved: '%{actor} werd bewaard.'
|
182
198
|
search_terms:
|
183
199
|
deleted: De lijst met gebruikte zoektermen werd gewist
|
data/config/routes.rb
CHANGED
@@ -23,7 +23,13 @@ Rails.application.routes.draw do
|
|
23
23
|
collection { delete '/', action: 'destroy_all' }
|
24
24
|
end
|
25
25
|
resources :users
|
26
|
-
resources :redirects
|
26
|
+
resources :redirects do
|
27
|
+
member do
|
28
|
+
scope 'test', controller: 'redirects/test', as: :test do
|
29
|
+
post 'resolve'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
27
33
|
resources :search_synonyms, except: :show
|
28
34
|
resources :tags do
|
29
35
|
concerns :translatable
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Udongo::Redirects
|
2
|
+
class JumpsCacher
|
3
|
+
def initialize(redirect)
|
4
|
+
@redirect = redirect
|
5
|
+
end
|
6
|
+
|
7
|
+
def cache!
|
8
|
+
top_most_redirect.trace_down.each(&:cache_jumps!)
|
9
|
+
end
|
10
|
+
|
11
|
+
def top_most_redirect
|
12
|
+
@redirect.trace_up.first
|
13
|
+
end
|
14
|
+
|
15
|
+
def already_on_top?
|
16
|
+
@redirect == top_most_redirect
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Udongo::Redirects
|
2
|
+
class Response
|
3
|
+
def initialize(response)
|
4
|
+
@response = response
|
5
|
+
end
|
6
|
+
|
7
|
+
def endpoint_matches?(destination)
|
8
|
+
sanitize_destination(destination) == headers['Location']
|
9
|
+
end
|
10
|
+
|
11
|
+
# Apparently Curb does not provide parsed headers... A bit sad.
|
12
|
+
# TODO: Handle multiple location headers so #endpoint_matches? can succeed.
|
13
|
+
# For now, the last location header is returned as a value.
|
14
|
+
def headers
|
15
|
+
list = @response.header_str.split(/[\r\n]+/).map(&:strip)
|
16
|
+
Hash[list.flat_map{ |s| s.scan(/^(\S+): (.+)/) }]
|
17
|
+
end
|
18
|
+
|
19
|
+
def raw
|
20
|
+
@response
|
21
|
+
end
|
22
|
+
|
23
|
+
def sanitize_destination(destination)
|
24
|
+
destination.to_s
|
25
|
+
.gsub('/?', '?')
|
26
|
+
.chomp('/')
|
27
|
+
end
|
28
|
+
|
29
|
+
def success?
|
30
|
+
['200 OK', '301 Moved Permanently'].include?(@response.status)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Udongo::Redirects
|
2
|
+
class StatusBadge
|
3
|
+
attr_reader :view
|
4
|
+
|
5
|
+
# TODO: find out why Module.takes does not override in an engine env.
|
6
|
+
def initialize(view, redirect)
|
7
|
+
@view = view
|
8
|
+
@redirect = redirect
|
9
|
+
end
|
10
|
+
|
11
|
+
def css_class
|
12
|
+
return 'success' if @redirect.working?
|
13
|
+
return 'info' if @redirect.working.nil?
|
14
|
+
'danger'
|
15
|
+
end
|
16
|
+
|
17
|
+
def html(attributes = {})
|
18
|
+
attributes.reverse_merge!(class: "badge badge-#{css_class}")
|
19
|
+
@view.content_tag(:span, @redirect.status_code, attributes)
|
20
|
+
end
|
21
|
+
|
22
|
+
def icon
|
23
|
+
return '' unless icon_identifier
|
24
|
+
@view.icon(icon_identifier)
|
25
|
+
end
|
26
|
+
|
27
|
+
def icon_identifier
|
28
|
+
return :question_circle if @redirect.working.nil?
|
29
|
+
return :check_circle if @redirect.working?
|
30
|
+
:times_circle
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'curb'
|
2
|
+
|
3
|
+
module Udongo::Redirects
|
4
|
+
class Test
|
5
|
+
def initialize(redirect)
|
6
|
+
@redirect = redirect
|
7
|
+
end
|
8
|
+
|
9
|
+
# follow_location means curl can find out if the eventual endpoint has
|
10
|
+
# a 200, a 404, a 500, etc,... It's a bit slower, but it's more reliable.
|
11
|
+
# (You don't want to have an OK for a 301 when that 301 leads to a 404)
|
12
|
+
def perform!(base_url: Udongo.config.base.host, follow_location: true)
|
13
|
+
response = Curl::Easy.perform(base_url + @redirect.source_uri) do |curl|
|
14
|
+
curl.head = true
|
15
|
+
curl.follow_location = follow_location
|
16
|
+
end
|
17
|
+
|
18
|
+
Response.new(response)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Udongo::Redirects
|
2
|
+
class UriSanitizer
|
3
|
+
def initialize(uri)
|
4
|
+
@uri = uri
|
5
|
+
end
|
6
|
+
|
7
|
+
def sanitize!
|
8
|
+
result = strip_whitespace(@uri)
|
9
|
+
result = remove_leading_slashes(result)
|
10
|
+
result = remove_trailing_slashes(result)
|
11
|
+
result
|
12
|
+
end
|
13
|
+
|
14
|
+
def strip_whitespace(value)
|
15
|
+
value.strip
|
16
|
+
end
|
17
|
+
|
18
|
+
def remove_leading_slashes(value)
|
19
|
+
value.gsub(/^(?!\/)/, '/')
|
20
|
+
end
|
21
|
+
|
22
|
+
def remove_trailing_slashes(value)
|
23
|
+
value.chomp('/').gsub('/?', '?').gsub('/#', '#')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/udongo/version.rb
CHANGED
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: 7.
|
4
|
+
version: 7.8.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: 2018-
|
12
|
+
date: 2018-11-30 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -353,6 +353,26 @@ dependencies:
|
|
353
353
|
- - ">="
|
354
354
|
- !ruby/object:Gem::Version
|
355
355
|
version: 1.0.5
|
356
|
+
- !ruby/object:Gem::Dependency
|
357
|
+
name: curb
|
358
|
+
requirement: !ruby/object:Gem::Requirement
|
359
|
+
requirements:
|
360
|
+
- - "~>"
|
361
|
+
- !ruby/object:Gem::Version
|
362
|
+
version: '0.9'
|
363
|
+
- - ">="
|
364
|
+
- !ruby/object:Gem::Version
|
365
|
+
version: 0.9.6
|
366
|
+
type: :runtime
|
367
|
+
prerelease: false
|
368
|
+
version_requirements: !ruby/object:Gem::Requirement
|
369
|
+
requirements:
|
370
|
+
- - "~>"
|
371
|
+
- !ruby/object:Gem::Version
|
372
|
+
version: '0.9'
|
373
|
+
- - ">="
|
374
|
+
- !ruby/object:Gem::Version
|
375
|
+
version: 0.9.6
|
356
376
|
- !ruby/object:Gem::Dependency
|
357
377
|
name: mysql2
|
358
378
|
requirement: !ruby/object:Gem::Requirement
|
@@ -433,6 +453,7 @@ files:
|
|
433
453
|
- app/assets/javascripts/backend/pages.js
|
434
454
|
- app/assets/javascripts/backend/plugins/autocomplete.js
|
435
455
|
- app/assets/javascripts/backend/plugins/tagbox.js
|
456
|
+
- app/assets/javascripts/backend/redirects.js
|
436
457
|
- app/assets/javascripts/backend/search.js
|
437
458
|
- app/assets/javascripts/backend/seo.js
|
438
459
|
- app/assets/javascripts/backend/sortable.js
|
@@ -486,6 +507,7 @@ files:
|
|
486
507
|
- app/controllers/backend/pages/base_controller.rb
|
487
508
|
- app/controllers/backend/pages/images_controller.rb
|
488
509
|
- app/controllers/backend/pages_controller.rb
|
510
|
+
- app/controllers/backend/redirects/test_controller.rb
|
489
511
|
- app/controllers/backend/redirects_controller.rb
|
490
512
|
- app/controllers/backend/search_controller.rb
|
491
513
|
- app/controllers/backend/search_synonyms_controller.rb
|
@@ -707,11 +729,16 @@ files:
|
|
707
729
|
- app/views/backend/pages/images/index.html.erb
|
708
730
|
- app/views/backend/pages/index.html.erb
|
709
731
|
- app/views/backend/pages/new.html.erb
|
732
|
+
- app/views/backend/redirects/_card.html.erb
|
710
733
|
- app/views/backend/redirects/_filter.html.erb
|
711
734
|
- app/views/backend/redirects/_form.html.erb
|
735
|
+
- app/views/backend/redirects/_row_columns.html.erb
|
736
|
+
- app/views/backend/redirects/_test_modal.html.erb
|
712
737
|
- app/views/backend/redirects/edit.html.erb
|
713
738
|
- app/views/backend/redirects/index.html.erb
|
714
739
|
- app/views/backend/redirects/new.html.erb
|
740
|
+
- app/views/backend/redirects/show.html.erb
|
741
|
+
- app/views/backend/redirects/test/resolve.js.erb
|
715
742
|
- app/views/backend/search/_page.html.erb
|
716
743
|
- app/views/backend/search_synonyms/_form.html.erb
|
717
744
|
- app/views/backend/search_synonyms/edit.html.erb
|
@@ -759,6 +786,7 @@ files:
|
|
759
786
|
- config/locales/en.yml
|
760
787
|
- config/locales/en_backend.yml
|
761
788
|
- config/locales/en_forms.yml
|
789
|
+
- config/locales/en_general.yml
|
762
790
|
- config/locales/fr.yml
|
763
791
|
- config/locales/nl.yml
|
764
792
|
- config/locales/nl_backend.yml
|
@@ -876,6 +904,9 @@ files:
|
|
876
904
|
- db/migrate/20180719160953_create_searches.rb
|
877
905
|
- db/migrate/20180719164016_rename_search_to_search_terms.rb
|
878
906
|
- db/migrate/20180821135446_create_attachments.rb
|
907
|
+
- db/migrate/20181019161330_add_working_to_redirects.rb
|
908
|
+
- db/migrate/20181129141539_add_depth_to_redirects.rb
|
909
|
+
- db/migrate/20181130093908_rename_depth_to_jumps_for_redirects.rb
|
879
910
|
- lib/tasks/task_extras.rb
|
880
911
|
- lib/tasks/udongo_tasks.rake
|
881
912
|
- lib/udongo.rb
|
@@ -914,6 +945,11 @@ files:
|
|
914
945
|
- lib/udongo/object_path.rb
|
915
946
|
- lib/udongo/pages/tree.rb
|
916
947
|
- lib/udongo/pages/tree_node.rb
|
948
|
+
- lib/udongo/redirects/jumps_cacher.rb
|
949
|
+
- lib/udongo/redirects/response.rb
|
950
|
+
- lib/udongo/redirects/status_badge.rb
|
951
|
+
- lib/udongo/redirects/test.rb
|
952
|
+
- lib/udongo/redirects/uri_sanitizer.rb
|
917
953
|
- lib/udongo/search/backend.rb
|
918
954
|
- lib/udongo/search/base.rb
|
919
955
|
- lib/udongo/search/frontend.rb
|