summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/models/node.rb23
-rw-r--r--app/models/user.rb6
-rw-r--r--app/views/nodes/show.html.erb26
-rw-r--r--config/locales/de.yml1
-rw-r--r--config/locales/en.yml1
-rw-r--r--public/stylesheets/admin.css9
-rw-r--r--test/models/node_test.rb45
7 files changed, 97 insertions, 14 deletions
diff --git a/app/models/node.rb b/app/models/node.rb
index ac3a6160..188b6c17 100644
--- a/app/models/node.rb
+++ b/app/models/node.rb
@@ -236,7 +236,7 @@ class Node < ApplicationRecord
236 # Return nil if nothing to publish and no staged changes 236 # Return nil if nothing to publish and no staged changes
237 return nil unless self.draft || staged_slug || staged_parent_id 237 return nil unless self.draft || staged_slug || staged_parent_id
238 238
239 guard_live_change!(current_user) 239 guard_live_change!(current_user, :target_path => prospective_unique_name)
240 240
241 if in_trash? || trash_node? 241 if in_trash? || trash_node?
242 errors.add(:base, :publish_in_trash) 242 errors.add(:base, :publish_in_trash)
@@ -574,10 +574,8 @@ class Node < ApplicationRecord
574 false 574 false
575 end 575 end
576 576
577 def restricted? 577 def self.restricted_path? name
578 return true if root? 578 name = name.to_s
579
580 name = unique_name.to_s
581 return false if name.empty? 579 return false if name.empty?
582 580
583 CccConventions::RESTRICTED_SUBTREES.any? do |prefix| 581 CccConventions::RESTRICTED_SUBTREES.any? do |prefix|
@@ -585,6 +583,17 @@ class Node < ApplicationRecord
585 end 583 end
586 end 584 end
587 585
586 def restricted?
587 root? || self.class.restricted_path?(unique_name)
588 end
589
590 def prospective_unique_name
591 target_parent = staged_parent_id ? Node.find_by(:id => staged_parent_id) : parent
592 return nil unless target_parent
593
594 [target_parent.unique_name.presence, staged_slug.presence || slug].compact.join("/")
595 end
596
588 # Returns immutable node id for all new nodes so that the atom feed entry ids 597 # Returns immutable node id for all new nodes so that the atom feed entry ids
589 # stay the same eventhough the slug or positions changes. 598 # stay the same eventhough the slug or positions changes.
590 # Can be removed after a year or so ;) 599 # Can be removed after a year or so ;)
@@ -697,9 +706,9 @@ class Node < ApplicationRecord
697 706
698 private 707 private
699 708
700 def guard_live_change! user 709 def guard_live_change! user, target_path: nil
701 return if user.nil? 710 return if user.nil?
702 return if user.may_change_live?(self) 711 return if user.may_change_live?(self) && user.may_change_live_at?(target_path)
703 712
704 errors.add(:base, :not_permitted) 713 errors.add(:base, :not_permitted)
705 raise ActiveRecord::RecordInvalid.new(self) 714 raise ActiveRecord::RecordInvalid.new(self)
diff --git a/app/models/user.rb b/app/models/user.rb
index e8c3b9bb..bf0f40ee 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -110,6 +110,12 @@ class User < ApplicationRecord
110 redaktion? 110 redaktion?
111 end 111 end
112 112
113 def may_change_live_at?(path)
114 return true if path.nil?
115 return true unless Node.restricted_path?(path)
116 redaktion?
117 end
118
113 def deactivate!(actor:) 119 def deactivate!(actor:)
114 return false if alumni? 120 return false if alumni?
115 transaction do 121 transaction do
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb
index 8b12e2eb..a6167c9e 100644
--- a/app/views/nodes/show.html.erb
+++ b/app/views/nodes/show.html.erb
@@ -1,4 +1,5 @@
1<% locked_by_other = @node.locked? && @node.lock_owner != current_user %> 1<% locked_by_other = @node.locked? && @node.lock_owner != current_user %>
2<% may_change_live = current_user.may_change_live?(@node) %>
2<div id="admin_layout" class="show_node"> 3<div id="admin_layout" class="show_node">
3 <h1><%= title_for_node(@node) %> <small>(<%= I18n.default_locale.to_s.upcase %>)</small></h1> 4 <h1><%= title_for_node(@node) %> <small>(<%= I18n.default_locale.to_s.upcase %>)</small></h1>
4 <div id="content"> 5 <div id="content">
@@ -48,8 +49,12 @@
48 <% unless locked_by_other %> 49 <% unless locked_by_other %>
49 <% if @node.draft && !@node.autosave && !@node.in_trash? && !@node.trash_node? %> 50 <% if @node.draft && !@node.autosave && !@node.in_trash? && !@node.trash_node? %>
50 <div class="info_item"> 51 <div class="info_item">
51 <%= button_to t(".publish"), publish_node_path(@node), method: :put, 52 <% if may_change_live %>
52 form: { data: { confirm: t(".confirm_publish") }, class: 'button_to state_changing' } %> 53 <%= button_to t(".publish"), publish_node_path(@node), method: :put,
54 form: { data: { confirm: t(".confirm_publish") }, class: 'button_to state_changing' } %>
55 <% else %>
56 <span class="disabled_action"><%= t(".publish") %></span>
57 <% end %>
53 </div> 58 </div>
54 <% end %> 59 <% end %>
55 <% if @node.autosave || (@node.draft && @node.head) %> 60 <% if @node.autosave || (@node.draft && @node.head) %>
@@ -64,10 +69,17 @@
64 <% end %> 69 <% end %>
65 <% unless @node.trash_node? || @node.in_trash? || @node.root? %> 70 <% unless @node.trash_node? || @node.in_trash? || @node.root? %>
66 <div class="info_item"> 71 <div class="info_item">
67 <%= button_to trash_node_path(@node), method: :put, 72 <% if may_change_live %>
68 form: { data: { confirm: t(".confirm_trash") }, class: 'button_to destructive' } do %> 73 <%= button_to trash_node_path(@node), method: :put,
69 <%= icon("trash", library: "tabler", "aria-hidden": true) %> 74 form: { data: { confirm: t(".confirm_trash") }, class: 'button_to destructive' } do %>
70 <%= t(".move_to_trash") %> 75 <%= icon("trash", library: "tabler", "aria-hidden": true) %>
76 <%= t(".move_to_trash") %>
77 <% end %>
78 <% else %>
79 <span class="disabled_action">
80 <%= icon("trash", library: "tabler", "aria-hidden": true) %>
81 <%= t(".move_to_trash") %>
82 </span>
71 <% end %> 83 <% end %>
72 </div> 84 </div>
73 <% end %> 85 <% end %>
@@ -76,6 +88,8 @@
76 88
77 <% if locked_by_other %> 89 <% if locked_by_other %>
78 <span class="field_hint"><%= t(".locked_hint") %></span> 90 <span class="field_hint"><%= t(".locked_hint") %></span>
91 <% elsif !may_change_live %>
92 <span class="field_hint"><%= t(".restricted_hint") %></span>
79 <% end %> 93 <% end %>
80 </div> 94 </div>
81 95
diff --git a/config/locales/de.yml b/config/locales/de.yml
index a7ad0f26..746aa803 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -493,6 +493,7 @@ de:
493 add_child: "Kind vom Typ %{kind} anlegen" 493 add_child: "Kind vom Typ %{kind} anlegen"
494 abstract_locale: "Abstract (%{lang})" 494 abstract_locale: "Abstract (%{lang})"
495 body_locale: "Text (%{lang})" 495 body_locale: "Text (%{lang})"
496 restricted_hint: "Geschützter Bereich — Veröffentlichen und Papierkorb sind der Redaktion vorbehalten. Entwürfe kannst du frei bearbeiten."
496 drafts: 497 drafts:
497 title: "Nodes mit Entwürfen, Autosaves oder Sperren" 498 title: "Nodes mit Entwürfen, Autosaves oder Sperren"
498 mine: 499 mine:
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 7e830325..22429c9e 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -436,6 +436,7 @@ en:
436 add_child: "Add child type %{kind}" 436 add_child: "Add child type %{kind}"
437 abstract_locale: "Abstract (%{lang})" 437 abstract_locale: "Abstract (%{lang})"
438 body_locale: "Body (%{lang})" 438 body_locale: "Body (%{lang})"
439 restricted_hint: "Protected section — publishing and trashing are reserved for Redaktion. You can edit drafts freely."
439 drafts: 440 drafts:
440 title: "Nodes with drafts, autosaves or locks" 441 title: "Nodes with drafts, autosaves or locks"
441 mine: 442 mine:
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css
index 9991fad0..55fdf15d 100644
--- a/public/stylesheets/admin.css
+++ b/public/stylesheets/admin.css
@@ -880,7 +880,9 @@ div.layout_row_content {
880} 880}
881 881
882.info_group .disabled_action { 882.info_group .disabled_action {
883 display: inline-block; 883 display: inline-flex;
884 align-items: center;
885 gap: 0.35rem;
884 border: 1px solid var(--border); 886 border: 1px solid var(--border);
885 border-radius: 2px; 887 border-radius: 2px;
886 padding: 4px 12px; 888 padding: 4px 12px;
@@ -889,6 +891,11 @@ div.layout_row_content {
889 cursor: not-allowed; 891 cursor: not-allowed;
890} 892}
891 893
894.info_group .disabled_action svg {
895 width: 1.25rem;
896 height: 1.25rem;
897}
898
892.info_group_items { 899.info_group_items {
893 display: flex; 900 display: flex;
894 flex-wrap: wrap; 901 flex-wrap: wrap;
diff --git a/test/models/node_test.rb b/test/models/node_test.rb
index f57f83bf..aa714427 100644
--- a/test/models/node_test.rb
+++ b/test/models/node_test.rb
@@ -1004,4 +1004,49 @@ class NodeTest < ActiveSupport::TestCase
1004 node.publish_draft! 1004 node.publish_draft!
1005 assert_not_nil node.reload.head 1005 assert_not_nil node.reload.head
1006 end 1006 end
1007
1008 test "publishing a staged move into a restricted subtree is refused" do
1009 editor = User.create!(:login => "move_editor", :email => "me@example.com",
1010 :password => "secret", :password_confirmation => "secret")
1011 updates = Node.root.children.create!(:slug => "updates")
1012 year = updates.children.create!(:slug => "2026")
1013 node = Node.root.children.create!(:slug => "outside-post")
1014 node.reload.draft.update!(:title => "Entwurf")
1015 node.update!(:staged_parent_id => year.id)
1016
1017 assert_raises(ActiveRecord::RecordInvalid) { node.publish_draft!(editor) }
1018 assert_nil node.reload.head
1019 assert_equal Node.root.id, node.parent_id
1020 end
1021
1022 test "a staged move within unrestricted space needs no role" do
1023 editor = User.create!(:login => "move_free", :email => "mf@example.com",
1024 :password => "secret", :password_confirmation => "secret")
1025 club = Node.root.children.create!(:slug => "club")
1026 node = Node.root.children.create!(:slug => "movable-post")
1027 node.reload.draft.update!(:title => "Entwurf")
1028 node.update!(:staged_parent_id => club.id)
1029
1030 node.publish_draft!(editor)
1031 assert_equal club.id, node.reload.parent_id
1032 end
1033
1034 test "publishing a staged rename onto a restricted path is refused" do
1035 editor = User.create!(:login => "rename_editor", :email => "re@example.com",
1036 :password => "secret", :password_confirmation => "secret")
1037 node = Node.root.children.create!(:slug => "harmless")
1038 node.reload.draft.update!(:title => "Entwurf")
1039 node.update!(:staged_slug => "updates")
1040
1041 assert_raises(ActiveRecord::RecordInvalid) { node.publish_draft!(editor) }
1042 assert_nil node.reload.head
1043 end
1044
1045 test "a restricted subtree root cannot be claimed once it exists" do
1046 Node.root.children.create!(:slug => "updates")
1047
1048 squatter = Node.root.children.build(:slug => "updates")
1049 assert_not squatter.valid?
1050 assert squatter.errors[:slug].any?
1051 end
1007end 1052end