class Assignment

Constants

MARKING_SCHEME_TYPE

Public Instance Methods

add_csv_group(row) click to toggle source

Add a group and corresponding grouping as provided in the passed in Array. Format: [ groupname, repo_name, member, member, etc ] Any member names that do not exist in the database will simply be ignored (This makes it possible to have empty groups created from a bad csv row)

# File app/models/assignment.rb, line 442
def add_csv_group(row)
  return if row.length == 0

  # Note: We cannot use find_or_create_by here, because it has its own
  # save semantics. We need to set and save attributes in a very particular
  # order, so that everything works the way we want it to.
  group = Group.find_by_group_name(row[0])
  if group.nil?
    group = Group.new
    group.group_name = row[0]
  end

  # Since repo_name of "group" will be set before the first save call, the
  # set repo_name will be used instead of the autogenerated name. See
  # set_repo_name and build_repository in the groups model. Also, see
  # create_group_for_working_alone_student in the students model for
  # similar semantics.
  if is_candidate_for_setting_custom_repo_name?(row)
    # Do this only if user_name exists and is a student.
    if Student.find_by_user_name(row[2])
      group.repo_name = row[0]
    else
      # Student name does not exist, use provided repo_name
      group.repo_name = row[1].strip # remove whitespace
    end
  end

  # If we are not repository admin, set the repository name as provided
  # in the csv upload file
  unless group.repository_admin?
    group.repo_name = row[1].strip # remove whitespace
  end
  # Note: after_create hook build_repository might raise
  # Repository::RepositoryCollision. If it does, it adds the colliding
  # repo_name to errors.on_base. This is how we can detect repo
  # collisions here. Unfortunately, we can't raise an exception
  # here, because we still want the grouping to be created. This really
  # shouldn't happen anyway, because the lookup earlier should prevent
  # repo collisions e.g. when uploading the same CSV file twice.
  group.save
  unless group.errors[:base].blank?
    collision_error = I18n.t('csv.repo_collision_warning',
                        { :repo_name => group.errors.on_base,
                          :group_name => row[0] })
  end

  # Create a new Grouping for this assignment and the newly
  # crafted group
  grouping = Grouping.new(:assignment => self, :group => group)
  grouping.save

  # Form groups
  start_index_group_members = 2 # first field is the group-name, second the repo name, so start at field 3
  (start_index_group_members..(row.length - 1)).each do |i|
    student = Student.find_by_user_name(row[i].strip) # remove whitespace
    if student
      if grouping.student_membership_number == 0
        # Add first valid member as inviter to group.
        grouping.group_id = group.id
        grouping.save # grouping has to be saved, before we can add members
        grouping.add_member(student, StudentMembership::STATUSES[:inviter])
      else
        grouping.add_member(student)
      end
    end

  end
  collision_error
end
add_group(new_group_name=nil) click to toggle source
# File app/models/assignment.rb, line 355
def add_group(new_group_name=nil)
  if self.group_name_autogenerated
    group = Group.new
    group.save(:validate => false)
    group.group_name = group.get_autogenerated_group_name
    group.save
  else
    return nil if new_group_name.nil?
    if Group.first(:conditions => {:group_name => new_group_name})
      group = Group.first(:conditions => {:group_name => new_group_name})
      unless self.groupings.find_by_group_id(group.id).nil?
        raise "Group #{new_group_name} already exists"
      end
    else
      group = Group.new
      group.group_name = new_group_name
      group.save
    end
  end
  grouping = Grouping.new
  grouping.group = group
  grouping.assignment = self
  grouping.save
  grouping
end
assigned_groupings() click to toggle source
# File app/models/assignment.rb, line 550
def assigned_groupings
  groupings.all(:joins => :ta_memberships, :include => [{:ta_memberships => :user}]).uniq
end
clone_groupings_from(assignment_id) click to toggle source

Clones the Groupings from the assignment with id assignment_id into self. Destroys any previously existing Groupings associated with this Assignment

