class Grouping

Represents a collection of students working together on an assignment in a group

Public Class Methods

assign_tas_by_csv(csv_file_contents, assignment_id, encoding) click to toggle source

Returns an array containing the group names that didn’t exist

# File app/models/grouping.rb, line 498
def self.assign_tas_by_csv(csv_file_contents, assignment_id, encoding)
  failures = []
  if encoding != nil
    csv_file_contents = StringIO.new(Iconv.iconv('UTF-8', encoding, csv_file_contents).join)
  end
  CsvHelper::Csv.parse(csv_file_contents) do |row|
    group_name = row.shift # Knocks the first item from array
    group = Group.find_by_group_name(group_name)
    if group.nil?
      failures.push(group_name)
    else
      grouping = group.grouping_for_assignment(assignment_id)
      if grouping.nil?
        failures.push(group_name)
      else
        grouping.add_tas_by_user_name_array(row) # The rest of the array
      end
    end
  end
  return failures
end

Public Instance Methods

accepted_students() click to toggle source
# File app/models/grouping.rb, line 59
def accepted_students
  self.accepted_student_memberships.collect do |memb|
    memb.user
  end
end
add_member(user, set_membership_status=StudentMembership::STATUSES[:accepted]) click to toggle source

Add a new member to base

# File app/models/grouping.rb, line 157
def add_member(user, set_membership_status=StudentMembership::STATUSES[:accepted])
   if user.has_accepted_grouping_for?(self.assignment_id) || user.hidden
     nil
   else
     member = StudentMembership.new(:user => user, :membership_status =>
     set_membership_status, :grouping => self)
     member.save
     # adjust repo permissions
     update_repository_permissions

     # remove any old deduction for this assignment
     remove_grace_period_deduction(member)

     # Add deductions for the new added member
     deduction = GracePeriodDeduction.new
     deduction.membership = member
     deduction.deduction = self.grace_period_deduction_single
     deduction.save

     member
   end
 end
add_tas(tas) click to toggle source
# File app/models/grouping.rb, line 432
def add_tas(tas)
  #this check was previously done every time a ta_membership was created,
  #however since the assignment is the same, validating it once for every new
  #membership is a huge waste, so validate once and only proceed if true.
  return unless self.assignment.valid?
  grouping_tas = self.tas
  tas = Array(tas)
  tas.each do |ta|
    unless grouping_tas.include? ta
      #due to the membership's validates_associated :grouping attribute, only
      #call its validation for the first grader as the grouping is constant
      #and all the tas are ensured to be valid in the add_graders action in
      #graders_controller
      if ta == tas.first
        #perform validation first time.
        ta_memberships.create(:user => ta)
      else
        #skip validation to increase performance (all aspects of validation
        #have already been performed elsewhere)
        member = ta_memberships.build(:user => ta)
        member.save(:validate => false)
      end
      grouping_tas += [ta]
    end
  end
  criteria = self.all_assigned_criteria(grouping_tas | tas)
  self.criteria_coverage_count = criteria.length
  if self.criteria_coverage_count >= 0
    #skip validation on save. grouping already gets validated on creation of
    #ta_membership. Ensure criteria_coverage_count >= 0 as this is the only
    #attribute that gets changed between the validation above and the save
    #below. This is done to improve performance, as any validations of the
    #grouping result in 5 extra database queries
    self.save(:validate => false)
  end
end
add_tas_by_user_name_array(ta_user_name_array) click to toggle source
# File app/models/grouping.rb, line 482
def add_tas_by_user_name_array(ta_user_name_array)
  grouping_tas = []
  ta_user_name_array.each do |ta_user_name|
    ta = Ta.find_by_user_name(ta_user_name)
    unless ta.nil?
      if ta_memberships.find_by_user_id(ta.id).nil?
        ta_memberships.create(:user => ta)
      end
    end
    grouping_tas += Array(ta)
  end
  self.criteria_coverage_count = self.all_assigned_criteria(grouping_tas).length
  self.save
end
all_assigned_criteria(ta_array) click to toggle source
# File app/models/grouping.rb, line 585
def all_assigned_criteria(ta_array)
  result = []
  if assignment.assign_graders_to_criteria
    ta_array.each do |ta|
      result = result.concat(ta.get_criterion_associations_by_assignment(assignment))
    end
  end
  result.map{|a| a.criterion}.uniq
