| Class | Grouping |
| In: |
app/models/grouping.rb
|
| Parent: | ActiveRecord::Base |
Represents a collection of students working together on an assignment in a group
Returns an array containing the group names that didn‘t exist
# File app/models/grouping.rb, line 491
491: def self.assign_tas_by_csv(csv_file_contents, assignment_id)
492: failures = []
493: FasterCSV.parse(csv_file_contents) do |row|
494: group_name = row.shift # Knocks the first item from array
495: group = Group.find_by_group_name(group_name)
496: if group.nil?
497: failures.push(group_name)
498: else
499: grouping = group.grouping_for_assignment(assignment_id)
500: if grouping.nil?
501: failures.push(group_name)
502: else
503: grouping.add_tas_by_user_name_array(row) # The rest of the array
504: end
505: end
506: end
507: return failures
508: end
# File app/models/grouping.rb, line 43
43: def accepted_students
44: accepted_students = self.accepted_student_memberships.collect do |memb|
45: memb.user
46: end
47: return accepted_students
48: end
Add a new member to base
# File app/models/grouping.rb, line 139
139: def add_member(user, set_membership_status=StudentMembership::STATUSES[:accepted])
140: if user.has_accepted_grouping_for?(self.assignment_id) || user.hidden
141: return nil
142: else
143: member = StudentMembership.new(:user => user, :membership_status =>
144: set_membership_status, :grouping => self)
145: member.save
146: # adjust repo permissions
147: update_repository_permissions
148: return member
149: end
150: end
# File app/models/grouping.rb, line 425
425: def add_tas(tas)
426: #this check was previously done every time a ta_membership was created,
427: #however since the assignment is the same, validating it once for every new
428: #membership is a huge waste, so validate once and only proceed if true.
429: return unless self.assignment.valid?
430: grouping_tas = self.tas
431: tas = Array(tas)
432: tas.each do |ta|
433: if !grouping_tas.include? ta
434: #due to the membership's validates_associated :grouping attribute, only
435: #call its validation for the first grader as the grouping is constant
436: #and all the tas are ensured to be valid in the add_graders action in
437: #graders_controller
438: if ta == tas.first
439: #perform validation first time.
440: ta_memberships.create(:user => ta)
441: else
442: #skip validation to increase performance (all aspects of validation
443: #have already been performed elsewhere)
444: member = ta_memberships.build(:user => ta)
445: member.save(false)
446: end
447: grouping_tas += [ta]
448: end
449: end
450: criteria = self.all_assigned_criteria(grouping_tas | tas)
451: self.criteria_coverage_count = criteria.length
452: if self.criteria_coverage_count >= 0
453: #skip validation on save. grouping already gets validated on creation of
454: #ta_membership. Ensure criteria_coverage_count >= 0 as this is the only
455: #attribute that gets changed between the validation above and the save
456: #below. This is done to improve performance, as any validations of the
457: #grouping result in 5 extra database queries
458: self.save(false)
459: end
460: end
# File app/models/grouping.rb, line 475
475: def add_tas_by_user_name_array(ta_user_name_array)
476: grouping_tas = []
477: ta_user_name_array.each do |ta_user_name|
478: ta = Ta.find_by_user_name(ta_user_name)
479: if !ta.nil?
480: if ta_memberships.find_by_user_id(ta.id).nil?
481: ta_memberships.create(:user => ta)
482: end
483: end
484: grouping_tas += Array(ta)
485: end
486: self.criteria_coverage_count = self.all_assigned_criteria(grouping_tas).length
487: self.save
488: end
# File app/models/grouping.rb, line 575
575: def all_assigned_criteria(ta_array)
576: result = []
577: if assignment.assign_graders_to_criteria
578: ta_array.each do |ta|
579: result = result.concat(ta.get_criterion_associations_by_assignment(assignment))
580: end
581: end
582: return result.map{|a| a.criterion}.uniq
583: end
# File app/models/grouping.rb, line 563
563: def assigned_tas_for_criterion(criterion)
564: result = []
565: if assignment.assign_graders_to_criteria
566: tas.each do |ta|
567: if ta.criterion_ta_associations.find_by_criterion_id(criterion.id)
568: result.push(ta)
569: end
570: end
571: end
572: return result
573: end
Returns last modified date of the assignment_folder in this grouping‘s repository
# File app/models/grouping.rb, line 393
393: def assignment_folder_last_modified_date
394: repo = self.group.repo
395: rev = repo.get_latest_revision
396: # get the full path of repository folder
397: path = self.assignment.repository_folder
398:
399: # split "repo_folder_path" into two parts
400: parent_path = File.dirname(path)
401: folder_name = File.basename(path)
402:
403: # turn "parent_path" into absolute path
404: parent_path = repo.expand_path(parent_path, "/")
405: last_date = rev.directories_at_path(parent_path)[folder_name].last_modified_date
406: repo.close()
407: return last_date
408: end
Grace Credit Query
# File app/models/grouping.rb, line 297
297: def available_grace_credits
298: total = []
299: accepted_students.each do |student|
300: total.push(student.remaining_grace_credits)
301: end
302: return total.min
303: end
define whether user can be invited in this grouping TODO unit test
# File app/models/grouping.rb, line 154
154: def can_invite?(user)
155: m_logger = MarkusLogger.instance
156: if user && user.student?
157: if user.hidden
158: errors.add_to_base(I18n.t('invite_student.fail.hidden',
159: :user_name => user.user_name))
160: m_logger.log(I18n.t('markus_logger.student_invited',
161: {:inviter => "",
162: :invitee => user.user_name,
163: :error => I18n.t('invite_student.fail.hidden',
164: :user_name =>
165: user.user_name)}),
166: MarkusLogger::ERROR)
167:
168: return false
169: end
170: if self.inviter == user
171: errors.add_to_base(I18n.t('invite_student.fail.inviting_self',
172: :user_name => user.user_name))
173: m_logger.log(I18n.t('markus_logger.student_invited',
174: {:inviter => "",
175: :invitee => user.user_name,
176: :error =>
177: I18n.t('invite_student.fail.inviting_self',
178: :user_name => user.user_name)}),
179: MarkusLogger::ERROR)
180:
181:
182: end
183: if self.assignment.past_collection_date?
184: errors.add_to_base(I18n.t('invite_student.fail.due_date_passed',
185: :user_name => user.user_name))
186: m_logger.log(I18n.t('markus_logger.student_invited',
187: {:inviter => "",
188: :invitee => user.user_name,
189: :error =>
190: I18n.t('invite_student.fail.due_date_passed',
191: :user_name => user.user_name)}),
192: MarkusLogger::ERROR)
193:
194: return false
195: end
196: if self.student_membership_number >= self.assignment.group_max
197: errors.add_to_base(I18n.t('invite_student.fail.group_max_reached',
198: :user_name => user.user_name))
199: m_logger.log(I18n.t('markus_logger.student_invited',
200: {:inviter => "",
201: :invitee => user.user_name,
202: :error => I18n.t('invite_student.fail.group_maw_reached',
203: :user_name => user.user_name)}),
204: MarkusLogger::ERROR)
205: return false
206: end
207: if self.assignment.section_groups_only &&
208: user.section != self.inviter.section
209: errors.add_to_base(I18n.t('invite_student.fail.not_same_section',
210: :user_name => user.user_name))
211: m_logger.log(I18n.t('markus_logger.student_invited',
212: {:inviter => "",
213: :invitee => user.user_name,
214: :error => I18n.t('invite_student.fail.not_same_section',
215: :user_name => user.user_name)}),
216: MarkusLogger::ERROR)
217:
218:
219: return false
220: end
221: if user.has_accepted_grouping_for?(self.assignment.id)
222: errors.add_to_base(I18n.t('invite_student.fail.already_grouped',
223: :user_name => user.user_name))
224: m_logger.log(I18n.t('markus_logger.student_invited',
225: {:inviter => "",
226: :invitee => user.user_name,
227: :error => I18n.t('invite_student.fail.already_grouped',
228: :user_name => user.user_name)}),
229: MarkusLogger::ERROR)
230: return false
231: end
232: if self.pending?(user)
233: errors.add_to_base(I18n.t('invite_student.fail.already_pending',
234: :user_name => user.user_name))
235: m_logger.log(I18n.t('markus_logger.student_invited',
236: {:inviter => "",
237: :invitee => user.user_name,
238: :error => I18n.t('invite_student.fail.already_pending',
239: :user_name => user.user_name)}),
240: MarkusLogger::ERROR)
241: return false
242: end
243: else
244: errors.add_to_base(I18n.t('invite_student.fail.dne',
245: :user_name => user.user_name))
246: m_logger.log(I18n.t('markus_logger.student_invited',
247: {:inviter => "",
248: :invitee => user.user_name,
249: :error => I18n.t('invite_student.fail.dne',
250: :user_name => user.user_name)}),
251: MarkusLogger::ERROR)
252: return false
253: end
254: return true
255: end
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 529
529: def create_grouping_repository_folder
530:
531: # create folder only if we are repo admin
532: if self.group.repository_admin?
533: self.group.access_repo do |repo|
534: revision = repo.get_latest_revision
535: assignment_folder = File.join('/', assignment.repository_folder)
536:
537: if revision.path_exists?(assignment_folder)
538: return true
539: else
540: txn = self.group.repo.get_transaction("markus")
541: txn.add_path(assignment_folder)
542: return self.group.repo.commit(txn)
543: end
544: end
545: end
546: end
# File app/models/grouping.rb, line 360
360: def decline_invitation(student)
361: membership = student.memberships.find_by_grouping_id(self.id)
362: membership.membership_status = StudentMembership::STATUSES[:rejected]
363: membership.save
364: # adjust repo permissions
365: update_repository_permissions
366: end
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 372
372: def deletable_by?(user)
373: return false unless self.inviter == user
374: return (!self.is_valid?) || (self.is_valid? &&
375: self.accepted_students.size == 1 &&
376: self.number_of_submitted_files == 0 &&
377: self.assignment.group_assignment? &&
378: !assignment.past_collection_date? )
379: end
# File app/models/grouping.rb, line 344
344: def delete_grouping
345: self.student_memberships.all(:include => :user).each do |member|
346: member.destroy
347: end
348: # adjust repository permissions
349: update_repository_permissions
350: self.destroy
351: end
# File app/models/grouping.rb, line 56
56: def display_for_note
57: return assignment.short_identifier + ": " + group_name_with_student_user_names
58: end
# File app/models/grouping.rb, line 305
305: def grace_period_deduction_sum
306: total = 0
307: grace_period_deductions.each do |grace_period_deduction|
308: total += grace_period_deduction.deduction
309: end
310: return total
311: end
# File app/models/grouping.rb, line 50
50: def group_name_with_student_user_names
51: student_user_names = student_memberships.collect {|m| m.user.user_name }
52: return group.group_name if student_user_names.size == 0
53: return group.group_name + ": " + student_user_names.join(', ')
54: end
Submission Functions
# File app/models/grouping.rb, line 314
314: def has_submission?
315: #Return true if and only if this grouping has at least one submission
316: #with attribute submission_version_used == true.
317: return !current_submission_used.nil?
318: end
Strips admin_approved privledge
# File app/models/grouping.rb, line 284
284: def invalidate_grouping
285: self.admin_approved = false
286: self.save
287: # update repository permissions
288: update_repository_permissions
289: end
invites each user in ‘members’ by its user name, to this group
TODO update unit test
# File app/models/grouping.rb, line 104
104: def invite(members, set_membership_status=StudentMembership::STATUSES[:pending], invoked_by_admin=false)
105: # overloading invite() to accept members arg as both a string and a array
106: members = [members] if !members.instance_of?(Array) # put a string in an
107: # array
108: members.each do |m|
109: next if m.blank? # ignore blank users
110: m = m.strip
111: user = User.find_by_user_name(m)
112: m_logger = MarkusLogger.instance
113: if !user
114: errors.add_to_base(I18n.t('invite_student.fail.dne',
115: :user_name => m))
116: else
117: if self.can_invite?(user) || invoked_by_admin
118: member = self.add_member(user, set_membership_status)
119: if !member
120: errors.add_to_base(I18n.t('invite_student.fail.error',
121: :user_name => user.user_name))
122: m_logger.log(I18n.t('markus_logger.student_invited',
123: {:inviter => "",
124: :invitee => user.user_name,
125: :error => I18n.t('invite_student.fail.error',
126: :user_name => user.user_name)}),
127: MarkusLogger::ERROR)
128: else
129: m_logger.log(I18n.t('markus_logger.student_inviter_student',
130: :inviter => "",
131: :invited => user.user_name))
132: end
133: end
134: end
135: end
136: end
Returns the member with ‘inviter’ status for this group
# File app/models/grouping.rb, line 82
82: def inviter
83: member = student_memberships.find_by_membership_status(StudentMembership::STATUSES[:inviter])
84: if member.nil?
85: return nil
86: end
87: inviting_student = Student.find(member.user_id)
88: return inviting_student
89: end
Returns whether or not the submission_collector is pending to collect this grouping‘s newest submission
# File app/models/grouping.rb, line 69
69: def is_collected?
70: return is_collected
71: end
# File app/models/grouping.rb, line 98
98: def is_inviter?(user)
99: return membership_status(user) == StudentMembership::STATUSES[:inviter]
100: end
# File app/models/grouping.rb, line 320
320: def marking_completed?
321: return has_submission? && current_submission_used.result.marking_state == Result::MARKING_STATES[:complete]
322: end
Returns the status of this user, or nil if user is not a member
# File app/models/grouping.rb, line 258
258: def membership_status(user)
259: member = student_memberships.find_by_user_id(user.id)
260: member ? member.membership_status : nil # return nil if user is not a member
261: end
Returns a list of missing assignment_files yet to be submitted
# File app/models/grouping.rb, line 411
411: def missing_assignment_files
412: missing_assignment_files = []
413: self.group.access_repo do |repo|
414: rev = repo.get_latest_revision
415: assignment = self.assignment
416: assignment.assignment_files.each do |assignment_file|
417: if !rev.path_exists?(File.join(assignment.repository_folder, assignment_file.filename))
418: missing_assignment_files.push(assignment_file)
419: end
420: end
421: end
422: return missing_assignment_files
423: end
Returns the number of files submitted by this grouping for a particular assignment.
# File app/models/grouping.rb, line 383
383: def number_of_submitted_files
384: path = '/'
385: repo = self.group.repo
386: rev = repo.get_latest_revision
387: files = rev.files_at_path(File.join(File.join(self.assignment.repository_folder, path)))
388: repo.close()
389: return files.keys.length
390: end
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 94
94: def pending?(user)
95: return membership_status(user) == StudentMembership::STATUSES[:pending]
96: end
EDIT METHODS Removes the member by its membership id
# File app/models/grouping.rb, line 326
326: def remove_member(mbr_id)
327: member = student_memberships.find(mbr_id)
328: if member
329: # Remove repository permissions first
330: # Corner case: members are removed by admins only.
331: # Hence, we do not require to check for validity of the group
332: revoke_repository_permissions_for_membership(member)
333: member.destroy
334: if member.membership_status == StudentMembership::STATUSES[:inviter]
335: if member.grouping.accepted_student_memberships.length > 0
336: membership = member.grouping.accepted_student_memberships.first
337: membership.membership_status = StudentMembership::STATUSES[:inviter]
338: membership.save
339: end
340: end
341: end
342: end
Removes the member rejected by its membership id Used as safeguard when student deletes the record
# File app/models/grouping.rb, line 355
355: def remove_rejected(mbr_id)
356: member = memberships.find(mbr_id)
357: member.destroy if member && member.membership_status == StudentMembership::STATUSES[:rejected]
358: end
# File app/models/grouping.rb, line 462
462: def remove_tas(ta_id_array)
463: #if no tas to remove, return.
464: return if ta_id_array == []
465: ta_memberships_to_remove = ta_memberships.find_all_by_user_id(ta_id_array, :include => :user)
466: ta_memberships_to_remove.each do |ta_membership|
467: ta_membership.destroy
468: ta_memberships.delete(ta_membership)
469: end
470: criteria = self.all_assigned_criteria(self.tas - ta_memberships_to_remove.collect{|mem| mem.user})
471: self.criteria_coverage_count = criteria.length
472: self.save
473: end
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 552
552: def repository_external_commits_only?
553: assignment = self.assignment
554: return !assignment.allow_web_submits
555: end
Update repository permissions for students, if we allow external commits
see: grant_repository_permissions and revoke_repository_permissions
# File app/models/grouping.rb, line 512
512: def update_repository_permissions
513: # we do not need to do anything if we are not accepting external
514: # command-line commits
515: return unless self.write_repo_permissions?
516:
517: self.reload # VERY IMPORTANT! Make sure grouping object is not stale
518:
519: if self.is_valid?
520: grant_repository_permissions
521: else
522: # grouping became invalid, remove repo permissions
523: revoke_repository_permissions
524: end
525: end
Validates a group
# File app/models/grouping.rb, line 276
276: def validate_grouping
277: self.admin_approved = true
278: self.save
279: # update repository permissions
280: update_repository_permissions
281: end