udongo 7.7.2 → 7.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/backend/general.js +8 -0
  3. data/app/assets/javascripts/backend/redirects.js +61 -0
  4. data/app/assets/stylesheets/backend/components/_form.scss +4 -0
  5. data/app/controllers/backend/redirects/test_controller.rb +38 -0
  6. data/app/controllers/backend/redirects_controller.rb +25 -5
  7. data/app/controllers/redirects_controller.rb +1 -0
  8. data/app/decorators/redirect_decorator.rb +23 -0
  9. data/app/helpers/icon_helper.rb +4 -0
  10. data/app/helpers/link_helper.rb +7 -7
  11. data/app/models/redirect.rb +81 -0
  12. data/app/views/backend/redirects/_card.html.erb +41 -0
  13. data/app/views/backend/redirects/_filter.html.erb +7 -2
  14. data/app/views/backend/redirects/_form.html.erb +31 -1
  15. data/app/views/backend/redirects/_row_columns.html.erb +0 -0
  16. data/app/views/backend/redirects/_test_modal.html.erb +20 -0
  17. data/app/views/backend/redirects/index.html.erb +30 -10
  18. data/app/views/backend/redirects/show.html.erb +16 -0
  19. data/app/views/backend/redirects/test/resolve.js.erb +9 -0
  20. data/changelog.md +7 -0
  21. data/config/locales/en_backend.yml +16 -0
  22. data/config/locales/en_general.yml +15 -0
  23. data/config/locales/nl_backend.yml +16 -0
  24. data/config/locales/nl_general.yml +3 -0
  25. data/config/routes.rb +7 -1
  26. data/db/migrate/20181019161330_add_working_to_redirects.rb +5 -0
  27. data/db/migrate/20181129141539_add_depth_to_redirects.rb +5 -0
  28. data/db/migrate/20181130093908_rename_depth_to_jumps_for_redirects.rb +5 -0
  29. data/lib/udongo/redirects/jumps_cacher.rb +19 -0
  30. data/lib/udongo/redirects/response.rb +33 -0
  31. data/lib/udongo/redirects/status_badge.rb +33 -0
  32. data/lib/udongo/redirects/test.rb +21 -0
  33. data/lib/udongo/redirects/uri_sanitizer.rb +26 -0
  34. data/lib/udongo/version.rb +1 -1
  35. metadata +38 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 90cb5755b0df3482d5d074d11028e0576c96e541
4
- data.tar.gz: fe5fb945dd8102abe82ebbce12bf0c4f17339a41
3
+ metadata.gz: e97f435ff6a84446f7ff5826ace9409c173a7cf2
4
+ data.tar.gz: 9e6c8e8ab81d2a9f9bd8a8341f8a8eeaaced1c17
5
5
  SHA512:
6
- metadata.gz: e850585aaf1552acfdb975c7b3a494654e36b555be5c26455e154ee06ee70b26bec28f1716f597bce6e2c2f550f2e7c09366734fcf064cd3225a20221c778b82
7
- data.tar.gz: c663649cdffc9efe5369944cbf7ebf48703f381d81cc66ffaf459907ef371517181a2258558363b29fe5e12427c44ce997a92b934de2d883227827245263d7f2
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(); });
@@ -26,3 +26,7 @@ label.collection_check_boxes {
26
26
  cursor: pointer;
27
27
  }
28
28
  }
29
+
30
+ button.btn, input.btn {
31
+ cursor: pointer;
32
+ }
@@ -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, only: [:edit, :update, :destroy]
5
+ before_action :find_model, except: [:index, :new, :create]
6
6
 
7
7
  def index
8
8
  @search = Redirect.ransack params[:q]
9
- @redirects = @search.result(distinct: true).order('times_used DESC').page(page_number).per_page(per_page)
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
- redirect_to backend_redirects_path, notice: translate_notice(:deleted, :redirect)
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
- redirect_to backend_redirects_path, notice: translate_notice(:edited, :redirect)
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 = Redirect.find(params[:id]).decorate
68
+ @redirect ||= Redirect.find(params[:id]).decorate
49
69
  end
50
70
  end
@@ -4,6 +4,7 @@ class RedirectsController < ActionController::Base
4
4
 
5
5
  if redirect && redirect.enabled?
6
6
  redirect.used!
7
+ redirect.working!
7
8
  redirect_to redirect.destination_uri, status: redirect.status_code
8
9
  else
9
10
  render plain: 'No such redirect or disabled.', status: 404
@@ -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
@@ -17,4 +17,8 @@ module IconHelper
17
17
  markup = content_tag :i, nil, options
18
18
  lbl ? "#{markup}&nbsp;#{lbl}".html_safe : markup
19
19
  end
20
+
21
+ def spinner_icon
22
+ icon :spinner, '', class: 'fa-spin'
23
+ end
20
24
  end
@@ -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 = nil)
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)
@@ -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-6 form-group">
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-6 form-group">
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
- <%= render 'backend/form_actions', cancel_url: backend_redirects_path %>
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 %>
@@ -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">&times;</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-striped table-hover">
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.status_code' %></th>
15
- <th><%= t 'b.enabled' %></th>
18
+ <th><%= t 'b.active' %></th>
16
19
  <th><%= t 'b.used' %></th>
17
20
  <th>&nbsp;</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.source_uri %></td>
25
- <td><%= r.destination_uri %></td>
26
- <td><%= r.status_code %></td>
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><%= r.times_used.to_i %></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
- <%= link_to_edit [:backend, r] %>
31
- <%= link_to_delete [:backend, r] %>
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
+ }
@@ -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.
@@ -0,0 +1,15 @@
1
+ en:
2
+ g:
3
+ collections:
4
+ working:
5
+ 'true': Working
6
+ 'false': Broken
7
+ form_field_type:
8
+ collection: Collection
9
+ email: E-mailaddress
10
+ integer: Number
11
+ string: String
12
+ tel: Telephone
13
+ text: Text
14
+
15
+ type: Type
@@ -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
@@ -1,6 +1,9 @@
1
1
  nl:
2
2
  g:
3
3
  collections:
4
+ working:
5
+ 'true': Werkend
6
+ 'false': Stuk
4
7
  form_field_type:
5
8
  collection: Collectie
6
9
  email: E-mailadres
@@ -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, except: :show
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,5 @@
1
+ class AddWorkingToRedirects < ActiveRecord::Migration[5.0]
2
+ def change
3
+ add_column :redirects, :working, :boolean, after: :times_used
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddDepthToRedirects < ActiveRecord::Migration[5.0]
2
+ def change
3
+ add_column :redirects, :depth, :integer, after: :times_used
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class RenameDepthToJumpsForRedirects < ActiveRecord::Migration[5.0]
2
+ def change
3
+ rename_column :redirects, :depth, :jumps
4
+ end
5
+ end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Udongo
2
- VERSION = '7.7.2'
2
+ VERSION = '7.8.0'
3
3
  end
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.7.2
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-10-04 00:00:00.000000000 Z
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