From 751468e15efd9c18bfa3bb55b6089c5d27ebe953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 18 Aug 2022 10:52:52 +0200 Subject: [PATCH] Restore deleted data journals with a migration Restoring the predecessor's information, or the work package's information, depending on where the journal version is at. --- app/models/journal.rb | 8 ++ ...0220818074159_fix_deleted_data_journals.rb | 107 ++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 db/migrate/20220818074159_fix_deleted_data_journals.rb diff --git a/app/models/journal.rb b/app/models/journal.rb index 600a0d6def..9680474db2 100644 --- a/app/models/journal.rb +++ b/app/models/journal.rb @@ -101,6 +101,14 @@ class Journal < ApplicationRecord predecessor end + def successor + @successor ||= self.class + .where(journable_type:, journable_id:) + .where("#{self.class.table_name}.version > ?", version) + .order(version: :asc) + .first + end + def noop? (!notes || notes&.empty?) && get_changes.empty? end diff --git a/db/migrate/20220818074159_fix_deleted_data_journals.rb b/db/migrate/20220818074159_fix_deleted_data_journals.rb new file mode 100644 index 0000000000..332c0b3f07 --- /dev/null +++ b/db/migrate/20220818074159_fix_deleted_data_journals.rb @@ -0,0 +1,107 @@ +class FixDeletedDataJournals < ActiveRecord::Migration[7.0] + def up + get_missing_journals.each do |journable_type, relation| + puts "Cleaning up journals on #{journable_type}" + + relation.find_each { |journal| fix_journal_data(journal) } + + count = relation.count + raise "There shouldn't be any missing data left for #{journable_type}, but found #{count}" if count > 0 + end + end + + def down + # nothing to do + end + + def fix_journal_data(journal) + # Best case, no successor + # restore data from work package itself + if journal.successor.nil? + raise "Previous also has data nil" if (journal.previous && journal.previous.data.nil?) + insert_journal_data(journal, journal.previous, write_message: false) + elsif predecessor = journal.previous + # Case 2, we do have a predecessor + take_over_from_predecessor(journal, predecessor) + elsif journal.successor + # Case 3, We are the first, but have a successor + # Look for data in the successor + take_over_from_successor(journal, journal.successor) + else + raise "This should not happen for #{journal.inspect}" + end + end + + def insert_journal_data(journal, predecessor, write_message: false) + service = Journals::CreateService.new(journal.journable, User.system) + insert_sql = service.instance_eval { insert_data_sql('placeholder', predecessor) } + + result = Journal.connection.uncached do + ::Journal + .connection + .select_one(insert_sql) + + end + + raise "ID is missing #{result.inspect}" unless result['id'] + + if write_message + update_with_new_data!(journal, result['id']) + else + journal.update_column(:data_id, result['id']) + end + end + + def get_missing_journals + Journal + .pluck('DISTINCT(journable_type)') + .to_h do |journable_type| + journable_type = 'WorkPackage' + journal_class = journable_type.constantize.journal_class + table_name = journal_class.table_name + + relation = Journal + .joins("LEFT OUTER JOIN #{table_name} ON journals.data_type = '#{journal_class.to_s}' AND #{table_name}.id = journals.data_id") + .where("#{table_name}.id IS NULL") + .where(journable_type: journable_type) + .where.not(data_type: nil) # Ignore special tenants with data_type nil errors + .order('journals.version ASC') + .includes(:journable) + + [journable_type, relation] + end + end + + def take_over_from_predecessor(journal, predecessor) + raise "Related journal does not have data, this shouldn't be!" if predecessor.data.nil? + new_data = predecessor.data.dup + new_data.save! + + update_with_new_data!(journal, new_data.id) + end + + def take_over_from_successor(journal, successor) + # The successor itself may also have its data deleted. + # in this case, look for the first journal with data, or insert + new_data = + if successor.data.nil? + first_journal_with_data = journal.journable.journals.detect { |j| j.data.present? } + return insert_journal_data(journal, journal.previous, write_message: true) if first_journal_with_data.nil? + + first_journal_with_data.data.dup + else + successor.data.dup + end + + new_data.save! + update_with_new_data!(journal, new_data.id) + end + + def update_with_new_data!(journal, data_id) + notes = journal.notes + notes << "\n" unless notes.empty? + notes << "_(This activity had to be modified by the system and may be missing some changes or contain changes from previous or following activities.)_" + + journal.update_columns(notes:, data_id: data_id) + end +end