<commit_before>class Result < ApplicationRecord

  MARKING_STATES = {
    complete: 'complete',
    incomplete: 'incomplete'
  }

  belongs_to :submission
  has_many :marks
  has_many :extra_marks
  has_many :annotations
  has_many :peer_reviews

  after_create :create_marks
  validates_presence_of :marking_state
  validates_inclusion_of :marking_state, in: MARKING_STATES.values

  validates_numericality_of :total_mark, greater_than_or_equal_to: 0

  before_update :unrelease_partial_results
  before_save :check_for_nil_marks

  scope :submitted_remarks_and_all_non_remarks, lambda {
    results = Result.arel_table
    where(results[:remark_request_submitted_at].eq(nil))
  }

  # Returns a list of total marks for each student whose submissions are graded
  # for the assignment specified by +assignment_id+, sorted in ascending order.
  # This includes duplicated marks for each student in the same group (marks
  # are given for a group, so each student in the same group gets the same
  # mark).
  def self.student_marks_by_assignment(assignment_id)
    # Need to get a list of total marks of students' latest results (i.e., not
    # including old results after having remarked results). This is a typical
    # greatest-n-per-group problem and can be implemented using a subquery
    # join.
    subquery = Result.select('max(results.id) max_id')
      .joins(submission: {grouping: {student_memberships: :user}})
      .where(groupings: {assignment_id: assignment_id},
             users: {hidden: false},
             submissions: {submission_version_used: true},
             marking_state: Result::MARKING_STATES[:complete])
      .group('users.id')
    Result.joins("JOIN (#{subquery.to_sql}) s ON id = s.max_id")
      .order(:total_mark).pluck(:total_mark)
  end

  # Update the total mark attribute
  #
  # If the +assignment+ value is nil, the assignment will be determined dynamically.
  # However, passing the +assignment+ value explicitly is more efficient if we are
  # updating multiple total marks for a single assignment since it allows for
  # caching of criteria.
  # Warning: this does not check if the +assignment+ passed as an argument is actually
  # the one associate with this result.
  def update_total_mark(assignment: nil)
    update_attributes(total_mark: get_total_mark(assignment: assignment))
  end

  # Calculate the total mark for this submission
  #
  # See the documentation for update_total_mark for information about when to explicitly
  # pass the +assignment+ variable and associated warnings.
  def get_total_mark(assignment: nil)
    user_visibility = is_a_review? ? :peer : :ta
    subtotal = get_subtotal(user_visibility: user_visibility, assignment: assignment)
    extra_marks = get_total_extra_marks(user_visibility: user_visibility, assignment: assignment)
    [0, subtotal + extra_marks].max
  end

  # The sum of the marks not including bonuses/deductions
  #
  # See the documentation for update_total_mark for information about when to explicitly
  # pass the +assignment+ variable and associated warnings.
  def get_subtotal(user_visibility: :ta, assignment: nil)
    if marks.empty?
      0
    else
      assignment ||= submission.grouping.assignment
      criteria = assignment.get_criteria(user_visibility).map { |c| [c.class.to_s, c.id] }
      marks_array = (marks.to_a.select { |m| criteria.member? [m.markable_type, m.markable_id] }).map &:mark
      # TODO: sum method does not work with empty arrays. Consider updating/replacing gem:
      #       see: https://github.com/thirtysixthspan/descriptive_statistics/issues/44
      marks_array.empty? ? 0 : marks_array.sum
    end
  end

  # The sum of the bonuses deductions and late penalties
  #
  # See the documentation for update_total_mark for information about when to explicitly
  # pass the +assignment+ variable and associated warnings.
  def get_total_extra_marks(user_visibility: :ta, assignment: nil)
    extras = extra_marks.pluck_to_hash(:extra_mark, :unit).group_by { |d| d['unit'] }
    extras&.transform_values! { |a| a.map { |h| h['extra_mark'] } }
    points = (extras['points']&.sum || 0).round(1)
    percs = (extras['percentage']&.sum || 0).round(1)
    assignment ||= submission.assignment
    perc_points = (percs * assignment.max_mark(user_visibility) / 100).round(1)
    points + perc_points
  end

  # The sum of the bonuses and deductions, other than late penalty
  def get_total_extra_points<commit_msg>rails5: count nil marks as 0 when calculating sum<commit_after>class Result < ApplicationRecord

  MARKING_STATES = {
    complete: 'complete',
    incomplete: 'incomplete'
  }

  belongs_to :submission
  has_many :marks
  has_many :extra_marks
  has_many :annotations
  has_many :peer_reviews

  after_create :create_marks
  validates_presence_of :marking_state
  validates_inclusion_of :marking_state, in: MARKING_STATES.values

  validates_numericality_of :total_mark, greater_than_or_equal_to: 0

  before_update :unrelease_partial_results
  before_save :check_for_nil_marks

  scope :submitted_remarks_and_all_non_remarks, lambda {
    results = Result.arel_table
    where(results[:remark_request_submitted_at].eq(nil))
  }

  # Returns a list of total marks for each student whose submissions are graded
  # for the assignment specified by +assignment_id+, sorted in ascending order.
  # This includes duplicated marks for each student in the same group (marks
  # are given for a group, so each student in the same group gets the same
  # mark).
  def self.student_marks_by_assignment(assignment_id)
    # Need to get a list of total marks of students' latest results (i.e., not
    # including old results after having remarked results). This is a typical
    # greatest-n-per-group problem and can be implemented using a subquery
    # join.
    subquery = Result.select('max(results.id) max_id')
      .joins(submission: {grouping: {student_memberships: :user}})
      .where(groupings: {assignment_id: assignment_id},
             users: {hidden: false},
             submissions: {submission_version_used: true},
             marking_state: Result::MARKING_STATES[:complete])
      .group('users.id')
    Result.joins("JOIN (#{subquery.to_sql}) s ON id = s.max_id")
      .order(:total_mark).pluck(:total_mark)
  end

  # Update the total mark attribute
  #
  # If the +assignment+ value is nil, the assignment will be determined dynamically.
  # However, passing the +assignment+ value explicitly is more efficient if we are
  # updating multiple total marks for a single assignment since it allows for
  # caching of criteria.
  # Warning: this does not check if the +assignment+ passed as an argument is actually
  # the one associate with this result.
  def update_total_mark(assignment: nil)
    update_attributes(total_mark: get_total_mark(assignment: assignment))
  end

  # Calculate the total mark for this submission
  #
  # See the documentation for update_total_mark for information about when to explicitly
  # pass the +assignment+ variable and associated warnings.
  def get_total_mark(assignment: nil)
    user_visibility = is_a_review? ? :peer : :ta
    subtotal = get_subtotal(user_visibility: user_visibility, assignment: assignment)
    extra_marks = get_total_extra_marks(user_visibility: user_visibility, assignment: assignment)
    [0, subtotal + extra_marks].max
  end

  # The sum of the marks not including bonuses/deductions
  #
  # See the documentation for update_total_mark for information about when to explicitly
  # pass the +assignment+ variable and associated warnings.
  def get_subtotal(user_visibility: :ta, assignment: nil)
    if marks.empty?
      0
    else
      assignment ||= submission.grouping.assignment
      criteria = assignment.get_criteria(user_visibility).map { |c| [c.class.to_s, c.id] }
      marks_array = (marks.to_a.select { |m| criteria.member? [m.markable_type, m.markable_id] }).map &:mark
      # TODO: sum method does not work with empty arrays or with arrays containing nil values.
      #       Consider updating/replacing gem:
      #       see: https://github.com/thirtysixthspan/descriptive_statistics/issues/44
      marks_array.map! { |m| m ? m : 0 }
      marks_array.empty? ? 0 : marks_array.sum
    end
  end

  # The sum of the bonuses deductions and late penalties
  #
  # See the documentation for update_total_mark for information about when to explicitly
  # pass the +assignment+ variable and associated warnings.
  def get_total_extra_marks(user_visibility: :ta, assignment: nil)
    extras = extra_marks.pluck_to_hash(:extra_mark, :unit).group_by { |d| d['unit'] }
    extras&.transform_values! { |a| a.map { |h| h['extra_mark'] } }
    points = (extras['points']&.sum || 0).round(1)
    percs = (extras['percentage']&.sum || 0).round(1)
    assignment ||= submission.assignment
    perc_points = (percs * assignment.max_mark(user_visibility) / 100).round(1)
    points + perc_points
  end

  # The sum of the bonuses and deductions, other than late penalty
  def get_total_extra_points
{'fraction_non_alphanumeric': 0.08390524684525127, 'fraction_numerical': 0.0034314810715076376, 'mean_word_length': 4.025027808676307, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 3, 'https://': 2, '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': '3700614', 'n_tokens_mistral': 2769, 'n_tokens_neox': 2670, 'n_words': 840}