end
assigned_tas_for_criterion(criterion) click to toggle source
# File app/models/grouping.rb, line 573
def assigned_tas_for_criterion(criterion)
  result = []
  if assignment.assign_graders_to_criteria
    tas.each do |ta|
      if ta.criterion_ta_associations.find_by_criterion_id(criterion.id)
        result.push(ta)
      end
    end
  end
  result
end
assignment_folder_last_modified_date() click to toggle source

Returns last modified date of the assignment_folder in this grouping’s repository

# File app/models/grouping.rb, line 400
def assignment_folder_last_modified_date
  repo = self.group.repo
  rev = repo.get_latest_revision
  # get the full path of repository folder
  path = self.assignment.repository_folder

  # split "repo_folder_path" into two parts
  parent_path = File.dirname(path)
  folder_name = File.basename(path)

  # turn "parent_path" into absolute path
  parent_path = repo.expand_path(parent_path, '/')
  last_date = rev.directories_at_path(parent_path)[folder_name].last_modified_date
  repo.close()
  last_date
end
available_grace_credits() click to toggle source

Grace Credit Query

# File app/models/grouping.rb, line 288
def available_grace_credits
  total = []
  accepted_students.each do |student|
    total.push(student.remaining_grace_credits)
  end
  total.min
end
can_invite?(user) click to toggle source

define whether user can be invited in this grouping

# File app/models/grouping.rb, line 181
def can_invite?(user)
  m_logger = MarkusLogger.instance
  if user && user.student?
    if user.hidden
      errors.add(:base, I18n.t('invite_student.fail.hidden',
                                :user_name => user.user_name))
      m_logger.log("Student failed to invite '#{user.user_name}' (account has been " +
                   'disabled).', MarkusLogger::ERROR)

      return false
    end
    if self.inviter == user
      errors.add(:base, I18n.t('invite_student.fail.inviting_self',
                                :user_name => user.user_name))
      m_logger.log("Student failed to invite '#{user.user_name}'. Tried to invite " +
                   'himself.', MarkusLogger::ERROR)


    end
    if self.assignment.past_collection_date?
      errors.add(:base, I18n.t('invite_student.fail.due_date_passed',
                                :user_name => user.user_name))
      m_logger.log("Student failed to invite '#{user.user_name}'. Current time past " +
                   'collection date.', MarkusLogger::ERROR)

      return false
    end
    if self.student_membership_number >= self.assignment.group_max
      errors.add(:base, I18n.t('invite_student.fail.group_max_reached',
                                :user_name => user.user_name))
      m_logger.log("Student failed to invite '#{user.user_name}'. Group maximum" +
                   ' reached.', MarkusLogger::ERROR)
      return false
    end
    if self.assignment.section_groups_only &&
      user.section != self.inviter.section
      errors.add(:base, I18n.t('invite_student.fail.not_same_section',
                                :user_name => user.user_name))
      m_logger.log("Student failed to invite '#{user.user_name}'. Students not in" +
                   ' same section.', MarkusLogger::ERROR)

      return false
    end
    if user.has_accepted_grouping_for?(self.assignment.id)
      errors.add(:base, I18n.t('invite_student.fail.already_grouped',
                                :user_name => user.user_name))
      m_logger.log("Student failed to invite '#{user.user_name}'. Invitee already part" +
                   ' of another group.', MarkusLogger::ERROR)
      return false
    end
    if self.pending?(user)
      errors.add(:base, I18n.t('invite_student.fail.already_pending',
                                :user_name => user.user_name))
      m_logger.log("Student failed to invite '#{user.user_name}'. Invitee is already " +
                   ' pending member of this group.', MarkusLogger::ERROR)
      return false
    end
  else
    errors.add(:base, I18n.t('invite_student.fail.dne',
                              :user_name => user.user_name))
    m_logger.log("Student failed to invite '#{user.user_name}'. Invitee does not " +
                 ' exist.', MarkusLogger::ERROR)
    return false
  end
  true
end
create_grouping_repository_folder() click to toggle source

