udongo 7.7.2 → 7.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|