expertiza/expertiza

View on GitHub
app/models/review_assignment.rb

Summary

Maintainability
A
1 hr
Test Coverage
F
21%
module ReviewAssignment
  def contributors
    # ACS Contributors are just teams, so removed check to see if it is a team assignment
    @contributors ||= teams # ACS
  end

  # Returns a set of topics that can be reviewed.
  # We choose the topics if one of its submissions has received the fewest reviews so far
  # reviewer, the parameter, is an object of Participant
  def candidate_topics_to_review(reviewer)
    return nil if sign_up_topics.empty? # This is not a topic assignment

    # Initialize contributor set with all teams participating in this assignment
    contributor_set = Array.new(contributors)

    # Reject contributors that have not selected a topic, or have no submissions
    contributor_set = reject_by_no_topic_selection_or_no_submission(contributor_set)

    # Reject contributions of topics whose deadline has passed, or which are not reviewable in the current stage
    contributor_set = reject_by_deadline(contributor_set)

    # Filter submissions already reviewed by reviewer
    contributor_set = reject_previously_reviewed_submissions(contributor_set, reviewer)

    # Filter submission by reviewer him/her self
    contributor_set = reject_own_submission(contributor_set, reviewer)

    # Filter the contributors with the least number of reviews
    # (using the fact that each contributor is associated with a topic)
    contributor_set = reject_by_least_reviewed(contributor_set)

    contributor_set = reject_by_max_reviews_per_submission(contributor_set)

    # if this assignment does not allow reviewer to review other artifacts on the same topic,
    # remove those teams from candidate list.
    contributor_set = reject_by_same_topic(contributor_set, reviewer) unless can_review_same_topic?

    # Add topics for all remaining submissions to a list of available topics for review
    candidate_topics = Set.new
    contributor_set.each do |contributor|
      candidate_topics.add(signed_up_topic(contributor))
    end
    candidate_topics
  end

  def signed_up_topic(team)
    # The purpose is to return the topic that the contributor has signed up to do for this assignment.
    # Returns a record from the sign_up_topic table that gives the topic_id for which the contributor has signed up
    # Look for the topic_id where the team_id equals the contributor id (contributor is a team or a participant)

    # If this is an assignment with quiz required
    if require_quiz?
      sign_ups = SignedUpTeam.where(team_id: team.id)
      sign_ups.each do |sign_up|
        sign_up_topic = SignUpTopic.find(sign_up.topic_id)
        if sign_up_topic.assignment_id == id
          contributors_sign_up_topic = sign_up_topic
          return contributors_sign_up_topic
        end
      end
    end

    # Look for the topic_id where the team_id equals the contributor id (contributor is a team)
    return if SignedUpTeam.where(team_id: team.id, is_waitlisted: 0).empty?

    topic_id = SignedUpTeam.find_by(team_id: team.id, is_waitlisted: 0).topic_id
    SignUpTopic.find(topic_id)
  end

  def assign_reviewer_dynamically(reviewer, topic)
    # The following method raises an exception if not successful which
    # has to be captured by the caller (in review_mapping_controller)
    contributor = contributor_to_review(reviewer, topic)
    contributor.assign_reviewer(reviewer)
  end

  # This method is only for the assignments without topics
  def candidate_assignment_teams_to_review(reviewer)
    # the contributors are AssignmentTeam objects
    contributor_set = Array.new(contributors)

    # Reject contributors that have no submissions
    contributor_set.select!(&:has_submissions?)

    # Filter submissions already reviewed by reviewer
    contributor_set = reject_previously_reviewed_submissions(contributor_set, reviewer)

    # Filter submission by reviewer him/her self
    contributor_set = reject_own_submission(contributor_set, reviewer)

    # Filter the contributors with the least number of reviews
    contributor_set = reject_by_least_reviewed(contributor_set)

    contributor_set = reject_by_max_reviews_per_submission(contributor_set)

    contributor_set
  end

  # assign the reviewer to review the assignment_team's submission. Only used in the assignments that do not have any topic
  # Parameter assignment_team is the candidate assignment team, it cannot be a team w/o submission, or have reviewed by reviewer, or reviewer's own team.
  # (guaranteed by candidate_assignment_teams_to_review method)
  def assign_reviewer_dynamically_no_topic(reviewer, assignment_team)
    raise 'There are no more submissions available for that review right now. Try again later.' if assignment_team.nil?

    assignment_team.assign_reviewer(reviewer)
  end

  private

  def reject_by_least_reviewed(contributor_set)
    contributor = contributor_set.min_by { |contributor_item| contributor_item.review_mappings.reject { |review_mapping| review_mapping.response.nil? }.count }
    min_reviews = begin
                    contributor.review_mappings.reject { |review_mapping| review_mapping.response.nil? }.count
                  rescue StandardError
                    0
                  end
    contributor_set.reject! { |contributor_item| contributor_item.review_mappings.reject { |review_mapping| review_mapping.response.nil? }.count > min_reviews + review_topic_threshold }
    contributor_set
  end

  def reject_by_max_reviews_per_submission(contributor_set)
    contributor_set.reject! { |contributor| contributor.responses.select(&:is_submitted).count >= max_reviews_per_submission }
    contributor_set
  end

  def reject_by_same_topic(contributor_set, reviewer)
    reviewer_team = AssignmentTeam.team(reviewer)
    # it is possible that this reviewer does not have a team, if so, do nothing
    if reviewer_team
      topic_id = reviewer_team.topic
      # it is also possible that this reviewer has team, but this team has no topic yet, if so, do nothing
      contributor_set = contributor_set.reject { |contributor| contributor.topic == topic_id } if topic_id
    end

    contributor_set
  end

  def reject_previously_reviewed_submissions(contributor_set, reviewer)
    contributor_set = contributor_set.reject { |contributor| contributor.reviewed_by?(reviewer) }
    contributor_set
  end

  def reject_own_submission(contributor_set, reviewer)
    contributor_set.reject! { |contributor| contributor.user?(User.find(reviewer.user_id)) }
    contributor_set
  end

  def reject_by_deadline(contributor_set)
    contributor_set.reject! do |contributor|
      (contributor.assignment.current_stage(signed_up_topic(contributor).id) == 'Complete') ||
        !contributor.assignment.can_review(signed_up_topic(contributor).id)
    end
    contributor_set
  end

  def reject_by_no_topic_selection_or_no_submission(contributor_set)
    contributor_set.reject! { |contributor| signed_up_topic(contributor).nil? || !contributor.has_submissions? }
    contributor_set
  end

  # Returns a contributor to review if available, otherwise will raise an error
  def contributor_to_review(reviewer, topic)
    raise 'Please select a topic' if topics? && topic.nil?
    raise 'This assignment does not have topics' if !topics? && topic
    # This condition might happen if the reviewer waited too much time in the
    # select topic page and other students have already selected this topic.
    # Another scenario is someone that deliberately modifies the view.
    raise 'This topic has too many reviews; please select another one.' if topic && !candidate_topics_to_review(reviewer).include?(topic)

    contributor_set = Array.new(contributors)
    work = topic.nil? ? 'assignment' : 'topic'

    # 1) Only consider contributors that worked on this topic; 2) remove reviewer as contributor
    # 3) remove contributors that have not submitted work yet
    contributor_set.reject! do |contributor|
      signed_up_topic(contributor) != topic || # both will be nil for assignments with no signup sheet
        contributor.includes?(reviewer) ||
        !contributor.has_submissions?
    end

    raise "There are no more submissions to review on this #{work}." if contributor_set.empty?

    # Reviewer can review each contributor only once
    contributor_set.reject! { |contributor| contributor.reviewed_by?(reviewer) }
    raise "You have already reviewed all submissions for this #{work}." if contributor_set.empty?

    # Reduce to the contributors with the least number of reviews ("responses") received
    min_contributor = contributor_set.min_by { |a| a.responses.count }
    min_reviews = min_contributor.responses.count
    contributor_set.reject! { |contributor| contributor.responses.count > min_reviews }

    # Pick the contributor whose most recent reviewer was assigned longest ago
    contributor_set.sort! { |a, b| a.review_mappings.last.id <=> b.review_mappings.last.id } if min_reviews > 0

    # Choose a contributor at random (.sample) from the remaining contributors.
    # Actually, we SHOULD pick the contributor who was least recently picked.  But sample
    # is much simpler, and probably almost as good, given that even if the contributors are
    # picked in round-robin fashion, the reviews will not be submitted in the same order that
    # they were picked.
    contributor_set.sample
  end
end