diff options
| -rw-r--r-- | CONTRIBUTING.md | 89 | ||||
| -rw-r--r-- | app/controllers/nodes_controller.rb | 2 | ||||
| -rw-r--r-- | app/controllers/page_translations_controller.rb | 3 | ||||
| -rw-r--r-- | app/models/node.rb | 65 | ||||
| -rw-r--r-- | app/models/node_action.rb | 33 | ||||
| -rw-r--r-- | app/views/admin/_recent_changes.html.erb | 24 | ||||
| -rw-r--r-- | app/views/admin/index.html.erb | 12 | ||||
| -rw-r--r-- | app/views/nodes/_recent_change_item.html.erb | 9 | ||||
| -rw-r--r-- | app/views/nodes/recent.html.erb | 6 | ||||
| -rw-r--r-- | config/locales/de.yml | 2 | ||||
| -rw-r--r-- | config/locales/en.yml | 2 | ||||
| -rw-r--r-- | db/migrate/20260714233414_create_node_actions.rb | 19 | ||||
| -rw-r--r-- | public/stylesheets/admin.css | 19 | ||||
| -rw-r--r-- | test/controllers/page_translations_controller_test.rb | 15 | ||||
| -rw-r--r-- | test/models/node_test.rb | 83 |
15 files changed, 318 insertions, 65 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c26956c --- /dev/null +++ b/CONTRIBUTING.md | |||
| @@ -0,0 +1,89 @@ | |||
| 1 | # Contributing to CCCMS | ||
| 2 | |||
| 3 | ## Stack | ||
| 4 | Rails 8.1, Ruby 3.2, PostgreSQL 16, ImageMagick 7 (`magick`, not `convert`). | ||
| 5 | No Sprockets pipeline for public or admin static assets except | ||
| 6 | `admin_bundle.js` — everything else is served straight from `public/` and | ||
| 7 | cache-busted via file mtime (`ApplicationHelper#mtime_busted_path`), not | ||
| 8 | fingerprinted content hashing. | ||
| 9 | |||
| 10 | ## Content model | ||
| 11 | - **Node** — the tree structure (`awesome_nested_set`-derived, migrating | ||
| 12 | toward a plain `parent_id` model — both may be present during that | ||
| 13 | transition). Holds three separate Page references: `head` (published), | ||
| 14 | `draft` (being worked on), `autosave` (in-progress typing). | ||
| 15 | - **Page** — one revision's content. `belongs_to :node`, except `autosave` | ||
| 16 | layers specifically: their `node_id` is deliberately `nil`, which is what | ||
| 17 | keeps them out of `node.pages` (the real revision history) entirely — | ||
| 18 | an autosave is not a revision. | ||
| 19 | - **Page::Translation** — Globalize's own generated class (`translates | ||
| 20 | :title, :abstract, :body` on `Page`). Not a top-level `PageTranslation` | ||
| 21 | constant, and it has `page_id`, not a `page` association reader. | ||
| 22 | |||
| 23 | ## Publishing lifecycle | ||
| 24 | There is exactly one path from an edit to a published change: | ||
| 25 | `autosave!` (writes into the autosave layer, creating it via | ||
| 26 | `clone_attributes_from` on the previous head/draft if it doesn't exist yet) | ||
| 27 | → `save_draft!` (clones the *entire* autosave into the draft layer, | ||
| 28 | wholesale — every locale, not just the one just edited) → `publish_draft!` | ||
| 29 | (promotes draft to head). Nothing writes to `draft` or `head` directly. | ||
| 30 | Locking (`lock_for_editing!`, raising `LockedByAnotherUser`) gates every | ||
| 31 | step; a lock can exist with no draft or autosave at all (acquisition is | ||
| 32 | deliberately separate from content creation). | ||
| 33 | |||
| 34 | ## Locale architecture | ||
| 35 | Presentation locale (which language the admin chrome or public page | ||
| 36 | renders in) and content-target locale (which translation a given screen is | ||
| 37 | reading or writing) are two different concerns and never share a | ||
| 38 | mechanism. The route's `:locale` segment governs the former only. The | ||
| 39 | primary node editor always targets the default locale, enforced via | ||
| 40 | `Globalize.with_locale` (never `I18n.locale`, which would leak into | ||
| 41 | generated URLs through `default_url_options`). Non-default-locale editing | ||
| 42 | uses a separately-named route param (`:translation_locale`), deliberately | ||
| 43 | distinct from `:locale` so the two can never be conflated by a link helper. | ||
| 44 | Globalize's configured fallback chain is correct for public rendering and | ||
| 45 | wrong for editing — anywhere a screen needs to know a translation's real, | ||
| 46 | unborrowed state, read the `Page::Translation` row directly rather than the | ||
| 47 | locale-dependent attribute accessor. | ||
| 48 | |||
| 49 | ## Assets | ||
| 50 | `FileAttachment` (`app/models/concerns/file_attachment.rb`) is a from- | ||
| 51 | scratch Paperclip replacement. `STYLES` is a hash of style name to a full | ||
| 52 | ImageMagick argument array (not just a geometry string — some styles need | ||
| 53 | `-gravity`/`-extent` alongside `-resize`, which a single string can't | ||
| 54 | express). `generate_variants` loops over `STYLES` generically; adding a | ||
| 55 | style needs no other code change, only `bundle exec rake | ||
| 56 | assets:regenerate_variants` to backfill existing assets. The URL contract | ||
| 57 | (`/system/uploads/:id/:style/:filename`) is stable and safe to parse | ||
| 58 | directly (the numeric id is always the second path segment) rather than | ||
| 59 | reconstructed through model methods. | ||
| 60 | |||
| 61 | ## Aggregator / shortcode system | ||
| 62 | `[aggregate ...]` in page body text is parsed by | ||
| 63 | `ContentHelper#aggregate?` into an options hash, then executed by | ||
| 64 | `Page.aggregate`. Only `tags`, `children` (`"direct"`/`"all"`, combined | ||
| 65 | with the current node), `order_by`/`order_direction`, and `limit` are | ||
| 66 | actually consumed. Only the first `[aggregate ...]` in a given body is | ||
| 67 | ever processed (`aggregate?` does a single, non-global match). | ||
| 68 | |||
| 69 | ## TinyMCE integration | ||
| 70 | `extended_valid_elements` must define a parent element and its commonly- | ||
| 71 | nested children symmetrically and explicitly together — leaving one | ||
| 72 | element on the schema's default rule while the other is explicitly | ||
| 73 | redefined can cause content loss on save, even when the child would be | ||
| 74 | perfectly valid under the untouched default alone. `valid_classes` denies | ||
| 75 | all classes by default (`'*': ''`); anything meant to render needs an | ||
| 76 | explicit per-element allowance. `content_style` injects CSS into the | ||
| 77 | editing iframe directly — the app's real public stylesheet is never loaded | ||
| 78 | there, so anything that needs to render correctly while editing needs its | ||
| 79 | own rule supplied this way. Constrained-input features (fixed placement | ||
| 80 | choices, for instance) are built as custom toolbar buttons with their own | ||
| 81 | dialog and `editor.insertContent()`, not the native image dialog or | ||
| 82 | `file_picker_callback` — those bring their own resize/edit affordances that | ||
| 83 | would undermine a genuinely fixed set of choices. | ||
| 84 | |||
| 85 | ## Testing | ||
| 86 | Shared test-only convenience methods (that need to operate across many | ||
| 87 | model/controller test files) live directly on `ActiveSupport::TestCase`, | ||
| 88 | in `test/test_helper.rb` — the framework's own designated extension point | ||
| 89 | for exactly this, not a monkey-patch of a production model class. | ||
diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb index bff1733..9c5d228 100644 --- a/app/controllers/nodes_controller.rb +++ b/app/controllers/nodes_controller.rb | |||
| @@ -147,7 +147,7 @@ class NodesController < ApplicationController | |||
| 147 | end | 147 | end |
| 148 | 148 | ||
| 149 | def publish | 149 | def publish |
| 150 | @node.publish_draft! | 150 | @node.publish_draft!(current_user) |
| 151 | flash[:notice] = "Draft has been published" | 151 | flash[:notice] = "Draft has been published" |
| 152 | redirect_to node_path(@node) | 152 | redirect_to node_path(@node) |
| 153 | end | 153 | end |
diff --git a/app/controllers/page_translations_controller.rb b/app/controllers/page_translations_controller.rb index 38a7c4f..be4f488 100644 --- a/app/controllers/page_translations_controller.rb +++ b/app/controllers/page_translations_controller.rb | |||
| @@ -63,6 +63,9 @@ class PageTranslationsController < ApplicationController | |||
| 63 | draft.translations.where(:locale => @locale).delete_all | 63 | draft.translations.where(:locale => @locale).delete_all |
| 64 | draft.reload | 64 | draft.reload |
| 65 | 65 | ||
| 66 | NodeAction.record!(:node => @node, :page => draft, :user => current_user, | ||
| 67 | :action => "translation_destroy", :locale => @locale) | ||
| 68 | |||
| 66 | flash[:notice] = "#{@locale.upcase} translation removed from the draft. Publish to make this permanent." | 69 | flash[:notice] = "#{@locale.upcase} translation removed from the draft. Publish to make this permanent." |
| 67 | redirect_to node_path(@node) | 70 | redirect_to node_path(@node) |
| 68 | rescue LockedByAnotherUser => e | 71 | rescue LockedByAnotherUser => e |
diff --git a/app/models/node.rb b/app/models/node.rb index 311c5c0..b41df06 100644 --- a/app/models/node.rb +++ b/app/models/node.rb | |||
| @@ -170,10 +170,12 @@ class Node < ApplicationRecord | |||
| 170 | self.autosave.destroy | 170 | self.autosave.destroy |
| 171 | self.autosave_id = nil | 171 | self.autosave_id = nil |
| 172 | self.save! | 172 | self.save! |
| 173 | NodeAction.record!(:node => self, :user => current_user, :action => "discard_autosave") | ||
| 173 | elsif self.draft && self.head | 174 | elsif self.draft && self.head |
| 174 | self.draft.destroy | 175 | self.draft.destroy |
| 175 | self.draft_id = nil | 176 | self.draft_id = nil |
| 176 | self.save! | 177 | self.save! |
| 178 | NodeAction.record!(:node => self, :user => current_user, :action => "destroy_draft") | ||
| 177 | end | 179 | end |
| 178 | 180 | ||
| 179 | self.unlock! unless self.draft | 181 | self.unlock! unless self.draft |
| @@ -188,43 +190,50 @@ class Node < ApplicationRecord | |||
| 188 | end | 190 | end |
| 189 | end | 191 | end |
| 190 | 192 | ||
| 191 | def publish_draft! | 193 | def publish_draft! current_user = nil |
| 192 | # Return nil if nothing to publish and no staged changes | 194 | # Return nil if nothing to publish and no staged changes |
| 193 | return nil unless self.draft || staged_slug || staged_parent_id | 195 | return nil unless self.draft || staged_slug || staged_parent_id |
| 194 | 196 | ||
| 195 | if self.draft | 197 | ActiveRecord::Base.transaction do |
| 196 | self.head = self.draft | 198 | if self.draft |
| 197 | self.head.published_at ||= Time.now | 199 | previous_title = self.head ? Globalize.with_locale(I18n.default_locale) { self.head.title } : nil |
| 198 | self.head.save! | 200 | self.head = self.draft |
| 199 | self.draft = nil | 201 | self.head.published_at ||= Time.now |
| 200 | end | 202 | self.head.save! |
| 201 | 203 | self.draft = nil | |
| 202 | if staged_slug && (staged_slug != slug) | ||
| 203 | self.slug = staged_slug | ||
| 204 | self.staged_slug = nil | ||
| 205 | end | ||
| 206 | 204 | ||
| 207 | if staged_parent_id && (staged_parent_id != parent_id) | 205 | new_title = Globalize.with_locale(I18n.default_locale) { self.head.title } |
| 208 | new_parent = Node.find(staged_parent_id) | 206 | NodeAction.record!(:node => self, :page => self.head, :user => current_user, |
| 207 | :action => "publish", :from => previous_title, :to => new_title) | ||
| 208 | end | ||
| 209 | 209 | ||
| 210 | if new_parent == self || self.descendants.include?(new_parent) | 210 | if staged_slug && (staged_slug != slug) |
| 211 | raise ActiveRecord::RecordInvalid.new(self), "Cannot move a node under itself or one of its own descendants" | 211 | self.slug = staged_slug |
| 212 | self.staged_slug = nil | ||
| 212 | end | 213 | end |
| 213 | 214 | ||
| 214 | self.staged_parent_id = nil | 215 | if staged_parent_id && (staged_parent_id != parent_id) |
| 215 | self.save! | 216 | new_parent = Node.find(staged_parent_id) |
| 216 | self.move_to_child_of(new_parent) | 217 | |
| 217 | else | 218 | if new_parent == self || self.descendants.include?(new_parent) |
| 218 | unless self.save | 219 | raise ActiveRecord::RecordInvalid.new(self), "Cannot move a node under itself or one of its own descendants" |
| 219 | raise ActiveRecord::RecordInvalid.new(self) | 220 | end |
| 221 | |||
| 222 | self.staged_parent_id = nil | ||
| 223 | self.save! | ||
| 224 | self.move_to_child_of(new_parent) | ||
| 225 | else | ||
| 226 | unless self.save | ||
| 227 | raise ActiveRecord::RecordInvalid.new(self) | ||
| 228 | end | ||
| 220 | end | 229 | end |
| 221 | end | ||
| 222 | 230 | ||
| 223 | self.reload | 231 | self.reload |
| 224 | self.update_unique_name | 232 | self.update_unique_name |
| 225 | self.send(:update_unique_names_of_children) | 233 | self.send(:update_unique_names_of_children) |
| 226 | self.unlock! | 234 | self.unlock! |
| 227 | self | 235 | self |
| 236 | end | ||
| 228 | end | 237 | end |
| 229 | 238 | ||
| 230 | def restore_revision! revision | 239 | def restore_revision! revision |
diff --git a/app/models/node_action.rb b/app/models/node_action.rb new file mode 100644 index 0000000..f32b5b7 --- /dev/null +++ b/app/models/node_action.rb | |||
| @@ -0,0 +1,33 @@ | |||
| 1 | class NodeAction < ApplicationRecord | ||
| 2 | belongs_to :node, optional: true | ||
| 3 | belongs_to :page, optional: true | ||
| 4 | belongs_to :user, optional: true | ||
| 5 | |||
| 6 | validates :action, presence: true | ||
| 7 | validates :occurred_at, presence: true | ||
| 8 | |||
| 9 | def self.record!(node:, action:, user: nil, page: nil, locale: nil, **extra) | ||
| 10 | create!( | ||
| 11 | :node => node, | ||
| 12 | :page => page, | ||
| 13 | :user => user, | ||
| 14 | :action => action, | ||
| 15 | :locale => locale, | ||
| 16 | :occurred_at => Time.now, | ||
| 17 | :metadata => { | ||
| 18 | "username" => user&.login, | ||
| 19 | "human_readable_node_name" => Globalize.with_locale(I18n.default_locale) { | ||
| 20 | node&.head&.title || node&.draft&.title | ||
| 21 | }, | ||
| 22 | }.merge(extra.stringify_keys) | ||
| 23 | ) | ||
| 24 | end | ||
| 25 | |||
| 26 | def actor_name | ||
| 27 | metadata["username"] || "unknown" | ||
| 28 | end | ||
| 29 | |||
| 30 | def subject_name | ||
| 31 | metadata["human_readable_node_name"] || node&.unique_name || "deleted node" | ||
| 32 | end | ||
| 33 | end | ||
diff --git a/app/views/admin/_recent_changes.html.erb b/app/views/admin/_recent_changes.html.erb deleted file mode 100644 index 88b5e93..0000000 --- a/app/views/admin/_recent_changes.html.erb +++ /dev/null | |||
| @@ -1,24 +0,0 @@ | |||
| 1 | <h2>Recent Changes</h2> | ||
| 2 | <div id="draft_list"> | ||
| 3 | <table> | ||
| 4 | <tr class="header"> | ||
| 5 | <th>Title</th> | ||
| 6 | <th>path</th> | ||
| 7 | <th>user</th> | ||
| 8 | <th>date</th> | ||
| 9 | <th></th> | ||
| 10 | </tr> | ||
| 11 | <% @recent_changes.each do |node| %> | ||
| 12 | <tr> | ||
| 13 | <td><%= truncated_title_for_node node %></td> | ||
| 14 | <td><%= node.unique_name %></td> | ||
| 15 | <td><%= node.draft.user.login rescue "" %></td> | ||
| 16 | <td><%= node.updated_at.to_fs(:db) %></td> | ||
| 17 | <td class="actions"> | ||
| 18 | <%= link_to 'Show', node_path(node) %> | ||
| 19 | <%= link_to "Revisions", revision_path(:id => node.id) %> | ||
| 20 | </td> | ||
| 21 | </tr> | ||
| 22 | <% end %> | ||
| 23 | </table> | ||
| 24 | </div> \ No newline at end of file | ||
diff --git a/app/views/admin/index.html.erb b/app/views/admin/index.html.erb index bdb555e..a9c0512 100644 --- a/app/views/admin/index.html.erb +++ b/app/views/admin/index.html.erb | |||
| @@ -43,17 +43,7 @@ | |||
| 43 | <div class="dashboard_widget"> | 43 | <div class="dashboard_widget"> |
| 44 | <h3>Recent changes</h3> | 44 | <h3>Recent changes</h3> |
| 45 | <ul> | 45 | <ul> |
| 46 | <% @recent_changes.each do |node| %> | 46 | <%= render partial: "nodes/recent_change_item", collection: @recent_changes, as: :node %> |
| 47 | <li> | ||
| 48 | <div> | ||
| 49 | <%= link_to title_for_node(node), node_path(node) %> | ||
| 50 | <span class="field_hint"><%= link_to_path("#{node.unique_name} ↗", node.unique_name) %></span> | ||
| 51 | </div> | ||
| 52 | <span class="dashboard_widget_meta"> | ||
| 53 | <%= (editor = node_head_editor(node)) ? t("published_by", editor: editor) : t("published") %>, <%= relative_time_phrase(node.head.updated_at) %> | ||
| 54 | </span> | ||
| 55 | </li> | ||
| 56 | <% end %> | ||
| 57 | </ul> | 47 | </ul> |
| 58 | <%= link_to "See all recent changes →", recent_nodes_path %> | 48 | <%= link_to "See all recent changes →", recent_nodes_path %> |
| 59 | </div> | 49 | </div> |
diff --git a/app/views/nodes/_recent_change_item.html.erb b/app/views/nodes/_recent_change_item.html.erb new file mode 100644 index 0000000..754a775 --- /dev/null +++ b/app/views/nodes/_recent_change_item.html.erb | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | <li> | ||
| 2 | <div> | ||
| 3 | <%= link_to title_for_node(node), node_path(node) %> | ||
| 4 | <span class="field_hint"><%= link_to_path("#{node.unique_name} ↗", node.unique_name) %></span> | ||
| 5 | </div> | ||
| 6 | <span class="dashboard_widget_meta"> | ||
| 7 | <%= (editor = node_head_editor(node)) ? t("last_edited_by", editor: editor) : t("last_edited") %>, <%= relative_time_phrase(node.head.updated_at) %> | ||
| 8 | </span> | ||
| 9 | </li> | ||
diff --git a/app/views/nodes/recent.html.erb b/app/views/nodes/recent.html.erb index 3602775..d256253 100644 --- a/app/views/nodes/recent.html.erb +++ b/app/views/nodes/recent.html.erb | |||
| @@ -1,3 +1,7 @@ | |||
| 1 | <h1>Recently changed</h1> | 1 | <h1>Recently changed</h1> |
| 2 | 2 | ||
| 3 | <%= render 'node_list' %> | 3 | <%= will_paginate @nodes %> |
| 4 | <ul id="recent_changes_full_list"> | ||
| 5 | <%= render partial: "nodes/recent_change_item", collection: @nodes, as: :node %> | ||
| 6 | </ul> | ||
| 7 | <%= will_paginate @nodes %> | ||
diff --git a/config/locales/de.yml b/config/locales/de.yml index 550ccb3..74cd183 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml | |||
| @@ -16,6 +16,8 @@ de: | |||
| 16 | open_today: "Das Chaos, lokal und heute offen" | 16 | open_today: "Das Chaos, lokal und heute offen" |
| 17 | published: "veröffentlicht" | 17 | published: "veröffentlicht" |
| 18 | published_by: "veröffentlicht durch %{editor}" | 18 | published_by: "veröffentlicht durch %{editor}" |
| 19 | last_edited: "zuletzt bearbeitet" | ||
| 20 | last_edited_by: "zuletzt bearbeitet durch %{editor}" | ||
| 19 | editor_self: "du" | 21 | editor_self: "du" |
| 20 | publisher_self: "dich" | 22 | publisher_self: "dich" |
| 21 | date: | 23 | date: |
diff --git a/config/locales/en.yml b/config/locales/en.yml index 73271ed..c52280d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml | |||
| @@ -17,6 +17,8 @@ en: | |||
| 17 | open_today: "Open today" | 17 | open_today: "Open today" |
| 18 | published: "published" | 18 | published: "published" |
| 19 | published_by: "published by %{editor}" | 19 | published_by: "published by %{editor}" |
| 20 | last_edited: "last edited" | ||
| 21 | last_edited_by: "last edited by %{editor}" | ||
| 20 | publisher_self: "you" | 22 | publisher_self: "you" |
| 21 | editor_self: "you" | 23 | editor_self: "you" |
| 22 | 24 | ||
diff --git a/db/migrate/20260714233414_create_node_actions.rb b/db/migrate/20260714233414_create_node_actions.rb new file mode 100644 index 0000000..6c18285 --- /dev/null +++ b/db/migrate/20260714233414_create_node_actions.rb | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | class CreateNodeActions < ActiveRecord::Migration[8.1] | ||
| 2 | def change | ||
| 3 | create_table :node_actions do |t| | ||
| 4 | t.references :node, foreign_key: { on_delete: :nullify } | ||
| 5 | t.references :page, foreign_key: { on_delete: :nullify } | ||
| 6 | t.references :user, foreign_key: { on_delete: :nullify } | ||
| 7 | t.string :action, null: false | ||
| 8 | t.string :locale | ||
| 9 | t.string :inferred_from | ||
| 10 | t.jsonb :metadata, null: false | ||
| 11 | t.datetime :occurred_at, null: false | ||
| 12 | |||
| 13 | t.timestamps | ||
| 14 | end | ||
| 15 | |||
| 16 | add_index :node_actions, [:node_id, :occurred_at] | ||
| 17 | add_index :node_actions, :action | ||
| 18 | end | ||
| 19 | end | ||
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index b5b8707..5f0b165 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css | |||
| @@ -1121,6 +1121,25 @@ div#draft_list table td.actions a { | |||
| 1121 | font-size: 0.9em; | 1121 | font-size: 0.9em; |
| 1122 | } | 1122 | } |
| 1123 | 1123 | ||
| 1124 | #recent_changes_full_list { | ||
| 1125 | list-style: none; | ||
| 1126 | padding: 0; | ||
| 1127 | margin: 0; | ||
| 1128 | } | ||
| 1129 | |||
| 1130 | #recent_changes_full_list li { | ||
| 1131 | padding: 0.75rem 0; | ||
| 1132 | border-bottom: 1px solid #eee; | ||
| 1133 | } | ||
| 1134 | |||
| 1135 | #recent_changes_full_list li:last-child { | ||
| 1136 | border-bottom: none; | ||
| 1137 | } | ||
| 1138 | |||
| 1139 | #recent_changes_full_list li > div { | ||
| 1140 | margin-bottom: 0.25rem; | ||
| 1141 | } | ||
| 1142 | |||
| 1124 | /* ============================================================ | 1143 | /* ============================================================ |
| 1125 | Search widgets | 1144 | Search widgets |
| 1126 | ============================================================ */ | 1145 | ============================================================ */ |
diff --git a/test/controllers/page_translations_controller_test.rb b/test/controllers/page_translations_controller_test.rb index 8fa732f..82f8bce 100644 --- a/test/controllers/page_translations_controller_test.rb +++ b/test/controllers/page_translations_controller_test.rb | |||
| @@ -82,4 +82,19 @@ class PageTranslationsControllerTest < ActionController::TestCase | |||
| 82 | assert_equal page_count_before, node.pages.count | 82 | assert_equal page_count_before, node.pages.count |
| 83 | assert_equal "in progress", Globalize.with_locale(:en) { node.autosave.title } | 83 | assert_equal "in progress", Globalize.with_locale(:en) { node.autosave.title } |
| 84 | end | 84 | end |
| 85 | |||
| 86 | test "destroy logs a translation_destroy NodeAction" do | ||
| 87 | login_as :quentin | ||
| 88 | node = Node.root.children.create!(:slug => "translation_destroy_log_test") | ||
| 89 | Globalize.with_locale(:en) { node.draft.update!(:title => "English title") } | ||
| 90 | |||
| 91 | delete :destroy, params: { :node_id => node.id, :translation_locale => "en" } | ||
| 92 | |||
| 93 | action = NodeAction.last | ||
| 94 | assert_equal node, action.node | ||
| 95 | assert_equal "en", action.locale | ||
| 96 | assert_equal "translation_destroy", action.action | ||
| 97 | assert_equal users(:quentin), action.user | ||
| 98 | assert_redirected_to node_path(node) | ||
| 99 | end | ||
| 85 | end | 100 | end |
diff --git a/test/models/node_test.rb b/test/models/node_test.rb index 959387d..022fff6 100644 --- a/test/models/node_test.rb +++ b/test/models/node_test.rb | |||
| @@ -621,4 +621,87 @@ class NodeTest < ActiveSupport::TestCase | |||
| 621 | assert_equal "Edited directly on autosave", Globalize.with_locale(:en) { autosave.title } | 621 | assert_equal "Edited directly on autosave", Globalize.with_locale(:en) { autosave.title } |
| 622 | assert_equal "v2", Globalize.with_locale(:de) { autosave.title } | 622 | assert_equal "v2", Globalize.with_locale(:de) { autosave.title } |
| 623 | end | 623 | end |
| 624 | |||
| 625 | test "publish_draft! logs a NodeAction crediting the actual publisher" do | ||
| 626 | node = Node.root.children.create!(:slug => "publish_log_test") | ||
| 627 | node.draft.update!(:title => "Version one") | ||
| 628 | |||
| 629 | node.publish_draft!(@user1) | ||
| 630 | |||
| 631 | action = NodeAction.last | ||
| 632 | assert_equal node, action.node | ||
| 633 | assert_equal node.head, action.page | ||
| 634 | assert_equal @user1, action.user | ||
| 635 | assert_equal "publish", action.action | ||
| 636 | end | ||
| 637 | |||
| 638 | test "publish_draft! called with no user logs no actor, not a guessed one" do | ||
| 639 | node = Node.root.children.create!(:slug => "publish_log_no_user_test") | ||
| 640 | node.draft.update!(:title => "Version one") | ||
| 641 | |||
| 642 | node.publish_draft! | ||
| 643 | |||
| 644 | action = NodeAction.last | ||
| 645 | assert_nil action.user | ||
| 646 | assert_nil action.metadata["username"] | ||
| 647 | end | ||
| 648 | |||
| 649 | test "publish_draft! with nothing pending creates no NodeAction" do | ||
| 650 | node = Node.root.children.create!(:slug => "publish_log_noop_test") | ||
| 651 | node.publish_draft! | ||
| 652 | count_before = NodeAction.count | ||
| 653 | |||
| 654 | result = node.publish_draft! | ||
| 655 | |||
| 656 | assert_nil result | ||
| 657 | assert_equal count_before, NodeAction.count | ||
| 658 | end | ||
| 659 | |||
| 660 | test "revert! logs discard_autosave for an in-progress autosave" do | ||
| 661 | node = create_node_with_published_page | ||
| 662 | node.lock_for_editing!(@user1) | ||
| 663 | node.autosave!({:title => "in progress"}, @user1) | ||
| 664 | |||
| 665 | node.revert!(@user1) | ||
| 666 | |||
| 667 | action = NodeAction.last | ||
| 668 | assert_equal node, action.node | ||
| 669 | assert_equal @user1, action.user | ||
| 670 | assert_equal "discard_autosave", action.action | ||
| 671 | end | ||
| 672 | |||
| 673 | test "revert! logs destroy_draft for a draft with a head behind it" do | ||
| 674 | node = create_node_with_published_page | ||
| 675 | find_or_create_draft(node, @user1) | ||
| 676 | |||
| 677 | node.revert!(@user1) | ||
| 678 | |||
| 679 | action = NodeAction.last | ||
| 680 | assert_equal node, action.node | ||
| 681 | assert_equal @user1, action.user | ||
| 682 | assert_equal "destroy_draft", action.action | ||
| 683 | end | ||
| 684 | |||
| 685 | test "revert! with nothing to revert logs nothing" do | ||
| 686 | node = create_node_with_published_page | ||
| 687 | node.lock_for_editing!(@user1) | ||
| 688 | count_before = NodeAction.count | ||
| 689 | |||
| 690 | node.revert!(@user1) | ||
| 691 | |||
| 692 | assert_equal count_before, NodeAction.count | ||
| 693 | end | ||
| 694 | |||
| 695 | test "publish_draft! records the title's from/to in metadata" do | ||
| 696 | node = create_node_with_published_page | ||
| 697 | Globalize.with_locale(:de) { node.head.update!(:title => "Original Title") } | ||
| 698 | find_or_create_draft(node, @user1) | ||
| 699 | Globalize.with_locale(:de) { node.draft.update!(:title => "New Title") } | ||
| 700 | |||
| 701 | node.publish_draft!(@user1) | ||
| 702 | |||
| 703 | action = NodeAction.last | ||
| 704 | assert_equal "Original Title", action.metadata["from"] | ||
| 705 | assert_equal "New Title", action.metadata["to"] | ||
| 706 | end | ||
| 624 | end | 707 | end |
