<commit_before># methods that handle replicating changes to copies of core objects (forms, questions, etc.) within and across missions
module Replicable
  extend ActiveSupport::Concern

  JOIN_CLASSES = %w(Optioning Questioning Condition)

  included do
    # dsl-style method for setting options from base class
    def self.replicable(options = {})
      options[:assocs] = Array.wrap(options[:assocs])
      options[:dont_copy] = Array.wrap(options[:dont_copy]).map(&:to_s)
      class_variable_set('@@replication_options', options)
    end

    # cleaner accessor for replication options
    def self.replication_options
      class_variable_defined?('@@replication_options') ? class_variable_get('@@replication_options') : nil
    end
  end

  # creates a duplicate in this or another mission
  def replicate(to_mission_or_replication = nil)

    # if mission or nil was passed in, we don't have a replication object, so we need to create one
    # a replication is an object to track replication parameters
    if to_mission_or_replication.is_a?(Replication)
      replication = to_mission_or_replication
    else
      replication = Replication.new(:obj => self, :to_mission => to_mission_or_replication)
    end
    
    # wrap in transaction if this is the first call
    return replication.redo_in_transaction unless replication.in_transaction?

    # if we're on a recursive step AND we're doing a shallow copy AND this is not a join class, 
    # we don't need to do any recursive copying, so just return self
    if replication.recursed? && replication.shallow_copy? && !JOIN_CLASSES.include?(self.class.name)
      add_copy_to_parent(self, replication)
      return self
    end

    # get the obj to copy stuff to, and also tell the replication object about it
    dest_obj = replication_destination_obj(replication)
    replication.dest_obj = dest_obj

    # set the proper mission if applicable
    dest_obj.mission_id = replication.to_mission.try(:id)

    # determine appropriate attribs to copy
    dont_copy = %w(id created_at updated_at mission_id mission is_standard standard_id standard) + replicable_opts(:dont_copy)

    # don't copy foreign key field of belongs_to associations
    replicable_opts(:assocs).each do |assoc|
      refl = self.class.reflect_on_association(assoc)
      dont_copy << refl.foreign_key if refl.macro == :belongs_to
    end

    # don't copy foreign key field of parent's has_* association, if applicable
    if replicable_opts(:parent)
      dont_copy << replicable_opts(:parent).to_s + '_id'
    end

    # copy attribs
    attribs_to_copy = attributes.except(*dont_copy)
    attribs_to_copy.each{|k,v| dest_obj.send("#{k}=", v)}

    # if uniqueness property is set, make sure the specified field is unique
    if params = replicable_opts(:uniqueness)
      dest_obj.send("#{params[:field]}=", self.ensure_unique_field(params.merge(:mission => replication.to_mission, :dest_obj => dest_obj)))
    end

    # call a callback if requested
    if replicable_opts(:after_copy_attribs)
      self.send(replicable_opts(:after_copy_attribs), dest_obj, replication.ancestors)
    end

    # add to parent before recursive step
    add_copy_to_parent(dest_obj, replication)

    # if this is a standard obj, add to copies if not there already
    copies << dest_obj if is_standard? && !copies.include?(dest_obj)

    # replicate associations
    replicable_opts(:assocs).each do |assoc|
      if self.class.reflect_on_association(assoc).collection?
        # destroy any children in copy that don't exist in standard
        std_child_ids = send(assoc).map(&:id)
        dest_obj.send(assoc).each do |o|
          unless std_child_ids.include?(o.standard_id)
            dest_obj.changing_in_replication = true
            dest_obj.send(assoc).destroy(o) 
          end
        end

        # RECURSIVE STEP: replicate the existing children
        send(assoc).each{|o| o.replicate(replication.clone_for_recursion(o, assoc, dest_obj))}
      else

        # if orig assoc is nil, make sure copy is also
        if send(assoc).nil?
          if !dest_obj.send(assoc).nil?
            dest_obj.changing_in_replication = true
            dest_obj.send(assoc).destroy
          end
        # else replicate
        else
          # RECURSIVE STEP: replicate the child
          send(assoc).replicate(replication.clone_for_recursion(send(assoc), assoc, dest_obj))
        end
      end
    end

    # set flag so that standardizable callback doesn't call replicate again unnecessarily
    dest_obj.changing_in_replication = true
    dest_obj.save!

    return dest_obj
  end

  def replicate_destruction(to_mission)
    if c = copy_for_mission(to_mission)
      c.destroy
    end
  end


  # gets the object to which the replication operation will copy attributes, etc.
  # may be a new object or an existing one depending on parameters
  def replication_destination_obj(replication)
    # if this is a standard object AND we're copying to a mission AND there exists a copy of this obj in the given mission,
    # then we don't need to create a new object, so return the existing copy
    if is_standard? && replication.has_to_mission? && (copy = copy_for_mission(replication.to_mission))
      return copy
    else
      # otherwise, we init and return the new object
      return self.class.new
    end
  end

  # adds the specified object to the parent object's association
  # we do it this way so that links between parent and children objects
  # are established during recursion instead of all at the end
  # this is because some child objects (e.g. conditions) need access to their parents
  def add_copy_to_parent(copy, replication)
    # trivial case
    return unless replication.has_ancestors?

    # get immediate parent and reflect on association
    refl = replication.parent.class.reflect_on_association(replication.current_assoc)
    
    # associate object with parent using appropriate method depending on assoc type
    if refl.collection?
      # only copy if not already there
      unless replication.parent.send(replication.current_assoc).include?(copy)
        replication.parent.send(replication.current_assoc).send('<<', copy)
      end
    else
      replication.parent.send("#{replication.current_assoc}=", copy)
    end
  end

  # ensures the given name or other field would be unique, and generates a new name if it wouldnt be<commit_msg>moved determination of attribs to copy into own method<commit_after># methods that handle replicating changes to copies of core objects (forms, questions, etc.) within and across missions
