diff options
| -rw-r--r-- | app/models/node.rb | 42 | ||||
| -rw-r--r-- | app/models/related_asset.rb | 2 | ||||
| -rw-r--r-- | config/locales/de.yml | 37 | ||||
| -rw-r--r-- | config/locales/en.yml | 30 | ||||
| -rw-r--r-- | test/controllers/nodes_controller_test.rb | 2 | ||||
| -rw-r--r-- | test/models/node_test.rb | 17 | ||||
| -rw-r--r-- | test/models/related_asset_test.rb | 2 |
7 files changed, 112 insertions, 20 deletions
diff --git a/app/models/node.rb b/app/models/node.rb index 1e61f9fe..602382fb 100644 --- a/app/models/node.rb +++ b/app/models/node.rb | |||
| @@ -236,7 +236,8 @@ class Node < ApplicationRecord | |||
| 236 | return nil unless self.draft || staged_slug || staged_parent_id | 236 | return nil unless self.draft || staged_slug || staged_parent_id |
| 237 | 237 | ||
| 238 | if in_trash? || trash_node? | 238 | if in_trash? || trash_node? |
| 239 | raise ActiveRecord::RecordInvalid.new(self), "Cannot publish a node in the Trash" | 239 | errors.add(:base, :publish_in_trash) |
| 240 | raise ActiveRecord::RecordInvalid.new(self) | ||
| 240 | end | 241 | end |
| 241 | 242 | ||
| 242 | path_before = self.unique_name | 243 | path_before = self.unique_name |
| @@ -265,7 +266,8 @@ class Node < ApplicationRecord | |||
| 265 | new_parent = Node.find(staged_parent_id) | 266 | new_parent = Node.find(staged_parent_id) |
| 266 | 267 | ||
| 267 | if new_parent == self || self.descendants.include?(new_parent) | 268 | if new_parent == self || self.descendants.include?(new_parent) |
| 268 | raise ActiveRecord::RecordInvalid.new(self), "Cannot move a node under itself or one of its own descendants" | 269 | errors.add(:base, :move_under_self) |
| 270 | raise ActiveRecord::RecordInvalid.new(self) | ||
| 269 | end | 271 | end |
| 270 | 272 | ||
| 271 | self.staged_parent_id = nil | 273 | self.staged_parent_id = nil |
| @@ -316,7 +318,10 @@ class Node < ApplicationRecord | |||
| 316 | # at the root, carrying the leaving-public-view snapshot. | 318 | # at the root, carrying the leaving-public-view snapshot. |
| 317 | def trash! current_user = nil | 319 | def trash! current_user = nil |
| 318 | return nil if in_trash? | 320 | return nil if in_trash? |
| 319 | raise ActiveRecord::RecordInvalid.new(self), "The Trash node itself cannot be trashed" if trash_node? | 321 | if trash_node? |
| 322 | errors.add(:base, :trash_the_trash) | ||
| 323 | raise ActiveRecord::RecordInvalid.new(self) | ||
| 324 | end | ||
| 320 | 325 | ||
| 321 | ActiveRecord::Base.transaction do | 326 | ActiveRecord::Base.transaction do |
| 322 | path_before = unique_name | 327 | path_before = unique_name |
| @@ -360,7 +365,8 @@ class Node < ApplicationRecord | |||
| 360 | 365 | ||
| 361 | if new_parent.nil? || new_parent == self || descendants.include?(new_parent) || | 366 | if new_parent.nil? || new_parent == self || descendants.include?(new_parent) || |
| 362 | new_parent.trash_node? || new_parent.in_trash? | 367 | new_parent.trash_node? || new_parent.in_trash? |
| 363 | raise ActiveRecord::RecordInvalid.new(self), "Restore target must be a living node" | 368 | errors.add(:base, :restore_target_invalid) |
| 369 | raise ActiveRecord::RecordInvalid.new(self) | ||
| 364 | end | 370 | end |
| 365 | 371 | ||
| 366 | ActiveRecord::Base.transaction do | 372 | ActiveRecord::Base.transaction do |
| @@ -376,7 +382,7 @@ class Node < ApplicationRecord | |||
| 376 | end | 382 | end |
| 377 | end | 383 | end |
| 378 | 384 | ||
| 379 | # Final deletion -- only from inside the Trash. Removes the whole | 385 | # Final deletion, only from inside the Trash. Removes the whole |
| 380 | # subtree, deepest first, each node through a real destroy! so every | 386 | # subtree, deepest first, each node through a real destroy! so every |
| 381 | # per-node cascade runs (the categorical difference from the old | 387 | # per-node cascade runs (the categorical difference from the old |
| 382 | # delete_all nuke). refuse_destroy_with_children on bare destroy is | 388 | # delete_all nuke). refuse_destroy_with_children on bare destroy is |
| @@ -384,7 +390,10 @@ class Node < ApplicationRecord | |||
| 384 | # One log entry at the root, per the subtree rule, written before the | 390 | # One log entry at the root, per the subtree rule, written before the |
| 385 | # rows die. | 391 | # rows die. |
| 386 | def destroy_from_trash! current_user = nil | 392 | def destroy_from_trash! current_user = nil |
| 387 | raise ActiveRecord::RecordInvalid.new(self), "Nodes are only destroyed from the Trash" unless in_trash? | 393 | unless in_trash? |
| 394 | errors.add(:base, :destroy_outside_trash) | ||
| 395 | raise ActiveRecord::RecordInvalid.new(self) | ||
| 396 | end | ||
| 388 | 397 | ||
| 389 | ActiveRecord::Base.transaction do | 398 | ActiveRecord::Base.transaction do |
| 390 | doomed = self_and_descendants_ordered_with_level | 399 | doomed = self_and_descendants_ordered_with_level |
| @@ -476,7 +485,8 @@ class Node < ApplicationRecord | |||
| 476 | # :headline => nil | :set | :kept_existing | :not_eligible } | 485 | # :headline => nil | :set | :kept_existing | :not_eligible } |
| 477 | def attach_asset! asset, user:, headline: false | 486 | def attach_asset! asset, user:, headline: false |
| 478 | if in_trash? || trash_node? | 487 | if in_trash? || trash_node? |
| 479 | raise ActiveRecord::RecordInvalid.new(self), "Cannot attach assets to a node in the Trash" | 488 | errors.add(:base, :attach_in_trash) |
| 489 | raise ActiveRecord::RecordInvalid.new(self) | ||
| 480 | end | 490 | end |
| 481 | 491 | ||
| 482 | if lock_owner && lock_owner != user | 492 | if lock_owner && lock_owner != user |
| @@ -610,7 +620,7 @@ class Node < ApplicationRecord | |||
| 610 | # The Trash feature will be the ordinary path to deletion. | 620 | # The Trash feature will be the ordinary path to deletion. |
| 611 | def refuse_destroy_with_children | 621 | def refuse_destroy_with_children |
| 612 | return unless children.exists? | 622 | return unless children.exists? |
| 613 | errors.add(:base, "Cannot destroy a node that still has children") | 623 | errors.add(:base, :has_children) |
| 614 | throw :abort | 624 | throw :abort |
| 615 | end | 625 | end |
| 616 | 626 | ||
| @@ -668,15 +678,15 @@ class Node < ApplicationRecord | |||
| 668 | 678 | ||
| 669 | def reserved_slug_stays_reserved | 679 | def reserved_slug_stays_reserved |
| 670 | if parent&.root? && !trash_node_already_me? | 680 | if parent&.root? && !trash_node_already_me? |
| 671 | errors.add(:slug, "is reserved for the Trash") if slug == CccConventions::TRASH_SLUG | 681 | errors.add(:slug, :reserved_for_trash) if slug == CccConventions::TRASH_SLUG |
| 672 | errors.add(:staged_slug, "is reserved for the Trash") if staged_slug == CccConventions::TRASH_SLUG | 682 | errors.add(:staged_slug, :reserved_for_trash) if staged_slug == CccConventions::TRASH_SLUG |
| 673 | end | 683 | end |
| 674 | 684 | ||
| 675 | if persisted? && slug_was == CccConventions::TRASH_SLUG && Node.find(id).trash_node? | 685 | if persisted? && slug_was == CccConventions::TRASH_SLUG && Node.find(id).trash_node? |
| 676 | errors.add(:slug, "of the Trash node cannot change") if slug_changed? | 686 | errors.add(:slug, :trash_immutable) if slug_changed? |
| 677 | errors.add(:parent_id, "of the Trash node cannot change") if parent_id_changed? | 687 | errors.add(:parent_id, :trash_immutable) if parent_id_changed? |
| 678 | errors.add(:staged_slug, "must stay empty on the Trash node") if staged_slug.present? | 688 | errors.add(:staged_slug, :trash_must_be_empty) if staged_slug.present? |
| 679 | errors.add(:staged_parent_id, "must stay empty on the Trash node") if staged_parent_id.present? | 689 | errors.add(:staged_parent_id, :trash_must_be_empty) if staged_parent_id.present? |
| 680 | end | 690 | end |
| 681 | end | 691 | end |
| 682 | 692 | ||
| @@ -687,12 +697,12 @@ class Node < ApplicationRecord | |||
| 687 | 697 | ||
| 688 | def no_head_inside_trash | 698 | def no_head_inside_trash |
| 689 | return unless head_id.present? | 699 | return unless head_id.present? |
| 690 | errors.add(:head_id, "cannot exist inside the Trash") if in_trash? || trash_node? | 700 | errors.add(:head_id, :inside_trash) if in_trash? || trash_node? |
| 691 | end | 701 | end |
| 692 | 702 | ||
| 693 | def refuse_destroying_trash_node | 703 | def refuse_destroying_trash_node |
| 694 | return unless trash_node? | 704 | return unless trash_node? |
| 695 | errors.add(:base, "The Trash node cannot be destroyed") | 705 | errors.add(:base, :trash_undeletable) |
| 696 | throw :abort | 706 | throw :abort |
| 697 | end | 707 | end |
| 698 | end | 708 | end |
diff --git a/app/models/related_asset.rb b/app/models/related_asset.rb index 8f8d49cb..caf8afda 100644 --- a/app/models/related_asset.rb +++ b/app/models/related_asset.rb | |||
| @@ -12,6 +12,6 @@ class RelatedAsset < ApplicationRecord | |||
| 12 | 12 | ||
| 13 | def headline_only_for_images | 13 | def headline_only_for_images |
| 14 | return unless asset | 14 | return unless asset |
| 15 | errors.add(:headline, "can only be set on image or PDF assets") if headline? && !(asset.image? || asset.pdf?) | 15 | errors.add(:headline, :images_and_pdfs_only) if headline? && !(asset.image? || asset.pdf?) |
| 16 | end | 16 | end |
| 17 | end | 17 | end |
diff --git a/config/locales/de.yml b/config/locales/de.yml index e8a65cea..647caf57 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml | |||
| @@ -114,6 +114,41 @@ de: | |||
| 114 | node_id: "Node-ID" | 114 | node_id: "Node-ID" |
| 115 | path: "Pfad" | 115 | path: "Pfad" |
| 116 | title: "Titel" | 116 | title: "Titel" |
| 117 | node: | ||
| 118 | slug: "Slug" | ||
| 119 | staged_slug: "Vorgemerkter Slug" | ||
| 120 | parent_id: "Eltern-Node" | ||
| 121 | staged_parent_id: "Vorgemerkter Eltern-Node" | ||
| 122 | head_id: "Head" | ||
| 123 | related_asset: | ||
| 124 | headline: "Aufmacher" | ||
| 125 | |||
| 126 | errors: | ||
| 127 | models: | ||
| 128 | node: | ||
| 129 | # Attribute messages: full_messages prefixes the human attribute | ||
| 130 | # name, so these read as fragments. Defined at model level rather | ||
| 131 | # than per attribute, because several attributes share a message. | ||
| 132 | reserved_for_trash: "ist für den Papierkorb reserviert" | ||
| 133 | trash_immutable: "des Papierkorb-Nodes kann nicht geändert werden" | ||
| 134 | trash_must_be_empty: "muss auf dem Papierkorb-Node leer bleiben" | ||
| 135 | inside_trash: "kann im Papierkorb nicht existieren" | ||
| 136 | attributes: | ||
| 137 | base: | ||
| 138 | # :base messages stand alone -- no attribute name is prefixed, | ||
| 139 | # so these are whole sentences. | ||
| 140 | has_children: "Ein Node mit Kindern kann nicht gelöscht werden" | ||
| 141 | trash_undeletable: "Der Papierkorb-Node kann nicht gelöscht werden" | ||
| 142 | publish_in_trash: "Ein Node im Papierkorb kann nicht veröffentlicht werden" | ||
| 143 | move_under_self: "Ein Node kann nicht unter sich selbst oder einen seiner Nachfahren verschoben werden" | ||
| 144 | trash_the_trash: "Der Papierkorb-Node selbst kann nicht in den Papierkorb verschoben werden" | ||
| 145 | restore_target_invalid: "Das Wiederherstellungsziel muss ein lebender Node sein" | ||
| 146 | destroy_outside_trash: "Nodes können nur aus dem Papierkorb gelöscht werden" | ||
| 147 | attach_in_trash: "An einen Node im Papierkorb können keine Assets angehängt werden" | ||
| 148 | related_asset: | ||
| 149 | attributes: | ||
| 150 | headline: | ||
| 151 | images_and_pdfs_only: "kann nur auf Bildern oder PDFs gesetzt werden" | ||
| 117 | 152 | ||
| 118 | tags: | 153 | tags: |
| 119 | index: | 154 | index: |
| @@ -639,7 +674,7 @@ de: | |||
| 639 | edit: | 674 | edit: |
| 640 | title: "Menüeintrag bearbeiten" | 675 | title: "Menüeintrag bearbeiten" |
| 641 | title_fields: | 676 | title_fields: |
| 642 | falls_back: "Menu-Title leer lassen, fall-back nach deutsch" | 677 | falls_back: "Menu-Titel. Leere Titel werden aus deutsch übernommen" |
| 643 | 678 | ||
| 644 | layouts: | 679 | layouts: |
| 645 | application: | 680 | application: |
diff --git a/config/locales/en.yml b/config/locales/en.yml index da87777d..1b112e3d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml | |||
| @@ -66,6 +66,36 @@ en: | |||
| 66 | node_id: "Node Id" | 66 | node_id: "Node Id" |
| 67 | path: "Path" | 67 | path: "Path" |
| 68 | title: "Title" | 68 | title: "Title" |
| 69 | node: | ||
| 70 | slug: "Slug" | ||
| 71 | staged_slug: "Staged slug" | ||
| 72 | parent_id: "Parent node" | ||
| 73 | staged_parent_id: "Staged parent node" | ||
| 74 | head_id: "Head" | ||
| 75 | related_asset: | ||
| 76 | headline: "Headline" | ||
| 77 | |||
| 78 | errors: | ||
| 79 | models: | ||
| 80 | node: | ||
| 81 | reserved_for_trash: "is reserved for the Trash" | ||
| 82 | trash_immutable: "of the Trash node cannot change" | ||
| 83 | trash_must_be_empty: "must stay empty on the Trash node" | ||
| 84 | inside_trash: "cannot exist inside the Trash" | ||
| 85 | attributes: | ||
| 86 | base: | ||
| 87 | has_children: "Cannot destroy a node that still has children" | ||
| 88 | trash_undeletable: "The Trash node cannot be destroyed" | ||
| 89 | publish_in_trash: "Cannot publish a node in the Trash" | ||
| 90 | move_under_self: "Cannot move a node under itself or one of its own descendants" | ||
| 91 | trash_the_trash: "The Trash node itself cannot be trashed" | ||
| 92 | restore_target_invalid: "Restore target must be a living node" | ||
| 93 | destroy_outside_trash: "Nodes are only destroyed from the Trash" | ||
| 94 | attach_in_trash: "Cannot attach assets to a node in the Trash" | ||
| 95 | related_asset: | ||
| 96 | attributes: | ||
| 97 | headline: | ||
| 98 | images_and_pdfs_only: "can only be set on image or PDF assets" | ||
| 69 | 99 | ||
| 70 | tags: | 100 | tags: |
| 71 | index: | 101 | index: |
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb index d7a3abbd..0b6e65ee 100644 --- a/test/controllers/nodes_controller_test.rb +++ b/test/controllers/nodes_controller_test.rb | |||
| @@ -628,7 +628,7 @@ class NodesControllerTest < ActionController::TestCase | |||
| 628 | test "chapters renders the curated heading" do | 628 | test "chapters renders the curated heading" do |
| 629 | login_as :quentin | 629 | login_as :quentin |
| 630 | get :chapters | 630 | get :chapters |
| 631 | assert_select "h1", "Chapters" | 631 | assert_select "h1", "Dezentrale" |
| 632 | end | 632 | end |
| 633 | 633 | ||
| 634 | test "sitemap collapses configured paths but leaves others open" do | 634 | test "sitemap collapses configured paths but leaves others open" do |
diff --git a/test/models/node_test.rb b/test/models/node_test.rb index c5b0d6a2..c1316ea6 100644 --- a/test/models/node_test.rb +++ b/test/models/node_test.rb | |||
| @@ -919,4 +919,21 @@ class NodeTest < ActiveSupport::TestCase | |||
| 919 | node.reload | 919 | node.reload |
| 920 | assert_raises(ActiveRecord::RecordInvalid) { node.publish_draft! } | 920 | assert_raises(ActiveRecord::RecordInvalid) { node.publish_draft! } |
| 921 | end | 921 | end |
| 922 | |||
| 923 | test "refusals localise their message rather than hardcoding it" do | ||
| 924 | trash = Node.trash | ||
| 925 | |||
| 926 | I18n.with_locale(:de) do | ||
| 927 | error = assert_raises(ActiveRecord::RecordInvalid) { trash.trash! } | ||
| 928 | assert_includes error.message, | ||
| 929 | I18n.t("activerecord.errors.models.node.attributes.base.trash_the_trash") | ||
| 930 | end | ||
| 931 | |||
| 932 | I18n.with_locale(:en) do | ||
| 933 | trash.errors.clear | ||
| 934 | error = assert_raises(ActiveRecord::RecordInvalid) { trash.trash! } | ||
| 935 | assert_includes error.message, | ||
| 936 | I18n.t("activerecord.errors.models.node.attributes.base.trash_the_trash") | ||
| 937 | end | ||
| 938 | end | ||
| 922 | end | 939 | end |
diff --git a/test/models/related_asset_test.rb b/test/models/related_asset_test.rb index bb86ddbb..ea7c3d35 100644 --- a/test/models/related_asset_test.rb +++ b/test/models/related_asset_test.rb | |||
| @@ -29,7 +29,7 @@ class RelatedAssetTest < ActiveSupport::TestCase | |||
| 29 | 29 | ||
| 30 | related.headline = true | 30 | related.headline = true |
| 31 | assert_not related.valid? | 31 | assert_not related.valid? |
| 32 | assert_includes related.errors[:headline], "can only be set on image or PDF assets" | 32 | assert_includes related.errors[:headline], I18n.t("activerecord.errors.models.related_asset.attributes.headline.images_and_pdfs_only") |
| 33 | end | 33 | end |
| 34 | 34 | ||
| 35 | test "the headline validation does not raise when asset is missing" do | 35 | test "the headline validation does not raise when asset is missing" do |
