diff options
| -rw-r--r-- | app/controllers/admin_controller.rb | 4 | ||||
| -rw-r--r-- | app/models/node.rb | 3 | ||||
| -rw-r--r-- | app/views/layouts/admin.html.erb | 2 | ||||
| -rw-r--r-- | app/views/nodes/new.html.erb | 6 | ||||
| -rw-r--r-- | config/locales/de.yml | 2 | ||||
| -rw-r--r-- | config/locales/en.yml | 2 | ||||
| -rw-r--r-- | public/javascripts/admin_search.js | 8 | ||||
| -rw-r--r-- | public/stylesheets/admin.css | 11 | ||||
| -rw-r--r-- | test/models/node_test.rb | 23 |
9 files changed, 58 insertions, 3 deletions
diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 3b8b3582..c23cec04 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb | |||
| @@ -67,7 +67,9 @@ class AdminController < ApplicationController | |||
| 67 | 67 | ||
| 68 | format.js do | 68 | format.js do |
| 69 | render( :json => @results.map do |node| | 69 | render( :json => @results.map do |node| |
| 70 | {:node_id => node.id, :title => node.title, :unique_name => node.unique_name, :node_path => node_path(node)} | 70 | { :node_id => node.id, :title => node.title, |
| 71 | :unique_name => node.unique_name, :node_path => node_path(node), | ||
| 72 | :needs_redaktion => !current_user.may_change_live?(node) } | ||
| 71 | end | 73 | end |
| 72 | ) | 74 | ) |
| 73 | 75 | ||
diff --git a/app/models/node.rb b/app/models/node.rb index 188b6c17..d6fe32ff 100644 --- a/app/models/node.rb +++ b/app/models/node.rb | |||
| @@ -375,6 +375,9 @@ class Node < ApplicationRecord | |||
| 375 | raise ActiveRecord::RecordInvalid.new(self) | 375 | raise ActiveRecord::RecordInvalid.new(self) |
| 376 | end | 376 | end |
| 377 | 377 | ||
| 378 | guard_live_change!(current_user, | ||
| 379 | :target_path => [new_parent.unique_name.presence, slug].compact.join("/")) | ||
| 380 | |||
| 378 | ActiveRecord::Base.transaction do | 381 | ActiveRecord::Base.transaction do |
| 379 | path_before = unique_name | 382 | path_before = unique_name |
| 380 | move_to_child_of(new_parent) | 383 | move_to_child_of(new_parent) |
diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index 97117710..43f09b96 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb | |||
| @@ -18,7 +18,7 @@ | |||
| 18 | <%= javascript_tag nonce: true do %> | 18 | <%= javascript_tag nonce: true do %> |
| 19 | var ADMIN_SEARCH_URL = "<%= admin_search_path %>"; | 19 | var ADMIN_SEARCH_URL = "<%= admin_search_path %>"; |
| 20 | var ADMIN_MENU_SEARCH_URL = "<%= admin_menu_search_path %>"; | 20 | var ADMIN_MENU_SEARCH_URL = "<%= admin_menu_search_path %>"; |
| 21 | var ADMIN_STRINGS = <%= raw({ :lock_lost_prefix => t(".lock_lost_prefix"), :lock_lost_link => t(".lock_lost_link"), :insert_image => t(".insert_image"), :insert_image_tooltip => t(".insert_image_tooltip"), :copied => t(".copied") }.to_json) %>; | 21 | var ADMIN_STRINGS = <%= raw({ :lock_lost_prefix => t(".lock_lost_prefix"), :lock_lost_link => t(".lock_lost_link"), :insert_image => t(".insert_image"), :insert_image_tooltip => t(".insert_image_tooltip"), :copied => t(".copied"), :needs_redaktion => t(".needs_redaktion") }.to_json) %>; |
| 22 | var PARAMETERIZE_PREVIEW_URL = "<%= parameterize_preview_nodes_path %>"; | 22 | var PARAMETERIZE_PREVIEW_URL = "<%= parameterize_preview_nodes_path %>"; |
| 23 | var DASHBOARD_SEARCH_URL = "<%= admin_dashboard_search_path %>"; | 23 | var DASHBOARD_SEARCH_URL = "<%= admin_dashboard_search_path %>"; |
| 24 | <% end %> | 24 | <% end %> |
diff --git a/app/views/nodes/new.html.erb b/app/views/nodes/new.html.erb index b56bbeed..883909a6 100644 --- a/app/views/nodes/new.html.erb +++ b/app/views/nodes/new.html.erb | |||
| @@ -12,13 +12,17 @@ | |||
| 12 | <div class="layout_row_label"><%= t(".type") %></div> | 12 | <div class="layout_row_label"><%= t(".type") %></div> |
| 13 | <div class="layout_row_content"> | 13 | <div class="layout_row_content"> |
| 14 | <% CccConventions::NODE_KINDS.each do |kind, config| %> | 14 | <% CccConventions::NODE_KINDS.each do |kind, config| %> |
| 15 | <% prefix = resolve_kind_text(config[:path_prefix]) %> | ||
| 15 | <p> | 16 | <p> |
| 16 | <%= radio_button_tag :kind, kind, kind == @selected_kind, | 17 | <%= radio_button_tag :kind, kind, kind == @selected_kind, |
| 17 | data: { path_prefix: resolve_kind_text(config[:path_prefix]) } %> | 18 | data: { path_prefix: prefix } %> |
| 18 | <%= resolve_kind_text(config[:label]) %> | 19 | <%= resolve_kind_text(config[:label]) %> |
| 19 | <% if config[:hint] %> | 20 | <% if config[:hint] %> |
| 20 | <span class="field_hint"><%= resolve_kind_text(config[:hint]) %></span> | 21 | <span class="field_hint"><%= resolve_kind_text(config[:hint]) %></span> |
| 21 | <% end %> | 22 | <% end %> |
| 23 | <% unless current_user.may_change_live_at?(prefix) %> | ||
| 24 | <span class="field_hint restricted_kind_hint"><%= t(".restricted_kind") %></span> | ||
| 25 | <% end %> | ||
| 22 | </p> | 26 | </p> |
| 23 | <% end %> | 27 | <% end %> |
| 24 | </div> | 28 | </div> |
diff --git a/config/locales/de.yml b/config/locales/de.yml index 746aa803..74f6e8ae 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml | |||
| @@ -382,6 +382,7 @@ de: | |||
| 382 | attachment: "Anhang" | 382 | attachment: "Anhang" |
| 383 | attach_hint: "Dieses Asset wird der neuen Seite automatisch angehängt." | 383 | attach_hint: "Dieses Asset wird der neuen Seite automatisch angehängt." |
| 384 | as_headline: "als Aufmacher der Seite" | 384 | as_headline: "als Aufmacher der Seite" |
| 385 | restricted_kind: "Veröffentlichen hier ist der Redaktion vorbehalten. Anlegen und schreiben kannst du aber." | ||
| 385 | edit: | 386 | edit: |
| 386 | save_draft: "Entwurf speichern" | 387 | save_draft: "Entwurf speichern" |
| 387 | save_unlock_exit: "Speichern + Entsperren + Verlassen" | 388 | save_unlock_exit: "Speichern + Entsperren + Verlassen" |
| @@ -743,6 +744,7 @@ de: | |||
| 743 | insert_image: "Bild einfügen" | 744 | insert_image: "Bild einfügen" |
| 744 | insert_image_tooltip: "Eines der angehängten Bilder dieser Seite einfügen" | 745 | insert_image_tooltip: "Eines der angehängten Bilder dieser Seite einfügen" |
| 745 | copied: "Kopiert!" | 746 | copied: "Kopiert!" |
| 747 | needs_redaktion: "Redaktion nötig" | ||
| 746 | nav_controls: | 748 | nav_controls: |
| 747 | dashboard: "Dashboard" | 749 | dashboard: "Dashboard" |
| 748 | search: "Suche" | 750 | search: "Suche" |
diff --git a/config/locales/en.yml b/config/locales/en.yml index 22429c9e..c23f2c33 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml | |||
| @@ -325,6 +325,7 @@ en: | |||
| 325 | attachment: "attachment" | 325 | attachment: "attachment" |
| 326 | attach_hint: "This asset will automatically be attached to the new page." | 326 | attach_hint: "This asset will automatically be attached to the new page." |
| 327 | as_headline: "as the page's headline" | 327 | as_headline: "as the page's headline" |
| 328 | restricted_kind: "Publishing here is reserved for Redaktion. You can create and write freely, though." | ||
| 328 | edit: | 329 | edit: |
| 329 | save_draft: "Save Draft" | 330 | save_draft: "Save Draft" |
| 330 | save_unlock_exit: "Save + Unlock + Exit" | 331 | save_unlock_exit: "Save + Unlock + Exit" |
| @@ -686,6 +687,7 @@ en: | |||
| 686 | insert_image: "Insert image" | 687 | insert_image: "Insert image" |
| 687 | insert_image_tooltip: "Insert one of this page's attached images" | 688 | insert_image_tooltip: "Insert one of this page's attached images" |
| 688 | copied: "Copied!" | 689 | copied: "Copied!" |
| 690 | needs_redaktion: "Redaktion required" | ||
| 689 | nav_controls: | 691 | nav_controls: |
| 690 | dashboard: "Dashboard" | 692 | dashboard: "Dashboard" |
| 691 | search: "Search" | 693 | search: "Search" |
diff --git a/public/javascripts/admin_search.js b/public/javascripts/admin_search.js index 792849f7..37b696e4 100644 --- a/public/javascripts/admin_search.js +++ b/public/javascripts/admin_search.js | |||
| @@ -46,6 +46,7 @@ function initSearchPicker(options) { | |||
| 46 | var isActive = options.isActive; | 46 | var isActive = options.isActive; |
| 47 | var resultsHeaderHtml = options.resultsHeaderHtml; | 47 | var resultsHeaderHtml = options.resultsHeaderHtml; |
| 48 | var renderResults = options.renderResults; | 48 | var renderResults = options.renderResults; |
| 49 | var showRestricted = options.showRestricted; | ||
| 49 | var loadOnFocus = options.loadOnFocus; // optional, fires an initial search on focus with no term typed yet | 50 | var loadOnFocus = options.loadOnFocus; // optional, fires an initial search on focus with no term typed yet |
| 50 | var requestId = 0; | 51 | var requestId = 0; |
| 51 | var timeout; | 52 | var timeout; |
| @@ -75,6 +76,10 @@ function initSearchPicker(options) { | |||
| 75 | var anchor = $("<a>").attr("href", onSelect ? "#" : node.node_path); | 76 | var anchor = $("<a>").attr("href", onSelect ? "#" : node.node_path); |
| 76 | anchor.append(document.createTextNode(node.title || "")); | 77 | anchor.append(document.createTextNode(node.title || "")); |
| 77 | anchor.append($("<span>", { "class": "result_path" }).text(node.unique_name || "")); | 78 | anchor.append($("<span>", { "class": "result_path" }).text(node.unique_name || "")); |
| 79 | if (showRestricted && node.needs_redaktion) { | ||
| 80 | anchor.append($("<span>", { "class": "restricted_result" }) | ||
| 81 | .text(ADMIN_STRINGS.needs_redaktion)); | ||
| 82 | } | ||
| 78 | var link = $("<p>").append(anchor); | 83 | var link = $("<p>").append(anchor); |
| 79 | 84 | ||
| 80 | if (onSelect) { | 85 | if (onSelect) { |
| @@ -192,6 +197,7 @@ parent_search = { | |||
| 192 | initSearchPicker({ | 197 | initSearchPicker({ |
| 193 | inputSelector: "#parent_search_term", | 198 | inputSelector: "#parent_search_term", |
| 194 | resultsSelector: "#parent_search_results", | 199 | resultsSelector: "#parent_search_results", |
| 200 | showRestricted: true, | ||
| 195 | onSelect: function(node) { | 201 | onSelect: function(node) { |
| 196 | $("#parent_search_term").val(node.title); | 202 | $("#parent_search_term").val(node.title); |
| 197 | $("#parent_id").val(node.node_id).attr("data-unique-name", node.unique_name); | 203 | $("#parent_id").val(node.node_id).attr("data-unique-name", node.unique_name); |
| @@ -248,6 +254,7 @@ move_to_search = { | |||
| 248 | initSearchPicker({ | 254 | initSearchPicker({ |
| 249 | inputSelector: "#move_to_search_term", | 255 | inputSelector: "#move_to_search_term", |
| 250 | resultsSelector: "#move_to_search_results", | 256 | resultsSelector: "#move_to_search_results", |
| 257 | showRestricted: true, | ||
| 251 | onSelect: function(node) { | 258 | onSelect: function(node) { |
| 252 | $("#move_to_search_term").val(node.title); | 259 | $("#move_to_search_term").val(node.title); |
| 253 | $("#node_staged_parent_id").val(node.node_id); | 260 | $("#node_staged_parent_id").val(node.node_id); |
| @@ -261,6 +268,7 @@ restore_search = { | |||
| 261 | initSearchPicker({ | 268 | initSearchPicker({ |
| 262 | inputSelector: "#restore_search_term", | 269 | inputSelector: "#restore_search_term", |
| 263 | resultsSelector: "#restore_search_results", | 270 | resultsSelector: "#restore_search_results", |
| 271 | showRestricted: true, | ||
| 264 | onSelect: function(node) { | 272 | onSelect: function(node) { |
| 265 | $("#restore_search_term").val(node.title); | 273 | $("#restore_search_term").val(node.title); |
| 266 | $("#parent_id").val(node.node_id); | 274 | $("#parent_id").val(node.node_id); |
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index 55fdf15d..7f286705 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css | |||
| @@ -889,6 +889,7 @@ div.layout_row_content { | |||
| 889 | font-weight: bold; | 889 | font-weight: bold; |
| 890 | color: var(--text-muted); | 890 | color: var(--text-muted); |
| 891 | cursor: not-allowed; | 891 | cursor: not-allowed; |
| 892 | line-height: 1.25rem; | ||
| 892 | } | 893 | } |
| 893 | 894 | ||
| 894 | .info_group .disabled_action svg { | 895 | .info_group .disabled_action svg { |
| @@ -973,6 +974,10 @@ form.button_to button[type="submit"] { | |||
| 973 | padding-bottom: 4px; | 974 | padding-bottom: 4px; |
| 974 | } | 975 | } |
| 975 | 976 | ||
| 977 | .restricted_kind_hint { | ||
| 978 | color: var(--accent); | ||
| 979 | } | ||
| 980 | |||
| 976 | .action_button + .field_hint { | 981 | .action_button + .field_hint { |
| 977 | display: inline; | 982 | display: inline; |
| 978 | margin-left: 0.5rem; | 983 | margin-left: 0.5rem; |
| @@ -1459,6 +1464,12 @@ input#menu_item_title { | |||
| 1459 | padding-bottom: 4px; | 1464 | padding-bottom: 4px; |
| 1460 | } | 1465 | } |
| 1461 | 1466 | ||
| 1467 | .search_results .restricted_result { | ||
| 1468 | display: block; | ||
| 1469 | font-size: 0.85rem; | ||
| 1470 | color: var(--accent); | ||
| 1471 | } | ||
| 1472 | |||
| 1462 | .search_results p.search_more { | 1473 | .search_results p.search_more { |
| 1463 | margin: 0; | 1474 | margin: 0; |
| 1464 | padding: 6px 4px; | 1475 | padding: 6px 4px; |
diff --git a/test/models/node_test.rb b/test/models/node_test.rb index aa714427..a34094a0 100644 --- a/test/models/node_test.rb +++ b/test/models/node_test.rb | |||
| @@ -1049,4 +1049,27 @@ class NodeTest < ActiveSupport::TestCase | |||
| 1049 | assert_not squatter.valid? | 1049 | assert_not squatter.valid? |
| 1050 | assert squatter.errors[:slug].any? | 1050 | assert squatter.errors[:slug].any? |
| 1051 | end | 1051 | end |
| 1052 | |||
| 1053 | test "restoring into a restricted subtree is refused" do | ||
| 1054 | editor = User.create!(:login => "restore_editor", :email => "rs@example.com", | ||
| 1055 | :password => "secret", :password_confirmation => "secret") | ||
| 1056 | updates = Node.root.children.create!(:slug => "updates") | ||
| 1057 | node = Node.root.children.create!(:slug => "restorable") | ||
| 1058 | node.reload.trash! | ||
| 1059 | |||
| 1060 | assert_raises(ActiveRecord::RecordInvalid) { node.restore_from_trash!(updates, editor) } | ||
| 1061 | assert node.reload.in_trash? | ||
| 1062 | end | ||
| 1063 | |||
| 1064 | test "restoring into unrestricted space needs no role" do | ||
| 1065 | editor = User.create!(:login => "restore_free", :email => "rf@example.com", | ||
| 1066 | :password => "secret", :password_confirmation => "secret") | ||
| 1067 | club = Node.root.children.create!(:slug => "club") | ||
| 1068 | node = Node.root.children.create!(:slug => "restorable_free") | ||
| 1069 | node.reload.trash! | ||
| 1070 | |||
| 1071 | node.restore_from_trash!(club, editor) | ||
| 1072 | assert_not node.reload.in_trash? | ||
| 1073 | assert_equal club.id, node.parent_id | ||
| 1074 | end | ||
| 1052 | end | 1075 | end |
