Coursemology/coursemology2

View on GitHub
app/services/course/duplication/course_duplication_service.rb

Summary

Maintainability
A
25 mins
Test Coverage
# frozen_string_literal: true

# Service to provide a full duplication of a Course.
class Course::Duplication::CourseDuplicationService < Course::Duplication::BaseService
  class << self
    # Constructor for the course duplication service.
    #
    # @param [Course] source_course The course to duplicate.
    # @param [Hash] options The options to be sent to the Duplicator object.
    # @option options [User] :current_user (+User.system+) The user triggering the duplication.
    # @option options [String] :new_title ('Duplicated') The title for the duplicated course.
    # @option options [DateTime] :new_start_at Start date and time for the duplicated course.
    # @option options [DateTime] :destination_instance_id The destination instance of the duplicated course.
    # @param [Array] all_objects All the objects in the course.
    # @param [Array] selected_objects The objects to duplicate.
    # @return [Course] The duplicated course
    def duplicate_course(source_course, options = {}, all_objects = [], selected_objects = [])
      destination_instance_id = options[:destination_instance_id]
      excluded_objects = all_objects - selected_objects
      options[:excluded_objects] = excluded_objects
      options[:source_course] = source_course
      options[:time_shift] =
        if options[:new_start_at]
          Time.zone.parse(options[:new_start_at]) - source_course.start_at
        else
          0
        end
      options.reverse_merge!(DEFAULT_COURSE_DUPLICATION_OPTIONS)
      service = new(options)
      service.duplicate_course(source_course, destination_instance_id)
    end
  end

  DEFAULT_COURSE_DUPLICATION_OPTIONS =
    { mode: :course, new_title: 'Duplicated', current_user: User.system }.freeze

  # Duplicate the course with the duplicator.
  # Do not just pass in @selected_objects or object parents could be set incorrectly.
  #
  # @return [Course] The duplicated course
  def duplicate_course(source_course, destination_instance_id)
    duplicated_course = Course.transaction do
      begin
        new_course = duplicator.duplicate(source_course)
        new_course.instance_id = destination_instance_id if destination_instance_id
        new_course.save!

        duplicator.set_option(:destination_course, new_course)

        # Destroy the new default reference timeline auto-created by `models/course.rb#set_defaults` to
        # make room for the default reference timeline that will be duplicated below.
        #
        # This reference timeline has to be set to default = false before it can be destroyed because
        # of the `models/course/reference_timeline.rb#prevent_destroy_if_default` invariant.
        #
        # Note that it is okay for a Course instance to have 0 default reference timeline, as seen in
        # `models/course.rb#validate_only_one_default_reference_timeline`. This is to accommodate
        # exactly this use case.
        default_reference_timeline = new_course.default_reference_timeline
        default_reference_timeline.default = false
        default_reference_timeline.destroy!

        new_course.reload

        source_course.duplication_manifest.each do |item|
          duplicator.duplicate(item).save!
          new_course.reload
        end

        update_course_settings(new_course, source_course)
        update_sidebar_settings(duplicator, new_course, source_course)

        # As per carrierwave v2.1.0, carrierwave image mounter that retains uploaded file as a cache
        # is reset upon reload (in our case it is new_course.reload).
        # As a result, logo duplication needs to be done after course reload.
        # https://github.com/carrierwaveuploader/carrierwave/issues/2482#issuecomment-762966926
        new_course.logo.duplicate_from(source_course.logo) if source_course.logo_url

        new_course
      rescue => _e # TO REMOVE - Testing for production duplication error
        Rails.logger.debug(message: 'Course duplication error debugging', error: _e, error_message: _e.message)
        raise ActiveRecord::Rollback
      end
    end
    notify_duplication_complete(duplicated_course)
    duplicated_course
  end

  private

  # Create a new duplication object to actually perform the duplication.
  # Initialize with the set of objects to be excluded from duplication, and the amount of time
  # to shift objects in the new course.
  #
  # @return [Duplicator]
  def initialize_duplicator(options)
    Duplicator.new(options[:excluded_objects], options.except(:excluded_objects))
  end

  # Sends an email to current_user to notify that the duplication is complete/failed.
  #
  # @param [Course] new_course The duplicated course
  def notify_duplication_complete(new_course)
    if new_course
      Course::Mailer.
        course_duplicated_email(@options[:source_course], new_course, @options[:current_user]).
        deliver_now
    else
      Course::Mailer.
        course_duplicate_failed_email(@options[:source_course], @options[:current_user]).
        deliver_now
    end
  end

  # Updates category_ids in the duplicated course settings. This is to be run after the course has
  # been saved and category_ids are available.
  def update_course_settings(new_course, old_course)
    component_key = Course::AssessmentsComponent.key
    old_category_settings = old_course.settings.public_send(component_key)
    return true if old_category_settings.nil?

    new_category_settings = {}
    old_category_settings.each do |key, value|
      new_category_settings[key] = value
    end
    new_course.settings.public_send("#{component_key}=", new_category_settings)
    new_course.save!
  end

  # Update sidebar settings keys with the new assessment category IDs.
  # Remove old keys with the original course's assessment category ID numbers from the sidebar
  # settings.
  def update_sidebar_settings(duplicator, new_course, old_course)
    old_course.assessment_categories.each do |old_category|
      new_category = duplicator.duplicate(old_category)
      weight = old_course.settings(:sidebar, "assessments_#{old_category.id}").weight
      next unless weight

      new_course.settings(:sidebar).settings("assessments_#{new_category.id}").weight = weight
      new_course.settings(:sidebar).public_send("assessments_#{old_category.id}=", nil)
    end
    new_course.save!
  end
end