module Replicable
  extend ActiveSupport::Concern

  JOIN_CLASSES = %w(Optioning Questioning Condition)

  # an initial list of attributes that we don't want to copy from the src_obj to the dest_obj
  ATTRIBS_NOT_TO_COPY = %w(id created_at updated_at mission_id mission is_standard standard_id standard)

  included do
    # dsl-style method for setting options from base class
    def self.replicable(options = {})
      options[:assocs] = Array.wrap(options[:assocs])
      options[:dont_copy] = Array.wrap(options[:dont_copy]).map(&:to_s)
      class_variable_set('@@replication_options', options)
    end

    # cleaner accessor for replication options
    def self.replication_options
      class_variable_defined?('@@replication_options') ? class_variable_get('@@replication_options') : nil
    end
  end

  # creates a duplicate in this or another mission
  def replicate(to_mission_or_replication = nil)

    # if mission or nil was passed in, we don't have a replication object, so we need to create one
    # a replication is an object to track replication parameters
    if to_mission_or_replication.is_a?(Replication)
      replication = to_mission_or_replication
    else
      replication = Replication.new(:obj => self, :to_mission => to_mission_or_replication)
    end
    
    # wrap in transaction if this is the first call
    return replication.redo_in_transaction unless replication.in_transaction?

    # if we're on a recursive step AND we're doing a shallow copy AND this is not a join class, 
    # we don't need to do any recursive copying, so just return self
    if replication.recursed? && replication.shallow_copy? && !JOIN_CLASSES.include?(self.class.name)
      add_copy_to_parent(self, replication)
      return self
    end

    # if we get this far we DO need to do recursive copying
    # get the obj to copy stuff to, and also tell the replication object about it
    dest_obj = replication_destination_obj(replication)
    replication.dest_obj = dest_obj

    # set the proper mission if applicable
    dest_obj.mission_id = replication.to_mission.try(:id)

    # copy attributes from src to parent
    attribs_to_copy_in_replication.each{|k,v| dest_obj.send("#{k}=", v)}

    # if uniqueness property is set, make sure the specified field is unique
    if params = replicable_opts(:uniqueness)
      dest_obj.send("#{params[:field]}=", self.ensure_unique_field(params.merge(:mission => replication.to_mission, :dest_obj => dest_obj)))
    end

    # call a callback if requested
    if replicable_opts(:after_copy_attribs)
      self.send(replicable_opts(:after_copy_attribs), dest_obj, replication.ancestors)
    end

    # add to parent before recursive step
    add_copy_to_parent(dest_obj, replication)

    # if this is a standard obj, add to copies if not there already
    copies << dest_obj if is_standard? && !copies.include?(dest_obj)

    # replicate associations
    replicable_opts(:assocs).each do |assoc|
      if self.class.reflect_on_association(assoc).collection?
        # destroy any children in copy that don't exist in standard
        std_child_ids = send(assoc).map(&:id)
        dest_obj.send(assoc).each do |o|
          unless std_child_ids.include?(o.standard_id)
            dest_obj.changing_in_replication = true
            dest_obj.send(assoc).destroy(o) 
          end
        end

        # RECURSIVE STEP: replicate the existing children
        send(assoc).each{|o| o.replicate(replication.clone_for_recursion(o, assoc, dest_obj))}
      else

        # if orig assoc is nil, make sure copy is also
        if send(assoc).nil?
          if !dest_obj.send(assoc).nil?
            dest_obj.changing_in_replication = true
            dest_obj.send(assoc).destroy
          end
        # else replicate
        else
          # RECURSIVE STEP: replicate the child
          send(assoc).replicate(replication.clone_for_recursion(send(assoc), assoc, dest_obj))
        end
      end
    end

    # set flag so that standardizable callback doesn't call replicate again unnecessarily
    dest_obj.changing_in_replication = true
    dest_obj.save!

    return dest_obj
  end

  def replicate_destruction(to_mission)
    if c = copy_for_mission(to_mission)
      c.destroy
    end
  end


  # gets the object to which the replication operation will copy attributes, etc.
  # may be a new object or an existing one depending on parameters
  def replication_destination_obj(replication)
    # if this is a standard object AND we're copying to a mission AND there exists a copy of this obj in the given mission,
    # then we don't need to create a new object, so return the existing copy
    if is_standard? && replication.has_to_mission? && (copy = copy_for_mission(replication.to_mission))
      return copy
    else
      # otherwise, we init and return the new object
      return self.class.new
    end
  end

  # gets a hash of attributes of this object that should be copied to the dest obj
  # in the current replication operation. this is surprisingly intricate
  def attribs_to_copy_in_replication
    # start with the initial, constant set
    dont_copy = ATTRIBS_NOT_TO_COPY

    # add the ones that are specified explicitly in the replicable options
    dont_copy += replicable_opts(:dont_copy)

    # don't copy foreign key field of belongs_to associations
    replicable_opts(:assocs).each do |assoc|
      refl = self.class.reflect_on_association(assoc)
      dont_copy << refl.foreign_key if refl.macro == :belongs_to
    end

    # don't copy foreign key field of parent's has_* association, if applicable
    if replicable_opts(:parent)
      dont_copy << replicable_opts(:parent).to_s + '_id'
    end

    # get hash and return
    attributes.except(*dont_copy)
  end

  # adds the specified object to the parent object's association
  # we do it this way so that links between parent and children objects
  # are established during recursion instead of all at the end
  # this is because some child objects (e.g. conditions) need access to their parents
  def add_copy_to_parent(copy, replication)
    # trivial case
    return unless replication.has_ancestors?

    # get immediate parent and reflect on association
    refl = replication.parent.class.reflect_on_association(replication.current_assoc)
    
    # associate object with parent using appropriate method depending on assoc type
    if refl.collection?
      # only copy if not already there
      unless replication.parent.send(replication.current_assoc).include?(copy)
        replication.parent.send(replication.current_assoc).send('<<', copy)
      end
    else
      replication.parent.send("#{replication.current_assoc}=", copy)
    end
  end

  # ensures the given name or other field would be unique, and generates a new name if it wouldnt be
{'fraction_non_alphanumeric': 0.07046353661267447, 'fraction_numerical': 0.0, 'mean_word_length': 3.4452554744525545, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4870965', 'n_tokens_mistral': 3974, 'n_tokens_neox': 3784, 'n_words': 1274}