scoring.pm

Anthony Siaudeau, 07/11/2023 03:16 pm

Download (45.2 kB)

 
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;