When a Grouping is created, automatically create the folder for the assignment in the repository, if it doesn’t already exist.

# File app/models/grouping.rb, line 539
def create_grouping_repository_folder

  # create folder only if we are repo admin
  if self.group.repository_admin?
    self.group.access_repo do |repo|
      revision = repo.get_latest_revision
      assignment_folder = File.join('/', assignment.repository_folder)

      if revision.path_exists?(assignment_folder)
        return true
      else
        txn = self.group.repo.get_transaction('markus')
        txn.add_path(assignment_folder)
        return self.group.repo.commit(txn)
      end
    end
  end
end
decline_invitation(student) click to toggle source
# File app/models/grouping.rb, line 367
def decline_invitation(student)
   membership = student.memberships.find_by_grouping_id(self.id)
   membership.membership_status = StudentMembership::STATUSES[:rejected]
   membership.save
   # adjust repo permissions
   update_repository_permissions
end
deletable_by?(user) click to toggle source

If a group is invalid OR valid and the user is the inviter of the group and she is the only member of this grouping it should be deletable by this user, provided there haven’t been any files submitted. Additionally, the grace period for the assignment should not have passed.

# File app/models/grouping.rb, line 379
def deletable_by?(user)
  return false unless self.inviter == user
  (!self.is_valid?) || (self.is_valid? &&
                        self.accepted_students.size == 1 &&
                        self.number_of_submitted_files == 0 &&
                        self.assignment.group_assignment? &&
                        !assignment.past_collection_date?)
end
delete_grouping() click to toggle source
# File app/models/grouping.rb, line 351
def delete_grouping
  self.student_memberships.all(:include => :user).each do |member|
    member.destroy
  end
  # adjust repository permissions
  update_repository_permissions
  self.destroy
end
display_for_note() click to toggle source
# File app/models/grouping.rb, line 77
def display_for_note
  assignment.short_identifier + ': ' + group_name_with_student_user_names
end
get_all_students_in_group() click to toggle source
# File app/models/grouping.rb, line 65
def get_all_students_in_group
  student_user_names = student_memberships.collect {|m| m.user.user_name }
  return I18n.t('assignment.group.empty') if student_user_names.size == 0
        student_user_names.join(', ')
end
get_ta_names() click to toggle source

Returns an array of the user_names for any TA’s assigned to mark this Grouping

# File app/models/grouping.rb, line 96
def get_ta_names
  ta_memberships.collect do |membership|
    membership.user.user_name
  end
end
give_tokens() click to toggle source

Token Credit Query

# File app/models/grouping.rb, line 283
def give_tokens
  Token.create(:grouping_id => self.id, :tokens => self.assignment.tokens_per_day) if self.assignment.enable_test
end
grace_period_deduction_single() click to toggle source

The grace credits deducted (of one student) for this specific submission in the grouping

# File app/models/grouping.rb, line 298
def grace_period_deduction_single
  single = 0
  # Since for an instance of a grouping all members of the group will get
  # deducted the same amount (for a specific assignment), it is safe to pick
  # any deduction
  if !grace_period_deductions.nil? && !grace_period_deductions.first.nil?
    single = grace_period_deductions.first.deduction
  end
  single
end
group_name_with_student_user_names() click to toggle source
# File app/models/grouping.rb, line 71
def group_name_with_student_user_names
              user_names = get_all_students_in_group
  return group.group_name if user_names == I18n.t('assignment.group.empty')
  group.group_name + ': ' + user_names
end
has_submission?() click to toggle source

Submission Functions

# File app/models/grouping.rb, line 321
def has_submission?
  #Return true if and only if this grouping has at least one submission
  #with attribute submission_version_used == true.
  !current_submission_used.nil?
end
has_ta_for_marking?() click to toggle source

Returns whether or not a TA is assigned to mark this Grouping

# File app/models/grouping.rb, line 84
def has_ta_for_marking?
  ta_memberships.count > 0
end
invalidate_grouping() click to toggle source

Strips admin_approved privledge

# File app/models/grouping.rb, line 275
def invalidate_grouping
  self.admin_approved = false
  self.save
  # update repository permissions
  update_repository_permissions
end
invite(members, set_membership_status=StudentMembership::STATUSES[:pending], invoked_by_admin=false) click to toggle source

