Coursemology/coursemology2

View on GitHub
app/controllers/course/assessment/assessments_controller.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# frozen_string_literal: true
class Course::Assessment::AssessmentsController < Course::Assessment::Controller
  include Course::Assessment::AssessmentsHelper

  before_action :load_submissions, only: [:show]

  include Course::Assessment::MonitoringConcern

  before_action :load_question_duplication_data, only: [:show, :reorder]

  COURSE_USERS = { my_students: 'my_students',
                   my_students_w_phantom: 'my_students_w_phantom',
                   students: 'students',
                   students_w_phantom: 'students_w_phantom' }.freeze

  def index
    @assessments = @assessments.ordered_by_date_and_title.with_submissions_by(current_user)

    @items_hash = @course.lesson_plan_items.where(actable_id: @assessments.pluck(:id),
                                                  actable_type: Course::Assessment.name).
                  preload(actable: :conditions).
                  with_reference_times_for(current_course_user, current_course).
                  with_personal_times_for(current_course_user).
                  to_h do |item|
      [item.actable_id, item]
    end

    @conditional_service = Course::Assessment::AchievementPreloadService.new(@assessments)
  end

  def show
    @assessment_time = @assessment.time_for(current_course_user)
    return render 'authenticate' unless can_access_assessment?

    @question_assessments = @assessment.question_assessments.with_question_actables
    @assessment_conditions = @assessment.assessment_conditions.includes({ conditional: :actable })
    @questions = @assessment.questions.includes({ actable: :test_cases })

    @requirements = @assessment.specific_conditions.map do |condition|
      {
        title: condition.title,
        satisfied: current_course_user.present? ? condition.satisfied_by?(current_course_user) : nil
      }.compact
    end
  end

  def new
  end

  def create
    # Randomized Assessment is temporarily hidden (PR#5406)
    # @assessment.update_randomization(randomization_params)

    ActiveRecord::Base.transaction do
      @assessment.save!
      upsert_monitoring! if can_manage_monitor?

      render json: { id: @assessment.id }
    end
  rescue StandardError
    render json: { errors: @assessment.errors }, status: :bad_request
  end

  def edit
    @assessment.description = helpers.format_ckeditor_rich_text(@assessment.description)
  end

  def update
    @assessment.update_mode(autograded_params)

    # Randomized Assessment is temporarily hidden (PR#5406)
    # @assessment.update_randomization(randomization_params)

    ActiveRecord::Base.transaction do
      @assessment.update!(assessment_params)
      upsert_monitoring! if can_manage_monitor?

      head :ok
    end
  rescue StandardError
    render json: { errors: @assessment.errors }, status: :bad_request
  end

  def destroy
    if @assessment.destroy
      render json: {
        redirect: course_assessments_path(current_course,
                                          category: @assessment.tab.category_id,
                                          tab: @assessment.tab_id)
      }
    else
      render json: { errors: @assessment.errors.full_messages.to_sentence }, status: :bad_request
    end
  end

  # Reorder questions for an assessment
  def reorder
    unless valid_ordering?(question_order_ids)
      return render json: {
        errors: I18n.t('course.assessment.assessments.invalid_questions_order')
      }, status: :bad_request
    end

    Course::QuestionAssessment.transaction do
      question_order_ids.each_with_index do |id, index|
        question_assessments_hash[id].update!(weight: index)
      end
    end

    head :ok
  rescue StandardError
    head :bad_request
  end

  def authenticate
    if assessment_not_started(@assessment.time_for(current_course_user)) ||
       authentication_service.authenticate(params.require(:assessment).permit(:password)[:password])
      render json: { redirectUrl: course_assessment_path(current_course, @assessment) }
    else
      render json: { errors: @assessment.errors }, status: :bad_request
    end
  end

  def remind
    authorize!(:manage, @assessment)
    return head :bad_request unless course_user_ids

    Course::Assessment::ReminderService.
      send_closing_reminder(@assessment, course_user_ids.pluck(:id), include_unsubscribed: true)
    head :ok
  end

  def requirements
    requirements = @assessment.specific_conditions.filter_map do |condition|
      condition.title unless current_course_user.present? && condition.satisfied_by?(current_course_user)
    end

    render json: requirements
  end

  # This endpoint provides the view. The actual data is fetched client-side from the statistics module.
  def statistics
    authorize!(:read_statistics, current_course)
  end

  protected

  def load_assessment_options
    return super if skip_tab_filter?

    { through: :tab }
  end

  private

  def question_order_ids
    @order_from_user ||= begin
      integer_type = ActiveModel::Type::Integer.new
      params.require(:question_order).map { |id| integer_type.cast(id) }
    end
  end

  def assessment_params
    base_params = [:title, :description, :base_exp, :time_bonus_exp, :start_at, :end_at, :tab_id,
                   :bonus_end_at, :published, :autograded, :show_mcq_mrq_solution, :show_private,
                   :show_evaluation, :use_public, :use_private, :use_evaluation, :has_personal_times,
                   :affects_personal_times, :block_student_viewing_after_submitted, :has_todo,
                   :allow_record_draft_answer]
    base_params += if autograded?
                     [:skippable, :allow_partial_submission, :show_mcq_answer]
                   else
                     [:view_password, :session_password, :tabbed_view, :delayed_grade_publication]
                   end
    params.require(:assessment).permit(*base_params, folder_params)
  end

  def autograded_params
    params.require(:assessment).permit(:autograded)
  end

  # Randomized Assessment is temporarily hidden (PR#5406)
  # def randomization_params
  #   params.require(:assessment).permit(:randomization)
  # end

  # Infer the autograded state from @assessment or params.
  def autograded?
    if @assessment&.autograded
      true
    elsif @assessment && @assessment.autograded == false
      false
    else
      params[:assessment] && params[:assessment][:autograded]
    end
  end

  # Merges the parameters for category and tab IDs from either the assessment parameter or the
  # query string.
  def tab_params
    params.permit(:category, :tab, assessment: [:category, :tab]).tap do |tab_params|
      tab_params.merge!(tab_params.delete(:assessment)) if tab_params.key?(:assessment)
    end
  end

  # Checks to see if the assessment resource requires should be filtered by tab and category.
  #
  # Currently only index, new, and create actions require filtering.
  def skip_tab_filter?
    !['index', 'new', 'create'].include?(params[:action])
  end

  def tab
    @tab ||=
      if skip_tab_filter?
        super
      elsif tab_params[:tab]
        category.tabs.find(tab_params[:tab])
      else
        category.tabs.first!
      end
  end

  def category
    @category ||=
      if skip_tab_filter?
        super
      elsif tab_params[:category]
        current_course.assessment_categories.find(tab_params[:category])
      else
        current_course.assessment_categories.first!
      end
  end

  def load_question_duplication_data
    @question_duplication_dropdown_data = ordered_assessments_by_tab
  end

  # Maps question ids to their respective questions
  #
  # @return [Hash{Integer => Course::QuestionAssessment}]
  def question_assessments_hash
    @question_assessments_hash ||= @assessment.question_assessments.to_h do |qa|
      [qa.id, qa]
    end
  end

  # Checks if a proposed question ordering is valid
  #
  # @param [Array<Integer>] proposed_ordering
  # @return [Boolean]
  def valid_ordering?(proposed_ordering)
    question_assessments_hash.keys.sort == proposed_ordering.sort
  end

  # Mapping of `tab_id`s to their compound titles. If the tab is the only one in its category,
  # the category title is used. Otherwise, the category is prepended to the tab title.
  #
  # @return [Hash{Integer => String}]
  def compound_tab_titles
    @compound_tab_titles ||= begin
      category_titles = current_course.assessment_categories.pluck(:id, :title).to_h
      current_course.assessment_tabs.pluck(:id, :category_id, :title).
        group_by { |_, category_id, _| category_id }.
        flat_map do |category_id, tabs|
          category_title = category_titles[category_id]
          tabs.map do |id, _, title|
            [id, tabs.length > 1 ? "#{category_title} - #{title}" : category_title]
          end
        end.to_h
    end
  end

  # Data used to populate the 'duplicate question' downdown.
  # The assessments are sectioned by tabs and ordered by date and time.
  #
  # @return [Array<Hash{title: String, assessments: Array}>] Array containing one hash per tab.
  def ordered_assessments_by_tab
    tabs = current_course.assessments.ordered_by_date_and_title.
           pluck(:id, :tab_id, 'course_lesson_plan_items.title').
           group_by { |_, tab_id, _| tab_id }.
           map do |tab_id, assessments|
             {
               title: compound_tab_titles[tab_id],
               assessments: assessments.map { |id, _, title| { id: id, title: title } }
             }
           end
    tabs.sort_by { |tab_hash| tab_hash[:title] }
  end

  def course_user_ids
    case params[:course_users]
    when COURSE_USERS[:my_students]
      current_course_user.my_students.without_phantom_users
    when COURSE_USERS[:my_students_w_phantom]
      current_course_user.my_students
    when COURSE_USERS[:students_w_phantom]
      @assessment.course.course_users.students
    when COURSE_USERS[:students]
      @assessment.course.course_users.students.without_phantom_users
    else
      false
    end
  end

  def can_access_assessment?
    return true unless @assessment.view_password_protected?

    can?(:access, @assessment) || can?(:manage, @assessment)
  end

  def authentication_service
    @authentication_service ||= Course::Assessment::AuthenticationService.new(@assessment, session)
  end

  def submissions
    @submissions ||=
      if @assessment.submissions.loaded?
        @assessment.submissions.select { |s| s.creator_id == current_user.id }
      else
        @assessment.submissions.where(creator_id: current_user.id)
      end
  end

  alias_method :load_submissions, :submissions
end