| Name | Total Lines | Lines of Code | Total Coverage | Code Coverage |
|---|---|---|---|---|
| app/models/assignment.rb | 549 | 401 | 84.88%
|
83.29%
|
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.
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