loomio/loomio

View on GitHub
app/services/record_cloner.rb

Summary

Maintainability
C
1 day
Test Coverage
class RecordCloner
  def initialize(recorded_at:)
    @recorded_at = recorded_at
    @cache = {}
  end

  def create_clone_group_for_public_demo(group, handle)
    clone_group = new_clone_group(group)
    clone_group.subscription = Subscription.new(plan: 'demo')
    clone_group.handle = handle
    clone_group.is_visible_to_public = true
    clone_group.members_can_create_subgroups = false
    clone_group.members_can_add_members = false
    clone_group.members_can_add_guests = false
    clone_group.members_can_announce = true
    clone_group.discussion_privacy_options = 'public_only'
    clone_group.membership_granted_upon = 'request'
    clone_group.discussions.each {|d| d.private = false }
    clone_group.polls.each {|p| p.specified_voters_only = false }

    clone_group.save!

    update_tag_colors(clone_group, group)

    clone_group.polls.each do |poll|
      poll.update_counts!
      poll.stances.each {|s| s.update_option_scores!}
    end
    clone_group.discussions.each {|d| EventService.repair_thread(d.id) }
    clone_group.reload
  end

  def update_tag_colors(clone_group, group)
    group.tags.pluck(:name, :color).each do |pair|
      Tag.where(group_id: clone_group.id, name: pair[0]).update_all(color: pair[1])
    end
  end

  def create_clone_group_for_actor(group, actor)
    # we don't really use this one except for testing

    clone_group = new_clone_group(group)
    clone_group.creator = actor
    clone_group.subscription = Subscription.new(plan: 'demo', owner: actor)
    clone_group.save!

    update_tag_colors(clone_group, group)
    store_source_record_ids(clone_group)


    clone_group.polls.each do |poll|
      poll.update_counts!
      poll.stances.each {|s| s.update_option_scores!}
    end
    clone_group.discussions.each {|d| EventService.repair_thread(d.id) }
    clone_group.add_member! actor
    clone_group.reload
  end

  def clone_trial_content_into_group(group, actor)
    source_group = Group.find_by(handle: 'trial-group-template')

    group.discussions = source_group.discussions.kept.map {|d| new_clone_discussion_and_events(d) }
    group.polls = source_group.polls.kept.map {|p| new_clone_poll(p) }
    group.save!

    update_tag_colors(group, source_group)
    store_source_record_ids(group)
    TranslationService.translate_group_content!(group, actor.locale)


    group.polls.each do |poll|
      poll.update_counts!
      poll.stances.each {|s| s.update_option_scores!}
    end

    group.discussions.each {|d| EventService.repair_thread(d.id) }
    group.reload

    group.save!

    group
  end

  def store_source_record_ids(clone_group)
    source_ids = {}
    @cache.each_pair do |key, value|
      class_name, id = key.split('-')
      source_ids["#{class_name}-#{value.id}"] = id.to_i
    end
    clone_group.info['source_record_ids'] = source_ids
    clone_group.save!
  end


  def create_clone_group(group)
    clone_group = new_clone_group(group)
    clone_group.save!

    update_tag_colors(clone_group, group)

    store_source_record_ids(clone_group)

    clone_group.polls.each do |poll|
      poll.update_counts!
      poll.stances.each {|s| s.update_option_scores!}
    end
    clone_group.discussions.each {|d| EventService.repair_thread(d.id) }
    clone_group.reload
    clone_group
  end

  def new_clone_group(group, clone_parent = nil)
    copy_fields = %w[
      name
      description
      description_format
      members_can_add_members
      members_can_edit_discussions
      members_can_edit_comments
      members_can_raise_motions
      members_can_vote
      members_can_start_discussions
      members_can_create_subgroups
      members_can_announce
      new_threads_max_depth
      new_threads_newest_first
      admins_can_edit_user_content
      members_can_add_guests
      members_can_delete_comments
      link_previews
      created_at
      updated_at
      category
    ]

    required_values = {
      handle: nil,
      is_visible_to_public: false,
      is_visible_to_parent_members: false,
      discussion_privacy_options: 'private_only',
      membership_granted_upon: 'approval',
      listed_in_explore: false
    }
    attachments = [:cover_photo, :logo, :files, :image_files]

    clone_group = new_clone(group, copy_fields, required_values, attachments)
    clone_group.parent = clone_parent

    clone_group.memberships = group.memberships.map {|m| new_clone_membership(m) }
    clone_group.discussions = group.discussions.kept.map {|d| new_clone_discussion_and_events(d) }
    clone_group.subgroups = group.subgroups.published.map {|g| new_clone_group(g, clone_group) }
    clone_group.polls = group.polls.kept.map {|p| new_clone_poll(p) }

    clone_group
  end

  def new_clone_discussion(discussion)
    copy_fields = %w[
      author_id
      title
      discussion_template_id
      discussion_template_key
      description
      description_format
      pinned_at
      max_depth
      newest_first
      content_locale
      link_previews
      created_at
      updated_at
      closed_at
      last_activity_at
      discarded_at
      template
      tags
    ]

    required_values = {
      private: true
    }

    attachments = [:files, :image_files]
    new_clone(discussion, copy_fields, required_values, attachments)
  end

  def new_clone_discussion_and_events(discussion)
    clone_discussion = new_clone_discussion(discussion)
    created_event = new_clone_event(discussion.created_event)
    created_event.eventable = clone_discussion
    clone_discussion.events << created_event
    drop_kinds = %w[poll_closed_by_user poll_expired poll_reopened]
    clone_discussion.items = discussion.items.order(:sequence_id).select{|i| !drop_kinds.include?(i.kind) }.map { |event| new_clone_event_and_eventable(event) }
    clone_discussion.polls = discussion.polls.map {|p| new_clone_poll(p) }
    clone_discussion.comments = discussion.comments.order(:id).map { |c| new_clone_comment(c) }
    clone_discussion
  end

  def new_clone_poll(poll)
    copy_fields = %w[
      author_id
      closing_at
      closed_at
      created_at
      updated_at
      discarded_at
      title
      details
      poll_type
      process_name
      process_subtitle
      voter_can_add_options
      anonymous
      details_format
      hide_results
      discarded_by
      specified_voters_only
      notify_on_closing_soon
      content_locale
      link_previews
      shuffle_options
      limit_reason_length
      meeting_duration
      time_zone
      dots_per_person
      minimum_stance_choices
      maximum_stance_choices
      can_respond_maybe
      min_score
      max_score
      template
      agree_target
      chart_type
      default_duration_in_days
      stance_reason_required
      poll_option_name_format
      reason_prompt
      tags
      poll_template_id
      poll_template_key
    ]
    attachments = [:files, :image_files]

    clone_poll = new_clone(poll, copy_fields, {}, attachments)
    clone_poll.poll_options = poll.poll_options.map {|poll_option| new_clone_poll_option(poll_option) }
    clone_poll.stances = poll.stances.map {|stance| new_clone_stance(stance) }
    clone_poll.outcomes = poll.outcomes.map {|outcome| new_clone_outcome(outcome) }
    if !clone_poll.template
      if poll.outcomes.empty?
        clone_poll.closed_at = nil
        clone_poll.closing_at = 3.days.from_now
      else
        clone_poll.closed_at = poll.outcomes.first.created_at
      end
    end

    clone_poll
  end

  def new_clone_poll_option(poll_option)
    copy_fields = %w[
      name
      icon
      meaning
      prompt
      priority
      score_counts
      total_score
      voter_scores
      voter_count
    ]
    clone_poll_option = new_clone(poll_option, copy_fields)
    clone_poll_option.poll = existing_clone(poll_option.poll)
    clone_poll_option
  end

  def new_clone_stance(stance)
    copy_fields = %w[
      accepted_at
      admin
      cast_at
      content_locale
      inviter_id
      latest
      link_previews
      participant_id
      reason
      reason_format
      revoked_at
      created_at
      updated_at
      volume
    ]
    attachments = [:files, :image_files]
    clone_stance = new_clone(stance, copy_fields, {}, attachments)
    clone_stance.stance_choices = stance.stance_choices.map {|sc| new_clone_stance_choice(sc) }
    clone_stance.poll = existing_clone(stance.poll)
    clone_stance
  end

  def new_clone_stance_choice(sc)
    copy_fields = %w[ score ]
    clone_sc = new_clone(sc, copy_fields)
    clone_sc.poll_option = existing_clone(sc.poll_option)
    clone_sc
  end

  def new_clone_outcome(outcome)
    copy_fields = %w[
      statement
      latest
      statement_format
      author_id
      review_on
      content_locale
      link_previews
      created_at
      updated_at
    ]

    attachments = [:files, :image_files]
    clone_outcome = new_clone(outcome, copy_fields, {}, attachments)
  end

  def new_clone_event(event)
    copy_fields = %w[
      user_id
      kind
      depth
      sequence_id
      position
      position_key
      child_count
      pinned
      descendant_count
      custom_fields
      created_at
    ]
    new_clone(event, copy_fields)
  end

  def new_clone_event_and_eventable(event)
    clone_event = new_clone_event(event)

    case event.eventable_type
    when 'Poll'
      clone_event.eventable = new_clone_poll(event.eventable)
    when 'Comment'
      clone_event.eventable = new_clone_comment(event.eventable)
    when 'Stance'
      clone_event.eventable = new_clone_stance(event.eventable)
    when 'Outcome'
      clone_event.eventable = new_clone_outcome(event.eventable)
    when 'Discussion'
      clone_event.eventable = new_clone_discussion(event.eventable)
    when nil
      # nothing
    else
      raise "unrecognised eventable_type #{event.eventable_type}"
    end

    clone_event
  end

  def new_clone_membership(membership)
    copy_fields = %w[
      user_id
      inviter_id
      revoked_at
      revoker_id
      admin
      volume
      experiences
      accepted_at
      title
    ]
    clone_membership = new_clone(membership, copy_fields)
    clone_membership.group = existing_clone(membership.group)
    clone_membership
  end

  def new_clone_comment(comment)
    copy_fields = %w[
      user_id
      body
      body_format
      discarded_at
      discarded_by
      content_locale
      link_previews
      created_at
    ]
    attachments = [:files, :image_files]
    clone_comment = new_clone(comment, copy_fields, {}, attachments)
    clone_comment.discussion = existing_clone(comment.discussion)
    clone_comment.parent = existing_clone(comment.parent)
    clone_comment
  end

  def new_clone_tag(tag)
    clone_tag = new_clone(tag, %w[name color priority])
    clone_tag.group = existing_clone(tag.group)
    clone_tag
  end

  def new_clone(record, copy_fields = [], required_values = {}, attachments = [])
    @cache["#{record.class}-#{record.id}"] ||= begin
      clone = record.class.new
      record_type = record.class.to_s.underscore.to_sym

      clone.attributes = new_clone_attributes(record, copy_fields, required_values)

      attachments.each do |name|
        if clone.send(name).class == ActiveStorage::Attached::Many
          clone.send(name).attach(record.send(name).blobs)
        else
          clone.send(name).attach record.send(name).blob
        end
      end

      clone
    end
  end

  def new_clone_attributes(record, copy_fields = [], required_values = {})
    attrs = {}
    copy_fields.each do |field|
      value = record.send(field)
      if value.nil?
        attrs[field] = value
      elsif field.ends_with?('_at')
        attrs[field] = value.to_datetime + (DateTime.now - @recorded_at.to_datetime)
      elsif field.ends_with?('_on')
        attrs[field] = value.to_date + (Date.today - @recorded_at.to_date)
      else
        attrs[field] = value
      end
    end

    required_values.each_pair do |key, value|
      attrs[key] = value
    end

    attrs
  end

  def existing_clone(record)
    @cache["#{record.class}-#{record.id}"]
  end
end