diff options
| -rw-r--r-- | app/controllers/nodes_controller.rb | 3 | ||||
| -rw-r--r-- | app/helpers/node_actions_helper.rb | 15 | ||||
| -rw-r--r-- | app/models/node.rb | 22 | ||||
| -rw-r--r-- | app/models/node_action.rb | 6 | ||||
| -rw-r--r-- | app/models/page.rb | 4 | ||||
| -rw-r--r-- | app/views/custom/page_templates/public/chapter_detail.html.erb | 4 | ||||
| -rw-r--r-- | app/views/custom/partials/_chapter.html.erb | 4 | ||||
| -rw-r--r-- | app/views/nodes/edit.html.erb | 3 | ||||
| -rw-r--r-- | config/locales/de.yml | 11 | ||||
| -rw-r--r-- | config/locales/en.yml | 4 | ||||
| -rw-r--r-- | lib/tasks/pages.rake | 28 | ||||
| -rw-r--r-- | test/models/node_test.rb | 25 |
12 files changed, 69 insertions, 60 deletions
diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb index 3009e25a..ef47f258 100644 --- a/app/controllers/nodes_controller.rb +++ b/app/controllers/nodes_controller.rb | |||
| @@ -281,7 +281,8 @@ class NodesController < ApplicationController | |||
| 281 | 281 | ||
| 282 | def page_params | 282 | def page_params |
| 283 | params.fetch(:page, {}).permit(:title, :abstract, :body, :template_name, | 283 | params.fetch(:page, {}).permit(:title, :abstract, :body, :template_name, |
| 284 | :published_at, :user_id, :slug, :parent_node_id) | 284 | :published_at, :user_id, :slug, :parent_node_id, |
| 285 | :external_url) | ||
| 285 | end | 286 | end |
| 286 | 287 | ||
| 287 | def find_node | 288 | def find_node |
diff --git a/app/helpers/node_actions_helper.rb b/app/helpers/node_actions_helper.rb index 19385543..f21fee61 100644 --- a/app/helpers/node_actions_helper.rb +++ b/app/helpers/node_actions_helper.rb | |||
| @@ -28,8 +28,7 @@ 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" | ||
| 33 | }.freeze | 32 | }.freeze |
| 34 | 33 | ||
| 35 | def verb_icon action | 34 | def verb_icon action |
| @@ -67,7 +66,7 @@ module NodeActionsHelper | |||
| 67 | return true if m["title"].is_a?(Hash) && m.dig("title", "from") != m.dig("title", "to") | 66 | 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 | return true if m["external_url"].present? |
| 69 | %w[author tags template_changed assets assets_changed assets_reordered | 68 | %w[author tags template_changed assets assets_changed assets_reordered |
| 70 | abstract_changed body_changed].any? { |key| m[key].present? } | 69 | abstract_changed body_changed external_url].any? { |key| m[key].present? } |
| 71 | end | 70 | end |
| 72 | 71 | ||
| 73 | def default_locale_changes action | 72 | def default_locale_changes action |
| @@ -95,6 +94,11 @@ module NodeActionsHelper | |||
| 95 | :names => linked_asset_names(action, names)).html_safe | 94 | :names => linked_asset_names(action, names)).html_safe |
| 96 | end | 95 | end |
| 97 | end | 96 | end |
| 97 | if m["external_url"] | ||
| 98 | items << t("node_actions.detail_external_url", | ||
| 99 | :from => m.dig("external_url", "from").presence || t("node_actions.event_none"), | ||
| 100 | :to => m.dig("external_url", "to").presence || t("node_actions.event_none")) | ||
| 101 | end | ||
| 98 | items << t("node_actions.assets_reordered") if m["assets_reordered"] | 102 | items << t("node_actions.assets_reordered") if m["assets_reordered"] |
| 99 | items << t("node_actions.assets_changed") if m["assets_changed"] | 103 | items << t("node_actions.assets_changed") if m["assets_changed"] |
| 100 | items << t("node_actions.detail_external_url", | 104 | items << t("node_actions.detail_external_url", |
| @@ -405,11 +409,6 @@ module NodeActionsHelper | |||
| 405 | event_sentence(action, "event_destroy") | 409 | event_sentence(action, "event_destroy") |
| 406 | end | 410 | end |
| 407 | 411 | ||
| 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 | |||
| 413 | def event_sentence action, key | 412 | def event_sentence action, key |
| 414 | if action.node | 413 | if action.node |
| 415 | t("node_actions.#{key}_on", :actor => actor_ref(action), | 414 | t("node_actions.#{key}_on", :actor => actor_ref(action), |
diff --git a/app/models/node.rb b/app/models/node.rb index 5c28a786..f5b76b45 100644 --- a/app/models/node.rb +++ b/app/models/node.rb | |||
| @@ -42,9 +42,6 @@ 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 } | ||
| 48 | 45 | ||
| 49 | # Everything outside the Trash subtree, the Trash node included. | 46 | # Everything outside the Trash subtree, the Trash node included. |
| 50 | # Relies on unique_name being authoritative for tree position -- | 47 | # Relies on unique_name being authoritative for tree position -- |
| @@ -260,6 +257,8 @@ class Node < ApplicationRecord | |||
| 260 | self.slug = self.head.slug | 257 | self.slug = self.head.slug |
| 261 | end | 258 | end |
| 262 | 259 | ||
| 260 | self.external_url = self.head.external_url | ||
| 261 | |||
| 263 | if self.head.parent_node_id && self.head.parent_node_id != parent_id | 262 | if self.head.parent_node_id && self.head.parent_node_id != parent_id |
| 264 | new_parent = Node.find_by(:id => self.head.parent_node_id) | 263 | new_parent = Node.find_by(:id => self.head.parent_node_id) |
| 265 | 264 | ||
| @@ -604,23 +603,6 @@ class Node < ApplicationRecord | |||
| 604 | self.created_at < new_id_format_date ? unique_path : id | 603 | self.created_at < new_id_format_date ? unique_path : id |
| 605 | end | 604 | end |
| 606 | 605 | ||
| 607 | def update_external_url!(url, current_user = nil) | ||
| 608 | normalised = url.presence | ||
| 609 | return false if normalised == external_url | ||
| 610 | |||
| 611 | guard_live_change!(current_user) | ||
| 612 | previous = external_url | ||
| 613 | |||
| 614 | transaction do | ||
| 615 | update!(:external_url => normalised) | ||
| 616 | NodeAction.record!(:node => self, :user => current_user, | ||
| 617 | :action => "node_external_url", | ||
| 618 | :path => unique_name, | ||
| 619 | :external_url => { "from" => previous, "to" => normalised }) | ||
| 620 | end | ||
| 621 | true | ||
| 622 | end | ||
| 623 | |||
| 624 | # Full-text search across all locale translations using PostgreSQL tsvector. | 606 | # Full-text search across all locale translations using PostgreSQL tsvector. |
| 625 | # Uses 'simple' dictionary (no stemming, no stopwords) so queries work | 607 | # Uses 'simple' dictionary (no stemming, no stopwords) so queries work |
| 626 | # across German and English content without language detection. | 608 | # across German and English content without language detection. |
diff --git a/app/models/node_action.rb b/app/models/node_action.rb index f1e4eaea..1e64861e 100644 --- a/app/models/node_action.rb +++ b/app/models/node_action.rb | |||
| @@ -35,6 +35,7 @@ class NodeAction < ApplicationRecord | |||
| 35 | # "title" -- pair, always; "from" null on first publish | 35 | # "title" -- pair, always; "from" null on first publish |
| 36 | # "author" -- pair, when the byline changed (incl. first publish) | 36 | # "author" -- pair, when the byline changed (incl. first publish) |
| 37 | # "tags" -- pair of arrays, when changed | 37 | # "tags" -- pair of arrays, when changed |
| 38 | # "external_url" -- pair, when a chapter's homepage changed | ||
| 38 | # "assets" -- {"added" => [asset names], "removed" => [asset names]}, | 39 | # "assets" -- {"added" => [asset names], "removed" => [asset names]}, |
| 39 | # keys only when any; a delta, not a pair. The event IS | 40 | # keys only when any; a delta, not a pair. The event IS |
| 40 | # the delta, full sets would bloat every entry. Changed | 41 | # the delta, full sets would bloat every entry. Changed |
| @@ -134,7 +135,6 @@ class NodeAction < ApplicationRecord | |||
| 134 | # from the node verbs' "tags", which is a pair, | 135 | # from the node verbs' "tags", which is a pair, |
| 135 | # so one renderer cannot mistake the other. | 136 | # so one renderer cannot mistake the other. |
| 136 | # "path" -- the node's unique_name, when it has a node | 137 | # "path" -- the node's unique_name, when it has a node |
| 137 | # "external_url" -- pair | ||
| 138 | # | 138 | # |
| 139 | # On "event_update" only, and only when something changed -- an | 139 | # On "event_update" only, and only when something changed -- an |
| 140 | # update that changes nothing records no entry at all: | 140 | # update that changes nothing records no entry at all: |
| @@ -199,12 +199,16 @@ class NodeAction < ApplicationRecord | |||
| 199 | "to" => title_of.call(new_page) } } | 199 | "to" => title_of.call(new_page) } } |
| 200 | unless old_page | 200 | unless old_page |
| 201 | diff[:author] = { "from" => nil, "to" => new_page.user&.login } if new_page.user | 201 | diff[:author] = { "from" => nil, "to" => new_page.user&.login } if new_page.user |
| 202 | diff[:external_url] = { "from" => nil, "to" => new_page.external_url } if new_page.external_url.present? | ||
| 202 | return diff | 203 | return diff |
| 203 | end | 204 | end |
| 204 | 205 | ||
| 205 | old_author, new_author = old_page.user&.login, new_page.user&.login | 206 | old_author, new_author = old_page.user&.login, new_page.user&.login |
| 206 | diff[:author] = { "from" => old_author, "to" => new_author } if old_author != new_author | 207 | diff[:author] = { "from" => old_author, "to" => new_author } if old_author != new_author |
| 207 | 208 | ||
| 209 | old_url, new_url = old_page.external_url, new_page.external_url | ||
| 210 | diff[:external_url] = { "from" => old_url, "to" => new_url } if old_url != new_url | ||
| 211 | |||
| 208 | old_tags, new_tags = old_page.tag_list.sort, new_page.tag_list.sort | 212 | old_tags, new_tags = old_page.tag_list.sort, new_page.tag_list.sort |
| 209 | diff[:tags] = { "from" => old_tags, "to" => new_tags } if old_tags != new_tags | 213 | diff[:tags] = { "from" => old_tags, "to" => new_tags } if old_tags != new_tags |
| 210 | 214 | ||
diff --git a/app/models/page.rb b/app/models/page.rb index bba7e6bc..3240057f 100644 --- a/app/models/page.rb +++ b/app/models/page.rb | |||
| @@ -20,6 +20,9 @@ class Page < ApplicationRecord | |||
| 20 | :inclusion => { :in => ->(_) { Page.custom_templates } }, | 20 | :inclusion => { :in => ->(_) { Page.custom_templates } }, |
| 21 | :allow_blank => true, | 21 | :allow_blank => true, |
| 22 | :if => :template_name_changed? | 22 | :if => :template_name_changed? |
| 23 | validates :external_url, :format => { :with => %r{\Ahttps?://}i, | ||
| 24 | :allow_blank => true, | ||
| 25 | :message => :must_be_http } | ||
| 23 | validates_format_of :slug, :with => /\A[A-Za-z0-9][A-Za-z0-9_-]*\z/, | 26 | validates_format_of :slug, :with => /\A[A-Za-z0-9][A-Za-z0-9_-]*\z/, |
| 24 | :unless => -> { slug.blank? } | 27 | :unless => -> { slug.blank? } |
| 25 | validate :page_slug_not_reserved | 28 | validate :page_slug_not_reserved |
| @@ -208,6 +211,7 @@ class Page < ApplicationRecord | |||
| 208 | # Clone untranslated attributes | 211 | # Clone untranslated attributes |
| 209 | self.slug = page.slug | 212 | self.slug = page.slug |
| 210 | self.parent_node_id = page.parent_node_id | 213 | self.parent_node_id = page.parent_node_id |
| 214 | self.external_url = page.external_url | ||
| 211 | self.tag_list = page.tag_list | 215 | self.tag_list = page.tag_list |
| 212 | self.template_name ||= page.template_name | 216 | self.template_name ||= page.template_name |
| 213 | self.published_at = page.published_at | 217 | self.published_at = page.published_at |
diff --git a/app/views/custom/page_templates/public/chapter_detail.html.erb b/app/views/custom/page_templates/public/chapter_detail.html.erb index 794be823..105ec21a 100644 --- a/app/views/custom/page_templates/public/chapter_detail.html.erb +++ b/app/views/custom/page_templates/public/chapter_detail.html.erb | |||
| @@ -1,8 +1,8 @@ | |||
| 1 | <div class="article" lang="<%= @page.effective_lang %>"> | 1 | <div class="article" lang="<%= @page.effective_lang %>"> |
| 2 | <h1 class="headline"><%= @page.title %></h1> | 2 | <h1 class="headline"><%= @page.title %></h1> |
| 3 | <%= headline_image %> | 3 | <%= headline_image %> |
| 4 | <% if @page.node.external_url.present? %> | 4 | <% if @page.external_url.present? %> |
| 5 | <div class="chapter_url"><%= link_to @page.node.external_url, @page.node.external_url, target: '_blank', rel: 'noopener' %></div> | 5 | <div class="chapter_url"><%= link_to @page.external_url, @page.external_url, target: '_blank', rel: 'noopener' %></div> |
| 6 | <% end %> | 6 | <% end %> |
| 7 | <p><%= sanitize(@page.abstract) %></p> | 7 | <p><%= sanitize(@page.abstract) %></p> |
| 8 | <%= aggregate?(@page.body) %> | 8 | <%= aggregate?(@page.body) %> |
diff --git a/app/views/custom/partials/_chapter.html.erb b/app/views/custom/partials/_chapter.html.erb index 8a92b46f..2065560a 100644 --- a/app/views/custom/partials/_chapter.html.erb +++ b/app/views/custom/partials/_chapter.html.erb | |||
| @@ -7,8 +7,8 @@ | |||
| 7 | <% if page.abstract.present? %> | 7 | <% if page.abstract.present? %> |
| 8 | <div class="chapter_location"><%= page.abstract %></div> | 8 | <div class="chapter_location"><%= page.abstract %></div> |
| 9 | <% end %> | 9 | <% end %> |
| 10 | <% if page.node.external_url.present? %> | 10 | <% if page.external_url.present? %> |
| 11 | <div class="chapter_url"><%= link_to page.node.external_url, page.node.external_url, target: '_blank', rel: 'noopener' %></div> | 11 | <div class="chapter_url"><%= link_to page.external_url, page.external_url, target: '_blank', rel: 'noopener' %></div> |
| 12 | <% end %> | 12 | <% end %> |
| 13 | <% open_days = page.node.events.tagged_with('open-day').order(:start_time) %> | 13 | <% open_days = page.node.events.tagged_with('open-day').order(:start_time) %> |
| 14 | <% if open_days.any? %> | 14 | <% if open_days.any? %> |
diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index ae206578..6b5a1234 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb | |||
| @@ -116,8 +116,7 @@ | |||
| 116 | 116 | ||
| 117 | <div class="layout_row_label"><%= t(".external_url") %></div> | 117 | <div class="layout_row_label"><%= t(".external_url") %></div> |
| 118 | <div class="layout_row_content"> | 118 | <div class="layout_row_content"> |
| 119 | <%= f.text_field :external_url %> | 119 | <%= d.text_field :external_url %> |
| 120 | <p class="field_hint"><%= t(".external_url_hint") %></p> | ||
| 121 | </div> | 120 | </div> |
| 122 | 121 | ||
| 123 | <div class="layout_row_label"><%= t(".tags") %></div> | 122 | <div class="layout_row_label"><%= t(".tags") %></div> |
diff --git a/config/locales/de.yml b/config/locales/de.yml index 032d7647..c6dc17b7 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml | |||
| @@ -120,17 +120,12 @@ de: | |||
| 120 | errors: | 120 | errors: |
| 121 | models: | 121 | models: |
| 122 | node: | 122 | node: |
| 123 | # Attribute messages: full_messages prefixes the human attribute | ||
| 124 | # name, so these read as fragments. Defined at model level rather | ||
| 125 | # than per attribute, because several attributes share a message. | ||
| 126 | reserved_for_trash: "ist für den Papierkorb reserviert" | 123 | reserved_for_trash: "ist für den Papierkorb reserviert" |
| 127 | trash_immutable: "des Papierkorb-Nodes kann nicht geändert werden" | 124 | trash_immutable: "des Papierkorb-Nodes kann nicht geändert werden" |
| 128 | trash_must_be_empty: "muss auf dem Papierkorb-Node leer bleiben" | 125 | trash_must_be_empty: "muss auf dem Papierkorb-Node leer bleiben" |
| 129 | inside_trash: "kann im Papierkorb nicht existieren" | 126 | inside_trash: "kann im Papierkorb nicht existieren" |
| 130 | attributes: | 127 | attributes: |
| 131 | base: | 128 | base: |
| 132 | # :base messages stand alone -- no attribute name is prefixed, | ||
| 133 | # so these are whole sentences. | ||
| 134 | has_children: "Ein Node mit Kindern kann nicht gelöscht werden" | 129 | has_children: "Ein Node mit Kindern kann nicht gelöscht werden" |
| 135 | trash_undeletable: "Der Papierkorb-Node kann nicht gelöscht werden" | 130 | trash_undeletable: "Der Papierkorb-Node kann nicht gelöscht werden" |
| 136 | publish_in_trash: "Ein Node im Papierkorb kann nicht veröffentlicht werden" | 131 | publish_in_trash: "Ein Node im Papierkorb kann nicht veröffentlicht werden" |
| @@ -145,6 +140,8 @@ de: | |||
| 145 | attributes: | 140 | attributes: |
| 146 | slug: | 141 | slug: |
| 147 | reserved_for_trash: "ist für den Papierkorb reserviert" | 142 | reserved_for_trash: "ist für den Papierkorb reserviert" |
| 143 | external_url: | ||
| 144 | must_be_http: "muss mit http:// oder https:// beginnen" | ||
| 148 | user: | 145 | user: |
| 149 | attributes: | 146 | attributes: |
| 150 | roles: | 147 | roles: |
| @@ -163,7 +160,7 @@ de: | |||
| 163 | event: | 160 | event: |
| 164 | attributes: | 161 | attributes: |
| 165 | url: | 162 | url: |
| 166 | must_be_http: "must start with http:// or https://" | 163 | must_be_http: "muss mit http:// oder https:// beginnen" |
| 167 | 164 | ||
| 168 | tags: | 165 | tags: |
| 169 | index: | 166 | index: |
| @@ -253,7 +250,6 @@ de: | |||
| 253 | detail_event_allday: "ganztägig %{from} → %{to}" | 250 | detail_event_allday: "ganztägig %{from} → %{to}" |
| 254 | detail_event_end: "Ende %{from} → %{to}" | 251 | detail_event_end: "Ende %{from} → %{to}" |
| 255 | event_none: "—" | 252 | event_none: "—" |
| 256 | node_external_url: "%{actor} hat die externe Homepage von %{subject} geändert" | ||
| 257 | unknown_event: "ein Termin" | 253 | unknown_event: "ein Termin" |
| 258 | 254 | ||
| 259 | open_gallery: "Gallerie anzeigen" | 255 | open_gallery: "Gallerie anzeigen" |
| @@ -456,7 +452,6 @@ de: | |||
| 456 | set_headline_title: "Dieses Foto als Aufmacherbild der Seite verwenden" | 452 | set_headline_title: "Dieses Foto als Aufmacherbild der Seite verwenden" |
| 457 | remove_image_label: "Bild entfernen" | 453 | remove_image_label: "Bild entfernen" |
| 458 | external_url: "Externe Homepage" | 454 | external_url: "Externe Homepage" |
| 459 | external_url_hint: "Die eigene Website einer Dezentrale, wird auf dessen Seite angezeigt. Vorsicht, Änderung wird ohne Draft sofort veröffentlicht." | ||
| 460 | show: | 455 | show: |
| 461 | status: "Status" | 456 | status: "Status" |
| 462 | head: "Head" | 457 | head: "Head" |
diff --git a/config/locales/en.yml b/config/locales/en.yml index e75000bc..bd339cc9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml | |||
| @@ -91,6 +91,8 @@ en: | |||
| 91 | attributes: | 91 | attributes: |
| 92 | slug: | 92 | slug: |
| 93 | reserved_for_trash: "is reserved for the Trash" | 93 | reserved_for_trash: "is reserved for the Trash" |
| 94 | external_url: | ||
| 95 | must_be_http: "must start with http:// or https://" | ||
| 94 | user: | 96 | user: |
| 95 | attributes: | 97 | attributes: |
| 96 | roles: | 98 | roles: |
| @@ -199,7 +201,6 @@ en: | |||
| 199 | detail_event_coordinates: "coordinates changed" | 201 | detail_event_coordinates: "coordinates changed" |
| 200 | detail_event_allday: "all-day %{from} → %{to}" | 202 | detail_event_allday: "all-day %{from} → %{to}" |
| 201 | detail_event_end: "end %{from} → %{to}" | 203 | detail_event_end: "end %{from} → %{to}" |
| 202 | node_external_url: "%{actor} changed the external homepage of %{subject}" | ||
| 203 | event_none: "—" | 204 | event_none: "—" |
| 204 | unknown_event: "an event" | 205 | unknown_event: "an event" |
| 205 | 206 | ||
| @@ -403,7 +404,6 @@ en: | |||
| 403 | set_headline_title: "Use this photo as the page's headline image" | 404 | set_headline_title: "Use this photo as the page's headline image" |
| 404 | remove_image_label: "Remove image" | 405 | remove_image_label: "Remove image" |
| 405 | external_url: "External homepage" | 406 | external_url: "External homepage" |
| 406 | external_url_hint: "A chapter's own website, shown on its page. N.B.: Changing takes effect immediately." | ||
| 407 | show: | 407 | show: |
| 408 | status: "Status" | 408 | status: "Status" |
| 409 | head: "Head" | 409 | head: "Head" |
diff --git a/lib/tasks/pages.rake b/lib/tasks/pages.rake index 0cfbc5dc..ea0776b7 100644 --- a/lib/tasks/pages.rake +++ b/lib/tasks/pages.rake | |||
| @@ -30,4 +30,32 @@ namespace :pages do | |||
| 30 | 30 | ||
| 31 | puts "#{write ? "updated" : "would update"} #{touched} pages" | 31 | puts "#{write ? "updated" : "would update"} #{touched} pages" |
| 32 | end | 32 | end |
| 33 | |||
| 34 | desc "Backfill pages.external_url from each page's node." | ||
| 35 | task :backfill_external_url => :environment do | ||
| 36 | write = ENV["WRITE"] == "1" | ||
| 37 | puts "DRY RUN -- nothing written. Re-run with WRITE=1." unless write | ||
| 38 | |||
| 39 | touched = 0 | ||
| 40 | Node.where.not(:external_url => [nil, ""]).find_each do |node| | ||
| 41 | scope = node.pages.where("external_url IS DISTINCT FROM :u", :u => node.external_url) | ||
| 42 | count = scope.count | ||
| 43 | next if count.zero? | ||
| 44 | |||
| 45 | scope.update_all(:external_url => node.external_url) if write | ||
| 46 | touched += count | ||
| 47 | end | ||
| 48 | |||
| 49 | # Autosaves carry no node_id, so has_many :pages does not cover them. | ||
| 50 | Node.where.not(:autosave_id => nil).where.not(:external_url => [nil, ""]) | ||
| 51 | .includes(:autosave).find_each do |node| | ||
| 52 | a = node.autosave | ||
| 53 | next if a.nil? || a.external_url == node.external_url | ||
| 54 | |||
| 55 | a.update_columns(:external_url => node.external_url) if write | ||
| 56 | touched += 1 | ||
| 57 | end | ||
| 58 | |||
| 59 | puts "#{write ? "updated" : "would update"} #{touched} pages" | ||
| 60 | end | ||
| 33 | end | 61 | end |
diff --git a/test/models/node_test.rb b/test/models/node_test.rb index df1e96cb..735d2abe 100644 --- a/test/models/node_test.rb +++ b/test/models/node_test.rb | |||
| @@ -1072,32 +1072,29 @@ class NodeTest < ActiveSupport::TestCase | |||
| 1072 | assert_equal club.id, node.parent_id | 1072 | assert_equal club.id, node.parent_id |
| 1073 | end | 1073 | end |
| 1074 | 1074 | ||
| 1075 | test "setting an external url is gated and witnessed" do | 1075 | test "an external url is carried by the draft and applied on publish" do |
| 1076 | node = Node.root.children.create!(:slug => "chapter_url_test") | 1076 | node = Node.root.children.create!(:slug => "chapter_url_test") |
| 1077 | 1077 | node.draft.update!(:external_url => "https://example.org") | |
| 1078 | assert_difference -> { NodeAction.where(:action => "node_external_url").count }, 1 do | 1078 | node.publish_draft!(users(:aaron)) |
| 1079 | assert node.update_external_url!("https://example.org", users(:aaron)) | ||
| 1080 | end | ||
| 1081 | 1079 | ||
| 1082 | assert_equal "https://example.org", node.reload.external_url | 1080 | assert_equal "https://example.org", node.reload.external_url |
| 1083 | entry = NodeAction.where(:action => "node_external_url").last | 1081 | assert_equal "https://example.org", node.head.external_url |
| 1084 | assert_equal({ "from" => nil, "to" => "https://example.org" }, | ||
| 1085 | entry.metadata["external_url"]) | ||
| 1086 | end | 1082 | end |
| 1087 | 1083 | ||
| 1088 | test "a javascript url is refused" do | 1084 | test "a javascript url is refused on the draft" do |
| 1089 | node = Node.root.children.create!(:slug => "chapter_url_reject") | 1085 | node = Node.root.children.create!(:slug => "chapter_url_reject") |
| 1090 | assert_raises(ActiveRecord::RecordInvalid) do | 1086 | assert_raises(ActiveRecord::RecordInvalid) do |
| 1091 | node.update_external_url!("javascript:alert(1)", users(:aaron)) | 1087 | node.draft.update!(:external_url => "javascript:alert(1)") |
| 1092 | end | 1088 | end |
| 1093 | end | 1089 | end |
| 1094 | 1090 | ||
| 1095 | test "setting an external url on a restricted node needs redaktion" do | 1091 | test "publishing an external url change on a restricted node needs redaktion" do |
| 1096 | node = Node.find_by(:unique_name => "updates") || | 1092 | updates = Node.root.children.create!(:slug => "updates") |
| 1097 | Node.root.children.create!(:slug => "updates") | 1093 | node = updates.children.create!(:slug => "chapter_url_gated") |
| 1094 | node.draft.update!(:external_url => "https://example.org") | ||
| 1098 | 1095 | ||
| 1099 | assert_raises(ActiveRecord::RecordInvalid) do | 1096 | assert_raises(ActiveRecord::RecordInvalid) do |
| 1100 | node.update_external_url!("https://example.org", users(:quentin)) | 1097 | node.publish_draft!(users(:quentin)) |
| 1101 | end | 1098 | end |
| 1102 | end | 1099 | end |
| 1103 | 1100 | ||
