Rcov C0 Coverage Information - RCov

app/models/grouping.rb

Name Total Lines Lines of Code Total Coverage Code Coverage
app/models/grouping.rb 675 484
98.81%
98.55%

Key

Code reported as executed by Ruby looks like this...and this: this line is also marked as covered.Lines considered as run by rcov, but not reported by Ruby, look like this,and this: these lines were inferred by rcov (using simple heuristics).Finally, here's a line marked as not executed.

Coverage Details

1 # we need repository permission constants
2 require 'repo/repository'
3 
4 # Represents a collection of students working together on an assignment in a group
5 class Grouping < ActiveRecord::Base
6 
7   before_create :create_grouping_repository_folder
8   before_destroy :revoke_repository_permissions_for_students
9   belongs_to :assignment, :counter_cache => true
10   belongs_to :group
11   belongs_to :grouping_queue
12   has_many :memberships
13   has_many :student_memberships, :order => 'id'
14   has_many :non_rejected_student_memberships,
15            :class_name => "StudentMembership",
16            :conditions => ['memberships.membership_status != ?',
17                            StudentMembership::STATUSES[:rejected]]
18   has_many :accepted_student_memberships,
19            :class_name => "StudentMembership",
20            :conditions => {
21               'memberships.membership_status' => [
22                     StudentMembership::STATUSES[:accepted],
23                     StudentMembership::STATUSES[:inviter]]}
24   has_many :notes, :as => :noteable, :dependent => :destroy
25   has_many :ta_memberships, :class_name => "TaMembership"
26   has_many :tas, :through => :ta_memberships, :source => :user
27   has_many :students, :through => :student_memberships, :source => :user
28   has_many :pending_students,
29            :class_name => 'Student',
30            :through => :student_memberships,
31            :conditions => {
32             'memberships.membership_status' => StudentMembership::STATUSES[:pending]},
33            :source => :user
34 
35   has_many :submissions
36   #The first submission found that satisfies submission_version_used == true.
37   #If there are multiple such submissions, one is chosen randomly.
38   has_one :current_submission_used,
39           :class_name => 'Submission',
40           :conditions => {:submission_version_used => true}
41   has_many :grace_period_deductions,
42            :through => :non_rejected_student_memberships
43 
44   has_one :token
45 
46   scope :approved_groupings, :conditions => {:admin_approved => true}
47 
48   validates_numericality_of :criteria_coverage_count, :greater_than_or_equal_to => 0
49 
50   # user association/validation
51   validates_presence_of :assignment_id
52   validates_associated :assignment, :on => :create, :message => "associated assignment need to be valid"
53 
54   validates_presence_of :group_id
55   validates_associated :group, :message => "associated group need to be valid"
56 
57   validates_inclusion_of :is_collected, :in => [true, false]
58 
59   def accepted_students
60     accepted_students = self.accepted_student_memberships.collect do |memb|
61       memb.user
62     end
63     return accepted_students
64   end
65 
66   def group_name_with_student_user_names
67     student_user_names = student_memberships.collect {|m| m.user.user_name }
68     return group.group_name if student_user_names.size == 0
69     return group.group_name + ": " + student_user_names.join(', ')
70   end
71 
72   def display_for_note
73     return assignment.short_identifier + ": " + group_name_with_student_user_names
74   end
75 
76   # Query Functions ------------------------------------------------------
77 
78   # Returns whether or not a TA is assigned to mark this Grouping
79   def has_ta_for_marking?
80     return ta_memberships.count > 0
81   end
82 
83   #Returns whether or not the submission_collector is pending to collect this
84   #grouping's newest submission
85   def is_collected?
86     return is_collected
87   end
88 
89   # Returns an array of the user_names for any TA's assigned to mark
90   # this Grouping
91   def get_ta_names
92     return ta_memberships.collect do |membership|
93       membership.user.user_name
94     end
95   end
96 
97   # Returns the member with 'inviter' status for this group
98   def inviter
99    member = student_memberships.find_by_membership_status(StudentMembership::STATUSES[:inviter])
100     if member.nil?
101       return nil
102     end
103     inviting_student = Student.find(member.user_id)
104     return inviting_student
105   end
106 
107 
108   # Returns true if this user has a pending status for this group;
109   # false otherwise, or if user is not in this group.
110   def pending?(user)
111     return membership_status(user) == StudentMembership::STATUSES[:pending]
112   end
113 
114   # returns whether the user is the inviter of this group or not.
115   def is_inviter?(user)
116     return membership_status(user) ==  StudentMembership::STATUSES[:inviter]
117   end
118 
119   # invites each user in 'members' by its user name, to this group
120   # If the method is invoked by an admin, checks on whether the students can
121   # be part of the group are skipped.
122   def invite(members,
123              set_membership_status=StudentMembership::STATUSES[:pending],
124              invoked_by_admin=false)
125     # overloading invite() to accept members arg as both a string and a array
126     members = [members] if !members.instance_of?(Array) # put a string in an
127                                                  # array
128     members.each do |m|
129       next if m.blank? # ignore blank users
130       m = m.strip
131       user = User.find_by_user_name(m)
132       m_logger = MarkusLogger.instance
133       if !user
134         errors.add(:base, I18n.t('invite_student.fail.dne',
135                                   :user_name => m))
136       else
137         if invoked_by_admin || self.can_invite?(user)
138           member = self.add_member(user, set_membership_status)
139           if !member
140             errors.add(:base, I18n.t('invite_student.fail.error',
141                                       :user_name => user.user_name))
142             m_logger.log("Student failed to invite '#{user.user_name}'",
143                           MarkusLogger::ERROR)
144           else
145             m_logger.log("Student invited '#{user.user_name}'.")
146           end
147         end
148       end
149     end
150   end
151 
152   # Add a new member to base
153  def add_member(user, set_membership_status=StudentMembership::STATUSES[:accepted])
154     if user.has_accepted_grouping_for?(self.assignment_id) || user.hidden
155       return nil
156     else
157       member = StudentMembership.new(:user => user, :membership_status =>
158       set_membership_status, :grouping => self)
159       member.save
160       # adjust repo permissions
161       update_repository_permissions
162       return member
163     end
164   end
165 
166   # define whether user can be invited in this grouping
167   def can_invite?(user)
168     m_logger = MarkusLogger.instance
169     if user && user.student?
170       if user.hidden
171         errors.add(:base, I18n.t('invite_student.fail.hidden',
172                                   :user_name => user.user_name))
173         m_logger.log("Student failed to invite '#{user.user_name}' (account has been " +
174                      "disabled).", MarkusLogger::ERROR)
175 
176         return false
177       end
178       if self.inviter == user
179         errors.add(:base, I18n.t('invite_student.fail.inviting_self',
180                                   :user_name => user.user_name))
181         m_logger.log("Student failed to invite '#{user.user_name}'. Tried to invite " +
182                      "himself.", MarkusLogger::ERROR)
183 
184 
185       end
186       if self.assignment.past_collection_date?
187         errors.add(:base, I18n.t('invite_student.fail.due_date_passed',
188                                   :user_name => user.user_name))
189         m_logger.log("Student failed to invite '#{user.user_name}'. Current time past " +
190                      "collection date.", MarkusLogger::ERROR)
191 
192         return false
193       end
194       if self.student_membership_number >= self.assignment.group_max
195         errors.add(:base, I18n.t('invite_student.fail.group_max_reached',
196                                   :user_name => user.user_name))
197         m_logger.log("Student failed to invite '#{user.user_name}'. Group maximum" +
198                      " reached.", MarkusLogger::ERROR)
199         return false
200       end
201       if self.assignment.section_groups_only &&
202         user.section != self.inviter.section
203         errors.add(:base, I18n.t('invite_student.fail.not_same_section',
204                                   :user_name => user.user_name))
205         m_logger.log("Student failed to invite '#{user.user_name}'. Students not in" +
206                      " same section.", MarkusLogger::ERROR)
207 
208         return false
209       end
210       if user.has_accepted_grouping_for?(self.assignment.id)
211         errors.add(:base, I18n.t('invite_student.fail.already_grouped',
212                                   :user_name => user.user_name))
213         m_logger.log("Student failed to invite '#{user.user_name}'. Invitee already part" +
214                      " of another group.", MarkusLogger::ERROR)
215         return false
216       end
217       if self.pending?(user)
218         errors.add(:base, I18n.t('invite_student.fail.already_pending',
219                                   :user_name => user.user_name))
220         m_logger.log("Student failed to invite '#{user.user_name}'. Invitee is already " +
221                      " pending member of this group.", MarkusLogger::ERROR)
222         return false
223       end
224     else
225       errors.add(:base, I18n.t('invite_student.fail.dne',
226                                 :user_name => user.user_name))
227       m_logger.log("Student failed to invite '#{user.user_name}'. Invitee does not " +
228                    " exist.", MarkusLogger::ERROR)
229       return false
230     end
231     return true
232   end
233 
234   # Returns the status of this user, or nil if user is not a member
235   def membership_status(user)
236     member = student_memberships.find_by_user_id(user.id)
237     member ? member.membership_status : nil  # return nil if user is not a member
238   end
239 
240   # returns the numbers of memberships, all includ (inviter, pending,
241   # accepted
242   def student_membership_number
243      return accepted_students.size + pending_students.size
244   end
245 
246   # Returns true if either this Grouping has met the assignment group
247   # size minimum, OR has been approved by an instructor
248   def is_valid?
249     return admin_approved || (non_rejected_student_memberships.size >= assignment.group_min)
250   end
251 
252   # Validates a group
253   def validate_grouping
254     self.admin_approved = true
255     self.save
256     # update repository permissions
257     update_repository_permissions
258   end
259 
260   # Strips admin_approved privledge
261   def invalidate_grouping
262     self.admin_approved = false
263     self.save
264     # update repository permissions
265     update_repository_permissions
266   end
267 
268   # Token Credit Query
269   def give_tokens
270     Token.create(:grouping_id => self.id, :tokens => self.assignment.tokens_per_day) if self.assignment.enable_test
271   end
272 
273   # Grace Credit Query
274   def available_grace_credits
275     total = []
276     accepted_students.each do |student|
277       total.push(student.remaining_grace_credits)
278     end
279     return total.min
280   end
281 
282   def grace_period_deduction_sum
283     total = 0
284     grace_period_deductions.each do |grace_period_deduction|
285       total += grace_period_deduction.deduction
286     end
287     return total
288   end
289 
290   # Submission Functions
291   def has_submission?
292     #Return true if and only if this grouping has at least one submission
293     #with attribute submission_version_used == true.
294     return !current_submission_used.nil?
295   end
296 
297   def marking_completed?
298     return has_submission? && current_submission_used.result.marking_state == Result::MARKING_STATES[:complete]
299   end
300 
301   # EDIT METHODS
302   # Removes the member by its membership id
303   def remove_member(mbr_id)
304     member = student_memberships.find(mbr_id)
305     if member
306       # Remove repository permissions first
307       #   Corner case: members are removed by admins only.
308       #   Hence, we do not require to check for validity of the group
309       revoke_repository_permissions_for_membership(member)
310       member.destroy
311       if member.membership_status == StudentMembership::STATUSES[:inviter]
312          if member.grouping.accepted_student_memberships.length > 0
313             membership = member.grouping.accepted_student_memberships.first
314             membership.membership_status = StudentMembership::STATUSES[:inviter]
315             membership.save
316          end
317       end
318     end
319   end
320 
321   def delete_grouping
322     self.student_memberships.all(:include => :user).each do |member|
323       member.destroy
324     end
325     # adjust repository permissions
326     update_repository_permissions
327     self.destroy
328   end
329 
330   # Removes the member rejected by its membership id
331   # Used as safeguard when student deletes the record
332   def remove_rejected(mbr_id)
333     member = memberships.find(mbr_id)
334     member.destroy if member && member.membership_status == StudentMembership::STATUSES[:rejected]
335   end
336 
337   def decline_invitation(student)
338      membership = student.memberships.find_by_grouping_id(self.id)
339      membership.membership_status = StudentMembership::STATUSES[:rejected]
340      membership.save
341      # adjust repo permissions
342      update_repository_permissions
343   end
344 
345   # If a group is invalid OR valid and the user is the inviter of the group and
346   # she is the _only_ member of this grouping it should be deletable
347   # by this user, provided there haven't been any files submitted. Additionally,
348   # the grace period for the assignment should not have passed.
349   def deletable_by?(user)
350     return false unless self.inviter == user
351     return (!self.is_valid?) || (self.is_valid? &&
352                                  self.accepted_students.size == 1 &&
353                                  self.number_of_submitted_files == 0 &&
354                                  self.assignment.group_assignment? &&
355                                  !assignment.past_collection_date? )
356   end
357 
358   # Returns the number of files submitted by this grouping for a
359   # particular assignment.
360   def number_of_submitted_files
361     path = '/'
362     repo = self.group.repo
363     rev = repo.get_latest_revision
364     files = rev.files_at_path(File.join(File.join(self.assignment.repository_folder, path)))
365     repo.close()
366     return files.keys.length
367   end
368 
369   # Returns last modified date of the assignment_folder in this grouping's repository
370   def assignment_folder_last_modified_date
371     repo = self.group.repo
372     rev = repo.get_latest_revision
373     # get the full path of repository folder
374     path = self.assignment.repository_folder
375 
376     # split "repo_folder_path" into two parts
377     parent_path = File.dirname(path)
378     folder_name = File.basename(path)
379 
380     # turn "parent_path" into absolute path
381     parent_path = repo.expand_path(parent_path, "/")
382     last_date = rev.directories_at_path(parent_path)[folder_name].last_modified_date
383     repo.close()
384     return last_date
385   end
386 
387   # Returns a list of missing assignment_files yet to be submitted
388   def missing_assignment_files
389     missing_assignment_files = []
390     self.group.access_repo do |repo|
391       rev = repo.get_latest_revision
392       assignment = self.assignment
393       assignment.assignment_files.each do |assignment_file|
394         if !rev.path_exists?(File.join(assignment.repository_folder, assignment_file.filename))
395           missing_assignment_files.push(assignment_file)
396         end
397       end
398     end
399     return missing_assignment_files
400   end
401 
402   def add_tas(tas)
403     #this check was previously done every time a ta_membership was created,
404     #however since the assignment is the same, validating it once for every new
405     #membership is a huge waste, so validate once and only proceed if true.
406     return unless self.assignment.valid?
407     grouping_tas = self.tas
408     tas = Array(tas)
409     tas.each do |ta|
410       if !grouping_tas.include? ta
411         #due to the membership's validates_associated :grouping attribute, only
412         #call its validation for the first grader as the grouping is constant
413         #and all the tas are ensured to be valid in the add_graders action in
414         #graders_controller
415         if ta == tas.first
416           #perform validation first time.
417           ta_memberships.create(:user => ta)
418         else
419           #skip validation to increase performance (all aspects of validation
420           #have already been performed elsewhere)
421           member = ta_memberships.build(:user => ta)
422           member.save(:validate => false)
423         end
424         grouping_tas += [ta]
425       end
426     end
427     criteria = self.all_assigned_criteria(grouping_tas | tas)
428     self.criteria_coverage_count = criteria.length
429     if self.criteria_coverage_count >= 0
430       #skip validation on save. grouping already gets validated on creation of
431       #ta_membership. Ensure criteria_coverage_count >= 0 as this is the only
432       #attribute that gets changed between the validation above and the save
433       #below. This is done to improve performance, as any validations of the
434       #grouping result in 5 extra database queries
435       self.save(:validate => false)
436     end
437   end
438 
439   def remove_tas(ta_id_array)
440     #if no tas to remove, return.
441     return if ta_id_array == []
442     ta_memberships_to_remove = ta_memberships.find_all_by_user_id(ta_id_array, :include => :user)
443     ta_memberships_to_remove.each do |ta_membership|
444       ta_membership.destroy
445       ta_memberships.delete(ta_membership)
446     end
447     criteria = self.all_assigned_criteria(self.tas - ta_memberships_to_remove.collect{|mem| mem.user})
448     self.criteria_coverage_count = criteria.length
449     self.save
450   end
451 
452   def add_tas_by_user_name_array(ta_user_name_array)
453     grouping_tas = []
454     ta_user_name_array.each do |ta_user_name|
455       ta = Ta.find_by_user_name(ta_user_name)
456       if !ta.nil?
457         if ta_memberships.find_by_user_id(ta.id).nil?
458           ta_memberships.create(:user => ta)
459         end
460       end
461       grouping_tas += Array(ta)
462     end
463     self.criteria_coverage_count = self.all_assigned_criteria(grouping_tas).length
464     self.save
465   end
466 
467   # Returns an array containing the group names that didn't exist
468   def self.assign_tas_by_csv(csv_file_contents, assignment_id)
469     failures = []
470     FasterCSV.parse(csv_file_contents) do |row|
471       group_name = row.shift # Knocks the first item from array
472       group = Group.find_by_group_name(group_name)
473       if group.nil?
474         failures.push(group_name)
475       else
476         grouping = group.grouping_for_assignment(assignment_id)
477         if grouping.nil?
478           failures.push(group_name)
479         else
480           grouping.add_tas_by_user_name_array(row) # The rest of the array
481         end
482       end
483     end
484     return failures
485   end
486 
487   # Update repository permissions for students, if we allow external commits
488   #   see: grant_repository_permissions and revoke_repository_permissions
489   def update_repository_permissions
490     # we do not need to do anything if we are not accepting external
491     # command-line commits
492     return unless self.write_repo_permissions?
493 
494     self.reload # VERY IMPORTANT! Make sure grouping object is not stale
495 
496     if self.is_valid?
497       grant_repository_permissions
498     else
499       # grouping became invalid, remove repo permissions
500       revoke_repository_permissions
501     end
502   end
503 
504   # When a Grouping is created, automatically create the folder for the
505   # assignment in the repository, if it doesn't already exist.
506   def create_grouping_repository_folder
507 
508     # create folder only if we are repo admin
509     if self.group.repository_admin?
510       self.group.access_repo do |repo|
511         revision = repo.get_latest_revision
512         assignment_folder = File.join('/', assignment.repository_folder)
513 
514         if revision.path_exists?(assignment_folder)
515           return true
516         else
517           txn = self.group.repo.get_transaction("markus")
518           txn.add_path(assignment_folder)
519           return self.group.repo.commit(txn)
520         end
521       end
522     end
523   end
524 
525   # Returns true, if and only if the configured repository setup
526   # allows for externally accessible repositories, in which case
527   # file submissions via the Web interface are not permitted. For
528   # now, this works for Subversion repositories only.
529   def repository_external_commits_only?
530     assignment = self.assignment
531     return !assignment.allow_web_submits
532   end
533 
534   # Should we write repository permissions for this grouping?
535   def write_repo_permissions?
536     return MarkusConfigurator.markus_config_repository_admin? &&
537            self.repository_external_commits_only?
538   end
539 
540   def assigned_tas_for_criterion(criterion)
541     result = []
542     if assignment.assign_graders_to_criteria
543       tas.each do |ta|
544         if ta.criterion_ta_associations.find_by_criterion_id(criterion.id)
545           result.push(ta)
546         end
547       end
548     end
549     return result
550   end
551 
552   def all_assigned_criteria(ta_array)
553     result = []
554     if assignment.assign_graders_to_criteria
555       ta_array.each do |ta|
556         result = result.concat(ta.get_criterion_associations_by_assignment(assignment))
557       end
558     end
559     return result.map{|a| a.criterion}.uniq
560   end
561 
562   # Get the section for this group. If assignment restricts member of a groupe
563   # to a section, all students are in the same section. Therefore, return only
564   # the inviters section
565   def section
566     if !self.inviter.nil? and self.inviter.has_section?
567       return self.inviter.section.name
568     end
569     return '-'
570   end
571 
572   private
573 
574   # Once a grouping is valid, grant (write) repository permissions for students
575   # who have accepted memberships (including the inviter)
576   #
577   # precondition: grouping is valid, self.reload has been called
578   def grant_repository_permissions
579     memberships = self.accepted_student_memberships
580     if !memberships.instance_of?(Array)
581       memberships = [memberships]
582     end
583     memberships.each do |member|
584       # Add repository read and write permissions for user,
585       # if we are required to do so
586       if self.write_repo_permissions?
587         begin
588           self.group.access_repo do |repo|
589             repo.add_user(member.user.user_name, Repository::Permission::READ_WRITE)
590           end
591         rescue Repository::UserAlreadyExistent
592           # ignore case if user has permissions already
593         end
594       end
595     end
596   end
597 
598   # We need to revoke repository permissions for student users in certain cases.
599   #
600   # For instance if the inviter has invited 2 students for a total of 3 students in
601   # that group, which in turn is the required group minimum. In that case, students
602   # who have accepted their membership, would have gotten repo permissions granted.
603   # But once one of the 2 invited students declines to be member of that group, the group
604   # becomes invalid (is below the group minimum of 3 people), and, hence, granted
605   # repo permissions for student users need to be revoked again.
606   #
607   # precondition: grouping is invalid, self.reload has been called
608   def revoke_repository_permissions
609     memberships = self.accepted_student_memberships
610     if !memberships.instance_of?(Array)
611       memberships = [memberships]
612     end
613     memberships.each do |member|
614       # Revoke permissions for students
615       if self.write_repo_permissions?
616         self.group.access_repo do |repo|
617           begin
618             # the following throws a Repository::UserNotFound
619             if repo.get_permissions(member.user.user_name) >= Repository::Permission::ANY
620               # user has some permissions, we need to remove them
621               repo.remove_user(member.user.user_name)
622             end
623           rescue Repository::UserNotFound
624             # if student has no permissions, we are safe
625           end
626         end
627       end
628     end
629   end
630 
631   # Removes repository permissions for a single StudentMembership object
632   def revoke_repository_permissions_for_membership(student_membership)
633     # Revoke permissions for student
634     self.group.access_repo do |repo|
635       if self.write_repo_permissions?
636         begin
637           # the following throws a Repository::UserNotFound
638           if repo.get_permissions(student_membership.user.user_name) >= Repository::Permission::ANY
639             # user has some permissions, we need to remove them
640             repo.remove_user(student_membership.user.user_name)
641           end
642         rescue Repository::UserNotFound
643           # if student has no permissions, we are safe
644         end
645       end
646     end
647   end
648 
649   # Removes any repository permissions of students for a to be destroyed
650   # grouping object. see :before_destroy callback above
651   def revoke_repository_permissions_for_students
652     self.reload # avoid a stale object
653 
654     memberships = self.student_memberships # get any student memberships
655     if !memberships.instance_of?(Array)
656       memberships = [memberships]
657     end
658     memberships.each do |member|
659       # Revoke permissions for students
660       self.group.access_repo do |repo|
661         if self.write_repo_permissions?
662           begin
663             # the following throws a Repository::UserNotFound
664             if repo.get_permissions(member.user.user_name) >= Repository::Permission::ANY
665               # user has some permissions, we need to remove them
666               repo.remove_user(member.user.user_name)
667             end
668           rescue Repository::UserNotFound
669             # if student has no permissions, we are safe
670           end
671         end
672       end
673     end
674   end
675 end # end class Grouping

Generated on Sun Feb 05 00:08:07 -0500 2012 with rcov 0.9.10