invites each user in ‘members’ by its user name, to this group If the method is invoked by an admin, checks on whether the students can be part of the group are skipped.

# File app/models/grouping.rb, line 126
def invite(members,
           set_membership_status=StudentMembership::STATUSES[:pending],
           invoked_by_admin=false)
  # overloading invite() to accept members arg as both a string and a array
  members = [members] if !members.instance_of?(Array) # put a string in an
                                               # array
  members.each do |m|
    next if m.blank? # ignore blank users
    m = m.strip
    user = User.find_by_user_name(m)
    m_logger = MarkusLogger.instance
    if user
      if invoked_by_admin || self.can_invite?(user)
        member = self.add_member(user, set_membership_status)
        if member
          m_logger.log("Student invited '#{user.user_name}'.")
        else
          errors.add(:base, I18n.t('invite_student.fail.error',
                                   :user_name => user.user_name))
          m_logger.log("Student failed to invite '#{user.user_name}'",
                       MarkusLogger::ERROR)
        end
      end
    else
      errors.add(:base, I18n.t('invite_student.fail.dne',
                               :user_name => m))
    end
  end
end
inviter() click to toggle source

Returns the member with ‘inviter’ status for this group

# File app/models/grouping.rb, line 103
def inviter
 member = student_memberships.find_by_membership_status(StudentMembership::STATUSES[:inviter])
  if member.nil?
    return nil
  end
  Student.find(member.user_id)
end
is_collected?() click to toggle source

Returns whether or not the submission_collector is pending to collect this grouping’s newest submission

# File app/models/grouping.rb, line 90
def is_collected?
  is_collected
end
is_inviter?(user) click to toggle source

returns whether the user is the inviter of this group or not.

# File app/models/grouping.rb, line 119
def is_inviter?(user)
  membership_status(user) ==  StudentMembership::STATUSES[:inviter]
end
is_valid?() click to toggle source

Returns true if either this Grouping has met the assignment group size minimum, OR has been approved by an instructor

# File app/models/grouping.rb, line 262
def is_valid?
  admin_approved || (non_rejected_student_memberships.size >= assignment.group_min)
end
marking_completed?() click to toggle source
# File app/models/grouping.rb, line 327
def marking_completed?
  has_submission? && current_submission_used.get_latest_result.marking_state == Result::MARKING_STATES[:complete]
end
membership_status(user) click to toggle source

Returns the status of this user, or nil if user is not a member

# File app/models/grouping.rb, line 249
def membership_status(user)
  member = student_memberships.find_by_user_id(user.id)
  member ? member.membership_status : nil  # return nil if user is not a member
end
missing_assignment_files() click to toggle source

Returns a list of missing assignment_files yet to be submitted

# File app/models/grouping.rb, line 418
def missing_assignment_files
  missing_assignment_files = []
  self.group.access_repo do |repo|
    rev = repo.get_latest_revision
    assignment = self.assignment
    assignment.assignment_files.each do |assignment_file|
      unless rev.path_exists?(File.join(assignment.repository_folder, assignment_file.filename))
        missing_assignment_files.push(assignment_file)
      end
    end
  end
  missing_assignment_files
end
number_of_submitted_files() click to toggle source

Returns the number of files submitted by this grouping for a particular assignment.

# File app/models/grouping.rb, line 390
def number_of_submitted_files
  path = '/'
  repo = self.group.repo
  rev = repo.get_latest_revision
  files = rev.files_at_path(File.join(File.join(self.assignment.repository_folder, path)))
  repo.close()
  files.keys.length
end
past_due_date?() click to toggle source

Find the correct due date (section or not) and check if it is after the last commit

# File app/models/grouping.rb, line 609
def past_due_date?

  timestamp = group.repo.get_latest_revision.timestamp
  due_dates = assignment.section_due_dates
  section = unless inviter.blank?
              inviter.section
            end
  section_due_date = unless section.blank? || due_dates.blank?
                       due_dates.find_by_section_id(section).due_date
                     end

  # condition to return
  (!due_dates.blank? && !section.blank? &&
      !section_due_date.blank? && timestamp > section_due_date) ||
      timestamp > assignment.due_date