# File app/models/assignment.rb, line 396
def clone_groupings_from(assignment_id)
  original_assignment = Assignment.find(assignment_id)
  self.transaction do
    self.group_min = original_assignment.group_min
    self.group_max = original_assignment.group_max
    self.student_form_groups = original_assignment.student_form_groups
    self.group_name_autogenerated = original_assignment.group_name_autogenerated
    self.group_name_displayed = original_assignment.group_name_displayed
    self.groupings.destroy_all
    self.save
    self.reload
    original_assignment.groupings.each do |g|
      unhidden_student_memberships = g.accepted_student_memberships.select do |m|
        !m.user.hidden
      end
      unhidden_ta_memberships = g.ta_memberships.select do |m|
        !m.user.hidden
      end
      #create the memberships for any user that is not hidden
      unless unhidden_student_memberships.empty?
        #create the groupings
        grouping = Grouping.new
        grouping.group_id = g.group_id
        grouping.assignment_id = self.id
        grouping.admin_approved = g.admin_approved
        raise 'Could not save grouping' if !grouping.save
        all_memberships = unhidden_student_memberships + unhidden_ta_memberships
        all_memberships.each do |m|
          membership = Membership.new
          membership.user_id = m.user_id
          membership.type = m.type
          membership.membership_status = m.membership_status
          raise 'Could not save membership' if !(grouping.memberships << membership)
        end
        # Ensure all student members have permissions on their group repositories
        grouping.update_repository_permissions
      end
    end
  end
end
create_groupings_when_students_work_alone() click to toggle source

Create all the groupings for an assignment where students don’t work in groups.

# File app/models/assignment.rb, line 384
def create_groupings_when_students_work_alone
   @students = Student.all
   for student in @students do
     unless student.has_accepted_grouping_for?(self.id)
      student.create_group_for_working_alone_student(self.id)
     end
   end
end
criteria_count() click to toggle source
# File app/models/assignment.rb, line 738
def criteria_count
  if self.marking_scheme_type == 'rubric'
    self.rubric_criteria.size
  else
    self.flexible_criteria.size
  end
end
display_for_note() click to toggle source
# File app/models/assignment.rb, line 267
def display_for_note
  short_identifier
end
export_rubric_criteria_yml() click to toggle source

Export a YAML formatted string created from the assignment rubric criteria.

# File app/models/assignment.rb, line 89
def export_rubric_criteria_yml
  criteria = self.rubric_criteria
  final = ActiveSupport::OrderedHash.new
  criteria.each do |criterion|
    inner = ActiveSupport::OrderedHash.new
    inner['weight'] =  criterion['weight']
    inner['level_0'] = {
      'name' =>  criterion['level_0_name'] ,
      'description' =>  criterion['level_0_description']
    }
    inner['level_1'] = {
      'name' =>  criterion['level_1_name'] ,
      'description' =>  criterion['level_1_description']
    }
    inner['level_2'] = {
      'name' =>  criterion['level_2_name'] ,
      'description' =>  criterion['level_2_description']
    }
    inner['level_3'] = {
      'name' =>  criterion['level_3_name'] ,
      'description' =>  criterion['level_3_description']
    }
    inner['level_4'] = {
      'name' =>  criterion['level_4_name'] ,
      'description' => criterion['level_4_description']
    }
    criteria_yml = {"#{criterion['rubric_criterion_name']}" => inner}
    final = final.merge(criteria_yml)
  end
  final.to_yaml
end
get_criteria() click to toggle source
# File app/models/assignment.rb, line 730
def get_criteria
  if self.marking_scheme_type == 'rubric'
    self.rubric_criteria
  else
    self.flexible_criteria
  end
end
get_detailed_csv_report() click to toggle source

Get a detailed CSV report of marks (includes each criterion) for this assignment. Produces slightly different reports, depending on which criteria type has been used the this assignment.

# File app/models/assignment.rb, line 604
def get_detailed_csv_report
  # which marking scheme do we have?
  if self.marking_scheme_type == MARKING_SCHEME_TYPE[:flexible]
    get_detailed_csv_report_flexible
  else
    # default to rubric
    get_detailed_csv_report_rubric
  end
end
get_detailed_csv_report_flexible() click to toggle source

