fiedl/your_platform

View on GitHub
app/models/concerns/dag_link_repair.rb

Summary

Maintainability
A
0 mins
Test Coverage
concern :DagLinkRepair do

  included do
    # If there are some inconsistencies in the DAG, correct them on the fly
    # instead of raising an error in the validator.
    #
    before_validation {
      self.internal_count = 1 if (not self.direct?) && (self.count < 1)
    }
  end

  class_methods do

    # This starts all automatic dag link repair operations.
    #
    def repair
      delete_links_without_edges
      delete_redundant_indirect_links
      fix_types
      recalculate_indirect_counts
      # # We don't need this as this is already done in `after_save`
      # # when doing `recalculate_indirect_counts`.
      # #
      # # Yes, we do; or only the memberships with wrong count will also
      # # be fixed in regard for the validity range.
      recalculate_indirect_validity_ranges
    end

    def fix_types
      DagLink.where(ancestor_type: "Group", descendant_type: "User").update_all type: "Membership"
      DagLink.where(ancestor_type: "Group", descendant_type: "User", ancestor_id: Group.where(type: "StatusGroup").pluck(:id)).update_all type: "Memberships::Status"
    end

    def delete_links_without_edges
      print "\n\nDeleting links without ancestor or descendant.\n".blue
      p DagLink.where(ancestor_id: nil).delete_all
      p DagLink.where(ancestor_type: nil).delete_all
      p DagLink.where(descendant_id: nil).delete_all
      p DagLink.where(descendant_type: nil).delete_all
    end

    def delete_redundant_indirect_links
      RedundantLinkRepairer.scan_and_repair
    end

    def recalculate_indirect_counts
      LinkCountRepairer.repair
    end

    def recalculate_indirect_validity_ranges
      print "\n\nRecalculate validity ranges of indirect memberships.\n".blue
      DagLink.where(ancestor_type: "Group", descendant_type: "User", direct: false).each do |membership|
        membership.recalculate_validity_range_from_direct_memberships
        if membership.save
          print "*".blue
        else
          print ".".green
        end
      end
    end

    def recalculate_indirect_validity_ranges_later
      dag_links = DagLink.where(ancestor_type: "Group", descendant_type: "User", direct: false)
      print "\nCreating #{dag_links.count} background jobs ...\n".blue
      dag_links.each do |membership|
        RecalculateIndirectMembershipsJob.perform_later(membership)
      end
      print "Done.\n".green
    end

    class RedundantLinkRepairer

      def self.scan_and_repair
        self.new.scan_and_repair
      end

      def scan_and_repair
        mute_sql_log
        scan
        delete_redundant_links
        print "\n\nFinished.\n".blue
        unmute_sql_log
      end

      def mute_sql_log
        @old_log_level = ApplicationRecord.logger.level
        ApplicationRecord.logger.level = 1
      end
      def unmute_sql_log
        ApplicationRecord.logger.level = @old_log_level
      end

      # There are cases when an indirect membership is represented by multiple dag links
      # by error. We don't know how those issues arise, yet. This method scans for such
      # occurances.
      #
      # Example:
      #
      #   Alle Amtsträger
      #         |-------- Alle Seniores -------.
      #         |                              |
      #         |                              |
      #         |------ Alle Admins ------- User
      #
      # In this example, the link between "Alle Amtsträger" and "User" should be one
      # DagLink(direct: false, count: 2). But, by error, there are two DagLink objects.
      #
      def scan
        @occurances = []
        print "Scanning for redundant links.\n".blue
        DagLink.where(direct: false).each do |link|
          redundant_links = DagLink.where(
            ancestor_type: link.ancestor_type, ancestor_id: link.ancestor_id,
            descendant_type: link.descendant_type, descendant_id: link.descendant_id
          )
          if redundant_links.count > 1
            @occurances << redundant_links
            print "DATA CORRUPTION: REDUNDANT INDIRECT LINKS: #{redundant_links.inspect}\n\n".red
          else
            print ".".green
          end
        end
        return @occurances
      end

      def delete_redundant_links
        print "\n\nRepairing redundant links.\n".blue
        @occurances.each do |redundant_links|
          redundant_links[1..-1].each do |redundant_link| # all links but the first, which is the original one
            redundant_link.delete
            print ".".blue
          end
        end
      end
    end

    class LinkCountRepairer

      def self.repair
        print "\n\nRecalculating counts of all #{DagLink.where(direct: false).count} indirect links.\n".blue
        DagLink.where(direct: false).find_each do |link|
          self.new(link).repair
        end
      end

      def initialize(dag_link)
        @dag_link = dag_link
      end

      def repair
        @dag_link.internal_count = recalculated_count
        if @dag_link.save
          print "*".blue
        else
          print ".".green
        end
      end

      def ancestor
        @dag_link.ancestor
      end

      def descendant
        @dag_link.descendant
      end

      def recalculated_count
        pages_count + groups_count + events_count + users_count
      end

      def pages_count
        abstract_count(:descendant_pages, 'Page')
      end

      def groups_count
        abstract_count(:descendant_groups, 'Group')
      end

      def events_count
        abstract_count(:descendant_events, 'Event')
      end

      def users_count
        abstract_count(:descendant_users, 'User')
      end

      def workflows_count
        abstract_count(:descendant_workflows, 'Workflow')
      end

      def projects_count
        abstract_count(:descendant_projects, 'Project')
      end

      def abstract_count(descendants_method_name, type_name)
        if ancestor && ancestor.respond_to?(descendants_method_name)
          DagLink
            .where(descendant_id: @dag_link.descendant_id, descendant_type: @dag_link.descendant_type)
            .where(ancestor_id: ancestor.send(descendants_method_name).pluck(:id), ancestor_type: type_name)
            .where(direct: true)
            .count
        else
          0
        end
      end

    end
  end
end