end
pending?(user) click to toggle source

Returns true if this user has a pending status for this group; false otherwise, or if user is not in this group.

# File app/models/grouping.rb, line 114
def pending?(user)
  membership_status(user) == StudentMembership::STATUSES[:pending]
end
remove_grace_period_deduction(membership) click to toggle source

remove all deductions for this assignment for a particular member

# File app/models/grouping.rb, line 310
def remove_grace_period_deduction(membership)
  deductions = membership.user.grace_period_deductions
  deductions.each do |deduction|
    if deduction.membership.grouping.assignment.id == assignment.id
      membership.grace_period_deductions.delete(deduction)
      deduction.destroy
    end
  end
end
remove_member(mbr_id) click to toggle source

EDIT METHODS Removes the member by its membership id

# File app/models/grouping.rb, line 333
def remove_member(mbr_id)
  member = student_memberships.find(mbr_id)
  if member
    # Remove repository permissions first
    #   Corner case: members are removed by admins only.
    #   Hence, we do not require to check for validity of the group
    revoke_repository_permissions_for_membership(member)
    member.destroy
    if member.membership_status == StudentMembership::STATUSES[:inviter]
       if member.grouping.accepted_student_memberships.length > 0
          membership = member.grouping.accepted_student_memberships.first
          membership.membership_status = StudentMembership::STATUSES[:inviter]
          membership.save
       end
    end
  end
end
remove_rejected(mbr_id) click to toggle source

Removes the member rejected by its membership id Used as safeguard when student deletes the record

# File app/models/grouping.rb, line 362
def remove_rejected(mbr_id)
  member = memberships.find(mbr_id)
  member.destroy if member && member.membership_status == StudentMembership::STATUSES[:rejected]
end
remove_tas(ta_id_array) click to toggle source
# File app/models/grouping.rb, line 469
def remove_tas(ta_id_array)
  #if no tas to remove, return.
  return if ta_id_array == []
  ta_memberships_to_remove = ta_memberships.find_all_by_user_id(ta_id_array, :include => :user)
  ta_memberships_to_remove.each do |ta_membership|
    ta_membership.destroy
    ta_memberships.delete(ta_membership)
  end
  criteria = self.all_assigned_criteria(self.tas - ta_memberships_to_remove.collect{|mem| mem.user})
  self.criteria_coverage_count = criteria.length
  self.save
end
repository_external_commits_only?() click to toggle source

Returns true, if and only if the configured repository setup allows for externally accessible repositories, in which case file submissions via the Web interface are not permitted. For now, this works for Subversion repositories only.

# File app/models/grouping.rb, line 562
def repository_external_commits_only?
  assignment = self.assignment
  !assignment.allow_web_submits
end
section() click to toggle source

Get the section for this group. If assignment restricts member of a groupe to a section, all students are in the same section. Therefore, return only the inviters section

# File app/models/grouping.rb, line 598
def section
  if !self.inviter.nil? and self.inviter.has_section?
    return self.inviter.section.name
  end
  '-'
end
student_membership_number() click to toggle source

returns the numbers of memberships, all includ (inviter, pending, accepted

# File app/models/grouping.rb, line 256
def student_membership_number
   accepted_students.size + pending_students.size
end
update_repository_permissions() click to toggle source

Update repository permissions for students, if we allow external commits

see: grant_repository_permissions and revoke_repository_permissions
# File app/models/grouping.rb, line 522
def update_repository_permissions
  # we do not need to do anything if we are not accepting external
  # command-line commits
  return unless self.write_repo_permissions?

  self.reload # VERY IMPORTANT! Make sure grouping object is not stale

  if self.is_valid?
    grant_repository_permissions
  else
    # grouping became invalid, remove repo permissions
    revoke_repository_permissions
  end
end
validate_grouping() click to toggle source

Validates a group

# File app/models/grouping.rb, line 267
def validate_grouping
  self.admin_approved = true
  self.save
  # update repository permissions
  update_repository_permissions
end
write_repo_permissions?() click to toggle source

Should we write repository permissions for this grouping?

# File app/models/grouping.rb, line 568
def write_repo_permissions?
  MarkusConfigurator.markus_config_repository_admin? &&
      self.repository_external_commits_only?
end