Get a detailed CSV report of flexible criteria based marks (includes each criterion, with it’s out-of value) for this assignment. Produces CSV rows such as the following:

student_name,95.22222,3,4,2,5,5,4,0/2

Criterion values should be read in pairs. I.e. 2,3 means 2 out-of 3. Last column are grace-credits.

# File app/models/assignment.rb, line 669
def get_detailed_csv_report_flexible
  out_of = self.total_mark
  students = Student.all
  flexible_criteria = self.flexible_criteria
  CsvHelper::Csv.generate do |csv|
    students.each do |student|
      final_result = []
      final_result.push(student.user_name)
      grouping = student.accepted_grouping_for(self.id)
      if grouping.nil? || !grouping.has_submission?
        # No grouping/no submission
        final_result.push('')                 # total percentage
        flexible_criteria.each do |criterion| ##  empty criteria
          final_result.push('')               # mark
          final_result.push(criterion.max)    # out-of
        end
        final_result.push('')                 # extra-marks
        final_result.push('')                 # extra-percentage
      else
        # Fill in actual values, since we have a grouping
        # and a submission.
        submission = grouping.current_submission_used
        final_result.push(submission.get_latest_result.total_mark / out_of * 100)
        flexible_criteria.each do |criterion|
          mark = submission.get_latest_result.marks.find_by_markable_id_and_markable_type(criterion.id, 'FlexibleCriterion')
          if mark.nil?
            final_result.push('')
          else
            final_result.push(mark.mark || '')
          end
          final_result.push(criterion.max)
        end
        final_result.push(submission.get_latest_result.get_total_extra_points)
        final_result.push(submission.get_latest_result.get_total_extra_percentage)
      end
      # push grace credits info
      grace_credits_data = student.remaining_grace_credits.to_s + '/' + student.grace_credits.to_s
      final_result.push(grace_credits_data)

      csv << final_result
    end
  end
end
get_detailed_csv_report_rubric() click to toggle source

Get a detailed CSV report of rubric based marks (includes each criterion) for this assignment. Produces CSV rows such as the following:

student_name,95.22222,3,4,2,5,5,4,0/2

Criterion values should be read in pairs. I.e. 2,3 means a student scored 2 for a criterion with weight 3. Last column are grace-credits.

# File app/models/assignment.rb, line 621
def get_detailed_csv_report_rubric
  out_of = self.total_mark
  students = Student.all
  rubric_criteria = self.rubric_criteria
  CsvHelper::Csv.generate do |csv|
    students.each do |student|
      final_result = []
      final_result.push(student.user_name)
      grouping = student.accepted_grouping_for(self.id)
      if grouping.nil? || !grouping.has_submission?
        # No grouping/no submission
        final_result.push('')                         # total percentage
        rubric_criteria.each do |rubric_criterion|
          final_result.push('')                       # mark
          final_result.push(rubric_criterion.weight)  # weight
        end
        final_result.push('')                         # extra-mark
        final_result.push('')                         # extra-percentage
      else
        submission = grouping.current_submission_used
        final_result.push(submission.get_latest_result.total_mark / out_of * 100)
        rubric_criteria.each do |rubric_criterion|
          mark = submission.get_latest_result.marks.find_by_markable_id_and_markable_type(rubric_criterion.id, 'RubricCriterion')
          if mark.nil?
            final_result.push('')
          else
            final_result.push(mark.mark || '')
          end
          final_result.push(rubric_criterion.weight)
        end
        final_result.push(submission.get_latest_result.get_total_extra_points)
        final_result.push(submission.get_latest_result.get_total_extra_percentage)
      end
      # push grace credits info
      grace_credits_data = student.remaining_grace_credits.to_s + '/' + student.grace_credits.to_s
      final_result.push(grace_credits_data)

      csv << final_result
    end
  end
end
get_simple_csv_report() click to toggle source

Get a simple CSV report of marks for this assignment

# File app/models/assignment.rb, line 582
def get_simple_csv_report
  students = Student.all
  out_of = self.total_mark
  CsvHelper::Csv.generate do |csv|
     students.each do |student|
       final_result = []
       final_result.push(student.user_name)
       grouping = student.accepted_grouping_for(self.id)
       if grouping.nil? || !grouping.has_submission?
         final_result.push('')
       else
         submission = grouping.current_submission_used
         final_result.push(submission.get_latest_result.total_mark / out_of * 100)
       end
       csv << final_result
     end
  end
