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.
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