| … | |
… | |
| 13 | =cut |
13 | =cut |
| 14 | |
14 | |
| 15 | use strict; |
15 | use strict; |
| 16 | use warnings; |
16 | use warnings; |
| 17 | use CGI qw(); |
17 | use CGI qw(); |
| 18 | use WeBWorK::Utils qw(readFile); |
18 | use WeBWorK::Utils qw(readFile formatDateTime); |
|
|
19 | use WeBWorK::Timing; |
|
|
20 | |
|
|
21 | sub initialize { |
|
|
22 | my ($self) = @_; |
|
|
23 | my $r = $self->{r}; |
|
|
24 | my $ce = $self->{ce}; |
|
|
25 | |
|
|
26 | my $scoringDir = $ce->{courseDirs}->{scoring}; |
|
|
27 | my $courseName = $ce->{courseName}; |
|
|
28 | if (defined $r->param('scoreSelected')) { |
|
|
29 | my @selected = $r->param('selectedSet'); |
|
|
30 | my @totals = (); |
|
|
31 | foreach my $setID (@selected) { |
|
|
32 | my @everything = $self->scoreSet($setID, "everything"); |
|
|
33 | my @normal = $self->everything2normal(@everything); |
|
|
34 | my @full = $self->everything2full(@everything); |
|
|
35 | my @info = $self->everything2info(@everything); |
|
|
36 | my @totalsColumn = $self->everything2totals(@everything); |
|
|
37 | @totals = @info unless @totals; |
|
|
38 | $self->appendColumns(\@totals, \@totalsColumn); |
|
|
39 | $self->writeCSV("$scoringDir/s${setID}scr.csv", @normal); |
|
|
40 | $self->writeCSV("$scoringDir/s${setID}ful.csv", @full); |
|
|
41 | } |
|
|
42 | $self->writeCSV("$scoringDir/${courseName}_totals.csv", @totals); |
|
|
43 | } |
|
|
44 | } |
|
|
45 | |
|
|
46 | # If, some day, it becomes possible to assign a different number of problems to each student, this code |
|
|
47 | # will have to be rewritten some. |
|
|
48 | # $format can be any of "normal", "full", "everything", "info", or "totals". An undefined value defaults to "normal" |
|
|
49 | # normal: student info, the status of each problem in the set, and a "totals" column |
|
|
50 | # full: student info, the status of each problem, and the number of correct and incorrect attempts |
|
|
51 | # everything: "full" plus a totals column |
|
|
52 | # info: student info columns only |
|
|
53 | # totals: total column only |
|
|
54 | sub scoreSet { |
|
|
55 | my ($self, $setID, $format) = @_; |
|
|
56 | my $db = $self->{db}; |
|
|
57 | my @scoringData; |
|
|
58 | |
|
|
59 | $format = "normal" unless defined $format; |
|
|
60 | $format = "normal" unless $format eq "full" or $format eq "everything" or $format eq "totals" or $format eq "info"; |
|
|
61 | my $columnsPerProblem = ($format eq "full" or $format eq "everything") ? 3 : 1; |
|
|
62 | my $setRecord = $db->getGlobalSet($setID); |
|
|
63 | my %users; |
|
|
64 | foreach my $userID ($db->listSetUsers($setID)) { |
|
|
65 | my $userRecord = $db->getUser($userID); |
|
|
66 | # The key is what we'd like to sort by. |
|
|
67 | $users{$userRecord->student_id} = $userRecord; |
|
|
68 | } |
|
|
69 | my @problemIDs = $db->listGlobalProblems($setID); |
|
|
70 | |
|
|
71 | # Initialize a two-dimensional array of the proper size |
|
|
72 | for (my $i = 0; $i < keys(%users) + 7; $i++) { # 7 is how many descriptive fields there are in each column |
|
|
73 | push @scoringData, []; |
|
|
74 | } |
|
|
75 | |
|
|
76 | unless ($format eq "totals") { |
|
|
77 | $scoringData[0][0] = "NO OF FIELDS"; |
|
|
78 | $scoringData[1][0] = "SET NAME"; |
|
|
79 | $scoringData[2][0] = "PROB NUMBER"; |
|
|
80 | $scoringData[3][0] = "DUE DATE"; |
|
|
81 | $scoringData[4][0] = "DUE TIME"; |
|
|
82 | $scoringData[5][0] = "PROB VALUE"; |
|
|
83 | } |
|
|
84 | |
|
|
85 | my @userInfoColumnHeadings = ("STUDENT ID", "LAST NAME", "FIRST NAME", "SECTION", "RECITATION"); |
|
|
86 | my @userInfoFields = ("student_id", "last_name", "first_name", "section", "recitation"); |
|
|
87 | my @userKeys = sort keys %users; |
|
|
88 | |
|
|
89 | # Write identifying information about the users |
|
|
90 | unless ($format eq "totals") { |
|
|
91 | for (my $field=0; $field < @userInfoFields; $field++) { |
|
|
92 | if ($field > 0) { |
|
|
93 | for (my $i = 0; $i < 6; $i++) { |
|
|
94 | $scoringData[$i][$field] = ""; |
|
|
95 | } |
|
|
96 | } |
|
|
97 | $scoringData[6][$field] = $userInfoColumnHeadings[$field]; |
|
|
98 | for (my $user = 0; $user < @userKeys; $user++) { |
|
|
99 | my $fieldName = $userInfoFields[$field]; |
|
|
100 | $scoringData[7 + $user][$field] = $users{$userKeys[$user]}->$fieldName; |
|
|
101 | } |
|
|
102 | } |
|
|
103 | } |
|
|
104 | return @scoringData if $format eq "info"; |
|
|
105 | |
|
|
106 | # Write the problem data |
|
|
107 | my $dueDateString = formatDateTime($setRecord->due_date); |
|
|
108 | my ($dueDate, $dueTime) = $dueDateString =~ m/^([^\s]*)\s*([^\s]*)$/; |
|
|
109 | my $valueTotal = 0; |
|
|
110 | my %userStatusTotals = (); |
|
|
111 | for (my $problem = 0; $problem < @problemIDs; $problem++) { |
|
|
112 | my $globalProblem = $db->getGlobalProblem($setID, $problemIDs[$problem]); |
|
|
113 | my $column = 5 + $problem * $columnsPerProblem; |
|
|
114 | unless ($format eq "totals") { |
|
|
115 | $scoringData[0][$column] = ""; |
|
|
116 | $scoringData[1][$column] = $setRecord->set_id; |
|
|
117 | $scoringData[2][$column] = $globalProblem->problem_id; |
|
|
118 | $scoringData[3][$column] = $dueDate; |
|
|
119 | $scoringData[4][$column] = $dueTime; |
|
|
120 | $scoringData[5][$column] = $globalProblem->value; |
|
|
121 | $scoringData[6][$column] = "STATUS"; |
|
|
122 | if ($format eq "full" or $format eq "everything") { # Fill in with blanks, or maybe the problem number |
|
|
123 | for (my $row = 0; $row < 6; $row++) { |
|
|
124 | for (my $col = $column+1; $col <= $column + 2; $col++) { |
|
|
125 | if ($row == 2) { |
|
|
126 | $scoringData[$row][$col] = $globalProblem->problem_id; |
|
|
127 | } else { |
|
|
128 | $scoringData[$row][$col] = ""; |
|
|
129 | } |
|
|
130 | } |
|
|
131 | } |
|
|
132 | $scoringData[6][$column + 1] = "#corr"; |
|
|
133 | $scoringData[6][$column + 2] = "#incorr"; |
|
|
134 | } |
|
|
135 | } |
|
|
136 | $valueTotal += $globalProblem->value; |
|
|
137 | for (my $user = 0; $user < @userKeys; $user++) { |
|
|
138 | my $userProblem = $db->getMergedProblem($users{$userKeys[$user]}->user_id, $setID, $problemIDs[$problem]); |
|
|
139 | $userStatusTotals{$user} = 0 unless exists $userStatusTotals{$user}; |
|
|
140 | $userStatusTotals{$user} += $userProblem->status * $userProblem->value; |
|
|
141 | unless ($format eq "totals") { |
|
|
142 | $scoringData[7 + $user][$column] = $userProblem->status; |
|
|
143 | if ($format eq "full" or $format eq "everything") { |
|
|
144 | $scoringData[7 + $user][$column + 1] = $userProblem->num_correct; |
|
|
145 | $scoringData[7 + $user][$column + 2] = $userProblem->num_incorrect; |
|
|
146 | } |
|
|
147 | } |
|
|
148 | } |
|
|
149 | } |
|
|
150 | |
|
|
151 | # write the status totals |
|
|
152 | unless ($format eq "full") { # Ironic, isn't it? |
|
|
153 | my $totalsColumn = $format eq "totals" ? 0 : 5 + @problemIDs * $columnsPerProblem; |
|
|
154 | $scoringData[0][$totalsColumn] = ""; |
|
|
155 | $scoringData[1][$totalsColumn] = $setRecord->set_id; |
|
|
156 | $scoringData[2][$totalsColumn] = ""; |
|
|
157 | $scoringData[3][$totalsColumn] = ""; |
|
|
158 | $scoringData[4][$totalsColumn] = ""; |
|
|
159 | $scoringData[5][$totalsColumn] = $valueTotal; |
|
|
160 | $scoringData[6][$totalsColumn] = "total"; |
|
|
161 | for (my $user = 0; $user < @userKeys; $user++) { |
|
|
162 | $scoringData[7+$user][$totalsColumn] = $userStatusTotals{$user}; |
|
|
163 | } |
|
|
164 | } |
|
|
165 | |
|
|
166 | return @scoringData; |
|
|
167 | } |
|
|
168 | |
|
|
169 | # Often it's more efficient to just get everything out of the database |
|
|
170 | # and then pick out what you want later. Hence, these "everything2*" functions |
|
|
171 | sub everything2info { |
|
|
172 | my ($self, @everything) = @_; |
|
|
173 | my @result = (); |
|
|
174 | foreach my $row (@everything) { |
|
|
175 | push @result, [@{$row}[0..4]]; |
|
|
176 | } |
|
|
177 | return @result; |
|
|
178 | } |
|
|
179 | |
|
|
180 | sub everything2normal { |
|
|
181 | my ($self, @everything) = @_; |
|
|
182 | my @result = (); |
|
|
183 | foreach my $row (@everything) { |
|
|
184 | my @row = @$row; |
|
|
185 | my @newRow = (); |
|
|
186 | push @newRow, @row[0..4]; |
|
|
187 | for (my $i = 5; $i < @row; $i+=3) { |
|
|
188 | push @newRow, $row[$i]; |
|
|
189 | } |
|
|
190 | push @newRow, $row[$#row]; |
|
|
191 | push @result, [@newRow]; |
|
|
192 | } |
|
|
193 | return @result; |
|
|
194 | } |
|
|
195 | |
|
|
196 | sub everything2full { |
|
|
197 | my ($self, @everything) = @_; |
|
|
198 | my @result = (); |
|
|
199 | foreach my $row (@everything) { |
|
|
200 | push @result, [@{$row}[0..($#{$row}-1)]]; |
|
|
201 | } |
|
|
202 | return @result; |
|
|
203 | } |
|
|
204 | |
|
|
205 | sub everything2totals { |
|
|
206 | my ($self, @everything) = @_; |
|
|
207 | my @result = (); |
|
|
208 | foreach my $row (@everything) { |
|
|
209 | push @result, [${$row}[$#{$row}]]; |
|
|
210 | } |
|
|
211 | } |
|
|
212 | |
|
|
213 | sub appendColumns { |
|
|
214 | my ($self, $a1, $a2) = @_; |
|
|
215 | my @a1 = @$a1; |
|
|
216 | my @a2 = @$a2; |
|
|
217 | # FIXME you were here |
|
|
218 | } |
| 19 | |
219 | |
| 20 | # Reads a CSV file and returns an array of arrayrefs, each containing a |
220 | # Reads a CSV file and returns an array of arrayrefs, each containing a |
| 21 | # column of data: |
221 | # row of data: |
| 22 | # (["c1r1", "c1r2", "c1r3"], ["c2r1", "c2r2", "c2r3"]) |
222 | # (["c1r1", "c1r2", "c1r3"], ["c2r1", "c2r2", "c2r3"]) |
| 23 | sub readCSV { |
223 | sub readCSV { |
| 24 | my ($self, $filename) = @_; |
224 | my ($self, $fileName) = @_; |
| 25 | my @result = (); |
225 | my @result = (); |
| 26 | my @columns = split m/\n/, readFile($fileName); |
226 | my @rows = split m/\n/, readFile($fileName); |
| 27 | foreach my $column (@columns) { |
227 | foreach my $row (@rows) { |
| 28 | push @result, [split m/\s*,\s*/, $column]; |
228 | push @result, [split m/\s*,\s*/, $row]; |
| 29 | } |
229 | } |
| 30 | return @result; |
230 | return @result; |
| 31 | } |
231 | } |
| 32 | |
232 | |
| 33 | # Write a CSV file from an array in the same format that readCSV produces |
233 | # Write a CSV file from an array in the same format that readCSV produces |
| 34 | sub writeCSV { |
234 | sub writeCSV { |
| 35 | my ($self, $filename, @csv) = @_; |
235 | my ($self, $filename, @csv) = @_; |
|
|
236 | |
|
|
237 | my @lengths = (); |
|
|
238 | for (my $row = 0; $row < @csv; $row++) { |
|
|
239 | for (my $column = 0; $column < @{$csv[$row]}; $column++) { |
|
|
240 | $lengths[$column] = 0 unless defined $lengths[$column]; |
|
|
241 | $lengths[$column] = length $csv[$row][$column] if length $csv[$row][$column] > $lengths[$column]; |
|
|
242 | } |
|
|
243 | } |
|
|
244 | |
| 36 | open my $fh, ">", $filename; |
245 | open my $fh, ">", $filename; |
| 37 | foreach my $column (@csv) { |
246 | foreach my $row (@csv) { |
| 38 | my $maxLength = $self->maxLength($column); |
247 | my @rowPadded = (); |
| 39 | print (join ",", map {$self->pad($_, $maxLength)} $@column); |
248 | foreach (my $column = 0; $column < @$row; $column++) { |
|
|
249 | push @rowPadded, $self->pad($row->[$column], $lengths[$column] + 1); |
|
|
250 | } |
|
|
251 | print $fh join(",", @rowPadded); |
| 40 | print "\n"; |
252 | print $fh "\n"; |
| 41 | } |
253 | } |
| 42 | close $fh; |
254 | close $fh; |
| 43 | } |
255 | } |
| 44 | |
256 | |
| 45 | # As soon as backwards compatability is no longer a concern and we don't expect to have |
257 | # As soon as backwards compatability is no longer a concern and we don't expect to have |
| 46 | # to use old ww1.x code to read the output anymore, I recommend switching to using |
258 | # to use old ww1.x code to read the output anymore, I recommend switching to using |
| 47 | # these routines, which are more versatile and compatable with other programs which |
259 | # these routines, which are more versatile and compatable with other programs which |
| 48 | # deal with CSV files. |
260 | # deal with CSV files. |
| 49 | sub readStandardCSV { |
261 | sub readStandardCSV { |
| 50 | my ($self, $filename) = @_; |
262 | my ($self, $fileName) = @_; |
| 51 | my @result = (); |
263 | my @result = (); |
| 52 | my @colunms = split m/\n/, readFile($fileName); |
264 | my @rows = split m/\n/, readFile($fileName); |
| 53 | foreach my $column (@columns) { |
265 | foreach my $row (@rows) { |
| 54 | push @result, [$self->splitQuote($column)]; |
266 | push @result, [$self->splitQuoted($row)]; |
| 55 | } |
267 | } |
| 56 | return @result; |
268 | return @result; |
| 57 | } |
269 | } |
| 58 | |
270 | |
| 59 | sub writeStandardCSV { |
271 | sub writeStandardCSV { |
| 60 | my ($self, $filename, @csv) = @_; |
272 | my ($self, $filename, @csv) = @_; |
| 61 | open my $fh, ">", $filename; |
273 | open my $fh, ">", $filename; |
| 62 | foreach my $column (@csv) { |
274 | foreach my $row (@csv) { |
| 63 | print (join ",", map {$self->quote} $@column); |
275 | print $fh (join ",", map {$self->quote($_)} @$row); |
| 64 | print "\n"; |
276 | print $fh "\n"; |
| 65 | } |
277 | } |
| 66 | close $fh; |
278 | close $fh; |
| 67 | } |
279 | } |
| 68 | |
280 | |
| 69 | ### |
281 | ### |
| … | |
… | |
| 83 | # to make it easy to do so. |
295 | # to make it easy to do so. |
| 84 | sub splitQuoted { |
296 | sub splitQuoted { |
| 85 | my ($self, $string) = @_; |
297 | my ($self, $string) = @_; |
| 86 | my ($leadingSpace, $preText, $quoted, $postText, $trailingSpace, $result); |
298 | my ($leadingSpace, $preText, $quoted, $postText, $trailingSpace, $result); |
| 87 | my @result = (); |
299 | my @result = (); |
| 88 | my $continue = 1 |
300 | my $continue = 1; |
| 89 | while ($continue) { |
301 | while ($continue) { |
| 90 | $string =~ m/\G(\s*)/; |
302 | $string =~ m/\G(\s*)/gc; |
| 91 | $leadingSpace = $1; |
303 | $leadingSpace = $1; |
| 92 | $string =~ m/\G([^",]*)/; |
304 | $string =~ m/\G([^",]*)/gc; |
| 93 | $preText = $1; |
305 | $preText = $1; |
| 94 | if ($string =~ m/\G"((?:[^"]|"")*)"/) { |
306 | if ($string =~ m/\G"((?:[^"]|"")*)"/gc) { |
| 95 | $quoted = $1; |
307 | $quoted = $1; |
| 96 | } |
308 | } |
| 97 | $string =~ m/\G([^,]*?)(\s*)(,?)/; |
309 | $string =~ m/\G([^,]*?)(\s*)(,?)/gc; |
| 98 | $postText, $trailingSpace, $continue = ($1, $2, $3); |
310 | ($postText, $trailingSpace, $continue) = ($1, $2, $3); |
|
|
311 | |
|
|
312 | $preText = "" unless defined $preText; |
|
|
313 | $postText = "" unless defined $postText; |
|
|
314 | $quoted = "" unless defined $quoted; |
|
|
315 | |
| 99 | if (defined $quoted and (not defined $preText and not defined $postText)) { |
316 | if ($quoted and (not $preText and not $postText)) { |
| 100 | $quoted = s/""/"/; |
317 | $quoted =~ s/""/"/; |
| 101 | $result = $quoted; |
318 | $result = $quoted; |
| 102 | } else { |
319 | } else { |
| 103 | $preText = "" unless defined $preText; |
|
|
| 104 | $postText = "" unless defined $postText; |
|
|
| 105 | $quoted = "" unless defined $quoted; |
|
|
| 106 | $result = "$preText$quoted$postText"; |
320 | $result = "$preText$quoted$postText"; |
| 107 | } |
321 | } |
| 108 | push @result, $result; |
322 | push @result, $result; |
| 109 | } |
323 | } |
| 110 | return @result; |
324 | return @result; |
| … | |
… | |
| 113 | # This particular quoting method does CSV-style (double a quote to escape it) quoting when necessary. |
327 | # This particular quoting method does CSV-style (double a quote to escape it) quoting when necessary. |
| 114 | sub quote { |
328 | sub quote { |
| 115 | my ($self, $string) = @_; |
329 | my ($self, $string) = @_; |
| 116 | if ($string =~ m/[", ]/) { |
330 | if ($string =~ m/[", ]/) { |
| 117 | $string =~ s/"/""/; |
331 | $string =~ s/"/""/; |
| 118 | $string =~ "\"$string\""; |
332 | $string = "\"$string\""; |
|
|
333 | } |
| 119 | return $string; |
334 | return $string; |
| 120 | } |
|
|
| 121 | } |
335 | } |
| 122 | |
336 | |
| 123 | sub pad { |
337 | sub pad { |
| 124 | my ($self, $string, $padTo) = @_; |
338 | my ($self, $string, $padTo) = @_; |
| 125 | my $spaces = $padTo - length $string; |
339 | my $spaces = $padTo - length $string; |
| … | |
… | |
| 132 | foreach my $cell (@$arrayRef) { |
346 | foreach my $cell (@$arrayRef) { |
| 133 | $max = length $cell unless length $cell < $max; |
347 | $max = length $cell unless length $cell < $max; |
| 134 | } |
348 | } |
| 135 | return $max; |
349 | return $max; |
| 136 | } |
350 | } |
|
|
351 | |
|
|
352 | 1; |