end
get_svn_export_commands() click to toggle source

Get a list of subversion client commands to be used for scripting

# File app/models/assignment.rb, line 559
def get_svn_export_commands
  svn_commands = [] # the commands to be exported

  self.groupings.each do |grouping|
    submission = grouping.current_submission_used
    if submission
      svn_commands.push("svn export -r #{submission.revision_number} #{grouping.group.repository_external_access_url}/#{self.repository_folder} \"#{grouping.group.group_name}\"")
    end
  end
  svn_commands
end
get_svn_repo_list() click to toggle source

Get a list of group_name, repo-url pairs

# File app/models/assignment.rb, line 572
def get_svn_repo_list
  CsvHelper::Csv.generate do |csv|
    self.groupings.each do |grouping|
      group = grouping.group
      csv << [group.group_name,group.repository_external_access_url]
    end
  end
end
grade_distribution_as_percentage(intervals=20) click to toggle source

Returns an array with the number of groupings who scored between certain percentage ranges [0-5%, 6-10%, …] intervals defaults to 20

# File app/models/assignment.rb, line 749
def grade_distribution_as_percentage(intervals=20)
  distribution = Array.new(intervals, 0)
  out_of = self.total_mark

  if out_of == 0
    return distribution
  end

  steps = 100 / intervals # number of percentage steps in each interval
  groupings = self.groupings.all(:include => [{:current_submission_used => :results}])

  groupings.each do |grouping|
    submission = grouping.current_submission_used
    if submission && submission.has_result?
      result = submission.get_latest_completed_result
      unless result.nil?
        percentage = (result.total_mark / out_of * 100).ceil
        if percentage == 0
          distribution[0] += 1
        elsif percentage >= 100
          distribution[intervals - 1] += 1
        elsif (percentage % steps) == 0
          distribution[percentage / steps - 1] += 1
        else
          distribution[percentage / steps] += 1
        end
      end
    end
  end # end of groupings loop

  distribution
end
graded_submissions() click to toggle source

Returns all the submissions that have been graded (completed)

# File app/models/assignment.rb, line 789
def graded_submissions
  results = []
  groupings.each do |grouping|
    if grouping.marking_completed?
      submission = grouping.current_submission_used
      results.push(submission.get_latest_result) unless submission.nil?
    end
  end
  results
end
group_assignment?() click to toggle source

Return true if this is a group assignment; false otherwise

# File app/models/assignment.rb, line 234
def group_assignment?
  invalid_override || group_min != 1 || group_max > 1
end
group_by(uid, pending=false) click to toggle source

Returns the group by the user for this assignment. If pending=true, it will return the group that the user has a pending invitation to. Returns nil if user does not have a group for this assignment, or if it is not a group assignment

# File app/models/assignment.rb, line 242
def group_by(uid, pending=false)
  return nil unless group_assignment?

  # condition = "memberships.user_id = ?"
  # condition += " and memberships.status != 'rejected'"
  # add non-pending status clause to condition
  # condition += " and memberships.status != 'pending'" unless pending
  # groupings.first(:include => :memberships, :conditions => [condition, uid]) #FIXME: needs schema update

  #FIXME: needs to be rewritten using a proper query...
  User.find(uid).accepted_grouping_for(self.id)
end
grouped_students() click to toggle source
# File app/models/assignment.rb, line 524
def grouped_students
  result_students = []
  student_memberships.each do |student_membership|
    result_students.push(student_membership.user)
  end
  result_students
end
groups_submitted() click to toggle source
# File app/models/assignment.rb, line 800
def groups_submitted
  self.groupings.select { |grouping| grouping.has_submission?}
end
invalid_groupings() click to toggle source
# File app/models/assignment.rb, line 546
def invalid_groupings
  groupings - valid_groupings
end
latest_due_date() click to toggle source

Calculate the latest due date. Used to calculate the collection time

