1 | # -*- perl -*-
|
2 | #
|
3 | # Copyright (C) 2011-2021 Alexis Bienvenüe <paamc@passoire.fr>
|
4 | #
|
5 | # This file is part of Auto-Multiple-Choice
|
6 | #
|
7 | # Auto-Multiple-Choice is free software: you can redistribute it
|
8 | # and/or modify it under the terms of the GNU General Public License
|
9 | # as published by the Free Software Foundation, either version 2 of
|
10 | # the License, or (at your option) any later version.
|
11 | #
|
12 | # Auto-Multiple-Choice is distributed in the hope that it will be
|
13 | # useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
14 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
15 | # General Public License for more details.
|
16 | #
|
17 | # You should have received a copy of the GNU General Public License
|
18 | # along with Auto-Multiple-Choice. If not, see
|
19 | # <http://www.gnu.org/licenses/>.
|
20 |
|
21 | use warnings;
|
22 | use 5.012;
|
23 |
|
24 | package AMC::DataModule::scoring;
|
25 |
|
26 | # AMC scoring management.
|
27 |
|
28 | # This module is used to store (in a SQLite database) and handle all
|
29 | # data concerning data scoring (scoring strategies, scores and marks).
|
30 |
|
31 | # TERMINOLOGY NOTE:
|
32 | #
|
33 | # 'student' refers to the student number that is written at the top of
|
34 | # each page, in the format +<student>/<page>/<check>+
|
35 | #
|
36 | # If the questions are printed from AMC, and not photocopied, each
|
37 | # physical student has a different student number on his sheet.
|
38 | #
|
39 | # If some questions are photocopied before beeing distributed to the
|
40 | # students, several students can have the same student number. To make
|
41 | # a difference between their completed answer sheets, a 'copy' number
|
42 | # is added. 'copy' is 1 for the first student using a given student
|
43 | # number sheet, then 2, and so on.
|
44 | #
|
45 | # Hence, a completed answer sheet is identified by the (student,copy)
|
46 | # couple, and a printed sheet (and correct answers, scoring
|
47 | # strategies) is identified by the student number only.
|
48 | #
|
49 | # 'question' is a number associated by LaTeX with every different
|
50 | # question (based on the small text given as XXX in the
|
51 | # \begin{question}{XXX} or \begin{questionmult}{XXX} commands).
|
52 | #
|
53 | # 'answer' is the answer number, starting from 1 for each question,
|
54 | # before beeing shuffled.
|
55 |
|
56 | # TABLES:
|
57 | #
|
58 | # title contains the titles (argument of the \begin{question} and
|
59 | # \begin{questionmult} commands) of all the questions
|
60 | #
|
61 | # * question is the question number, as created by LaTeX and used in
|
62 | # the databases <layout>, <capture>.
|
63 | #
|
64 | # * title id the title of the question.
|
65 | #
|
66 | # default holds the default scoring strategies, as specified with the
|
67 | # \scoringDefaultM and \scoringDefaultS commands in the LaTeX source
|
68 | # file. This table contains 2 rows.
|
69 | #
|
70 | # * type is the question type, either QUESTION_SIMPLE or QUESTION_MULT
|
71 | # (these constants are defined in this module).
|
72 | #
|
73 | # * strategy is the default strategy string for this question type.
|
74 | #
|
75 | # main holds scoring strategies defined outside question/questionmult
|
76 | # environments, either outside the onecopy/examcopy data (with
|
77 | # student=-1), or inside (student=current student number).
|
78 | #
|
79 | # * student is the student number.
|
80 | #
|
81 | # * strategy is the strategy string given in the LaTeX file as an
|
82 | # argument of the \scoring command.
|
83 | #
|
84 | # question holds scoring strategies for questions.
|
85 | #
|
86 | # * student is the student number.
|
87 | #
|
88 | # * question is the question number.
|
89 | #
|
90 | # * type is the question type, either QUESTION_SIMPLE or
|
91 | # QUESTION_MULT
|
92 | #
|
93 | # * indicative is 1 if the question is indicative (the score is not
|
94 | # taken into account when computing the student mark).
|
95 | #
|
96 | # * strategy is the question scoring strategy string, given in the
|
97 | # LaTeX file inside the question/questionmult environment (but
|
98 | # before \correctchoice and \wrongchoice commands).
|
99 | #
|
100 | # answer holds scoring strategies concerning answers.
|
101 | #
|
102 | # * student is the student number.
|
103 | #
|
104 | # * question is the question number.
|
105 | #
|
106 | # * answer is the answer number, starting from 1 for each question.
|
107 | #
|
108 | # * correct is 1 if this choice is correct (use of \correctchoice).
|
109 | #
|
110 | # * strategy is the answer scoring strategy string, given in the LaTeX
|
111 | # file after the corresponding correctchoice/wrongchoice commands.
|
112 | #
|
113 | # score holds the questions scores for each student.
|
114 | #
|
115 | # * student is the student number.
|
116 | #
|
117 | # * copy is the copy number.
|
118 | #
|
119 | # * question is the question number.
|
120 | #
|
121 | # * score is the score resulting from applying the scoring strategy to
|
122 | # the student's answers.
|
123 | #
|
124 | # * why is a small string that is used to know when special cases has
|
125 | # been encountered:
|
126 | #
|
127 | # E means syntax error (several boxes ticked for a simple
|
128 | # question, or " none of the above" AND another box ticked for a
|
129 | # multiple question).
|
130 | #
|
131 | # V means that no box are ticked.
|
132 | #
|
133 | # P means that a floor has been applied.
|
134 | #
|
135 | # * max is the question score associated to a copy where all answers
|
136 | # are correct (or 1 for indicative questions).
|
137 | #
|
138 | # mark holds global marks of the students.
|
139 | #
|
140 | # * student is the student number.
|
141 | #
|
142 | # * copy is the copy number.
|
143 | #
|
144 | # * total is the total score (sum of the questions scores).
|
145 | #
|
146 | # * max is the total score associated to a perfect copy.
|
147 | #
|
148 | # * mark is the student mark.
|
149 | #
|
150 | # code holds the codes entered by the students (see \AMCcode).
|
151 | #
|
152 | # * student is the student number.
|
153 | #
|
154 | # * copy is the copy number.
|
155 | #
|
156 | # * code is the code name.
|
157 | #
|
158 | # * value is the code value.
|
159 | #
|
160 | # * direct is 1 if the score comes directly from a decoded zone image,
|
161 | # and 0 if it is computed while scoring.
|
162 |
|
163 | # VARIABLES:
|
164 | #
|
165 | # postcorrect_flag is 1 if the postcorrect mode is supposed to be used
|
166 | # (correct answers are not indicated in the LaTeX source, but will be
|
167 | # set from a teacher completed answer sheet).
|
168 | #
|
169 | # postcorrect_student
|
170 | # postcorrect_copy identify the sheet completed by the teacher.
|
171 | #
|
172 | # postcorrect_set_multiple (see postcorrect function)
|
173 | #
|
174 | # --- the following values are supplied in the Preferences window
|
175 | #
|
176 | # darkness_threshold and darkness_threshold_up are the parameters used
|
177 | # for determining wether a box is ticked or not.
|
178 | #
|
179 | # mark_floor is the minimum mark to be given to a student.
|
180 | #
|
181 | # mark_max is the mark to be given to a perfect completed answer
|
182 | # sheet.
|
183 | #
|
184 | # ceiling is true if AMC should put a ceiling on the students marks
|
185 | # (this can be useful if the SUF global scoring strategy is used).
|
186 | #
|
187 | # rounding is the rounding type to be used for the marks.
|
188 | #
|
189 | # granularity is the granularity for the marks rounding.
|
190 |
|
191 | use Exporter qw(import);
|
192 |
|
193 | use constant {
|
194 | QUESTION_SIMPLE => 1,
|
195 | QUESTION_MULT => 2,
|
196 |
|
197 | DIRECT_MARK => 0,
|
198 | DIRECT_NAMEFIELD => 1,
|
199 | };
|
200 |
|
201 | our @EXPORT_OK = qw(QUESTION_SIMPLE QUESTION_MULT DIRECT_MARK DIRECT_NAMEFIELD);
|
202 | our %EXPORT_TAGS = (
|
203 | question => [qw/QUESTION_SIMPLE QUESTION_MULT/],
|
204 | direct => [qw/DIRECT_MARK DIRECT_NAMEFIELD/],
|
205 | );
|
206 |
|
207 | use AMC::Basic;
|
208 | use AMC::DataModule;
|
209 | use AMC::DataModule::capture ':zone';
|
210 | use AMC::DataModule::layout ':flags';
|
211 |
|
212 | use XML::Simple;
|
213 |
|
214 | our @ISA = ("AMC::DataModule");
|
215 |
|
216 | sub version_current {
|
217 | return (2);
|
218 | }
|
219 |
|
220 | sub version_upgrade {
|
221 | my ( $self, $old_version ) = @_;
|
222 | if ( $old_version == 0 ) {
|
223 |
|
224 | # Upgrading from version 0 (empty database) to version 1 :
|
225 | # creates all the tables.
|
226 |
|
227 | debug "Creating scoring tables...";
|
228 | $self->sql_do( "CREATE TABLE IF NOT EXISTS "
|
229 | . $self->table("title")
|
230 | . " (question INTEGER, title TEXT)" );
|
231 | $self->sql_do( "CREATE TABLE IF NOT EXISTS "
|
232 | . $self->table("default")
|
233 | . " (type INTEGER, strategy TEXT)" );
|
234 | $self->sql_do( "CREATE TABLE IF NOT EXISTS "
|
235 | . $self->table("main")
|
236 | . " (student INTEGER, strategy TEXT)" );
|
237 | $self->sql_do( "CREATE TABLE IF NOT EXISTS "
|
238 | . $self->table("question")
|
239 | . " (student INTEGER, question INTEGER, type INTEGER, indicative INTEGER DEFAULT 0, strategy TEXT, PRIMARY KEY (student,question))"
|
240 | );
|
241 | $self->sql_do( "CREATE TABLE IF NOT EXISTS "
|
242 | . $self->table("answer")
|
243 | . " (student INTEGER, question INTEGER, answer INTEGER, correct INTEGER, strategy INTEGER, PRIMARY KEY (student,question,answer))"
|
244 | );
|
245 | $self->sql_do( "CREATE TABLE IF NOT EXISTS "
|
246 | . $self->table("alias")
|
247 | . " (student INTEGER,see INTEGER)" );
|
248 |
|
249 | $self->sql_do( "CREATE TABLE IF NOT EXISTS "
|
250 | . $self->table("score")
|
251 | . " (student INTEGER, copy INTEGER, question INTEGER, score REAL, why TEXT, max REAL, PRIMARY KEY (student,copy,question))"
|
252 | );
|
253 | $self->sql_do( "CREATE TABLE IF NOT EXISTS "
|
254 | . $self->table("mark")
|
255 | . " (student INTEGER, copy INTEGER, total REAL, max REAL, mark REAL, PRIMARY KEY (student,copy))"
|
256 | );
|
257 | $self->sql_do( "CREATE TABLE IF NOT EXISTS "
|
258 | . $self->table("code")
|
259 | . " (student INTEGER, copy INTEGER, code TEXT, value TEXT, direct INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (student,copy,code))"
|
260 | );
|
261 |
|
262 | $self->statement('NEWdefault')->execute( QUESTION_SIMPLE, "" );
|
263 | $self->statement('NEWdefault')->execute( QUESTION_MULT, "" );
|
264 |
|
265 | $self->populate_from_xml;
|
266 |
|
267 | return (2);
|
268 | } elsif ( $old_version == 1 ) {
|
269 | $self->sql_do( "ALTER TABLE "
|
270 | . $self->table("code")
|
271 | . " ADD COLUMN direct INTEGER NOT NULL DEFAULT 0" );
|
272 | return (2);
|
273 | }
|
274 | return ('');
|
275 | }
|
276 |
|
277 | # populate_from_xml read the old format XML files (if any) and inserts
|
278 | # them in the new SQLite database
|
279 |
|
280 | sub populate_from_xml {
|
281 | my ($self) = @_;
|
282 | my $scoring_file = $self->{data}->directory;
|
283 | $scoring_file =~ s:/[^/]+/?$:/bareme.xml:;
|
284 | return if ( !-f $scoring_file );
|
285 |
|
286 | $self->progression( 'begin',
|
287 | __ "Fetching scoring data from old format XML files..." );
|
288 |
|
289 | my $xml = XMLin( $scoring_file, ForceArray => 1, KeyAttr => ['id'] );
|
290 |
|
291 | $self->main_strategy( -1, $xml->{main} );
|
292 | my @s = ( keys %{ $xml->{etudiant} } );
|
293 | my $frac = 0;
|
294 |
|
295 | for my $student (@s) {
|
296 | my $s = $xml->{etudiant}->{$student};
|
297 | if ( $student eq 'defaut' ) {
|
298 | $self->default_strategy( QUESTION_SIMPLE,
|
299 | $s->{question}->{S}->{bareme} );
|
300 | $self->default_strategy( QUESTION_MULT,
|
301 | $s->{question}->{M}->{bareme} );
|
302 | } elsif ( $student =~ /^[0-9]+$/ ) {
|
303 | $self->main_strategy( $student, $s->{main} );
|
304 | for my $question ( keys %{ $s->{question} } ) {
|
305 | if ( $question =~ /^[0-9]+$/ ) {
|
306 | my $q = $s->{question}->{$question};
|
307 | $self->question_title( $question, $q->{titre} );
|
308 | $self->new_question(
|
309 | $student,
|
310 | $question,
|
311 | ( $q->{multiple} ? QUESTION_MULT : QUESTION_SIMPLE ),
|
312 | ( $q->{indicative} ? 1 : 0 ),
|
313 | $q->{bareme}
|
314 | );
|
315 |
|
316 | if ( $q->{reponse} ) {
|
317 | if ( ref( $q->{reponse} ) eq 'HASH' ) {
|
318 | for my $answer ( keys %{ $q->{reponse} } ) {
|
319 | my $a = $q->{reponse}->{$answer};
|
320 | $self->new_answer(
|
321 | $student, $question, $answer,
|
322 | $a->{bonne}, $a->{bareme}
|
323 | );
|
324 | }
|
325 | } else {
|
326 | debug
|
327 | "WARNING: reponse is not a HASHREF for S=$student Q=$question";
|
328 | }
|
329 | }
|
330 | } else {
|
331 | debug "Unknown question id: <$question>";
|
332 | }
|
333 | }
|
334 | } else {
|
335 | debug "Unknown student id: <$student>";
|
336 | }
|
337 | $frac++;
|
338 | $self->progression( 'fraction', 0.5 * $frac / ( $#s + 1 ) );
|
339 | }
|
340 |
|
341 | $scoring_file = $self->{data}->directory;
|
342 | $scoring_file =~ s:/[^/]+/?$:/notes.xml:;
|
343 | return if ( !-f $scoring_file );
|
344 |
|
345 | $frac = 0;
|
346 |
|
347 | $xml = XMLin( $scoring_file, ForceArray => 1, KeyAttr => ['id'] );
|
348 |
|
349 | $self->variable( 'darkness_threshold', $xml->{seuil} );
|
350 | $self->variable( 'darkness_threshold_up', 1.0 );
|
351 | $self->variable( 'mark_floor', $xml->{notemin} );
|
352 | $self->variable( 'mark_max', $xml->{notemax} );
|
353 | $self->variable( 'ceiling', $xml->{plafond} );
|
354 | $self->variable( 'rounding', $xml->{arrondi} );
|
355 | $self->variable( 'granularity', $xml->{grain} );
|
356 |
|
357 | @s = ( keys %{ $xml->{copie} } );
|
358 | for my $student (@s) {
|
359 | my $s = $xml->{copie}->{$student};
|
360 |
|
361 | if ( $student =~ /^(moyenne|max)$/ ) {
|
362 | debug "Skipping student <$student>";
|
363 | } elsif ( $student =~ /^[0-9]+$/ ) {
|
364 | $self->statement('NEWMark')
|
365 | ->execute( $student, 0,
|
366 | map { $s->{total}->[0]->{$_} } (qw/total max note/) );
|
367 |
|
368 | for my $title ( keys %{ $s->{question} } ) {
|
369 | my $q = $s->{question}->{$title};
|
370 | my $question = $self->question_number($title);
|
371 | $self->statement('NEWScore')
|
372 | ->execute( $student, 0, $question, $q->{note}, $q->{max},
|
373 | $q->{raison} );
|
374 | }
|
375 |
|
376 | for my $code ( keys %{ $s->{code} } ) {
|
377 | $self->statement('NEWCode')
|
378 | ->execute( $student, 0, $code, $s->{code}->{$code}->{content},
|
379 | DIRECT_MARK );
|
380 | }
|
381 | } else {
|
382 | debug "WARNING: Unknown student <$student> importing XML marks";
|
383 | }
|
384 | $frac++;
|
385 | $self->progression( 'fraction', 0.5 * ( 1 + $frac / ( $#s + 1 ) ) );
|
386 | }
|
387 |
|
388 | $self->progression('end');
|
389 | }
|
390 |
|
391 | # defines all the SQL statements that will be used
|
392 |
|
393 | sub define_statements {
|
394 | my ($self) = @_;
|
395 | my $t_answer = $self->table("answer");
|
396 | my $t_default = $self->table("default");
|
397 | my $t_zone = $self->table( "zone", "capture" );
|
398 | $self->{statements} = {
|
399 | NEWdefault =>
|
400 | { sql => "INSERT INTO $t_default" . " (type,strategy) VALUES (?,?)" },
|
401 | getDefault =>
|
402 | { sql => "SELECT strategy FROM $t_default" . " WHERE type=?" },
|
403 | setDefault =>
|
404 | { sql => "UPDATE $t_default" . " SET strategy=? WHERE type=?" },
|
405 | noDefault => { sql => "UPDATE $t_default" . " SET strategy=''" },
|
406 | NEWMain => {
|
407 | sql => "INSERT INTO "
|
408 | . $self->table("main")
|
409 | . " (student,strategy) VALUES (?,?)"
|
410 | },
|
411 | getMain => {
|
412 | sql => "SELECT strategy FROM "
|
413 | . $self->table("main")
|
414 | . " WHERE student=?"
|
415 | },
|
416 | getAllMain => {
|
417 | sql => "SELECT strategy FROM "
|
418 | . $self->table("main")
|
419 | . " WHERE student=? OR student=-1 OR student=0 ORDER BY student"
|
420 | },
|
421 | setMain => {
|
422 | sql => "UPDATE "
|
423 | . $self->table("main")
|
424 | . " SET strategy=? WHERE student=?"
|
425 | },
|
426 | NEWTitle => {
|
427 | sql => "INSERT INTO "
|
428 | . $self->table("title")
|
429 | . " (question,title) VALUES (?,?)"
|
430 | },
|
431 | getTitle => {
|
432 | sql => "SELECT title FROM "
|
433 | . $self->table("title")
|
434 | . " WHERE question=?"
|
435 | },
|
436 | getQNumber => {
|
437 | sql => "SELECT question FROM "
|
438 | . $self->table("title")
|
439 | . " WHERE title=?"
|
440 | },
|
441 | setTitle => {
|
442 | sql => "UPDATE "
|
443 | . $self->table("title")
|
444 | . " SET title=? WHERE question=?"
|
445 | },
|
446 | NEWQuestion => {
|
447 | sql => "INSERT OR REPLACE INTO "
|
448 | . $self->table("question")
|
449 | . " (student,question,type,indicative,strategy)"
|
450 | . " VALUES (?,?,?,?,?)"
|
451 | },
|
452 | NEWAnswer => {
|
453 | sql => "INSERT OR REPLACE INTO "
|
454 | . $self->table("answer")
|
455 | . " (student,question,answer,correct,strategy)"
|
456 | . " VALUES (?,?,?,?,?)"
|
457 | },
|
458 | setAnswerStrat => {
|
459 | sql => "UPDATE "
|
460 | . $self->table("answer")
|
461 | . " SET strategy=? WHERE student=? AND question=? AND answer=?"
|
462 | },
|
463 | addAnswerStrat => {
|
464 | sql => "UPDATE "
|
465 | . $self->table("answer")
|
466 | . " SET strategy=strategy||? WHERE student=? AND question=? AND answer=?"
|
467 | },
|
468 | NEWAlias => {
|
469 | sql => "INSERT INTO "
|
470 | . $self->table("alias")
|
471 | . " (student,see) VALUES (?,?)"
|
472 | },
|
473 | getAlias => {
|
474 | sql => "SELECT see FROM "
|
475 | . $self->table("alias")
|
476 | . " WHERE student=?"
|
477 | },
|
478 | postCorrectNew => {
|
479 | sql => "CREATE TEMPORARY TABLE IF NOT EXISTS"
|
480 | . " pc_temp (q INTEGER, a INTEGER, c REAL, PRIMARY KEY(q,a))"
|
481 | },
|
482 | postCorrectClr => { sql => "DELETE FROM pc_temp" },
|
483 | postCorrectPop => {
|
484 | sql => "INSERT INTO pc_temp (q,a,c) "
|
485 | . " SELECT id_a,id_b,CASE"
|
486 | . " WHEN manual >= 0 THEN manual"
|
487 | . " WHEN total<=0 THEN -1"
|
488 | . " WHEN black >= ? * total AND black <= ? * total THEN 1"
|
489 | . " ELSE 0" . " END"
|
490 | . " FROM "
|
491 | . $self->table( "zone", "capture" )
|
492 | . " WHERE capture_zone.student=? AND capture_zone.copy=? AND capture_zone.type=?"
|
493 | },
|
494 | postCorrectMul => {
|
495 | sql => "UPDATE "
|
496 | . $self->table("question")
|
497 | . " SET type=CASE"
|
498 | . " WHEN (SELECT sum(c) FROM pc_temp"
|
499 | . " WHERE q=question)>1"
|
500 | . " THEN ?"
|
501 | . " ELSE ?" . " END"
|
502 | },
|
503 | postCorrectSet => {
|
504 | sql => "UPDATE "
|
505 | . $self->table("answer")
|
506 | . " SET correct=(SELECT c FROM pc_temp"
|
507 | . " WHERE q=question AND a=answer)"
|
508 | },
|
509 | NEWScore => {
|
510 | sql => "INSERT INTO "
|
511 | . $self->table("score")
|
512 | . " (student,copy,question,score,max,why)"
|
513 | . " VALUES (?,?,?,?,?,?)"
|
514 | },
|
515 | cancelScore => {
|
516 | sql => "UPDATE "
|
517 | . $self->table("score")
|
518 | . " SET why=? WHERE student=? AND copy=? AND question=?"
|
519 | },
|
520 | NEWMark => {
|
521 | sql => "INSERT INTO "
|
522 | . $self->table("mark")
|
523 | . " (student,copy,total,max,mark)"
|
524 | . " VALUES (?,?,?,?,?)"
|
525 | },
|
526 | NEWCode => {
|
527 | sql => "INSERT OR REPLACE INTO "
|
528 | . $self->table("code")
|
529 | . " (student,copy,code,value,direct)"
|
530 | . " VALUES (?,?,?,?,?)"
|
531 | },
|
532 |
|
533 | studentMark => {
|
534 | sql => "SELECT * FROM "
|
535 | . $self->table("mark")
|
536 | . " WHERE student=? AND copy=?"
|
537 | },
|
538 | marks => { sql => "SELECT * FROM " . $self->table("mark") },
|
539 | marksCount => { sql => "SELECT COUNT(*) FROM " . $self->table("mark") },
|
540 | codes => {
|
541 | sql => "SELECT code FROM "
|
542 | . $self->table("code")
|
543 | . " GROUP BY code ORDER BY code"
|
544 | },
|
545 | qStrat => {
|
546 | sql => "SELECT strategy FROM "
|
547 | . $self->table("question")
|
548 | . " WHERE student=? AND question=?"
|
549 | },
|
550 | aStrat => {
|
551 | sql => "SELECT strategy FROM "
|
552 | . $self->table("answer")
|
553 | . " WHERE student=? AND question=? AND answer=?"
|
554 | },
|
555 | answers => {
|
556 | sql => "SELECT answer FROM "
|
557 | . $self->table("answer")
|
558 | . " WHERE student=? AND question=?"
|
559 | . " ORDER BY answer"
|
560 | },
|
561 | studentQuestions => {
|
562 | sql => "SELECT question FROM "
|
563 | . $self->table("question")
|
564 | . " WHERE student=?"
|
565 | },
|
566 | questions => {
|
567 | sql => "SELECT question,title FROM "
|
568 | . $self->table("title")
|
569 | . " ORDER BY title"
|
570 | },
|
571 | qMaxMax => {
|
572 | sql => "SELECT MAX(max) FROM "
|
573 | . $self->table("score")
|
574 | . " WHERE question=?"
|
575 | },
|
576 | correct => {
|
577 | sql => "SELECT correct FROM "
|
578 | . $self->table("answer")
|
579 | . " WHERE student=? AND question=? AND answer=?"
|
580 | },
|
581 | correctChars => {
|
582 | sql => "SELECT char FROM "
|
583 | . " (SELECT answer FROM "
|
584 | . $self->table("answer")
|
585 | . " WHERE student=? AND question=? AND correct>0) AS correct,"
|
586 | . " (SELECT answer,char FROM "
|
587 | . $self->table( "box", "layout" )
|
588 | . " WHERE student=? AND question=? AND role=?) AS char"
|
589 | . " ON correct.answer=char.answer ORDER BY correct.answer"
|
590 | },
|
591 | correctForAll => {
|
592 | sql => "SELECT question,answer,"
|
593 | . " MIN(correct) AS correct_min,"
|
594 | . " MAX(correct) AS correct_max "
|
595 | . " FROM "
|
596 | . $self->table("answer")
|
597 | . " GROUP BY question,answer"
|
598 | },
|
599 | multiple => {
|
600 | sql => "SELECT type FROM "
|
601 | . $self->table("question")
|
602 | . " WHERE student=? AND question=?"
|
603 | },
|
604 | indicative => {
|
605 | sql => "SELECT indicative FROM "
|
606 | . $self->table("question")
|
607 | . " WHERE student=? AND question=?"
|
608 | },
|
609 | numQIndic => {
|
610 | sql => "SELECT COUNT(*) FROM"
|
611 | . " ( SELECT question FROM "
|
612 | . $self->table("question")
|
613 | . " WHERE indicatve=? GROUP BY question)"
|
614 | },
|
615 | oneIndic => {
|
616 | sql => "SELECT COUNT(*) FROM "
|
617 | . $self->table("question")
|
618 | . " WHERE question=? AND indicative=?"
|
619 | },
|
620 | getScore => {
|
621 | sql => "SELECT score FROM "
|
622 | . $self->table("score")
|
623 | . " WHERE student=? AND copy=? AND question=?"
|
624 | },
|
625 | getScoreC => {
|
626 | sql => "SELECT score,max,why FROM "
|
627 | . $self->table("score")
|
628 | . " WHERE student=? AND copy=? AND question=?"
|
629 | },
|
630 | getCode => {
|
631 | sql => "SELECT value FROM "
|
632 | . $self->table("code")
|
633 | . " WHERE student=? AND copy=? AND code=?"
|
634 | },
|
635 | codesCounts => {
|
636 | sql => "SELECT student,copy,value,COUNT(*) as nb"
|
637 | . " FROM "
|
638 | . $self->table("code")
|
639 | . " WHERE code=? GROUP BY value"
|
640 | },
|
641 | preAssocCounts => {
|
642 | sql => "SELECT m.student,m.copy,l.id AS value,COUNT(*) AS nb"
|
643 | . " FROM "
|
644 | . $self->table("mark") . " AS m"
|
645 | . " , "
|
646 | . $self->table( "association", "layout" ) . " AS l"
|
647 | . " ON m.student=l.student AND m.copy=0"
|
648 | . " GROUP BY l.id"
|
649 | },
|
650 |
|
651 | avgMark => {
|
652 | sql => "SELECT AVG(mark) FROM "
|
653 | . $self->table("mark")
|
654 | . " WHERE NOT (student=? AND copy=?)"
|
655 | },
|
656 | avgQuest => {
|
657 | sql => "SELECT CASE"
|
658 | . " WHEN SUM(max)>0 THEN 100*SUM(score)/SUM(max)"
|
659 | . " ELSE '-' END"
|
660 | . " FROM "
|
661 | . $self->table("score")
|
662 | . " WHERE question=?"
|
663 | . " AND NOT (student=? AND copy=?)"
|
664 | },
|
665 | studentAnswersBase => {
|
666 | sql => "SELECT question,answer"
|
667 | . ",correct,strategy"
|
668 | . ",(SELECT CASE"
|
669 | . " WHEN manual >= 0 THEN manual"
|
670 | . " WHEN total<=0 THEN -1"
|
671 | . " WHEN black >= ? * total AND black <= ? * total THEN 1"
|
672 | . " ELSE 0"
|
673 | . " END FROM $t_zone"
|
674 | . " WHERE $t_zone.student=? AND $t_zone.copy=? AND $t_zone.type=?"
|
675 | . " AND $t_zone.id_a=$t_answer.question AND $t_zone.id_b=$t_answer.answer"
|
676 | . " ) AS ticked"
|
677 | . " FROM "
|
678 | . $self->table("answer")
|
679 | . " WHERE student=?"
|
680 | },
|
681 | #
|
682 | #
|
683 | ##
|
684 | ### A RAJOUTER 1 sur 1
|
685 | ##
|
686 | #
|
687 | #
|
688 | 'studentSkillLevel'=>
|
689 | {'sql'=>"SELECT SUBSTR(t.title, 1, INSTR(t.title, ':')-1) AS skill, SUM(s.score)/SUM(s.max)"
|
690 | ." FROM ".$self->table("title"). " t"
|
691 | ." INNER JOIN ".$self->table("score")." s"
|
692 | ." ON s.question=t.question"
|
693 | ." WHERE t.title LIKE '%:%' AND s.student=? AND s.copy=?"
|
694 | ." GROUP BY s.copy, s.student, skill"},
|
695 | #
|
696 | #
|
697 | ##
|
698 | ### FIN A RAJOUTER 1 sur 1
|
699 | ##
|
700 | #
|
701 | #
|
702 | studentQuestionsBase => {
|
703 | sql => "SELECT q.question,q.type,q.indicative,q.strategy,t.title"
|
704 | . ",d.strategy AS default_strategy"
|
705 | . " FROM "
|
706 | . $self->table("question") . " q"
|
707 | . " LEFT OUTER JOIN "
|
708 | . $self->table("default") . " d"
|
709 | . " ON q.type=d.type"
|
710 | . " LEFT OUTER JOIN "
|
711 | . $self->table("title") . " t"
|
712 | . " ON q.question=t.question"
|
713 | . " WHERE student=?"
|
714 | },
|
715 | deleteScores => {
|
716 | sql => "DELETE FROM "
|
717 | . $self->table('score')
|
718 | . " WHERE student=? AND copy=?"
|
719 | },
|
720 | deleteMarks => {
|
721 | sql => "DELETE FROM "
|
722 | . $self->table('mark')
|
723 | . " WHERE student=? AND copy=?"
|
724 | },
|
725 | deleteCodes => {
|
726 | sql => "DELETE FROM "
|
727 | . $self->table('code')
|
728 | . " WHERE student=? AND copy=?"
|
729 | },
|
730 | pagesWhy => {
|
731 | sql =>
|
732 | "SELECT s.student,s.copy,GROUP_CONCAT(s.why) as why,b.page FROM "
|
733 | . $self->table('score') . " s"
|
734 | . " JOIN "
|
735 | . " ( SELECT student,page,question FROM "
|
736 | . $self->table( "box", "layout" )
|
737 | . " WHERE role=?"
|
738 | . " GROUP BY student,page,question )" . " b"
|
739 | . " ON s.student=b.student AND s.question=b.question"
|
740 | . " GROUP BY s.student,b.page,s.copy"
|
741 | },
|
742 | clearDirect =>
|
743 | { sql => "DELETE FROM " . $self->table("code") . " WHERE direct=?" },
|
744 | };
|
745 | }
|
746 |
|
747 | # page_why() returns a list of items like
|
748 | # {student=>1,copy=>0,page=>1,why=>',V,E,,'}
|
749 | # that collects all 'why' attributes for questions that are on each page.
|
750 |
|
751 | sub pages_why {
|
752 | my ($self) = @_;
|
753 | return (
|
754 | @{
|
755 | $self->dbh->selectall_arrayref(
|
756 | $self->statement('pagesWhy'), { Slice => {} },
|
757 | BOX_ROLE_ANSWER
|
758 | )
|
759 | }
|
760 | );
|
761 | }
|
762 |
|
763 | # default_strategy($type) returns the default scoring strategy string
|
764 | # to be used for questions with type $type (QUESTION_SIMPLE or
|
765 | # QUESTION_MULT).
|
766 | #
|
767 | # default_strategy($type,$strategy) sets the default strategy string
|
768 | # for questions with type $type.
|
769 |
|
770 | sub default_strategy {
|
771 | my ( $self, $type, $strategy ) = @_;
|
772 | if ( defined($strategy) ) {
|
773 | $self->statement('setDefault')->execute( $strategy, $type );
|
774 | } else {
|
775 | return ( $self->sql_single( $self->statement('getDefault'), $type ) );
|
776 | }
|
777 | }
|
778 |
|
779 | # main_strategy($student) returns the main scoring strategy string for
|
780 | # student $student. If $student<=0 (-1 in the database), this refers
|
781 | # to the argument of the \scoring command used outside the
|
782 | # onecopy/examcopy loop. If $student>0, this refers to the argument of
|
783 | # the \scoring command used inside the onecopy/examcopy loop, but
|
784 | # outside question/questionmult environments.
|
785 | #
|
786 | # main_strategy($student,$strategy) sets the main scoring strategy
|
787 | # string.
|
788 |
|
789 | sub main_strategy {
|
790 | my ( $self, $student, $strategy ) = @_;
|
791 | $student = -1 if ( $student <= 0 );
|
792 | if ( defined($strategy) ) {
|
793 | if ( defined( $self->main_strategy($student) ) ) {
|
794 | $self->statement('setMain')->execute( $strategy, $student );
|
795 | } else {
|
796 | $self->statement('NEWMain')->execute( $student, $strategy );
|
797 | }
|
798 | } else {
|
799 | return ( $self->sql_single( $self->statement('getMain'), $student ) );
|
800 | }
|
801 | }
|
802 |
|
803 | #add_main_strategy($student,$strategy) adds the strategy string at the
|
804 | #end of the student's main strategy string.
|
805 |
|
806 | sub add_main_strategy {
|
807 | my ( $self, $student, $strategy ) = @_;
|
808 | $student = -1 if ( $student <= 0 );
|
809 | my $old = $self->main_strategy($student);
|
810 | if ( defined($old) ) {
|
811 | $self->statement('setMain')
|
812 | ->execute( $old . ',' . $strategy, $student );
|
813 | } else {
|
814 | $self->statement('NEWMain')->execute( $student, $strategy );
|
815 | }
|
816 | }
|
817 |
|
818 | # main_strategy_all($student) returns a concatenation of the the main
|
819 | # strategies for student=-1, student=0 and student=$student.
|
820 |
|
821 | sub main_strategy_all {
|
822 | my ( $self, $student ) = @_;
|
823 | return (
|
824 | join(
|
825 | ',', $self->sql_list( $self->statement('getAllMain'), $student )
|
826 | )
|
827 | );
|
828 | }
|
829 |
|
830 | # new_question($student,$question,$type,$indicative,$strategy) adds a
|
831 | # question in the database, giving its characteristics. If the
|
832 | # question already exists, it is updated with no error.
|
833 |
|
834 | sub new_question {
|
835 | my ( $self, $student, $question, $type, $indicative, $strategy ) = @_;
|
836 | $self->statement('NEWQuestion')
|
837 | ->execute( $student, $question, $type, $indicative, $strategy );
|
838 | }
|
839 |
|
840 | # question_strategy($student,$question) returns the scoring strategy
|
841 | # string for a particlar question: argument of the \scoring command
|
842 | # used inside a question/questionmult environment, before the
|
843 | # \correctchoice and \wrongchoice commands.
|
844 |
|
845 | sub question_strategy {
|
846 | my ( $self, $student, $question ) = @_;
|
847 | return (
|
848 | $self->sql_single( $self->statement('qStrat'), $student, $question ) );
|
849 | }
|
850 |
|
851 | # new_answer($student,$question,$answer,$correct,$strategy) adds an
|
852 | # answer in the database, giving its characteristics. If the answer
|
853 | # already exists, it is updated with no error.
|
854 |
|
855 | sub new_answer {
|
856 | my ( $self, $student, $question, $answer, $correct, $strategy ) = @_;
|
857 | $self->statement('NEWAnswer')
|
858 | ->execute( $student, $question, $answer, $correct, $strategy );
|
859 | }
|
860 |
|
861 | # answer_strategy($student,$question,$answer) returns the scoring
|
862 | # strategy string for a particular answer: argument of the \scoring
|
863 | # command used after \correctchoice and \wrongchoice commands.
|
864 |
|
865 | sub answer_strategy {
|
866 | my ( $self, $student, $question, $answer ) = @_;
|
867 | return (
|
868 | $self->sql_single(
|
869 | $self->statement('aStrat'),
|
870 | $student, $question, $answer
|
871 | )
|
872 | );
|
873 | }
|
874 |
|
875 | # answers($student,$question) returns an ordered list of answers
|
876 | # numbers for a particular question. Answer number 0, placed at the
|
877 | # end, corresponds to the answer "None of the above", when present.
|
878 |
|
879 | sub answers {
|
880 | my ( $self, $student, $question ) = @_;
|
881 | my @a = $self->sql_list( $self->statement('answers'), $student, $question );
|
882 | if ( $a[0] == 0 ) {
|
883 | shift @a;
|
884 | push @a, 0;
|
885 | }
|
886 | return (@a);
|
887 | }
|
888 |
|
889 | # correct_answer($student,$question,$answer) returns 1 if the
|
890 | # corresponding box has to be ticked (the answer is a correct one),
|
891 | # and 0 if not.
|
892 |
|
893 | sub correct_answer {
|
894 | my ( $self, $student, $question, $answer ) = @_;
|
895 | return (
|
896 | $self->sql_single(
|
897 | $self->statement('correct'),
|
898 | $student, $question, $answer
|
899 | )
|
900 | );
|
901 | }
|
902 |
|
903 | # correct_chars($student,$question) returns the list of the chars
|
904 | # written inside (or beside) the boxes corresponding to correct
|
905 | # answers for a particular question
|
906 |
|
907 | sub correct_chars {
|
908 | my ( $self, $student, $question ) = @_;
|
909 | $self->{data}->require_module('layout');
|
910 | return (
|
911 | $self->sql_list(
|
912 | $self->statement('correctChars'), $student,
|
913 | $question, $student,
|
914 | $question, BOX_ROLE_ANSWER
|
915 | )
|
916 | );
|
917 | }
|
918 |
|
919 | # Same as correct_chars, but paste the chars if they all exist, and
|
920 | # return undef otherwise
|
921 |
|
922 | sub correct_chars_pasted {
|
923 | my ( $self, @args ) = @_;
|
924 | my @c = $self->correct_chars(@args);
|
925 | if ( grep { !defined($_) } @c ) {
|
926 | return (undef);
|
927 | } else {
|
928 | return ( join( "", @c ) );
|
929 | }
|
930 | }
|
931 |
|
932 | # correct_for_all() returns a reference to an array like
|
933 | #
|
934 | # [{question=>1,answer=>1,correct_min=>0,correct_max=>0},
|
935 | # {question=>1,answer=>2,correct_min=>1,correct_max=>1},
|
936 | # ]
|
937 | #
|
938 | # This gives, for each question/answer, the minumum and maximum of the
|
939 | # <correct> column for all students. Usualy, minimum and maximum are
|
940 | # equal because the answer is either correct for all students either
|
941 | # not correct for all students, but one can also encounter
|
942 | # correct_min=0 and correct_max=1, in situations where the answers are
|
943 | # not the same for all students (for example for questions with random
|
944 | # numerical values).
|
945 |
|
946 | sub correct_for_all {
|
947 | my ( $self, $question, $answer ) = @_;
|
948 | return (
|
949 | $self->dbh->selectall_arrayref(
|
950 | $self->statement('correctForAll'),
|
951 | { Slice => {} }
|
952 | )
|
953 | );
|
954 | }
|
955 |
|
956 | # multiple($student,$question) returns 1 if the corresponding
|
957 | # question is multiple (type=QUESTION_MULT), and 0 if not.
|
958 |
|
959 | sub multiple {
|
960 | my ( $self, $student, $question ) = @_;
|
961 | return (
|
962 | $self->sql_single( $self->statement('multiple'), $student, $question )
|
963 | == QUESTION_MULT );
|
964 | }
|
965 |
|
966 | # correct_answer($student,$question) returns 1 if the corresponding
|
967 | # question is indicative (use of \QuestionIndicative), and 0 if not.
|
968 |
|
969 | sub indicative {
|
970 | my ( $self, $student, $question ) = @_;
|
971 | return (
|
972 | $self->sql_single(
|
973 | $self->statement('indicative'), $student, $question
|
974 | )
|
975 | );
|
976 | }
|
977 |
|
978 | # one_indicative($question,$indic) returns the number of students for
|
979 | # which the question has indicative=$indic. In fact, a single question
|
980 | # SHOULD be indicative for all students, or for none...
|
981 |
|
982 | sub one_indicative {
|
983 | my ( $self, $question, $indic ) = @_;
|
984 | $indic = 1 if ( !defined($indic) );
|
985 | return (
|
986 | $self->sql_single( $self->statement('oneIndic'), $question, $indic ) );
|
987 | }
|
988 |
|
989 | # num_questions_indic($i) returns the number of questions that have
|
990 | # indicative=$i ($i is 0 or 1).
|
991 |
|
992 | sub num_questions_indic {
|
993 | my ( $self, $indicative ) = @_;
|
994 | return ( $self->sql_single( $self->statement('numQIndic'), $indicative ) );
|
995 | }
|
996 |
|
997 | # question_title($question) returns a question title.
|
998 | #
|
999 | # question_title($question,$title) sets a question title.
|
1000 |
|
1001 | sub question_title {
|
1002 | my ( $self, $question, $title ) = @_;
|
1003 | if ( defined($title) ) {
|
1004 | if ( defined( $self->question_title($question) ) ) {
|
1005 | $self->statement('setTitle')->execute( $title, $question );
|
1006 | } else {
|
1007 | $self->statement('NEWTitle')->execute( $question, $title );
|
1008 | }
|
1009 | } else {
|
1010 | return ( $self->sql_single( $self->statement('getTitle'), $question ) );
|
1011 | }
|
1012 | }
|
1013 |
|
1014 | # question_number($title) returns the question number corresponding to
|
1015 | # the given title.
|
1016 |
|
1017 | sub question_number {
|
1018 | my ( $self, $title ) = @_;
|
1019 | return ( $self->sql_single( $self->statement('getQNumber'), $title ) );
|
1020 | }
|
1021 |
|
1022 | # question_maxmax($question) returns the maximum of the max value for
|
1023 | # question $question accross all students sheets
|
1024 |
|
1025 | sub question_maxmax {
|
1026 | my ( $self, $question ) = @_;
|
1027 | return ( $self->sql_single( $self->statement('qMaxMax'), $question ) );
|
1028 | }
|
1029 |
|
1030 | # clear_strategy clears all data concerning the scoring strategy of
|
1031 | # the exam.
|
1032 |
|
1033 | sub clear_strategy {
|
1034 | my ($self) = @_;
|
1035 | $self->clear_variables;
|
1036 | $self->statement('noDefault')->execute;
|
1037 | for my $t (qw/title main question answer alias/) {
|
1038 | $self->sql_do( "DELETE FROM " . $self->table($t) );
|
1039 | }
|
1040 | }
|
1041 |
|
1042 | # clear_score clears all data concerning the scores/marks of the
|
1043 | # students.
|
1044 |
|
1045 | sub clear_score {
|
1046 | my ($self) = @_;
|
1047 | for my $t (qw/score mark/) {
|
1048 | $self->sql_do( "DELETE FROM " . $self->table($t) );
|
1049 | }
|
1050 | $self->clear_code_direct(DIRECT_MARK);
|
1051 | }
|
1052 |
|
1053 | sub clear_code_direct {
|
1054 | my ( $self, $category ) = @_;
|
1055 | $self->statement('clearDirect')->execute($category);
|
1056 | }
|
1057 |
|
1058 | # set_answer_strategy($student,$question,$answer,$strategy) sets the
|
1059 | # scoring strategy string associated to a particular answer.
|
1060 |
|
1061 | sub set_answer_strategy {
|
1062 | my ( $self, $student, $question, $answer, $strategy ) = @_;
|
1063 | $self->statement('setAnswerStrat')
|
1064 | ->execute( $strategy, $student, $question, $answer );
|
1065 | }
|
1066 |
|
1067 | # add_answer_strategy($student,$question,$answer,$strategy) adds the
|
1068 | # scoring strategy string to a particular answer's one.
|
1069 |
|
1070 | sub add_answer_strategy {
|
1071 | my ( $self, $student, $question, $answer, $strategy ) = @_;
|
1072 | $self->statement('addAnswerStrat')
|
1073 | ->execute( "," . $strategy, $student, $question, $answer );
|
1074 | }
|
1075 |
|
1076 | # replicate($see,$student) tells that the scoring strategy used for
|
1077 | # student $see has to be also used for student $student. This can be
|
1078 | # used only when the questions/answers are not different from a sheet
|
1079 | # to another (contrary to the use of random numerical values for
|
1080 | # exemple).
|
1081 |
|
1082 | sub replicate {
|
1083 | my ( $self, $see, $student ) = @_;
|
1084 | $self->statement('NEWAlias')->execute( $student, $see );
|
1085 | }
|
1086 |
|
1087 | # unalias($student) gives the student number where to find scoring
|
1088 | # strategy for student $student (following a replicate path if
|
1089 | # present -- see previous method).
|
1090 |
|
1091 | sub unalias {
|
1092 | my ( $self, $student ) = @_;
|
1093 | my $s = $student;
|
1094 | do {
|
1095 | $student = $s;
|
1096 | $s = $self->sql_single( $self->statement('getAlias'), $student );
|
1097 | } while ( defined($s) );
|
1098 | return ($student);
|
1099 | }
|
1100 |
|
1101 | # postcorrect($student,$copy,$darkness_threshold,$darkness_threshold_up,$set_multiple)
|
1102 | # uses the ticked values from the copy ($student,$copy) (filled by a
|
1103 | # teacher) to determine which answers are correct for all sheets. This
|
1104 | # can be used only when the questions/answers are not different from a
|
1105 | # sheet to another (contrary to the use of random numerical values for
|
1106 | # exemple).
|
1107 | #
|
1108 | # If $set_multiple is true, postcorrect also sets the type of all
|
1109 | # questions for which 2 or more answers are ticked on the
|
1110 | # ($student,$copy) answer sheet to be QUESTION_MULT, ans the type of
|
1111 | # all other questions to QUESTION_SIMPLE.
|
1112 |
|
1113 | sub postcorrect {
|
1114 | my ( $self, $student, $copy,
|
1115 | $darkness_threshold, $darkness_threshold_up, $set_multiple )
|
1116 | = @_;
|
1117 | die "Missing parameters in postcorrect call"
|
1118 | if ( !defined($darkness_threshold_up) );
|
1119 | $self->{data}->require_module('capture');
|
1120 | $self->statement('postCorrectNew')->execute();
|
1121 | $self->statement('postCorrectClr')->execute();
|
1122 | $self->statement('postCorrectPop')
|
1123 | ->execute( $darkness_threshold, $darkness_threshold_up, $student, $copy,
|
1124 | ZONE_BOX );
|
1125 | $self->statement('postCorrectMul')
|
1126 | ->execute( QUESTION_MULT, QUESTION_SIMPLE )
|
1127 | if ($set_multiple);
|
1128 | $self->statement('postCorrectSet')->execute();
|
1129 | }
|
1130 |
|
1131 | # new_score($student,$copy,$question,$score,$score_max,$why) adds a
|
1132 | # question score row.
|
1133 |
|
1134 | sub new_score {
|
1135 | my ( $self, $student, $copy, $question, $score, $score_max, $why ) = @_;
|
1136 | $self->statement('NEWScore')
|
1137 | ->execute( $student, $copy, $question, $score, $score_max, $why );
|
1138 | }
|
1139 |
|
1140 | # cancel_score($student,$copy,$question) cancels scoring (sets the
|
1141 | # score and maximum score to zero) for this question.
|
1142 |
|
1143 | sub cancel_score {
|
1144 | my ( $self, $student, $copy, $question ) = @_;
|
1145 | $self->statement('cancelScore')->execute( 'C', $student, $copy, $question );
|
1146 | }
|
1147 |
|
1148 | # new_mark($student,$copy,$total,$max,$mark) adds a mark row.
|
1149 |
|
1150 | sub new_mark {
|
1151 | my ( $self, $student, $copy, $total, $max, $mark ) = @_;
|
1152 | $self->statement('NEWMark')
|
1153 | ->execute( $student, $copy, $total, $max, $mark );
|
1154 | }
|
1155 |
|
1156 | # new_code($student,$copy,$code,$value) adds a code row.
|
1157 |
|
1158 | sub new_code {
|
1159 | my ( $self, $student, $copy, $code, $value, $direct ) = @_;
|
1160 | $direct = 0 if ( !$direct );
|
1161 | $self->statement('NEWCode')
|
1162 | ->execute( $student, $copy, $code, $value, $direct );
|
1163 | }
|
1164 |
|
1165 | # student_questions($student) returns a list of the question numbers
|
1166 | # used in the sheets for student number $student.
|
1167 |
|
1168 | sub student_questions {
|
1169 | my ( $self, $student ) = @_;
|
1170 | return (
|
1171 | $self->sql_list( $self->statement('studentQuestions'), $student ) );
|
1172 | }
|
1173 |
|
1174 | # questions returns an array of pointers (one for each question) to
|
1175 | # hashes (question=><question_number>,title=>'question_title').
|
1176 |
|
1177 | sub questions {
|
1178 | my ($self) = @_;
|
1179 | return (
|
1180 | @{
|
1181 | $self->dbh->selectall_arrayref( $self->statement('questions'),
|
1182 | { Slice => {} } )
|
1183 | }
|
1184 | );
|
1185 | }
|
1186 |
|
1187 | # average_mark returns the average mark from all students marks.
|
1188 |
|
1189 | sub average_mark {
|
1190 | my ($self) = @_;
|
1191 | my @pc = $self->postcorrect_sc;
|
1192 | return ( $self->sql_single( $self->statement('avgMark'), @pc ) );
|
1193 | }
|
1194 |
|
1195 | # codes returns a list of codes names.
|
1196 |
|
1197 | sub codes {
|
1198 | my ($self) = @_;
|
1199 | return ( $self->sql_list( $self->statement('codes') ) );
|
1200 | }
|
1201 |
|
1202 | # marks returns a pointer to an array of pointers (one for each
|
1203 | # student) to hashes giving all information from the mark table.
|
1204 |
|
1205 | sub marks {
|
1206 | my ($self) = @_;
|
1207 | return (
|
1208 | @{
|
1209 | $self->dbh->selectall_arrayref( $self->statement('marks'),
|
1210 | { Slice => {} } )
|
1211 | }
|
1212 | );
|
1213 | }
|
1214 |
|
1215 | # marks_count returns the nmber of marks computed.
|
1216 |
|
1217 | sub marks_count {
|
1218 | my ($self) = @_;
|
1219 | return ( $self->sql_single( $self->statement('marksCount') ) );
|
1220 | }
|
1221 |
|
1222 | # question_score($student,$copy,$question) returns the score of a
|
1223 | # particular student for a particular question.
|
1224 |
|
1225 | sub question_score {
|
1226 | my ( $self, $student, $copy, $question ) = @_;
|
1227 | return (
|
1228 | $self->sql_single(
|
1229 | $self->statement('getScore'),
|
1230 | $student, $copy, $question
|
1231 | )
|
1232 | );
|
1233 | }
|
1234 |
|
1235 | # question_result($student,$copy,$question) returns a pointer to a
|
1236 | # hash (score=>XXX,max=>XXX,why=>XXX) extracted from the
|
1237 | # question table.
|
1238 |
|
1239 | sub question_result {
|
1240 | my ( $self, $student, $copy, $question ) = @_;
|
1241 | my $sth = $self->statement('getScoreC');
|
1242 | $sth->execute( $student, $copy, $question );
|
1243 | return ( $sth->fetchrow_hashref );
|
1244 | }
|
1245 |
|
1246 | # student_code($student,$copy,$code) returns the value of the code
|
1247 | # named $code entered by a particular student.
|
1248 |
|
1249 | sub student_code {
|
1250 | my ( $self, $student, $copy, $code ) = @_;
|
1251 | return (
|
1252 | $self->sql_single(
|
1253 | $self->statement('getCode'), $student, $copy, $code
|
1254 | )
|
1255 | );
|
1256 | }
|
1257 |
|
1258 | # postcorrect_sc returns (postcorrect_student,postcorrect_copy), or
|
1259 | # (0,0) if not in postcorrect mode.
|
1260 |
|
1261 | sub postcorrect_sc {
|
1262 | my ($self) = @_;
|
1263 | return (
|
1264 | $self->variable('postcorrect_student') || 0,
|
1265 | $self->variable('postcorrect_copy') || 0
|
1266 | );
|
1267 | }
|
1268 |
|
1269 | # question_average($question) returns the average (as a percentage of
|
1270 | # the maximum score, from 0 to 100) of the scores for a particular
|
1271 | # question.
|
1272 |
|
1273 | sub question_average {
|
1274 | my ( $self, $question ) = @_;
|
1275 | my @pc = $self->postcorrect_sc;
|
1276 | return (
|
1277 | $self->sql_single( $self->statement('avgQuest'), $question, @pc ) );
|
1278 | }
|
1279 |
|
1280 | # student_global($student,$copy) returns a pointer to a hash
|
1281 | # (student=>XXX,copy=>XXX,total=>XXX,max=>XXX,mark=>XXX)
|
1282 | # extracted from the mark table.
|
1283 |
|
1284 | sub student_global {
|
1285 | my ( $self, $student, $copy ) = @_;
|
1286 | my $sth = $self->statement('studentMark');
|
1287 | $sth->execute( $student, $copy );
|
1288 | return ( $sth->fetchrow_hashref );
|
1289 | }
|
1290 |
|
1291 | # student_scoring_base($student,$copy,$darkness_threshold,$darkness_threshold_up)
|
1292 | # returns useful data to compute questions scores for a particular
|
1293 | # student (identified by $student and $copy), as a reference to a hash
|
1294 | # grouping questions and answers. For exemple :
|
1295 | #
|
1296 | # 'main_strategy'=>"",
|
1297 | # 'questions'=>
|
1298 | # { 1 =>{ question=>1,
|
1299 | # 'title' => 'questionID',
|
1300 | # 'type'=>1,
|
1301 | # 'indicative'=>0,
|
1302 | # 'strategy'=>'',
|
1303 | # 'answers'=>[ { question=>1, answer=>1,
|
1304 | # 'correct'=>1, ticked=>0, strategy=>"b=2" },
|
1305 | # {question=>1, answer=>2,
|
1306 | # 'correct'=>0, ticked=>0, strategy=>"" },
|
1307 | # ],
|
1308 | # },
|
1309 | # ...
|
1310 | # }
|
1311 |
|
1312 | sub student_scoring_base {
|
1313 | my ( $self, $student, $copy, $darkness_threshold, $darkness_threshold_up )
|
1314 | = @_;
|
1315 | die "Missing parameters in student_scoring_base call"
|
1316 | if ( !defined($darkness_threshold_up) );
|
1317 | $self->{data}->require_module('capture');
|
1318 | my $student_strategy = $self->unalias($student);
|
1319 | my $r = {
|
1320 | student_alias => $student_strategy,
|
1321 | questions => {},
|
1322 | main_strategy => $self->main_strategy_all($student_strategy)
|
1323 | };
|
1324 | my @sid = ($student);
|
1325 | push @sid, $student_strategy if ( $student != $student_strategy );
|
1326 | for my $s (@sid) {
|
1327 | my $sth;
|
1328 | $sth = $self->statement('studentQuestionsBase');
|
1329 | $sth->execute($s);
|
1330 | while ( my $qa = $sth->fetchrow_hashref ) {
|
1331 | $r->{questions}->{ $qa->{question} } = $qa;
|
1332 | }
|
1333 | $sth = $self->statement('studentAnswersBase');
|
1334 | $sth->execute( $darkness_threshold, $darkness_threshold_up,
|
1335 | $student, $copy, ZONE_BOX, $s );
|
1336 | while ( my $qa = $sth->fetchrow_hashref ) {
|
1337 | push @{ $r->{questions}->{ $qa->{question} }->{answers} }, $qa;
|
1338 | }
|
1339 | }
|
1340 | return ($r);
|
1341 | }
|
1342 |
|
1343 | # student_scoring_base_sorted(...) organizes the data from
|
1344 | # student_scoring_base to get sorted questions, relative to their IDs
|
1345 | # (lexicographic order)
|
1346 | #
|
1347 | # 'main_strategy'=>"",
|
1348 | # 'questions'=>
|
1349 | # [ { question=>1,
|
1350 | # 'title' => 'questionID',
|
1351 | # 'type'=>1,
|
1352 | # 'indicative'=>0,
|
1353 | # 'strategy'=>'',
|
1354 | # 'answers'=>[ { question=>1, answer=>1,
|
1355 | # 'correct'=>1, ticked=>0, strategy=>"b=2" },
|
1356 | # {question=>1, answer=>2,
|
1357 | # 'correct'=>0, ticked=>0, strategy=>"" },
|
1358 | # ],
|
1359 | # },
|
1360 | # ...
|
1361 | # ]
|
1362 |
|
1363 | sub student_scoring_base_sorted {
|
1364 | my ( $self, @args ) = @_;
|
1365 |
|
1366 | my $ssb = $self->student_scoring_base(@args);
|
1367 | my @n = sort {
|
1368 | $ssb->{questions}->{$a}->{title}
|
1369 | cmp $ssb->{questions}->{$b}->{title}
|
1370 | } ( keys %{ $ssb->{questions} } );
|
1371 | my $sorted_q = [ map { $ssb->{questions}->{$_} } (@n) ];
|
1372 | $ssb->{questions} = $sorted_q;
|
1373 |
|
1374 | return ($ssb);
|
1375 | }
|
1376 |
|
1377 | # delete_scoring_data($student,$copy) deletes all scoring data
|
1378 | # relative to a particular answer sheet.
|
1379 |
|
1380 | sub delete_scoring_data {
|
1381 | my ( $self, $student, $copy ) = @_;
|
1382 | for my $part (qw/Scores Marks Codes/) {
|
1383 | $self->statement( 'delete' . $part )->execute( $student, $copy );
|
1384 | }
|
1385 | }
|
1386 |
|
1387 | 1;
|