diff options
| -rw-r--r-- | app/controllers/nodes_controller.rb | 3 | ||||
| -rw-r--r-- | app/helpers/node_actions_helper.rb | 12 | ||||
| -rw-r--r-- | app/models/node.rb | 20 | ||||
| -rw-r--r-- | app/models/node_action.rb | 1 | ||||
| -rw-r--r-- | app/views/nodes/edit.html.erb | 6 | ||||
| -rw-r--r-- | config/locales/de.yml | 3 | ||||
| -rw-r--r-- | config/locales/en.yml | 3 | ||||
| -rw-r--r-- | test/models/node_test.rb | 29 |
8 files changed, 75 insertions, 2 deletions
diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb index 5b919b1f..22606674 100644 --- a/app/controllers/nodes_controller.rb +++ b/app/controllers/nodes_controller.rb | |||
| @@ -116,7 +116,8 @@ class NodesController < ApplicationController | |||
| 116 | rescue LockedByAnotherUser => e | 116 | rescue LockedByAnotherUser => e |
| 117 | flash[:error] = e.message | 117 | flash[:error] = e.message |
| 118 | redirect_to node_path(@node) | 118 | redirect_to node_path(@node) |
| 119 | rescue ActiveRecord::RecordInvalid | 119 | rescue ActiveRecord::RecordInvalid => e |
| 120 | flash.now[:error] = e.record.errors.full_messages.to_sentence | ||
| 120 | @page = @node.autosave || @node.draft || @node.head | 121 | @page = @node.autosave || @node.draft || @node.head |
| 121 | render :action => :edit | 122 | render :action => :edit |
| 122 | end | 123 | end |
diff --git a/app/helpers/node_actions_helper.rb b/app/helpers/node_actions_helper.rb index 4cf990b8..c4de46cc 100644 --- a/app/helpers/node_actions_helper.rb +++ b/app/helpers/node_actions_helper.rb | |||
| @@ -28,7 +28,8 @@ module NodeActionsHelper | |||
| 28 | "admin_revoke" => "shield-minus", | 28 | "admin_revoke" => "shield-minus", |
| 29 | "event_create" => "calendar-plus", | 29 | "event_create" => "calendar-plus", |
| 30 | "event_update" => "calendar-event", | 30 | "event_update" => "calendar-event", |
| 31 | "event_destroy" => "calendar-x" | 31 | "event_destroy" => "calendar-x", |
| 32 | "node_external_url" => "world" | ||
| 32 | }.freeze | 33 | }.freeze |
| 33 | 34 | ||
| 34 | def verb_icon action | 35 | def verb_icon action |
| @@ -64,6 +65,7 @@ module NodeActionsHelper | |||
| 64 | return true if m["translation_diff"].present? | 65 | return true if m["translation_diff"].present? |
| 65 | return true if m["changes"].present? || m["description_changed"] | 66 | return true if m["changes"].present? || m["description_changed"] |
| 66 | return true if m["title"].is_a?(Hash) && m.dig("title", "from") != m.dig("title", "to") | 67 | return true if m["title"].is_a?(Hash) && m.dig("title", "from") != m.dig("title", "to") |
| 68 | return true if m["external_url"].present? | ||
| 67 | %w[author tags template_changed assets assets_changed assets_reordered | 69 | %w[author tags template_changed assets assets_changed assets_reordered |
| 68 | abstract_changed body_changed].any? { |key| m[key].present? } | 70 | abstract_changed body_changed].any? { |key| m[key].present? } |
| 69 | end | 71 | end |
| @@ -95,6 +97,9 @@ module NodeActionsHelper | |||
| 95 | end | 97 | end |
| 96 | items << t("node_actions.assets_reordered") if m["assets_reordered"] | 98 | items << t("node_actions.assets_reordered") if m["assets_reordered"] |
| 97 | items << t("node_actions.assets_changed") if m["assets_changed"] | 99 | items << t("node_actions.assets_changed") if m["assets_changed"] |
| 100 | items << t("node_actions.detail_external_url", | ||
| 101 | :from => m.dig("external_url", "from").presence || t("node_actions.event_none"), | ||
| 102 | :to => m.dig("external_url", "to").presence || t("node_actions.event_none")) if m["external_url"] | ||
| 98 | items | 103 | items |
| 99 | end | 104 | end |
| 100 | 105 | ||
| @@ -400,6 +405,11 @@ module NodeActionsHelper | |||
| 400 | event_sentence(action, "event_destroy") | 405 | event_sentence(action, "event_destroy") |
| 401 | end | 406 | end |
| 402 | 407 | ||
| 408 | def summarize_node_external_url action | ||
| 409 | t("node_actions.node_external_url", :actor => actor_ref(action), | ||
| 410 | :subject => subject_ref(action)).html_safe | ||
| 411 | end | ||
| 412 | |||
| 403 | def event_sentence action, key | 413 | def event_sentence action, key |
| 404 | if action.node | 414 | if action.node |
| 405 | t("node_actions.#{key}_on", :actor => actor_ref(action), | 415 | t("node_actions.#{key}_on", :actor => actor_ref(action), |
diff --git a/app/models/node.rb b/app/models/node.rb index f8c29b48..4b7c9772 100644 --- a/app/models/node.rb +++ b/app/models/node.rb | |||
| @@ -42,6 +42,9 @@ class Node < ApplicationRecord | |||
| 42 | :inclusion => { :in => ->(_) { Page.custom_templates } }, | 42 | :inclusion => { :in => ->(_) { Page.custom_templates } }, |
| 43 | :allow_blank => true, | 43 | :allow_blank => true, |
| 44 | :if => :default_template_name_changed? | 44 | :if => :default_template_name_changed? |
| 45 | validates :external_url, :format => { :with => %r{\Ahttps?://}i, | ||
| 46 | :allow_blank => true, | ||
| 47 | :message => :must_be_http } | ||
| 45 | 48 | ||
| 46 | # Everything outside the Trash subtree, the Trash node included. | 49 | # Everything outside the Trash subtree, the Trash node included. |
| 47 | # Relies on unique_name being authoritative for tree position -- | 50 | # Relies on unique_name being authoritative for tree position -- |
| @@ -605,6 +608,23 @@ class Node < ApplicationRecord | |||
| 605 | self.created_at < new_id_format_date ? unique_path : id | 608 | self.created_at < new_id_format_date ? unique_path : id |
| 606 | end | 609 | end |
| 607 | 610 | ||
| 611 | def update_external_url!(url, current_user = nil) | ||
| 612 | normalised = url.presence | ||
| 613 | return false if normalised == external_url | ||
| 614 | |||
| 615 | guard_live_change!(current_user) | ||
| 616 | previous = external_url | ||
| 617 | |||
| 618 | transaction do | ||
| 619 | update!(:external_url => normalised) | ||
| 620 | NodeAction.record!(:node => self, :user => current_user, | ||
| 621 | :action => "node_external_url", | ||
| 622 | :path => unique_name, | ||
| 623 | :external_url => { "from" => previous, "to" => normalised }) | ||
| 624 | end | ||
| 625 | true | ||
| 626 | end | ||
| 627 | |||
| 608 | # Full-text search across all locale translations using PostgreSQL tsvector. | 628 | # Full-text search across all locale translations using PostgreSQL tsvector. |
| 609 | # Uses 'simple' dictionary (no stemming, no stopwords) so queries work | 629 | # Uses 'simple' dictionary (no stemming, no stopwords) so queries work |
| 610 | # across German and English content without language detection. | 630 | # across German and English content without language detection. |
diff --git a/app/models/node_action.rb b/app/models/node_action.rb index bfa469b1..f1e4eaea 100644 --- a/app/models/node_action.rb +++ b/app/models/node_action.rb | |||
| @@ -134,6 +134,7 @@ class NodeAction < ApplicationRecord | |||
| 134 | # from the node verbs' "tags", which is a pair, | 134 | # from the node verbs' "tags", which is a pair, |
| 135 | # so one renderer cannot mistake the other. | 135 | # so one renderer cannot mistake the other. |
| 136 | # "path" -- the node's unique_name, when it has a node | 136 | # "path" -- the node's unique_name, when it has a node |
| 137 | # "external_url" -- pair | ||
| 137 | # | 138 | # |
| 138 | # On "event_update" only, and only when something changed -- an | 139 | # On "event_update" only, and only when something changed -- an |
| 139 | # update that changes nothing records no entry at all: | 140 | # update that changes nothing records no entry at all: |
diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index b65416e1..c153c241 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb | |||
| @@ -114,6 +114,12 @@ | |||
| 114 | %> | 114 | %> |
| 115 | </div> | 115 | </div> |
| 116 | 116 | ||
| 117 | <div class="layout_row_label"><%= t(".external_url") %></div> | ||
| 118 | <div class="layout_row_content"> | ||
| 119 | <%= f.text_field :external_url %> | ||
| 120 | <p class="field_hint"><%= t(".external_url_hint") %></p> | ||
| 121 | </div> | ||
| 122 | |||
| 117 | <div class="layout_row_label"><%= t(".tags") %></div> | 123 | <div class="layout_row_label"><%= t(".tags") %></div> |
| 118 | <div class="layout_row_content"> | 124 | <div class="layout_row_content"> |
| 119 | <%= text_field_tag :tag_list, @page.tag_list.join(', ') %> | 125 | <%= text_field_tag :tag_list, @page.tag_list.join(', ') %> |
diff --git a/config/locales/de.yml b/config/locales/de.yml index bfa58360..5197b00c 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml | |||
| @@ -255,6 +255,7 @@ de: | |||
| 255 | detail_event_allday: "ganztägig %{from} → %{to}" | 255 | detail_event_allday: "ganztägig %{from} → %{to}" |
| 256 | detail_event_end: "Ende %{from} → %{to}" | 256 | detail_event_end: "Ende %{from} → %{to}" |
| 257 | event_none: "—" | 257 | event_none: "—" |
| 258 | node_external_url: "%{actor} hat die externe Homepage von %{subject} geändert" | ||
| 258 | unknown_event: "ein Termin" | 259 | unknown_event: "ein Termin" |
| 259 | 260 | ||
| 260 | open_gallery: "Gallerie anzeigen" | 261 | open_gallery: "Gallerie anzeigen" |
| @@ -456,6 +457,8 @@ de: | |||
| 456 | set_headline_label: "Als Aufmacherbild setzen" | 457 | set_headline_label: "Als Aufmacherbild setzen" |
| 457 | set_headline_title: "Dieses Foto als Aufmacherbild der Seite verwenden" | 458 | set_headline_title: "Dieses Foto als Aufmacherbild der Seite verwenden" |
| 458 | remove_image_label: "Bild entfernen" | 459 | remove_image_label: "Bild entfernen" |
| 460 | external_url: "Externe Homepage" | ||
| 461 | external_url_hint: "Die eigene Website einer Dezentrale, wird auf dessen Seite angezeigt. Vorsicht, Änderung wird ohne Draft sofort veröffentlicht." | ||
| 459 | show: | 462 | show: |
| 460 | status: "Status" | 463 | status: "Status" |
| 461 | head: "Head" | 464 | head: "Head" |
diff --git a/config/locales/en.yml b/config/locales/en.yml index f786ca81..7e484c66 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml | |||
| @@ -201,6 +201,7 @@ en: | |||
| 201 | detail_event_coordinates: "coordinates changed" | 201 | detail_event_coordinates: "coordinates changed" |
| 202 | detail_event_allday: "all-day %{from} → %{to}" | 202 | detail_event_allday: "all-day %{from} → %{to}" |
| 203 | detail_event_end: "end %{from} → %{to}" | 203 | detail_event_end: "end %{from} → %{to}" |
| 204 | node_external_url: "%{actor} changed the external homepage of %{subject}" | ||
| 204 | event_none: "—" | 205 | event_none: "—" |
| 205 | unknown_event: "an event" | 206 | unknown_event: "an event" |
| 206 | 207 | ||
| @@ -403,6 +404,8 @@ en: | |||
| 403 | set_headline_label: "Set as headline image" | 404 | set_headline_label: "Set as headline image" |
| 404 | set_headline_title: "Use this photo as the page's headline image" | 405 | set_headline_title: "Use this photo as the page's headline image" |
| 405 | remove_image_label: "Remove image" | 406 | remove_image_label: "Remove image" |
| 407 | external_url: "External homepage" | ||
| 408 | external_url_hint: "A chapter's own website, shown on its page. N.B.: Changing takes effect immediately." | ||
| 406 | show: | 409 | show: |
| 407 | status: "Status" | 410 | status: "Status" |
| 408 | head: "Head" | 411 | head: "Head" |
diff --git a/test/models/node_test.rb b/test/models/node_test.rb index a34094a0..c7fee58e 100644 --- a/test/models/node_test.rb +++ b/test/models/node_test.rb | |||
| @@ -1072,4 +1072,33 @@ class NodeTest < ActiveSupport::TestCase | |||
| 1072 | assert_not node.reload.in_trash? | 1072 | assert_not node.reload.in_trash? |
| 1073 | assert_equal club.id, node.parent_id | 1073 | assert_equal club.id, node.parent_id |
| 1074 | end | 1074 | end |
| 1075 | |||
| 1076 | test "setting an external url is gated and witnessed" do | ||
| 1077 | node = Node.root.children.create!(:slug => "chapter_url_test") | ||
| 1078 | |||
| 1079 | assert_difference -> { NodeAction.where(:action => "node_external_url").count }, 1 do | ||
| 1080 | assert node.update_external_url!("https://example.org", users(:aaron)) | ||
| 1081 | end | ||
| 1082 | |||
| 1083 | assert_equal "https://example.org", node.reload.external_url | ||
| 1084 | entry = NodeAction.where(:action => "node_external_url").last | ||
| 1085 | assert_equal({ "from" => nil, "to" => "https://example.org" }, | ||
| 1086 | entry.metadata["external_url"]) | ||
| 1087 | end | ||
| 1088 | |||
| 1089 | test "a javascript url is refused" do | ||
| 1090 | node = Node.root.children.create!(:slug => "chapter_url_reject") | ||
| 1091 | assert_raises(ActiveRecord::RecordInvalid) do | ||
| 1092 | node.update_external_url!("javascript:alert(1)", users(:aaron)) | ||
| 1093 | end | ||
| 1094 | end | ||
| 1095 | |||
| 1096 | test "setting an external url on a restricted node needs redaktion" do | ||
| 1097 | node = Node.find_by(:unique_name => "updates") || | ||
| 1098 | Node.root.children.create!(:slug => "updates") | ||
| 1099 | |||
| 1100 | assert_raises(ActiveRecord::RecordInvalid) do | ||
| 1101 | node.update_external_url!("https://example.org", users(:quentin)) | ||
| 1102 | end | ||
| 1103 | end | ||
| 1075 | end | 1104 | end |