# File app/models/assignment.rb, line 185
def latest_due_date
  if self.section_due_dates_type
    due_date = self.due_date
    self.section_due_dates.each do |d|
      if !d.due_date.nil? && due_date < d.due_date
        due_date = d.due_date
      end
    end
    due_date
  else
    self.due_date
  end
end
minimum_number_of_groups() click to toggle source
# File app/models/assignment.rb, line 121
def minimum_number_of_groups
  if (group_max && group_min) && group_max < group_min
    errors.add(:group_max, 'must be greater than the minimum number of groups')
    false
  end
end
next_criterion_position() click to toggle source
# File app/models/assignment.rb, line 724
def next_criterion_position
  # We're using count here because this fires off a DB query, thus
  # grabbing the most up-to-date count of the rubric criteria.
  self.rubric_criteria.count + 1
end
no_grouping_students_list() click to toggle source

Make a list of students without any groupings

# File app/models/assignment.rb, line 256
def no_grouping_students_list
 @students = Student.all(:order => :last_name, :conditions => {:hidden => false})
 @students_list = []
 @students.each do |s|
   unless s.has_accepted_grouping_for?(self.id)
     @students_list.push(s)
    end
 end
 @students_list
end
past_collection_date?() click to toggle source
# File app/models/assignment.rb, line 199
def past_collection_date?
  Time.zone.now > submission_rule.calculate_collection_time
end
past_due_date?() click to toggle source

Are we past all the due dates for this assignment?

# File app/models/assignment.rb, line 129
def past_due_date?
  # If no section due dates /!\ do not check empty? it could be wrong
  unless self.section_due_dates_type
    return !due_date.nil? && Time.zone.now > due_date
  end

  # If section due dates
  self.section_due_dates.each do |d|
    if !d.due_date.nil? && Time.zone.now > d.due_date
      return true
    end
  end
  false
end
past_remark_due_date?() click to toggle source
# File app/models/assignment.rb, line 203
def past_remark_due_date?
  !remark_due_date.nil? && Time.zone.now > remark_due_date
end
replace_submission_rule(new_submission_rule) click to toggle source
# File app/models/assignment.rb, line 713
def replace_submission_rule(new_submission_rule)
  if self.submission_rule.nil?
    self.submission_rule = new_submission_rule
    self.save
  else
    self.submission_rule.destroy
    self.submission_rule = new_submission_rule
    self.save
  end
end
section_due_date(section) click to toggle source

return the due date for a section

# File app/models/assignment.rb, line 175
def section_due_date(section)
  if self.section_due_dates_type
    unless section.nil?
      return SectionDueDate.due_date_for(section, self)
    end
  end
  self.due_date
end
section_past_due_date?(grouping) click to toggle source

Are we past the due date for this assignment, for this grouping ?

# File app/models/assignment.rb, line 164
def section_past_due_date?(grouping)
  if self.section_due_dates_type and !grouping.inviter.section.nil?
      section_due_date =
  SectionDueDate.due_date_for(grouping.inviter.section, self)
      !section_due_date.nil? && Time.zone.now > section_due_date
  else
    self.past_due_date?
  end
end
set_results_statistics() click to toggle source

calculates summary statistics of released results for this assignment

# File app/models/assignment.rb, line 284
def set_results_statistics
  groupings = Grouping.find_all_by_assignment_id(self.id)
  results = Array.new
  results_count = 0
  results_sum = 0
  results_fails = 0
  results_zeros = 0
  students_count = 0
  groupings.each do |grouping|
    submission = grouping.current_submission_used
    unless submission.nil?
      result = submission.get_latest_result
      if result.released_to_students
        results.push result.total_mark
        results_sum += result.total_mark * grouping.student_membership_number
        results_count += 1
        students_count += grouping.student_membership_number
        if result.total_mark < (self.total_mark / 2)
          results_fails += 1
        end
        if result.total_mark == 0
          results_zeros += 1
        end
      end
    end
  end
  if results_count == 0
    return false # no marks released for this assignment
  end
  self.results_fails = results_fails
  self.results_zeros = results_zeros
  results_sorted = results.sort
  median_quantity = 0
  if (results_count % 2) == 0
     median_quantity = (results_sorted[results_count/2 - 1]
                      + results_sorted[results_count/2]).to_f / 2
  else
     median_quantity = results_sorted[results_count/2]
  end
  self.results_median = (median_quantity * 100 / self.total_mark)
  # Need to avoid divide by zero
  if results_sum == 0
    self.results_average = 0
    return self.save
  end
  avg_quantity = results_sum / students_count
  # compute average in percent
  self.results_average = (avg_quantity * 100 / self.total_mark)
  self.save
