diff options
| -rw-r--r-- | app/models/page.rb | 65 | ||||
| -rw-r--r-- | test/models/page_test.rb | 99 |
2 files changed, 152 insertions, 12 deletions
diff --git a/app/models/page.rb b/app/models/page.rb index f796228..e5a8d9d 100644 --- a/app/models/page.rb +++ b/app/models/page.rb | |||
| @@ -176,18 +176,34 @@ class Page < ApplicationRecord | |||
| 176 | return nil unless page | 176 | return nil unless page |
| 177 | 177 | ||
| 178 | self.reload | 178 | self.reload |
| 179 | page.translations.reload | ||
| 179 | 180 | ||
| 180 | # Clone untranslated attributes | 181 | # Clone untranslated attributes |
| 181 | self.tag_list = page.tag_list | 182 | self.tag_list = page.tag_list |
| 182 | self.template_name ||= page.template_name | 183 | self.template_name ||= page.template_name |
| 183 | self.published_at = page.published_at | 184 | self.published_at = page.published_at |
| 184 | 185 | ||
| 185 | # Getting rid of the auto-generated empty translations | 186 | # Clone translated attributes -- update each locale in place rather |
| 186 | self.translations.delete_all | 187 | # than delete-and-recreate, so a locale whose content is genuinely |
| 188 | # unchanged keeps its real created_at/updated_at instead of looking | ||
| 189 | # freshly touched on every single save (which was silently defeating | ||
| 190 | # Page.find_with_outdated_translations' whole staleness comparison). | ||
| 191 | # search_vector is excluded deliberately: it's DB-trigger-maintained | ||
| 192 | # from title/abstract, not real content, and comparing a precomputed | ||
| 193 | # tsvector risked a false "changed" from representation noise alone. | ||
| 194 | source_locales = page.translations.map(&:locale) | ||
| 195 | self.translations.where.not(:locale => source_locales).destroy_all | ||
| 187 | 196 | ||
| 188 | # Clone translated attributes | 197 | page.translations.each do |source_translation| |
| 189 | page.translations.each do |translation| | 198 | attrs = source_translation.attributes.except("id", "page_id", "created_at", "updated_at", "search_vector") |
| 190 | self.translations.create!(translation.attributes.except("id", "page_id", "created_at", "updated_at")) | 199 | mine = self.translations.find_by(:locale => source_translation.locale) |
| 200 | |||
| 201 | if mine | ||
| 202 | changed_attrs = attrs.reject { |k, v| mine.public_send(k) == v } | ||
| 203 | mine.update!(changed_attrs) if changed_attrs.any? | ||
| 204 | else | ||
| 205 | self.translations.create!(attrs) | ||
| 206 | end | ||
| 191 | end | 207 | end |
| 192 | 208 | ||
| 193 | # Clone asset references | 209 | # Clone asset references |
| @@ -196,19 +212,28 @@ class Page < ApplicationRecord | |||
| 196 | self.save | 212 | self.save |
| 197 | end | 213 | end |
| 198 | 214 | ||
| 199 | def diff_against other, view: :inline | 215 | def diff_against other, view: :inline, locale: nil |
| 216 | if locale | ||
| 217 | mine, theirs = translations.find_by(:locale => locale), other.translations.find_by(:locale => locale) | ||
| 218 | my_title, my_abstract, my_body = mine&.title, mine&.abstract, mine&.body | ||
| 219 | their_title, their_abstract, their_body = theirs&.title, theirs&.abstract, theirs&.body | ||
| 220 | else | ||
| 221 | my_title, my_abstract, my_body = title, abstract, body | ||
| 222 | their_title, their_abstract, their_body = other.title, other.abstract, other.body | ||
| 223 | end | ||
| 224 | |||
| 200 | text_diffs = | 225 | text_diffs = |
| 201 | if view == :side_by_side | 226 | if view == :side_by_side |
| 202 | { | 227 | { |
| 203 | title: HtmlWordDiff.side_by_side(other.title.to_s, title.to_s), | 228 | title: HtmlWordDiff.side_by_side(their_title.to_s, my_title.to_s), |
| 204 | abstract: HtmlWordDiff.side_by_side(other.abstract.to_s, abstract.to_s), | 229 | abstract: HtmlWordDiff.side_by_side(their_abstract.to_s, my_abstract.to_s), |
| 205 | body: HtmlWordDiff.side_by_side(other.body.to_s, body.to_s) | 230 | body: HtmlWordDiff.side_by_side(their_body.to_s, my_body.to_s) |
| 206 | } | 231 | } |
| 207 | else | 232 | else |
| 208 | { | 233 | { |
| 209 | title: HtmlWordDiff.inline(other.title.to_s, title.to_s), | 234 | title: HtmlWordDiff.inline(their_title.to_s, my_title.to_s), |
| 210 | abstract: HtmlWordDiff.inline(other.abstract.to_s, abstract.to_s), | 235 | abstract: HtmlWordDiff.inline(their_abstract.to_s, my_abstract.to_s), |
| 211 | body: HtmlWordDiff.inline(other.body.to_s, body.to_s) | 236 | body: HtmlWordDiff.inline(their_body.to_s, my_body.to_s) |
| 212 | } | 237 | } |
| 213 | end | 238 | end |
| 214 | 239 | ||
| @@ -219,6 +244,22 @@ class Page < ApplicationRecord | |||
| 219 | ) | 244 | ) |
| 220 | end | 245 | end |
| 221 | 246 | ||
| 247 | def locale_diff_summary other | ||
| 248 | (translated_locales | other.translated_locales).sort_by(&:to_s).map do |locale| | ||
| 249 | mine = translations.find_by(:locale => locale) | ||
| 250 | theirs = other.translations.find_by(:locale => locale) | ||
| 251 | { | ||
| 252 | :locale => locale, | ||
| 253 | :exists_here => mine.present?, | ||
| 254 | :exists_there => theirs.present?, | ||
| 255 | :changed => mine.present? != theirs.present? || | ||
| 256 | mine&.title != theirs&.title || | ||
| 257 | mine&.abstract != theirs&.abstract || | ||
| 258 | mine&.body != theirs&.body | ||
| 259 | } | ||
| 260 | end | ||
| 261 | end | ||
| 262 | |||
| 222 | def public? | 263 | def public? |
| 223 | published_at.nil? ? true : published_at < Time.now | 264 | published_at.nil? ? true : published_at < Time.now |
| 224 | end | 265 | end |
diff --git a/test/models/page_test.rb b/test/models/page_test.rb index bcddc89..24bc7c5 100644 --- a/test/models/page_test.rb +++ b/test/models/page_test.rb | |||
| @@ -177,6 +177,56 @@ class PageTest < ActiveSupport::TestCase | |||
| 177 | assert first_page.published_at.present? | 177 | assert first_page.published_at.present? |
| 178 | end | 178 | end |
| 179 | 179 | ||
| 180 | test "clone_attributes_from preserves an unchanged locale's original timestamp" do | ||
| 181 | n = Node.root.children.create!(:slug => "clone_preserve_timestamp_test") | ||
| 182 | source = n.draft | ||
| 183 | Globalize.with_locale(:de) { source.update!(:title => "Deutscher Titel") } | ||
| 184 | Globalize.with_locale(:en) { source.update!(:title => "English Title") } | ||
| 185 | |||
| 186 | target = Page.create! | ||
| 187 | target.clone_attributes_from(source) | ||
| 188 | original_en_updated_at = target.translations.find_by(:locale => :en).updated_at | ||
| 189 | |||
| 190 | Globalize.with_locale(:de) { source.update!(:title => "Deutscher Titel (bearbeitet)") } | ||
| 191 | target.clone_attributes_from(source) | ||
| 192 | |||
| 193 | en_translation = target.translations.find_by(:locale => :en) | ||
| 194 | assert_equal "English Title", en_translation.title | ||
| 195 | assert_equal original_en_updated_at, en_translation.updated_at | ||
| 196 | end | ||
| 197 | |||
| 198 | test "clone_attributes_from gives a genuinely changed locale a fresh timestamp" do | ||
| 199 | n = Node.root.children.create!(:slug => "clone_fresh_timestamp_test") | ||
| 200 | source = n.draft | ||
| 201 | Globalize.with_locale(:de) { source.update!(:title => "Erste Version") } | ||
| 202 | |||
| 203 | target = Page.create! | ||
| 204 | target.clone_attributes_from(source) | ||
| 205 | original_de_updated_at = target.translations.find_by(:locale => :de).updated_at | ||
| 206 | |||
| 207 | Globalize.with_locale(:de) { source.update!(:title => "Zweite Version") } | ||
| 208 | target.clone_attributes_from(source) | ||
| 209 | |||
| 210 | de_translation = target.translations.find_by(:locale => :de) | ||
| 211 | assert_equal "Zweite Version", de_translation.title | ||
| 212 | assert_operator de_translation.updated_at, :>, original_de_updated_at | ||
| 213 | end | ||
| 214 | |||
| 215 | test "clone_attributes_from removes a locale no longer present in the source" do | ||
| 216 | n = Node.root.children.create!(:slug => "clone_removed_locale_test") | ||
| 217 | source = n.draft | ||
| 218 | Globalize.with_locale(:en) { source.update!(:title => "English Title") } | ||
| 219 | |||
| 220 | target = Page.create! | ||
| 221 | target.clone_attributes_from(source) | ||
| 222 | assert_includes target.translations.map(&:locale), :en | ||
| 223 | |||
| 224 | source.translations.where(:locale => :en).delete_all | ||
| 225 | target.clone_attributes_from(source) | ||
| 226 | |||
| 227 | assert_not_includes target.reload.translations.map(&:locale), :en | ||
| 228 | end | ||
| 229 | |||
| 180 | def test_diff_against_inline_keeps_tags_and_marks_only_the_changed_word | 230 | def test_diff_against_inline_keeps_tags_and_marks_only_the_changed_word |
| 181 | n = Node.root.children.create! :slug => "diff_against_test" | 231 | n = Node.root.children.create! :slug => "diff_against_test" |
| 182 | d = n.find_or_create_draft @user1 | 232 | d = n.find_or_create_draft @user1 |
| @@ -271,4 +321,53 @@ class PageTest < ActiveSupport::TestCase | |||
| 271 | assert_equal [added_asset], diff[:assets][:added] | 321 | assert_equal [added_asset], diff[:assets][:added] |
| 272 | assert_equal [removed_asset], diff[:assets][:removed] | 322 | assert_equal [removed_asset], diff[:assets][:removed] |
| 273 | end | 323 | end |
| 324 | |||
| 325 | test "diff_against with an explicit locale compares that locale's own translation on each side" do | ||
| 326 | n = Node.root.children.create!(:slug => "diff_locale_test") | ||
| 327 | d = n.find_or_create_draft(@user1) | ||
| 328 | Globalize.with_locale(:en) { d.update!(:title => "Old English") } | ||
| 329 | d.save! | ||
| 330 | n.publish_draft! | ||
| 331 | |||
| 332 | d2 = n.find_or_create_draft(@user1) | ||
| 333 | Globalize.with_locale(:en) { d2.update!(:title => "New English") } | ||
| 334 | d2.save! | ||
| 335 | |||
| 336 | diff = d2.diff_against(n.head, :locale => :en) | ||
| 337 | |||
| 338 | assert_match "<del>Old</del>", diff[:title] | ||
| 339 | assert_match "<ins>New</ins>", diff[:title] | ||
| 340 | end | ||
| 341 | |||
| 342 | test "diff_against with an explicit locale ignores content in other locales entirely" do | ||
| 343 | n = Node.root.children.create!(:slug => "diff_locale_isolation_test") | ||
| 344 | d = n.find_or_create_draft(@user1) | ||
| 345 | d.save! | ||
| 346 | n.publish_draft! | ||
| 347 | |||
| 348 | d2 = n.find_or_create_draft(@user1) | ||
| 349 | Globalize.with_locale(:de) { d2.update!(:title => "Nur Deutsch geƤndert") } | ||
| 350 | d2.save! | ||
| 351 | |||
| 352 | diff = d2.diff_against(n.head, :locale => :en) | ||
| 353 | |||
| 354 | assert_no_match(/Deutsch/, diff[:title]) | ||
| 355 | end | ||
| 356 | |||
| 357 | test "locale_diff_summary flags a locale that only exists on one side as changed" do | ||
| 358 | n = Node.root.children.create!(:slug => "diff_locale_summary_test") | ||
| 359 | d = n.find_or_create_draft(@user1) | ||
| 360 | d.save! | ||
| 361 | n.publish_draft! | ||
| 362 | |||
| 363 | d2 = n.find_or_create_draft(@user1) | ||
| 364 | Globalize.with_locale(:en) { d2.update!(:title => "New English translation") } | ||
| 365 | d2.save! | ||
| 366 | |||
| 367 | summary = d2.locale_diff_summary(n.head) | ||
| 368 | en_entry = summary.find { |s| s[:locale] == :en } | ||
| 369 | |||
| 370 | assert en_entry[:changed] | ||
| 371 | refute en_entry[:exists_there] | ||
| 372 | end | ||
| 274 | end | 373 | end |
