opf/openproject

View on GitHub
lib_static/plugins/acts_as_journalized/lib/acts/journalized/journable_differ.rb

Summary

Maintainability
A
0 mins
Test Coverage
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2010-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
# ++

module Acts::Journalized
  class JournableDiffer
    class << self
      def changes(original, changed)
        original_data = original ? normalize_newlines(journaled_attributes(original)) : {}

        normalize_newlines(journaled_attributes(changed))
          .select { |attribute, new_value| no_nil_to_empty_strings?(original_data, attribute, new_value) }
          .to_h { |attribute, new_value| [attribute, [original_data[attribute], new_value]] }
          .with_indifferent_access
      end

      def association_changes(original, changed, *)
        get_association_changes(original, changed, *)
      end

    def association_changes_multiple_attributes(original, changed, association, association_name, key, values)
        list = {}
        values.each do |value|
          list.store(value, get_association_changes(original, changed, association, association_name, key, value))
        end

        transformed = {}
        list.each do |key, value|
          value.each do |agenda_item, data|
            transformed["#{agenda_item}_#{key}"] ||= {}
            transformed["#{agenda_item}_#{key}"] = data
          end
        end

        transformed
      end

      private

      def normalize_newlines(data)
        data.each_with_object({}) do |e, h|
          h[e[0]] = (e[1].is_a?(String) ? e[1].gsub("\r\n", "\n") : e[1])
        end
      end

      def no_nil_to_empty_strings?(normalized_old_data, attribute, new_value)
        old_value = normalized_old_data[attribute]
        new_value != old_value && ([new_value, old_value] - ['', nil]).present?
      end

      def journaled_attributes(object)
        if object.is_a?(Journal::BaseJournal)
          object.journaled_attributes.stringify_keys
        else
          object.attributes.slice(*object.class.journal_class.journaled_attributes.map(&:to_s))
        end
      end

      def get_association_changes(original, changed, association, association_name, key, value)
        new_journals = changed.send(association).map(&:attributes)
        old_journals = original&.send(association)&.map(&:attributes) || []

        changes_on_association(new_journals, old_journals, association_name, key, value)
      end

      def changes_on_association(current, original, association_name, key, value)
        merged_journals = merge_reference_journals_by_id(current, original, key.to_s, value.to_s)

        changes = added_references(merged_journals)
                    .merge(removed_references(merged_journals))
                    .merge(changed_references(merged_journals))

        to_changes_format(changes, association_name.to_s)
      end

      def added_references(merged_references)
        merged_references
          .select { |_, (old_value, new_value)| old_value.to_s.empty? && new_value.present? }
      end

      def removed_references(merged_references)
        merged_references
          .select { |_, (old_value, new_value)| old_value.present? && new_value.to_s.empty? }
      end

      def changed_references(merged_references)
        merged_references
          .select { |_, (old_value, new_value)| old_value.present? && new_value.present? && old_value.strip != new_value.strip }
      end

      def to_changes_format(references, key)
        references.each_with_object({}) do |(id, (old_value, new_value)), result|
          result["#{key}_#{id}"] = [old_value, new_value]
        end
      end

      def merge_reference_journals_by_id(new_journals, old_journals, id_key, value)
        all_associated_journal_ids = (new_journals.pluck(id_key) | old_journals.pluck(id_key)).compact

        all_associated_journal_ids.index_with do |id|
          [select_and_combine_journals(old_journals, id, id_key, value),
           select_and_combine_journals(new_journals, id, id_key, value)]
        end
      end

      def select_and_combine_journals(journals, id, key, value)
        selected_journals = journals.select { |j| j[key] == id }.pluck(value)

        if selected_journals.empty?
          nil
        else
          selected_journals.sort.join(',')
        end
      end
    end
  end
end