end
submission_by(user) click to toggle source

Returns a Submission instance for this user depending on whether this assignment is a group or individual assignment

# File app/models/assignment.rb, line 209
def submission_by(user) #FIXME: needs schema updates

  # submission owner is either an individual (user) or a group
  owner = self.group_assignment? ? self.group_by(user.id) : user
  return nil unless owner

  # create a new submission for the owner
  # linked to this assignment, if it doesn't exist yet

  # submission = owner.submissions.find_or_initialize_by_assignment_id(id)
  # submission.save if submission.new_record?
  # return submission

  assignment_groupings = user.active_groupings.delete_if {|grouping|
    grouping.assignment.id != self.id
  }

  if assignment_groupings.empty?
    nil
  else
    assignment_groupings.first.submissions.first
  end
end
tas() click to toggle source

Returns all the TAs associated with the assignment

# File app/models/assignment.rb, line 783
def tas
  ids = self.ta_memberships.map { |m| m.user_id }
  Ta.find(ids)
end
total_criteria_weight() click to toggle source
# File app/models/assignment.rb, line 350
def total_criteria_weight
  factor = 10.0 ** 2
  (rubric_criteria.sum('weight') * factor).floor / factor
end
total_mark() click to toggle source
# File app/models/assignment.rb, line 271
def total_mark
  total = 0
  if self.marking_scheme_type == 'rubric'
    rubric_criteria.each do |criterion|
      total = total + criterion.weight * 4
    end
  else
    total = flexible_criteria.sum('max')
  end
  total
end
unassigned_groupings() click to toggle source
# File app/models/assignment.rb, line 554
def unassigned_groupings
  groupings - assigned_groupings
end
ungrouped_students() click to toggle source
# File app/models/assignment.rb, line 532
def ungrouped_students
  Student.all(:conditions => {:hidden => false}) - grouped_students
end
update_remark_request_count() click to toggle source
# File app/models/assignment.rb, line 335
def update_remark_request_count
  outstanding_count = 0
  groupings.each do |grouping|
    submission = grouping.current_submission_used
    if !submission.nil? && submission.has_remark?
      if submission.get_remark_result.marking_state ==
          Result::MARKING_STATES[:partial]
        outstanding_count += 1
      end
    end
  end
  self.outstanding_remark_request_count = outstanding_count
  self.save
end
update_repository_permissions_forall_groupings() click to toggle source

Updates repository permissions for all groupings of an assignment. This is a handy method, if for example grouping creation/deletion gets rolled back. The rollback does not reestablish proper repository permissions.

# File app/models/assignment.rb, line 516
def update_repository_permissions_forall_groupings
  # IMPORTANT: need to reload from DB
  self.reload
  groupings.each do |grouping|
    grouping.update_repository_permissions
  end
end
valid_groupings() click to toggle source
# File app/models/assignment.rb, line 536
def valid_groupings
  result = []
  groupings.all(:include => [{:student_memberships => :user}]).each do |grouping|
    if grouping.admin_approved || grouping.student_memberships.count >= group_min
      result.push(grouping)
    end
  end
  result
end
what_past_due_date() click to toggle source

Return an array with names of sections past

# File app/models/assignment.rb, line 145
def what_past_due_date
  sections_past = []

  unless self.section_due_dates_type
    if !due_date.nil? && Time.zone.now > due_date
      return sections_past << 'Due Date'
    end
  end

  self.section_due_dates.each do |d|
    if !d.due_date.nil? && Time.zone.now > d.due_date
      sections_past << d.section.name
    end
  end

  sections_past
end