Browse Source

Fixes #4020 - Problems with duplicate records in Translations table.

Martin Gruner 2 years ago
parent
commit
08e4de7cb6

+ 2 - 0
app/models/translation.rb

@@ -5,6 +5,8 @@ class Translation < ApplicationModel
 
   before_create :set_initial
 
+  validates :locale, presence: true
+
 =begin
 
 reset translations to origin

+ 36 - 0
db/migrate/20220329075919_remove_duplicate_translations.rb

@@ -0,0 +1,36 @@
+# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+class RemoveDuplicateTranslations < ActiveRecord::Migration[6.1]
+  def change
+    # return if it's a new setup
+    return if !Setting.exists?(name: 'system_init_done')
+
+    # For some obsure reason, in legacy systems there was a chance that Translation records with the same locale and source
+    #   appeared multiple times.
+
+    Locale.all.each do |locale|
+
+      # Remove duplicates of all synchronized strings first.
+      cleanup_duplicates_of_synchronized(locale)
+
+      # Remove other duplicates of unsynchronized strings as well.
+      cleanup_duplicates_of_unsynchronized(locale)
+    end
+  end
+
+  def cleanup_duplicates_of_synchronized(locale)
+    unsync_translations = Translation.where(locale: locale.locale, is_synchronized_from_codebase: false).all
+    Translation.where(locale: locale.locale, is_synchronized_from_codebase: true).all.each do |t|
+      unsync_translations.select { |unsync_t| unsync_t.source == t.source }.each(&:destroy)
+    end
+  end
+
+  def cleanup_duplicates_of_unsynchronized(locale)
+    unsync_translations = Translation.where(locale: locale.locale, is_synchronized_from_codebase: false).order(:id).all
+    unsync_translations.each do |t|
+      next if t.destroyed?
+
+      unsync_translations.select { |check_t| check_t.id > t.id && check_t.source == t.source && !check_t.destroyed? }.each(&:destroy)
+    end
+  end
+end

+ 60 - 0
spec/db/migrate/remove_duplicate_translations_spec.rb

@@ -0,0 +1,60 @@
+# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+require 'rails_helper'
+
+RSpec.describe RemoveDuplicateTranslations, type: :db_migration do
+
+  def create_translation(attributes)
+    Translation.new(attributes.merge({ locale: 'de-de', created_by_id: 1, updated_by_id: 1 })).tap(&:save!)
+  end
+
+  context 'when having unsynchronized entries with duplicates' do
+    let!(:unrelated_entry_one) { create_translation({ source: 'unknown entry', target: 'unknown translation', is_synchronized_from_codebase: false }) }
+    let!(:unrelated_entry_two) { create_translation({ source: 'unknown entry', target: 'unknown translation', is_synchronized_from_codebase: false }) }
+    let!(:unrelated_entry_three) { create_translation({ source: 'unknown entry', target: 'unknown translation', is_synchronized_from_codebase: false }) }
+
+    before do
+      migrate
+    end
+
+    it 'does not delete the first' do
+      expect(unrelated_entry_one.reload).to be_present
+    end
+
+    it 'deletes the second' do
+      expect { unrelated_entry_two.reload }.to raise_error(ActiveRecord::RecordNotFound)
+    end
+
+    it 'deletes the third' do
+      expect { unrelated_entry_three.reload }.to raise_error(ActiveRecord::RecordNotFound)
+    end
+  end
+
+  context 'when having multiple duplicate records of existing synchronized records' do
+    # 'yes': 'ja' already exists as synchronized entry in the translations for 'de-de'.
+    let!(:duplicate_one) { create_translation({ source: 'yes', target: 'ja', is_synchronized_from_codebase: false }) }
+    let!(:duplicate_two) { create_translation({ source: 'yes', target: 'ja', is_synchronized_from_codebase: false }) }
+
+    before do
+      migrate
+    end
+
+    it 'deletes the first' do
+      expect { duplicate_one.reload }.to raise_error(ActiveRecord::RecordNotFound)
+    end
+
+    it 'deletes the second' do
+      expect { duplicate_two.reload }.to raise_error(ActiveRecord::RecordNotFound)
+    end
+
+    it 'keeps the original' do
+      expect(Translation.where(locale: 'de-de', source: 'yes').count { |e| e.source == 'yes' }).to eq(1)
+    end
+  end
+
+  context 'when no duplicates are present' do
+    it 'does nothing' do
+      expect { migrate }.not_to change(Translation, :count)
+    end
+  end
+end