osunyorg/admin

View on GitHub
app/models/communication/website/with_connected_objects.rb

Summary

Maintainability
A
0 mins
Test Coverage
module Communication::Website::WithConnectedObjects
  extend ActiveSupport::Concern

  included do
    CONNECTIONS_BLACKLIST = [
      # Les blobs ne sont jamais modifiés, donc on n'a aucun besoin de savoir à quoi ils sont connectés
      'ActiveStorage::Blob'
    ].freeze

    has_many :connections, dependent: :destroy

    after_save :connect_about, if: :saved_change_to_about_id?
  end

  # Appelé uniquement en asynchrone par Communication::Website::CleanAndRebuildJob
  def clean_and_rebuild
    direct_objects_association_names.each do |association_name|
      # We use find_each to avoid loading all the objects in memory
      public_send(association_name).find_each(&:connect_dependencies)
    end
    connect(about, self) if about.present?
    delete_obsolete_connections_for_self_and_direct_sources
    # In the same job
    create_missing_special_pages
    initialize_menus
    sync_with_git_safely
    destroy_obsolete_git_files_safely
    get_current_theme_version!
    screenshot!
  end

  # Appelé uniquement en asynchrone par Communication::Website::CleanJob
  def clean
    delete_obsolete_connections_for_self_and_direct_sources
    destroy_obsolete_git_files
  end

  # Le site fait le ménage de ses connexions directes uniquement
  def delete_obsolete_connections
    Communication::Website::Connection.delete_useless_connections(
      # On ne liste pas toutes les connexions du website,
      # mais juste les connexions pour lesquelles le site est la source.
      connections.where(direct_source: self),
      # On prend l'about et ses dépendances récursives.
      # On ne prend pas toutes les dépendances parce qu'on s'intéresse
      # uniquement à la connexion via about.
      about_dependencies
    )
  end

  # Le site fait son ménage de printemps
  # Appelé
  # - par un objet avec des connexions lorsqu'il est destroyed
  # - par le website lui-même au changement du about
  def delete_obsolete_connections_for_self_and_direct_sources
    direct_source_ids_per_type_through_connections.each do |direct_source_type, direct_source_ids|
      next if direct_source_type.nil?
      # On récupère une liste d'objets directs d'une même classe
      direct_sources = direct_source_type.safe_constantize.where(id: direct_source_ids)
      # On exécute en synchrone pour chaque objet
      direct_sources.find_each(&:delete_obsolete_connections)
    end
  end

  def has_connected_object?(indirect_object)
    connections.for_object(indirect_object).exists?
  end

  def connect(indirect_object, direct_source, direct_source_type: nil)
    # https://developers.osuny.org/docs/admin/sites-web/git/dependencies/iteration-9/
    connect_object(
      indirect_object,
      direct_source,
      direct_source_type: direct_source_type
    ) if should_connect?(indirect_object, direct_source)
    return unless should_connect_recursive_dependencies?(indirect_object)
    indirect_object.recursive_dependencies.each do |dependency|
      connect_object dependency, direct_source
    end
  end

  def connect_and_sync(indirect_object, direct_source, direct_source_type: nil)
    connect(indirect_object, direct_source, direct_source_type: direct_source_type)
    direct_source.sync_with_git
  end

  def disconnect(indirect_object, direct_source, direct_source_type: nil)
    direct_source_type ||= direct_source.class.base_class.to_s
    connections.where(university: university,
                      indirect_object: indirect_object,
                      direct_source_id: direct_source.id,
                      direct_source_type: direct_source_type)
                .delete_all
  end

  def disconnect_and_sync(indirect_object, direct_source, direct_source_type: nil)
    disconnect(indirect_object, direct_source, direct_source_type: direct_source_type)
    direct_source.sync_with_git
    destroy_obsolete_git_files
  end

  # TODO factoriser avec les extranets
  def connected_people
    ids = connections.where(indirect_object_type: 'University::Person').pluck(:indirect_object_id)
    University::Person.where(id: ids)
  end

  def connected_organizations
    ids = connections.where(indirect_object_type: 'University::Organization').pluck(:indirect_object_id)
    University::Organization.where(id: ids)
  end

  def connected_publications
    ids = connections.where(indirect_object_type: 'Research::Publication').pluck(:indirect_object_id)
    Research::Publication.where(id: ids)
  end

  # ensure the object "website" respond to both is_direct_object? and is_indirect_object? as website doesn't include neither as_direct_object nor as_indirect_object
  def is_direct_object?
    true
  end

  def is_indirect_object?
    false
  end

  protected

  def direct_objects_association_names
    [
      :pages,
      :posts,
      :post_categories,
      :events,
      :agenda_categories,
      :projects,
      :portfolio_categories,
      :menus
    ]
  end

  def connect_about
    self.connect(about, self) if about.present? && about.try(:is_indirect_object?)
    Communication::Website::DeleteObsoleteConnectionsJob.perform_later(id)
  end

  def about_dependencies
    about.present?  ? [about] + about.recursive_dependencies
                    : []
  end

  def connect_object(indirect_object, direct_source, direct_source_type: nil)
    return unless should_connect?(indirect_object, direct_source)
    # puts "connect #{object} (#{object.class})"
    direct_source_type ||= direct_source.class.base_class.to_s
    connection = connections.where( university: university,
                                    indirect_object: indirect_object,
                                    direct_source_id: direct_source.id,
                                    direct_source_type: direct_source_type)
                            .first_or_create
    connection.touch if connection.persisted?
  end

  def should_connect?(indirect_object, direct_source)
    # Ce cas se produit quand on save un new website et qu'on ne passe pas un validateur
    persisted? &&
    # On ne connecte pas les objets inexistants
    indirect_object.present? &&
    # On ne connecte pas les objets sans source
    direct_source.present? &&
    # On ne connecte pas le site à lui-même
    !indirect_object.is_a?(Communication::Website) &&
    # On ne connecte pas les objets directs (en principe ça n'arrive pas)
    !indirect_object.try(:is_direct_object?) &&
    # On ne connecte pas des objets qui ne sont pas issus de modèles ActiveRecord (comme les composants des blocs)
    indirect_object.is_a?(ActiveRecord::Base) &&
    # On ne connecte pas certains types d'objets, listés dans une black list
    !indirect_object.class.to_s.in?(CONNECTIONS_BLACKLIST)
  end

  def should_connect_recursive_dependencies?(indirect_object)
    # On ne suit pas les objets inexistants
    indirect_object.present? &&
    # On ne suit pas les objets qui n'ont pas de dépendances
    indirect_object.respond_to?(:recursive_dependencies) &&
    # On ne suit pas les objets directs
    !indirect_object.try(:is_direct_object?)
  end

  # On passe par les connexions pour éviter d'analyser des objets directs qui n'ont pas d'objets indirects du tout
  # Le website lui même est inclus dans le retour (s'il a un about qui déclenche des connexions)
  def direct_source_ids_per_type_through_connections
    # {
    #  'Communication::Website::Post': ['ID1', 'ID2', ...],
    #  'Communication::Website::Page': ['ID1', 'ID2', ...],
    #  'Communication::Website': ['ID1'],
    #  ...
    # }
    connections.group(:direct_source_type)
               .pluck(:direct_source_type, Arel.sql('array_agg(DISTINCT direct_source_id)'))
               .to_h
  end
end