noesya/osuny

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

Summary

Maintainability
A
50 mins
Test Coverage
A
98%
# Les objets ont souvent besoin de WithGit et WithDependencies, mais pas toujours :
# - les blocks ont des dépendances, mais ne sont pas envoyés sur Git en tant qu'objets, ils passent par leur 'about'
# - les menu items passent par le menu
# - les templates et les components de blocks passent par les blocks qui passent par les 'about'
module WithDependencies
  extend ActiveSupport::Concern

  included do
    attr_accessor :previous_dependencies

    if self < ActiveRecord::Base
      before_save :snapshot_dependencies
      after_save :clean_websites_if_necessary
    end
  end

  def destroy
    # On est obligés d'overwrite la méthode destroy pour éviter un problème d'œuf et de poule.
    # On a besoin que les websites puissent recalculer leurs recursive_dependencies
    # et on a besoin que ces recursive_dependencies n'incluent pas l'objet courant, puisqu'il est "en cours de destruction" (ni ses propres recursive_dependencies).
    # Mais si on détruit juste l'objet et qu'on fait un `after_destroy :clean_website_connections`
    # on ne peut plus accéder aux websites (puisque l'objet est déjà détruit et ses connexions en cascades).
    # Egalement, quand on supprime un objet indirect, il faut synchroniser ses anciennes sources directes pour supprimer toute référence éventuelle
    # Donc :
    # 1. on stocke les websites (et les sources directes si nécessaire)
    # 2. on laisse la méthode destroy normale faire son travail
    # 3. PUIS on demande aux websites stockés de nettoyer leurs connexions et leurs git files (et on synchronise les potentielles sources directes)
    self.transaction do
      snapshot_direct_sources = try(:direct_sources).to_a || []
      website_ids = websites_to_clean.pluck(:id)
      super
      snapshot_direct_sources.each do |direct_source|
        direct_source.sync_with_git
      end
      clean_websites(website_ids)
      # TODO: Actuellement, on ne nettoie pas les références
      # Exemple : Quand on supprime un auteur, il n'est pas nettoyé dans le static de ses anciens posts.
      # Un save du website le fera en nocturne pour l'instant.
    end
  end

  # Cette méthode doit être définie dans chaque objet,
  # et renvoyer un tableau de ses références directes.
  # Jamais de référence indirecte !
  # Elles sont gérées récursivement.
  def dependencies
    []
  end

  # Method is often overriden
  def syncable?
    if respond_to? :published_now?
      published_now?
    elsif respond_to? :published
      published
    else
      true
    end
  end

  # On ne liste pas les objets en cours de suppression
  # return array if respond_to?(:mark_for_destruction?) && mark_for_destruction
  # On renvoie l'array tel quel, non modifié, si on demande les contenus syncable_only et que le contenu ne l'est pas
  def recursive_dependencies(array: [], syncable_only: false, follow_direct: false)
    if dependency_should_be_synced?(self, syncable_only)
      dependencies.each do |dependency|
        array = recursive_dependencies_add(array, dependency, syncable_only, follow_direct)
      end
    end
    array.compact
  end

  def recursive_dependencies_syncable
    @recursive_dependencies_syncable ||= recursive_dependencies(syncable_only: true)
  end

  def recursive_dependencies_syncable_following_direct
    @recursive_dependencies_syncable_following_direct ||= recursive_dependencies(syncable_only: true, follow_direct: true)
  end

  protected

  def recursive_dependencies_add(array, dependency, syncable_only, follow_direct)
    # Si l'objet ne doit pas être ajouté on n'ajoute pas non plus ses dépendances récursives
    # C'est le fait de couper ici qui évite la boucle infinie
    return array unless dependency_should_be_added?(array, dependency, syncable_only)
    array << dependency
    return array if !follow_direct && dependency.try(:is_direct_object?)
    return array unless dependency.respond_to?(:recursive_dependencies)
    dependency.recursive_dependencies(array: array, syncable_only: syncable_only, follow_direct: follow_direct)
  end

  # Si l'objet est déjà là, on ne doit pas l'ajouter
  # Si l'objet n'est pas syncable, on ne doit pas l'ajouter non plus
  def dependency_should_be_added?(array, dependency, syncable_only)
    !dependency.in?(array) && dependency_should_be_synced?(dependency, syncable_only)
  end

  # Si on n'est pas en syncable only on liste tout, sinon, il faut analyser
  def dependency_should_be_synced?(dependency, syncable_only)
    !syncable_only || (dependency.respond_to?(:syncable?) && dependency.syncable?)
  end

  # Stockage en RAM des dépendances avant enregistrement
  def snapshot_dependencies
    @previous_dependencies = persisted? ? reloaded_recursive_dependencies_syncable_filtered : []
  end

  def clean_websites_if_necessary
    # Debug :)
    # puts self
    # puts "  previous_dependencies           #{ @previous_dependencies }"
    # puts "  recursive_dependencies_syncable #{ reloaded_recursive_dependencies_syncable_filtered }"
    # puts "  missing_dependencies_after_save #{ missing_dependencies_after_save }"
    # puts
    if missing_dependencies_after_save.any? || unpublished_by_last_save?
      clean_websites(websites_to_clean.pluck(:id))
    end
  end

  def clean_websites(websites_ids)
    # Les objets directs et les objets indirects (et les websites) répondent !
    return unless respond_to?(:is_direct_object?)
    Communication::CleanWebsitesJob.perform_later(websites_ids)
  end

  def websites_to_clean
    is_direct_object? ? [website] : websites
  end

  def missing_dependencies_after_save
    @previous_dependencies - reloaded_recursive_dependencies_syncable_filtered
  end

  def reloaded_recursive_dependencies_syncable_filtered
    reloaded_object = self.class.unscoped.find(id)
    reloaded_dependencies = reloaded_object.recursive_dependencies_syncable
    DependenciesFilter.filtered(reloaded_dependencies)
  end

  def unpublished_by_last_save?
    return unless respond_to?(:published)
    return true if saved_change_to_published? && !published?
    if respond_to?(:published_at)
      return saved_change_to_published_at? && (published_at.nil? || published_at > Time.now)
    end
    false
  end
end