| Name | Total Lines | Lines of Code | Total Coverage | Code Coverage |
|---|---|---|---|---|
| app/models/rubric_criterion.rb | 318 | 209 | 98.11%
|
97.13%
|
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 'fastercsv' |
2 require 'csv' |
3 |
4 class RubricCriterion < ActiveRecord::Base |
5 before_save :round_weight |
6 after_save :update_existing_results |
7 set_table_name "rubric_criteria" # set table name correctly |
8 belongs_to :assignment, :counter_cache => true |
9 has_many :marks, :as => :markable, :dependent => :destroy |
10 has_many :criterion_ta_associations, |
11 :as => :criterion, |
12 :dependent => :destroy |
13 has_many :tas, :through => :criterion_ta_associations |
14 |
15 validates_associated :assignment, :on => :create |
16 validates_uniqueness_of :rubric_criterion_name, |
17 :scope => :assignment_id |
18 validates_presence_of :rubric_criterion_name |
19 validates_presence_of :weight |
20 validates_presence_of :assignment_id |
21 validates_presence_of :assigned_groups_count |
22 validates_numericality_of :assignment_id, |
23 :only_integer => true, |
24 :greater_than => 0 |
25 validates_numericality_of :weight |
26 validates_numericality_of :assigned_groups_count |
27 validate(:validate_total_weight, :on => :update) |
28 |
29 before_validation :update_assigned_groups_count |
30 |
31 def update_assigned_groups_count |
32 result = [] |
33 criterion_ta_associations.each do |cta| |
34 result = result.concat(cta.ta.get_groupings_by_assignment(assignment)) |
35 end |
36 self.assigned_groups_count = result.uniq.length |
37 end |
38 |
39 def validate_total_weight |
40 errors.add(:assignment, I18n.t("rubric_criteria.error_total")) if self.assignment.total_mark + (4 * (self.weight - self.weight_was)) <= 0 |
41 end |
42 |
43 # Just a small effort here to remove magic numbers... |
44 RUBRIC_LEVELS = 5 |
45 DEFAULT_WEIGHT = 1.0 |
46 DEFAULT_LEVELS = [ |
47 {'name'=> I18n.t("rubric_criteria.defaults.level_0"), 'description'=> I18n.t("rubric_criteria.defaults.description_0")}, |
48 {'name'=> I18n.t("rubric_criteria.defaults.level_1"), 'description'=> I18n.t("rubric_criteria.defaults.description_1")}, |
49 {'name'=> I18n.t("rubric_criteria.defaults.level_2"), 'description'=> I18n.t("rubric_criteria.defaults.description_2")}, |
50 {'name'=> I18n.t("rubric_criteria.defaults.level_3"), 'description'=> I18n.t("rubric_criteria.defaults.description_3")}, |
51 {'name'=> I18n.t("rubric_criteria.defaults.level_4"), 'description'=> I18n.t("rubric_criteria.defaults.description_4")} |
52 ] |
53 |
54 def mark_for(result_id) |
55 return marks.find_by_result_id(result_id) |
56 end |
57 |
58 def set_default_levels |
59 DEFAULT_LEVELS.each_with_index do |level, index| |
60 self['level_' + index.to_s + '_name'] = level['name'] |
61 self['level_' + index.to_s + '_description'] = level['description'] |
62 end |
63 end |
64 |
65 # Set all the level names at once and saves the object. |
66 # |
67 # ===Params: |
68 # |
69 # levels:: An array containing every level name. A rubric criterion contains |
70 # RUBRIC_LEVELS levels. If the array is smaller, only the first levels |
71 # are set. If the array is bigger, higher indexes are ignored. |
72 # |
73 # ===Returns: |
74 # |
75 # Wether the save operation was successful or not. |
76 def set_level_names(levels) |
77 levels.each_with_index do |level, index| |
78 self['level_' + index.to_s + '_name'] = level |
79 end |
80 save |
81 end |
82 |
83 # Create a CSV string from all the rubric criteria related to an assignment. |
84 # |
85 # ===Returns: |
86 # |
87 # A string. See create_or_update_from_csv_row for format reference. |
88 def self.create_csv(assignment) |
89 csv_string = FasterCSV.generate do |csv| |
90 assignment.rubric_criteria.each do |criterion| |
91 criterion_array = [criterion.rubric_criterion_name,criterion.weight] |
92 (0..RUBRIC_LEVELS - 1).each do |i| |
93 criterion_array.push(criterion['level_' + i.to_s + '_name']) |
94 end |
95 (0..RUBRIC_LEVELS - 1).each do |i| |
96 criterion_array.push(criterion['level_' + i.to_s + '_description']) |
97 end |
98 csv << criterion_array |
99 end |
100 end |
101 return csv_string |
102 end |
103 |
104 # Instantiate a RubricCriterion from a CSV row and attach it to the supplied |
105 # assignment. |
106 # |
107 # ===Params: |
108 # |
109 # row:: An array representing one CSV file row. Should be in the following |
110 # format: [name, weight, _names_, _descriptions_] where the _names_ part |
111 # must contain RUBRIC_LEVELS elements representing the name of each |
112 # level and the _descriptions_ part (optional) can contain up to |
113 # RUBRIC_LEVELS description (one for each level). |
114 # assignment:: The assignment to which the newly created criterion should belong. |
115 # |
116 # ===Raises: |
117 # |
118 # RuntimeError If the row does not contains enough information, if the weight value |
119 # is zero (or doesn't evaluate to a float) |
120 def self.create_or_update_from_csv_row(row, assignment) |
121 if row.length < RUBRIC_LEVELS + 2 |
122 raise I18n.t('criteria_csv_error.incomplete_row') |
123 end |
124 working_row = row.clone |
125 rubric_criterion_name = working_row.shift |
126 # If a RubricCriterion of the same name exits, load it up. Otherwise, |
127 # create a new one. |
128 criterion = assignment.rubric_criteria.find_or_create_by_rubric_criterion_name(rubric_criterion_name) |
129 #Check that the weight is not a string. |
130 begin |
131 criterion.weight = Float(working_row.shift) |
132 rescue ArgumentError => e |
133 raise I18n.t('criteria_csv_error.weight_not_number') |
134 end |
135 # Only set the position if this is a new record. |
136 if criterion.new_record? |
137 criterion.position = assignment.next_criterion_position |
138 end |
139 # next comes the level names. |
140 (0..RUBRIC_LEVELS-1).each do |i| |
141 criterion['level_' + i.to_s + '_name'] = working_row.shift |
142 end |
143 # the rest of the values are level descriptions. |
144 working_row.each_with_index do |desc, i| |
145 criterion['level_' + i.to_s + '_description'] = desc |
146 end |
147 if !criterion.save |
148 raise RuntimeError.new(criterion.errors) |
149 end |
150 return criterion |
151 end |
152 |
153 # Instantiate a RubricCriterion from a YML key |
154 # |
155 # ===Params: |
156 # |
157 # key:: key corresponding to a single RubricCriterion in the |
158 # following format: |
159 # criterion_name: |
160 # weight: # |
161 # level_0: |
162 # name: level_name |
163 # description: level_description |
164 # level_1: |
165 # [...] |
166 # assignment:: The assignment to which the newly created criterion should belong. |
167 # |
168 # ===Raises: |
169 # |
170 # RuntimeError If there is not enough information, if the weight value |
171 # is zero (or doesn't evaluate to a float) |
172 def self.create_or_update_from_yml_key(key, assignment) |
173 rubric_criterion_name = key[0] |
174 # If a RubricCriterion of the same name exits, load it up. Otherwise, |
175 # create a new one. |
176 criterion = assignment.rubric_criteria.find_or_create_by_rubric_criterion_name(rubric_criterion_name) |
177 #Check that the weight is not a string. |
178 begin |
179 criterion.weight = Float(key[1]["weight"]) |
180 rescue ArgumentError => e |
181 raise I18n.t('criteria_csv_error.weight_not_number') |
182 rescue TypeError => e |
183 raise I18n.t('criteria_csv_error.weight_not_number') |
184 rescue NoMethodError => e |
185 raise I18n.t('rubric_criteria.upload.empty_error') |
186 end |
187 # Only set the position if this is a new record. |
188 if criterion.new_record? |
189 criterion.position = assignment.next_criterion_position |
190 end |
191 # next comes the level names. |
192 (0..RUBRIC_LEVELS-1).each do |i| |
193 if key[1]["level_" + i.to_s] |
194 criterion['level_' + i.to_s + '_name'] = key[1]["level_" + i.to_s]["name"] |
195 criterion['level_' + i.to_s + '_description'] = |
196 key[1]["level_" + i.to_s]["description"] |
197 end |
198 end |
199 if !criterion.save |
200 raise RuntimeError.new(criterion.errors) |
201 end |
202 return criterion |
203 end |
204 |
205 # Parse a rubric criteria CSV file. |
206 # |
207 # ===Params: |
208 # |
209 # file:: A file object which will be tried for parsing. |
210 # assignment:: The assignment to which the new criteria should belong to. |
211 # invalid_lines:: An object to recieve all encountered _invalid_ lines. |
212 # Strings representing the faulty line followed by |
213 # a human readable error message are appended to the object |
214 # via the << operator. |
215 # |
216 # *Hint*: An array allows for an easy |
217 # access of single invalid lines. |
218 # ===Returns: |
219 # |
220 # The number of successfully created criteria. |
221 def self.parse_csv(file, assignment, invalid_lines) |
222 nb_updates = 0 |
223 FasterCSV.parse(file.read) do |row| |
224 next if FasterCSV.generate_line(row).strip.empty? |
225 begin |
226 RubricCriterion.create_or_update_from_csv_row(row, assignment) |
227 nb_updates += 1 |
228 rescue RuntimeError => e |
229 invalid_lines << row.join(',') + ": " + e.message unless invalid_lines.nil? |
230 end |
231 end |
232 return nb_updates |
233 end |
234 |
235 def get_weight |
236 return self.weight |
237 end |
238 |
239 def round_weight |
240 factor = 10.0 ** 3 |
241 self.weight = (self.weight * factor).round.to_f / factor |
242 end |
243 |
244 def all_assigned_groups |
245 result = [] |
246 tas.each do |ta| |
247 result = result.concat(ta.get_groupings_by_assignment(assignment)) |
248 end |
249 return result.uniq |
250 end |
251 |
252 def add_tas(ta_array) |
253 ta_array = Array(ta_array) |
254 associations = criterion_ta_associations.all(:conditions => {:ta_id => ta_array}) |
255 ta_array.each do |ta| |
256 # & is the mathematical set intersection operator between two arrays |
257 if (ta.criterion_ta_associations & associations).size < 1 |
258 criterion_ta_associations.create(:ta => ta, :criterion => self, :assignment => self.assignment) |
259 end |
260 end |
261 end |
262 |
263 |
264 def get_name |
265 return rubric_criterion_name |
266 end |
267 |
268 def remove_tas(ta_array) |
269 ta_array = Array(ta_array) |
270 associations_for_criteria = criterion_ta_associations.all(:conditions => {:ta_id => ta_array}) |
271 ta_array.each do |ta| |
272 # & is the mathematical set intersection operator between two arrays |
273 assoc_to_remove = (ta.criterion_ta_associations & associations_for_criteria) |
274 if assoc_to_remove.size > 0 |
275 criterion_ta_associations.delete(assoc_to_remove) |
276 assoc_to_remove.first.destroy |
277 end |
278 end |
279 end |
280 |
281 def get_ta_names |
282 return criterion_ta_associations.collect {|association| association.ta.user_name} |
283 end |
284 |
285 def has_associated_ta?(ta) |
286 if !ta.ta? |
287 return false |
288 end |
289 return !(criterion_ta_associations.find_by_ta_id(ta.id) == nil) |
290 end |
291 |
292 def add_tas_by_user_name_array(ta_user_name_array) |
293 result = ta_user_name_array.map{|ta_user_name| |
294 Ta.find_by_user_name(ta_user_name)}.compact |
295 add_tas(result) |
296 end |
297 |
298 # Returns an array containing the criterion names that didn't exist |
299 def self.assign_tas_by_csv(csv_file_contents, assignment_id) |
300 failures = [] |
301 FasterCSV.parse(csv_file_contents) do |row| |
302 criterion_name = row.shift # Knocks the first item from array |
303 criterion = RubricCriterion.find_by_assignment_id_and_rubric_criterion_name(assignment_id, criterion_name) |
304 if criterion.nil? |
305 failures.push(criterion_name) |
306 else |
307 criterion.add_tas_by_user_name_array(row) # The rest of the array |
308 end |
309 end |
310 return failures |
311 end |
312 |
313 # Updates results already entered with new criteria |
314 def update_existing_results |
315 self.assignment.submissions.each { |submission| submission.result.update_total_mark } |
316 end |
317 |
318 end |
Generated on Sun Feb 05 00:08:07 -0500 2012 with rcov 0.9.10