Rcov C0 Coverage Information - RCov

app/models/assignment.rb

Name Total Lines Lines of Code Total Coverage Code Coverage
app/models/assignment.rb 549 401
84.88%
83.29%

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 require 'csv_invalid_line_error'
2 class Assignment < ActiveRecord::Base
3   
4   MARKING_SCHEME_TYPE = {
5     :flexible => 'flexible',
6     :rubric => 'rubric'
7   }
8 
9   has_many :rubric_criteria, :class_name => "RubricCriterion", :order => :position
10   has_many :flexible_criteria, :class_name => "FlexibleCriterion", :order => :position
11   has_many :assignment_files
12   has_many :test_files
13   has_many :criterion_ta_associations
14   has_one  :submission_rule 
15   accepts_nested_attributes_for :submission_rule, :allow_destroy => true
16   accepts_nested_attributes_for :assignment_files, :allow_destroy => true
17   accepts_nested_attributes_for :test_files, :allow_destroy => true
18 
19   has_many :annotation_categories
20 
21   has_many :groupings
22   has_many :ta_memberships, :class_name => "TaMembership", :through => :groupings
23   has_many :student_memberships, :through => :groupings
24 
25   has_many :submissions, :through => :groupings
26   has_many :groups, :through => :groupings
27 
28   has_many :notes, :as => :noteable, :dependent => :destroy
29 
30   validates_associated :assignment_files
31 
32   validates_presence_of     :repository_folder
33   validates_presence_of     :short_identifier, :group_min
34   validates_uniqueness_of   :short_identifier, :case_sensitive => true
35 
36   validates_numericality_of :group_min, :only_integer => true,  :greater_than => 0
37   validates_numericality_of :group_max, :only_integer => true
38   validates_numericality_of :tokens_per_day, :only_integer => true,  :greater_than_or_equal_to => 0
39 
40   validates_associated :submission_rule
41   validates_presence_of :submission_rule
42 
43   validates_presence_of :marking_scheme_type
44   # since allow_web_submits is a boolean, validates_presence_of does not work:
45   # see the Rails API documentation for validates_presence_of (Model validations)
46   validates_inclusion_of :allow_web_submits, :in => [true, false]
47   validates_inclusion_of :display_grader_names_to_students, :in => [true, false]
48   validates_inclusion_of :enable_test, :in => [true, false]
49   validates_inclusion_of :assign_graders_to_criteria, :in => [true, false]
50   
51  before_save :reset_collection_time
52 
53   def validate
54     if (group_max && group_min) && group_max < group_min
55       errors.add(:group_max, "must be greater than the minimum number of groups")
56     end
57     if Time.zone.parse(due_date.to_s).nil?
58       errors.add :due_date, 'is not a valid date'
59     end
60   end
61 
62   # Are we past the due date for this assignment?
63   def past_due_date?
64     return !due_date.nil? && Time.now > due_date
65   end
66 
67   def past_collection_date?
68     return Time.now > submission_rule.calculate_collection_time
69   end
70 
71   # Returns a Submission instance for this user depending on whether this
72   # assignment is a group or individual assignment
73   def submission_by(user) #FIXME: needs schema updates
74 
75     # submission owner is either an individual (user) or a group
76     owner = self.group_assignment? ? self.group_by(user.id) : user
77     return nil unless owner
78 
79     # create a new submission for the owner
80     # linked to this assignment, if it doesn't exist yet
81 
82     # submission = owner.submissions.find_or_initialize_by_assignment_id(id)
83     # submission.save if submission.new_record?
84     # return submission
85 
86 
87     assignment_groupings = user.active_groupings.delete_if {|grouping|
88       grouping.assignment.id != self.id
89     }
90 
91     unless assignment_groupings.empty?
92       return assignment_groupings.first.submissions.first
93     else
94       return nil
95     end
96   end
97 
98   # Return true if this is a group assignment; false otherwise
99   def group_assignment?
100     instructor_form_groups || group_min != 1 || group_max > 1
101   end
102 
103   # Returns the group by the user for this assignment. If pending=true,
104   # it will return the group that the user has a pending invitation to.
105   # Returns nil if user does not have a group for this assignment, or if it is
106   # not a group assignment
107   def group_by(uid, pending=false)
108     return nil unless group_assignment?
109 
110     # condition = "memberships.user_id = ?"
111     # condition += " and memberships.status != 'rejected'"
112     # add non-pending status clause to condition
113     # condition += " and memberships.status != 'pending'" unless pending
114     # groupings.find(:first, :include => :memberships, :conditions => [condition, uid]) #FIXME: needs schema update
115 
116     #FIXME: needs to be rewritten using a proper query...
117     return User.find(uid).accepted_grouping_for(self.id)
118   end
119 
120   # Make a list of students without any groupings
121   def no_grouping_students_list
122    @students = Student.all(:order => :last_name, :conditions => {:hidden => false})
123    @students_list = []
124    @students.each do |s|
125      if !s.has_accepted_grouping_for?(self.id)
126        @students_list.push(s)
127       end
128    end
129    return @students_list
130   end
131 
132   def display_for_note
133     return short_identifier
134   end
135 
136   # Make a list of the students an inviter can invite for his grouping
137   # TODO check if this method is ever used anywhere [Not used anywhere as of 2010/03/30]
138   # TODO unit tests
139   def can_invite_for(gid)
140     grouping = Grouping.find(gid)
141     students = self.no_grouping_students_list
142     students_list = []
143     students.each do |s|
144       if !grouping.pending?(s)
145         # if assignment doesn't restrict groups member per sections
146         if !self.section_groups_only
147           students_list.push(s)
148         else
149           # if assignment restricts groupmembers per section
150           if student.section == grouping.inviter.section
151             students_list.push(s)
152           end
153         end
154       end
155     end
156     return students_list
157   end
158 
159   def total_mark
160     total = 0
161     if self.marking_scheme_type == 'rubric'
162       rubric_criteria.each do |criterion|
163         total = total + criterion.weight * 4
164       end
165     else
166       total = flexible_criteria.sum('max')
167     end
168     return total
169   end
170 
171   # calculates the average of released results for this assignment
172   def set_results_average
173     groupings = Grouping.find_all_by_assignment_id(self.id)
174     results_count = 0
175     results_sum = 0
176     groupings.each do |grouping|
177       submission = grouping.current_submission_used
178       if !submission.nil? && submission.has_result?
179         result = submission.result
180         if result.released_to_students
181           results_sum += result.total_mark
182           results_count += 1
183         end
184       end
185     end
186     if results_count == 0
187       return false # no marks released for this assignment
188     end
189     # Need to avoid divide by zero
190     if results_sum == 0
191       self.results_average = 0
192       return self.save
193     end
194     avg_quantity = results_sum / results_count
195     # compute average in percent
196     self.results_average = (avg_quantity * 100 / self.total_mark)
197     self.save
198   end
199 
200   def total_criteria_weight
201     factor = 10.0 ** 2
202     return (rubric_criteria.sum('weight') * factor).floor / factor
203   end
204 
205   def add_group(new_group_name=nil)
206     if self.group_name_autogenerated
207       group = Group.new
208       group.save(false)
209       group.group_name = group.get_autogenerated_group_name
210       group.save
211     else
212       return nil if new_group_name.nil?
213       if Group.find(:first, :conditions => {:group_name => new_group_name})
214         group = Group.find(:first, :conditions => {:group_name =>       new_group_name})
215         if !self.groupings.find_by_group_id(group.id).nil?
216           raise "Group #{new_group_name} already exists"
217         end
218       else
219         group = Group.new
220         group.group_name = new_group_name
221         group.save
222       end
223     end
224     grouping = Grouping.new
225     grouping.group = group
226     grouping.assignment = self
227     grouping.save
228     return grouping
229   end
230 
231 
232   # Create all the groupings for an assignment where students don't work
233   # in groups.
234   def create_groupings_when_students_work_alone
235      @students = Student.find(:all)
236      for student in @students do
237        if !student.has_accepted_grouping_for?(self.id)
238         student.create_group_for_working_alone_student(self.id)
239        end
240      end
241   end
242 
243   # Clones the Groupings from the assignment with id assignment_id
244   # into self.  Destroys any previously existing Groupings associated
245   # with this Assignment
246   def clone_groupings_from(assignment_id)
247     original_assignment = Assignment.find(assignment_id)
248     self.transaction do
249       self.group_min = original_assignment.group_min
250       self.group_max = original_assignment.group_max
251       self.student_form_groups = original_assignment.student_form_groups
252       self.group_name_autogenerated = original_assignment.group_name_autogenerated
253       self.group_name_displayed = original_assignment.group_name_displayed
254       self.groupings.destroy_all
255       self.save
256       self.reload
257       original_assignment.groupings.each do |g|
258         unhidden_student_memberships = g.accepted_student_memberships.select do |m|
259           !m.user.hidden
260         end
261         unhidden_ta_memberships = g.ta_memberships.select do |m|
262           !m.user.hidden
263         end
264         #create the memberships for any user that is not hidden
265         if !unhidden_student_memberships.empty?
266           #create the groupings
267           grouping = Grouping.new
268           grouping.group_id = g.group_id
269           grouping.assignment_id = self.id
270           grouping.admin_approved = g.admin_approved
271           raise "Could not save grouping" if !grouping.save
272           all_memberships = unhidden_student_memberships + unhidden_ta_memberships
273           all_memberships.each do |m|
274             membership = Membership.new
275             membership.user_id = m.user_id
276             membership.type = m.type
277             membership.membership_status = m.membership_status
278             raise "Could not save membership" if !(grouping.memberships << membership)
279           end
280           # Ensure all student members have permissions on their group repositories
281           grouping.update_repository_permissions
282         end
283       end
284     end
285   end
286 
287   # Add a group and corresponding grouping as provided in
288   # the passed in Array.
289   # Format: [ groupname, repo_name, member, member, etc ]
290   # Any member names that do not exist in the database will simply be ignored
291   # (This makes it possible to have empty groups created from a bad csv row)
292   def add_csv_group(row)
293     return if row.length == 0
294 
295     # Note: We cannot use find_or_create_by here, because it has its own
296     # save semantics. We need to set and save attributes in a very particular
297     # order, so that everything works the way we want it to.
298     group = Group.find_by_group_name(row[0])
299     if group.nil?
300       group = Group.new
301       group.group_name = row[0]
302     end
303 
304     # Since repo_name of "group" will be set before the first save call, the
305     # set repo_name will be used instead of the autogenerated name. See
306     # set_repo_name and build_repository in the groups model. Also, see
307     # create_group_for_working_alone_student in the students model for
308     # similar semantics.
309     if is_candidate_for_setting_custom_repo_name?(row)
310       # Do this only if user_name exists and is a student.
311       if !Student.find_by_user_name(row[2]).nil?
312         group.repo_name = row[0]
313       else
314         # Student name does not exist, use provided repo_name
315         group.repo_name = row[1].strip # remove whitespace
316       end
317     end
318 
319     # If we are not repository admin, set the repository name as provided
320     # in the csv upload file
321     if !group.repository_admin?
322       group.repo_name = row[1].strip # remove whitespace
323     end
324     # Note: after_create hook build_repository might raise
325     # Repository::RepositoryCollision. If it does, it adds the colliding
326     # repo_name to errors.on_base. This is how we can detect repo
327     # collisions here. Unfortunately, we can't raise an exception
328     # here, because we still want the grouping to be created. This really
329     # shouldn't happen anyway, because the lookup earlier should prevent
330     # repo collisions e.g. when uploading the same CSV file twice.
331     group.save
332     if !group.errors.on_base.nil?
333       collision_error = I18n.t("csv.repo_collision_warning",
334                           { :repo_name => group.errors.on_base,
335                             :group_name => row[0] })
336     end
337 
338     # Create a new Grouping for this assignment and the newly
339     # crafted group
340     grouping = Grouping.new(:assignment => self, :group => group)
341     grouping.save
342 
343     # Form groups
344     start_index_group_members = 2 # first field is the group-name, second the repo name, so start at field 3
345     (start_index_group_members..(row.length - 1)).each do |i|
346       student = Student.find_by_user_name(row[i].strip) # remove whitespace
347       if !student.nil?
348         if (grouping.student_membership_number == 0)
349           # Add first valid member as inviter to group.
350           grouping.group_id = group.id
351           grouping.save # grouping has to be saved, before we can add members
352           grouping.add_member(student, StudentMembership::STATUSES[:inviter])
353         else
354           grouping.add_member(student)
355         end
356       end
357       
358     end
359     return collision_error
360   end
361 
362   # Updates repository permissions for all groupings of
363   # an assignment. This is a handy method, if for example grouping
364   # creation/deletion gets rolled back. The rollback does not
365   # reestablish proper repository permissions.
366   def update_repository_permissions_forall_groupings
367     # IMPORTANT: need to reload from DB
368     self.reload
369     groupings.each do |grouping|
370       grouping.update_repository_permissions
371     end
372   end
373 
374   def grouped_students
375     result_students = []
376     student_memberships.each do |student_membership|
377       result_students.push(student_membership.user)
378     end
379     return result_students
380   end
381 
382   def ungrouped_students
383     Student.all(:conditions => {:hidden => false}) - grouped_students
384   end
385 
386   def valid_groupings
387     result = []
388     groupings.all(:include => [{:student_memberships => :user}]).each do |grouping|
389       if grouping.admin_approved || grouping.student_memberships.count >= group_min
390         result.push(grouping)
391       end
392     end
393     return result
394   end
395 
396   def invalid_groupings
397     return groupings - valid_groupings
398   end
399 
400   def assigned_groupings
401     return groupings.all(:joins => :ta_memberships, :include => [{:ta_memberships => :user}]).uniq
402 
403   end
404 
405   def unassigned_groupings
406     return groupings - assigned_groupings
407   end
408 
409   # Get a list of subversion client commands to be used for scripting
410   def get_svn_export_commands
411     svn_commands = [] # the commands to be exported
412     self.submissions.each do |submission|
413       grouping = submission.grouping
414       svn_commands.push("svn export -r #{submission.revision_number} #{grouping.group.repository_external_access_url}/#{self.repository_folder} \"#{grouping.group.group_name}\"")
415     end
416     return svn_commands
417   end
418 
419   # Get a list of group_name, repo-url pairs
420   def get_svn_repo_list
421     string = FasterCSV.generate do |csv|
422       self.groupings.each do |grouping|
423         group = grouping.group
424         csv << [group.group_name,group.repository_external_access_url]
425       end
426     end
427     return string
428   end
429 
430   # Get a simple CSV report of marks for this assignment
431   def get_simple_csv_report
432     students = Student.all
433     out_of = self.total_mark
434     csv_string = FasterCSV.generate do |csv|
435        students.each do |student|
436          final_result = []
437          final_result.push(student.user_name)
438          grouping = student.accepted_grouping_for(self.id)
439          if grouping.nil? || !grouping.has_submission?
440            final_result.push('')
441          else
442            submission = grouping.current_submission_used
443            final_result.push(submission.result.total_mark / out_of * 100)
444          end
445          csv << final_result
446        end
447     end
448     return csv_string
449   end
450 
451   # Get a detailed CSV report of marks (includes each criterion) for this assignment
452   def get_detailed_csv_report
453     out_of = self.total_mark
454     students = Student.all
455     rubric_criteria = self.rubric_criteria
456     csv_string = FasterCSV.generate do |csv|
457       students.each do |student|
458         final_result = []
459         final_result.push(student.user_name)
460         grouping = student.accepted_grouping_for(self.id)
461         if grouping.nil? || !grouping.has_submission?
462           final_result.push('')
463           rubric_criteria.each do |rubric_criterion|
464             final_result.push('')
465             final_result.push(rubric_criterion.weight)
466           end
467           final_result.push('')
468           final_result.push('')
469         else
470           submission = grouping.current_submission_used
471           final_result.push(submission.result.total_mark / out_of * 100)
472           rubric_criteria.each do |rubric_criterion|
473             mark = submission.result.marks.find_by_markable_id_and_markable_type(rubric_criterion.id, "RubricCriterion")
474             if mark.nil?
475               final_result.push('')
476             else
477               final_result.push(mark.mark || '')
478             end
479             final_result.push(rubric_criterion.weight)
480           end
481           final_result.push(submission.result.get_total_extra_points)
482           final_result.push(submission.result.get_total_extra_percentage)
483         end
484         # push grace credits info
485         grace_credits_data = student.remaining_grace_credits.to_s + "/" + student.grace_credits.to_s
486         final_result.push(grace_credits_data)
487 
488         csv << final_result
489       end
490     end
491     return csv_string
492   end
493 
494   def replace_submission_rule(new_submission_rule)
495     if self.submission_rule.nil?
496       self.submission_rule = new_submission_rule
497       self.save
498     else
499       self.submission_rule.destroy
500       self.submission_rule = new_submission_rule
501       self.save
502     end
503   end
504 
505   def next_criterion_position
506     return self.rubric_criteria.size + 1
507   end
508 
509   def get_criteria
510     if self.marking_scheme_type == 'rubric'
511        return self.rubric_criteria
512     else
513        return self.flexible_criteria
514     end
515   end
516 
517   def criteria_count
518     if self.marking_scheme_type == 'rubric'
519        return self.rubric_criteria.size
520     else
521        return self.flexible_criteria.size
522     end
523   end
524 
525   private
526 
527   # Returns true if we are safe to set the repository name
528   # to a non-autogenerated value. Called by add_csv_group.
529   def is_candidate_for_setting_custom_repo_name?(row)
530     # Repository name can be customized if
531     #  - this assignment is set up to allow external submits only
532     #  - group_max = 1
533     #  - there's only one student member in this row of the csv and
534     #  - the group name is equal to the only group member
535     if MarkusConfigurator.markus_config_repository_admin? &&
536        self.allow_web_submits == false &&
537        row.length == 3 && self.group_max == 1 &&
538        !row[2].blank? && row[0] == row[2]
539       return true
540     else
541       return false
542     end
543   end
544 
545   def reset_collection_time
546     submission_rule.reset_collection_time
547   end
548 
549 end

Generated on Thu Sep 09 00:10:34 -0400 2010 with rcov 0.9.8