AMC-prepare.pl

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

Download (34.5 kB)

 
1
#! /usr/bin/perl
2
#
3
# Copyright (C) 2008-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
22
#A RAJOUTER 1 sur 1
23
use Encode qw(decode encode);
24
#FIN A RAJOUTER 1 sur 1
25
26
use warnings;
27
use 5.012;
28
29
use File::Copy;
30
use File::Spec::Functions
31
  qw/splitpath catpath splitdir catdir catfile rel2abs tmpdir/;
32
use File::Temp qw/ tempfile tempdir /;
33
34
use Module::Load;
35
36
use Getopt::Long;
37
38
use AMC::Basic;
39
use AMC::Gui::Avancement;
40
use AMC::Data;
41
use AMC::DataModule::scoring ':question';
42
43
use_gettext;
44
use_amc_plugins();
45
46
my $cmd_pid      = '';
47
my @output_files = ();
48
49
sub catch_signal {
50
    my $signame = shift;
51
    debug "*** AMC-prepare : signal $signame, transfered to $cmd_pid...";
52
    kill 9, $cmd_pid if ($cmd_pid);
53
    if (@output_files) {
54
        debug "Removing files that are beeing built: "
55
          . join( " ", @output_files );
56
        unlink(@output_files);
57
    }
58
    die "Killed";
59
}
60
61
$SIG{INT} = \&catch_signal;
62
63
# PARAMETERS
64
65
my $mode     = "mbs";
66
my $data_dir = "";
67
my $calage   = '';
68
69
my $latex_engine    = 'latex';
70
my @engine_args     = ();
71
my $engine_topdf    = '';
72
my $prefix          = '';
73
my $filter          = '';
74
my $filtered_source = '';
75
my $codedigit       = '';
76
77
my $latex_stdout = '';
78
79
my $n_procs          = 0;
80
my $number_of_copies = 0;
81
82
my $progress    = 1;
83
my $progress_id = '';
84
85
my $out_calage        = '';
86
my $out_sujet         = '';
87
my $out_corrige       = '';
88
my $out_corrige_indiv = '';
89
my $out_catalog       = '';
90
91
my $jobname = "amc-compiled";
92
93
my $f_tex;
94
95
my $epoch = '';
96
97
unpack_args();
98
99
GetOptions(
100
    "mode=s"              => \$mode,
101
    "with=s"              => \$latex_engine,
102
    "data=s"              => \$data_dir,
103
    "calage=s"            => \$calage,
104
    "out-calage=s"        => \$out_calage,
105
    "out-sujet=s"         => \$out_sujet,
106
    "out-corrige=s"       => \$out_corrige,
107
    "out-corrige-indiv=s" => \$out_corrige_indiv,
108
    "out-catalog=s"       => \$out_catalog,
109
    "latex-stdout!"       => \$latex_stdout,
110
    "progression=s"       => \$progress,
111
    "progression-id=s"    => \$progress_id,
112
    "prefix=s"            => \$prefix,
113
    "n-procs=s"           => \$n_procs,
114
    "n-copies=s"          => \$number_of_copies,
115
    "filter=s"            => \$filter,
116
    "filtered-source=s"   => \$filtered_source,
117
    "codedigit=s"         => \$codedigit,
118
    "epoch=s"             => \$epoch,
119
);
120
121
debug("AMC-prepare / DEBUG") if (get_debug());
122
123
my %global_opts = (qw/NoWatermarkExterne 1 NoHyperRef 1/);
124
125
# Split the LaTeX engine string, to get
126
#
127
# 1) the engine command $latex_engine (eg. pdflatex)
128
#
129
# 2) the engine arguments @engine_args to be passed to this command
130
#
131
# 3) the command used to make a PDF file from the engine output
132
# (eg. dvipdfmx)
133
#
134
# The LaTeX engine string is on the form
135
#   <latex_engine>[+<pdf_engine>] <engine_args>
136
#
137
# For exemple:
138
#
139
# pdflatex
140
# latex+dvipdfmx
141
# platex+dvipdfmx
142
# lualatex --shell-escape
143
# latex+dvipdfmx --shell-escape
144
145
sub split_latex_engine {
146
    my ($engine) = @_;
147
148
    $latex_engine = $engine if ($engine);
149
150
    if ( $latex_engine =~ /([^ ]+)\s+(.*)/ ) {
151
        $latex_engine = $1;
152
        @engine_args  = split( / +/, $2 );
153
    }
154
155
    if ( $latex_engine =~ /(.*)\+(.*)/ ) {
156
        $latex_engine = $1;
157
        $engine_topdf = $2;
158
    }
159
}
160
161
split_latex_engine();
162
163
sub set_filtered_source {
164
    my ($filtered_source) = @_;
165
166
    # change directory where the $filtered_source is, and set $f_base to
167
    # the $filtered_source without path and without extension
168
169
    my ( $v, $d, $f_base );
170
171
    ( $v, $d, $f_tex ) = splitpath($filtered_source);
172
    chdir( catpath( $v, $d, "" ) );
173
    $f_base = $f_tex;
174
    $f_base =~ s/\.tex$//i;
175
176
    # AMC usualy sets $prefix to "DOC-", but if $prefix is empty, uses
177
    # the base name
178
179
    $prefix = $f_base . "-" if ( !$prefix );
180
}
181
182
# Uses an AMC::Gui::Avancement object to tell regularly the calling
183
# program how much work we have done so far.
184
185
my $avance = AMC::Gui::Avancement::new( $progress, id => $progress_id );
186
187
# Get and test the source file
188
189
my $source = $ARGV[0];
190
191
die "Nonexistent source file: $source" if ( !-f $source );
192
193
# $base is the source file base name (with the path but without
194
# extension).
195
196
my $base = $source;
197
$base =~ s/\.[a-zA-Z0-9]{1,4}$//gi;
198
199
# $filtered_source is the LaTeX fil made from the source file by the
200
# filter (for exemple, LaTeX or AMC-TXT).
201
202
$filtered_source = $base . '_filtered.tex' if ( !$filtered_source );
203
204
# default $data_dir value (hardly ever used):
205
206
$data_dir = "$base-data" if ( !$data_dir );
207
208
# make these filenames global
209
210
for ( \$data_dir, \$source, \$filtered_source ) {
211
    $$_ = rel2abs($$_);
212
}
213
214
my $data = AMC::Data->new($data_dir);
215
216
set_filtered_source($filtered_source);
217
218
# set environment variables for reproducible output
219
220
if ($epoch) {
221
    $ENV{SOURCE_DATE_EPOCH}                = $epoch;
222
    $ENV{SOURCE_DATE_EPOCH_TEX_PRIMITIVES} = 1;
223
    $ENV{FORCE_SOURCE_DATE}                = 1;
224
}
225
226
# These variables are used to track errors from LaTeX compiling
227
228
my $a_errors;    # the number of errors
229
my @errors_msg   = ();    # errors messages (questions specifications problems)
230
my @latex_errors = ();    # LaTeX compilation errors
231
232
sub flush_errors {
233
    debug(@errors_msg);
234
    print join( '', @errors_msg );
235
    @errors_msg = ();
236
}
237
238
# %info_vars collects the variables values that LaTeX wants to give us
239
240
my %info_vars = ();
241
242
sub relay_info_vars {
243
244
    # Relays variables to calling process
245
246
    print "Variables :\n";
247
    for my $k ( keys %info_vars ) {
248
        print "VAR: $k=" . $info_vars{$k} . "\n";
249
    }
250
}
251
252
sub exit_with_error {
253
    relay_info_vars();
254
    exit(1);
255
}
256
257
# check_question checks that, if the question question is a simple
258
# one, the number of correct answers is exactly one.
259
260
sub check_question {
261
    my ($q) = @_;
262
263
    my $t = $q->{etu} . ":" . $q->{titre};
264
265
    # if postcorrection is used, this check cannot be made as we will
266
    # only know which answers are correct after having captured the
267
    # teacher's copy.
268
    return () if ( $info_vars{postcorrect} );
269
270
    # if is_alias is true, the questions has already been checked...
271
    return if ( $q->{is_alias} );
272
273
    $q = $q->{q};
274
275
    if ($q) {
276
277
        # For multiple questions, no problem. $q->{partial} means that
278
        # all the question answers have not yet been parsed (this can
279
        # happen when using AMCnumericChoices or AMCOpen, because the
280
        # answers are only given in the separate answer sheet).
281
        if ( !( $q->{mult} || $q->{partial} ) ) {
282
            my $n_correct = 0;
283
            my $n_total   = 0;
284
            for my $i ( grep { /^R/ } ( keys %$q ) ) {
285
                $n_total++;
286
                $n_correct++ if ( $q->{$i} );
287
            }
288
            if ( $n_correct != 1 && !$q->{indicative} ) {
289
                $a_errors++;
290
                push @errors_msg,
291
                  "ERR: "
292
                  . sprintf(
293
                    __("%d/%d good answers not coherent for a simple question")
294
                      . " [%s]\n",
295
                    $n_correct, $n_total, $t );
296
            }
297
        }
298
    }
299
}
300
301
# analyse_cslog get the chars written in the boxes from the catalog
302
# build, and updates the layout_char database accordingly
303
304
sub analyse_cslog {
305
    my ($cslog_file) = @_;
306
307
    my $layout = $data->module('layout');
308
309
    $layout->begin_transaction('Char');
310
    $layout->clear_char();
311
    open( CSLOG, $cslog_file ) or die "Unable to open $cslog_file: $!";
312
    while (<CSLOG>) {
313
        if (/\\answer\{.*:(\d+),(\d+)\}\{(.*)\}$/) {
314
            my $question = $1;
315
            my $answer   = $2;
316
            my $char     = $3;
317
            $layout->char( $question, $answer, $char );
318
        }
319
    }
320
    close(CSLOG);
321
    $layout->end_transaction('Char');
322
}
323
324
# analyse_amclog checks common errors in LaTeX about questions:
325
#
326
# * same question ID used multiple times for the same paper, or same
327
# answer ID used multiple times for the same question
328
#
329
# * simple questions with number of good answers != 1
330
#
331
# * answer given outside a question
332
#
333
# These errors can be detected parsing the *.amc log file produced by
334
# LaTeX compilation, through AUTOQCM[...] messages.
335
336
sub analyse_amclog {
337
    my ($amclog_file) = @_;
338
339
    my $analyse_data = { etu => 0, titre => '', qs => {} };
340
    my %titres       = ();
341
    @errors_msg = ();
342
343
    debug("Check AMC log : $amclog_file");
344
345
    open( AMCLOG, $amclog_file ) or die "Unable to open $amclog_file: $!";
346
    while (<AMCLOG>) {
347
348
        # AUTOQCM[Q=N] tells that we begin with question number N
349
350
        if (/AUTOQCM\[Q=([0-9]+)\]/) {
351
352
            # first check that the previous question is ok:
353
            check_question($analyse_data);
354
355
            # then clear current question data:
356
            $analyse_data->{q} = {};
357
358
            # if this question has already be seen for current student...
359
            if ( $analyse_data->{qs}->{$1} ) {
360
361
                if ( $analyse_data->{qs}->{$1}->{partial} ) {
362
363
                    # if the question was partial (answers was not given in the
364
                    # question, but are now given in the answer sheet), it's
365
                    # ok. Simply get back the data already processed, and clear
366
                    # 'partial' and 'closed' flags:
367
368
                    $analyse_data->{q} = $analyse_data->{qs}->{$1};
369
                    for my $flag (qw/partial closed/) {
370
                        delete( $analyse_data->{q}->{$flag} );
371
                    }
372
                } else {
373
374
                    # if the question was NOT partial, this is an error!
375
376
                    $a_errors++;
377
                    push @errors_msg,
378
                      "ERR: "
379
                      . sprintf(
380
                        __(
381
"question ID used several times for the same paper: \"%s\""
382
                          )
383
                          . " [%s]\n",
384
                        $titres{$1},
385
                        $analyse_data->{etu}
386
                      );
387
                }
388
            }
389
390
            # register question data
391
            $analyse_data->{titre} = $titres{$1};
392
            $analyse_data->{titre} = 'unknown'
393
              if ( !defined( $analyse_data->{titre} ) );
394
            $analyse_data->{qs}->{$1} = $analyse_data->{q};
395
        }
396
397
        # AUTOQCM[QPART] tells that we end with a question without having
398
        # given all the answers
399
400
        if (/AUTOQCM\[QPART\]/) {
401
            $analyse_data->{q}->{partial} = 1;
402
        }
403
404
        # AUTOQCM[FQ] tells that we have finished with the current question
405
406
        if (/AUTOQCM\[FQ\]/) {
407
            $analyse_data->{q}->{closed} = 1;
408
        }
409
410
        # AUTOQCM[ETU=N] tells that we begin with student number N.
411
412
        if (/AUTOQCM\[ETU=([0-9]+)\]/) {
413
            my $student = $1;
414
415
            # first check the last question from preceding student is ok:
416
417
            check_question($analyse_data);
418
419
            # then clear all $analyse_data to begin with this student:
420
421
            $analyse_data = { etu => $student, titre => '', qs => {} };
422
        }
423
424
        # AUTOQCM[BR=N] tells that this student is a replicate of student N
425
426
        if (/AUTOQCM\[BR=([0-9]+)\]/) {
427
            my $alias = $1;
428
429
            $analyse_data->{is_alias} = 1;
430
            $analyse_data->{alias}    = $alias;    # unused ;)
431
        }
432
433
        # AUTOACM[NUM=N=ID] tells that question number N (internal
434
        # question number, not the question number shown on the sheet)
435
        # refers to ID (question name, string given as an argument to
436
        # question environment)
437
438
        if (/AUTOQCM\[NUM=([0-9]+)=(.+)\]/) {
439
440
            # stores this association (two-way)
441
442
            $titres{$1} = $2;
443
            $analyse_data->{titres}->{$2} = 1;
444
        }
445
446
        # AUTOQCM[MULT] tells that current question is a multiple question
447
448
        if (/AUTOQCM\[MULT\]/) {
449
            $analyse_data->{q}->{mult} = 1;
450
        }
451
452
        # AUTOQCM[INDIC] tells that current question is an indicative
453
        # question
454
455
        if (/AUTOQCM\[INDIC\]/) {
456
            $analyse_data->{q}->{indicative} = 1;
457
        }
458
459
        # AUTOQCM[REP=N:S] tells that answer number N is S (S can be 'B'
460
        # for 'correct' or 'M' for wrong)
461
462
        if (/AUTOQCM\[REP=([0-9]+):([BM])\]/) {
463
            my $rep = "R" . $1;
464
465
            if ( $analyse_data->{q}->{closed} ) {
466
467
                # If current question is already closed, this is an error!
468
469
                $a_errors++;
470
                push @errors_msg,
471
                  "ERR: "
472
                  . sprintf(
473
                    __(
474
"An answer appears to be given outside a question environment, after question \"%s\""
475
                      )
476
                      . " [%s]\n",
477
                    $analyse_data->{titre},
478
                    $analyse_data->{etu}
479
                  );
480
            }
481
482
            if ( defined( $analyse_data->{q}->{$rep} ) ) {
483
484
                # if we already saw an answer with the same N, this is an error!
485
486
                $a_errors++;
487
                push @errors_msg,
488
                  "ERR: "
489
                  . sprintf(
490
                    __(
491
"Answer number ID used several times for the same question: %s"
492
                      )
493
                      . " [%s]\n",
494
                    $1,
495
                    $analyse_data->{titre}
496
                  );
497
            }
498
499
            # stores the answer's status
500
            $analyse_data->{q}->{$rep} = ( $2 eq 'B' ? 1 : 0 );
501
        }
502
503
        # AUTOQCM[VAR:N=V] tells that variable named N has value V
504
505
        if (/AUTOQCM\[VAR:([0-9a-zA-Z.:-]+)=([^\]]+)\]/) {
506
            $info_vars{$1} = $2;
507
        }
508
509
    }
510
    close(AMCLOG);
511
512
    # check that the last question from the last student is ok:
513
514
    check_question($analyse_data);
515
516
    # Send error messages to the calling program through STDOUT
517
518
    flush_errors();
519
520
    debug("AMC log $amclog_file : $a_errors errors.");
521
}
522
523
# execute(%oo) launches the LaTeX engine with the right arguments, call it
524
# again if needed (for exemple when a second run is necessary to get
525
# references right), and then produces a PDF file from LaTeX output.
526
#
527
# $oo{command_opts} should be the options to be passed to latex_cmd, to
528
# build the LaTeX command to run, with all necessary arguments
529
530
my $filter_engine;
531
532
sub execute {
533
    my %oo   = (@_);
534
    my $errs = 0;
535
536
    prepare_filter();
537
538
    # gives the processing command to the filter
539
    $oo{command}  = [ latex_cmd( @{ $oo{command_opts} } ) ];
540
    $ENV{AMC_CMD} = join( ' ', @{ $oo{command} } );
541
542
    if ($filter) {
543
        if (  !$filter_engine->get_filter_result('done')
544
            || $filter_engine->get_filter_result('jobspecific') )
545
        {
546
            $errs = do_filter();
547
            $filter_engine->set_filter_result( 'done', 1 ) if ( !$errs );
548
        }
549
    }
550
551
    # first removes previous run's outputs
552
553
    for my $ext (qw/pdf dvi ps/) {
554
        if ( -f "$jobname.$ext" ) {
555
            debug "Removing old $ext";
556
            unlink("$jobname.$ext");
557
        }
558
    }
559
560
    exit_with_error() if ($errs);
561
562
    # the filter could have changed the latex engine, so update it
563
    $oo{command}  = [ latex_cmd( @{ $oo{command_opts} } ) ];
564
    $ENV{AMC_CMD} = join( ' ', @{ $oo{command} } );
565
566
    check_engine();
567
568
    my $min_runs = 1;     # minimum number of runs
569
    my $max_runs = 2;     # maximum number of runs
570
    my $n_run    = 0;     # number of runs so far
571
    my $rerun    = 0;     # has to re-run?
572
    my $format   = '';    # output format
573
574
    do {
575
576
        $n_run++;
577
578
        # clears errors from previous run
579
580
        $a_errors     = 0;
581
        @latex_errors = ();
582
583
        debug "%%% Compiling: pass $n_run";
584
585
        # lauches the command
586
587
        debug "COMMAND: $ENV{AMC_CMD}";
588
589
        $cmd_pid = open( EXEC, "-|", @{ $oo{command} } );
590
        die "Can't exec " . join( ' ', @{ $oo{command} } ) if ( !$cmd_pid );
591
592
        # parses the output
593
594
        while (<EXEC>) {
595
596
            # LaTeX Warning: Label(s) may have changed. Rerun to get
597
            # cross-references right. -> has to re-run
598
599
            $rerun = 1
600
              if (/^LaTeX Warning:.*Rerun to get cross-references right/);
601
            $min_runs = 2
602
              if (/Warning: .*run twice/);
603
604
            # Output written on jobname.pdf (10 pages) -> output
605
            # format is pdf
606
607
            $format = $1 if (/^Output written on .*\.([a-z]+) \(/);
608
609
            # Lines beginning with '!' are errors: collect them
610
611
            if (/^\!\s*(.*)$/) {
612
                my $e = $1;
613
                $e .= "..." if ( $e !~ /\.$/ );
614
                push @latex_errors, $e;
615
            }
616
617
            # detect style file path
618
619
            if (m=\(((?:[^\)]+/)?automultiplechoice.sty)(\)|$)=) {
620
                $info_vars{stypath} = $1;
621
            }
622
623
            # detect style file version
624
625
            if (/^AMC version: (.*)/) {
626
                $info_vars{styversion} = $1;
627
            }
628
629
            # Relays LaTeX log to calling program
630
631
            print STDERR $_ if (/^.+$/);
632
            print $_ if ( $latex_stdout && /^.+$/ );
633
        }
634
        close(EXEC);
635
        $cmd_pid = '';
636
637
    } while ( ( ( $n_run < $min_runs ) || ( $rerun && $n_run < $max_runs ) )
638
        && !$oo{once} );
639
640
    # For these engines, we already know what is the output format:
641
    # override detected one
642
643
    $format = 'dvi' if ( $latex_engine eq 'latex' );
644
    $format = 'pdf' if ( $latex_engine eq 'pdflatex' );
645
    $format = 'pdf' if ( $latex_engine eq 'xelatex' );
646
647
    print "Output format: $format\n";
648
    debug "Output format: $format\n";
649
650
    # Now converts output to PDF. Output format can be DVI or PDF. If
651
    # PDF, nothing has to be done...
652
653
    if ( $format eq 'dvi' ) {
654
        if ( -f "$jobname.dvi" ) {
655
656
            # default DVI->PDF engine is dvipdfmx
657
658
            $engine_topdf = 'dvipdfm'
659
              if ( !$engine_topdf );
660
661
            # if the choosend DVI->PDF engine is not present, try to get
662
            # another one
663
664
            if ( !commande_accessible($engine_topdf) ) {
665
                debug_and_stderr "WARNING: command $engine_topdf not available";
666
                $engine_topdf =
667
                  choose_command( 'dvipdfmx', 'dvipdfm', 'xdvipdfmx',
668
                    'dvipdf' );
669
            }
670
671
            if ($engine_topdf) {
672
673
                # Now, convert DVI to PDF
674
675
                debug "Converting DVI to PDF with $engine_topdf ...";
676
                if ( $engine_topdf eq 'dvipdf' ) {
677
                    system_debug( cmd =>
678
                          [ $engine_topdf, "$jobname.dvi", "$jobname.pdf" ] );
679
                } else {
680
                    system_debug(
681
                        cmd => [
682
                            $engine_topdf,  "-o",
683
                            "$jobname.pdf", "$jobname.dvi"
684
                        ]
685
                    );
686
                }
687
            } else {
688
689
                # No available DVI->PDF engine!
690
691
                debug_and_stderr
692
                  "ERROR: I can't find dvipdf/dvipdfm/xdvipdfmx command !";
693
            }
694
        } else {
695
            debug "No DVI";
696
        }
697
    }
698
699
}
700
701
# do_filter() converts the source file to LaTeX format, using the
702
# right AMC::Filter::* module
703
704
sub prepare_filter {
705
    if ($filter) {
706
        if ( !$filter_engine ) {
707
            load("AMC::Filter::$filter");
708
            $filter_engine = "AMC::Filter::$filter"->new( jobname => $jobname );
709
            $filter_engine->pre_filter($source);
710
711
            # sometimes the filter says that the source file don't need to
712
            # be changed
713
714
            set_filtered_source($source)
715
              if ( $filter_engine->unchanged );
716
        }
717
    } else {
718
719
        # Empty filter: the source is already a LaTeX file
720
        set_filtered_source($source);
721
    }
722
}
723
724
sub do_filter {
725
    my $n_err = 0;
726
727
    if ($filter) {
728
729
        # Loads and call appropriate filter to convert $source to
730
        # $filtered_source
731
732
        prepare_filter();
733
        $filter_engine->filter( $source, $filtered_source );
734
735
        # show conversion errors
736
737
        for ( $filter_engine->errors() ) {
738
            print "ERR: $_\n";
739
            $n_err++;
740
        }
741
742
        # sometimes the filter asks to override the LaTeX engine
743
744
        split_latex_engine(
745
            $filter_engine->{project_options}->{moteur_latex_b} )
746
          if ( $filter_engine->{project_options}->{moteur_latex_b} );
747
748
        # or to set the number of copies to a particular value
749
750
        $number_of_copies =
751
          $filter_engine->{project_options}->{'nombre_copies'}
752
          if (
753
            exists(
754
                $filter_engine->{project_options}->{'nombre_copies'}
755
            )
756
          );
757
758
    }
759
760
    return ($n_err);
761
}
762
763
# give_latex_errors($context) Relay suitably formatted LaTeX errors to
764
# calling program (usualy AMC GUI). $context is the name of the
765
# document we are building.
766
767
sub give_latex_errors {
768
    my ($context) = @_;
769
    if (@latex_errors) {
770
        print "ERR: <i>"
771
          . sprintf( __("%d errors during LaTeX compiling") . " (%s)</i>\n",
772
            ( 1 + $#latex_errors ), $context );
773
        for (@latex_errors) {
774
            print "ERR>$_\n";
775
        }
776
        exit_with_error();
777
    }
778
}
779
780
# transfer($orig,$dest) moves $orig to $dest, removing $dest if $orig
781
# does not exist
782
783
sub transfer {
784
    my ( $orig, $dest ) = @_;
785
    if ( -f $orig ) {
786
        debug "Moving $orig --> $dest";
787
        move( $orig, $dest );
788
    } else {
789
        debug "No source: removing $dest";
790
        unlink($dest);
791
    }
792
}
793
794
# latex_reprocucible_commands($engine) returns commands suitable for
795
# the given engine to get reproducible output.
796
797
sub latex_reproducible_commands {
798
    my ($engine) = @_;
799
    if ( $engine eq 'pdflatex' ) {
800
        return ("\\pdfinfo{ /Producer (LaTeX) /Creator () }\\pdfsuppressptexinfo=-1\\pdftrailerid{}");
801
    } else {
802
        return ("");
803
    }
804
}
805
806
# latex_cmd(%o) builds the LaTeX command and arguments to be passed to
807
# the execute command, using the engine specifications and extra
808
# options %o to pass to LaTeX: for each name=>value from %o, a LaTeX
809
# command '\def\name{value}' is passed to LaTeX through the
810
# jobname-config.tex, that will be read by the automultiplechoice
811
# package. This allows to relay some options to LaTeX (number of
812
# copies, document needed for exemple).
813
814
sub latex_cmd {
815
    my (%o) = @_;
816
817
    $o{AMCNombreCopies} = $number_of_copies if ( $number_of_copies > 0 );
818
819
    # build a configuration tex file, that will be read by the
820
    # autoultiplechoice LaTeX package, from the %o options:
821
    open( CONFIG, ">:utf8", "$jobname-config.tex" )
822
      or die "Unable to open config file: $!";
823
    print CONFIG latex_reproducible_commands($latex_engine) if ($epoch);
824
    for my $k ( keys %o ) {
825
        print CONFIG "\\def\\" . $k . "{" . $o{$k} . "}";
826
    }
827
    print CONFIG "\n";
828
    close(CONFIG);
829
830
    return ( $latex_engine, "--jobname=" . $jobname,
831
        "-interaction=nonstopmode", @engine_args, $f_tex );
832
}
833
834
# check_engine() checks that the requeted LaTeX engine is available on
835
# the system
836
837
sub check_engine {
838
    if ( !commande_accessible($latex_engine) ) {
839
        print "ERR: "
840
          . sprintf(
841
            __(
842
"LaTeX command configured is not present (%s). Install it or change configuration, and then rerun."
843
            ),
844
            $latex_engine
845
          ) . "\n";
846
        exit_with_error();
847
    }
848
}
849
850
# the $mode option passed to AMC-prepare contains characters that
851
# explains what is to be prepared...
852
853
my %to_do = ();
854
while ( $mode =~ s/^[^a-z]*([a-z])(\[[a-z]*\])?//i ) {
855
    $to_do{$1} = ( defined($2) ? $2 : 1 );
856
}
857
858
############################################################################
859
# MODE f: filter source file to LaTeX format
860
############################################################################
861
862
if ( $to_do{f} ) {
863
864
    # FILTER
865
    do_filter();
866
}
867
868
############################################################################
869
# MODE S: builds the solution
870
############################################################################
871
872
sub build_solution {
873
    execute( command_opts => [ %global_opts, CorrigeExterne => 1 ] );
874
    transfer( "$jobname.pdf", $out_corrige );
875
    give_latex_errors( __ "solution" );
876
}
877
878
if ( $to_do{S} ) {
879
    build_solution();
880
}
881
882
############################################################################
883
# MODE C: builds the catalog
884
############################################################################
885
886
sub build_catalog {
887
    execute( command_opts => [ %global_opts, CatalogExterne => 1 ] );
888
    transfer( "$jobname.pdf", $out_catalog );
889
    analyse_cslog("$jobname.cs");
890
    give_latex_errors( __ "catalog" );
891
}
892
893
if ( $to_do{C} ) {
894
    build_catalog();
895
}
896
897
############################################################################
898
# MODE s: builds the subject and a solution (with all the answers for
899
# questions, but with a different layout)
900
############################################################################
901
902
if ( $to_do{s} ) {
903
    $to_do{s} = '[sc]' if ( $to_do{s} eq '1' );
904
905
    @output_files = ( $out_sujet, $out_calage, $out_corrige, $out_catalog );
906
907
    $out_calage  = $prefix . "calage.xy"   if ( !$out_calage );
908
    $out_corrige = $prefix . "corrige.pdf" if ( !$out_corrige );
909
    $out_catalog = $prefix . "catalog.pdf" if ( !$out_catalog );
910
    $out_sujet   = $prefix . "sujet.pdf"   if ( !$out_sujet );
911
912
    for my $f ( $out_calage, $out_corrige, $out_corrige_indiv, $out_sujet,
913
        $out_catalog )
914
    {
915
        if ( -f $f ) {
916
            debug "Removing already existing file: $f";
917
            unlink($f);
918
        }
919
    }
920
921
    # 1) SUBJECT
922
923
    my $report = $data->module('report');
924
925
    $report->begin_transaction("prtX");
926
    $report->printed_clear();
927
    $report->end_transaction("prtX");
928
929
    execute( command_opts => [ %global_opts, SujetExterne => 1 ] );
930
    analyse_amclog("$jobname.amc");
931
    give_latex_errors( __ "question sheet" );
932
933
    if ( $a_errors > 0 ) {
934
        debug("$a_errors errors detected: EXIT");
935
        exit_with_error();
936
    }
937
938
    transfer( "$jobname.pdf", $out_sujet );
939
    transfer( "$jobname.xy",  $out_calage );
940
941
    # Looks for accents problems in question IDs...
942
943
    my %qids        = ();
944
    my $unknown_qid = 0;
945
    if ( open( XYFILE, $out_calage ) ) {
946
        binmode(XYFILE);
947
        while (<XYFILE>) {
948
            if ( !utf8::decode($_) || /\\IeC/ ) {
949
                if (
950
/\\tracepos\{[^:]*:[^:]*:(.+):[^:]*\}\{([+-]?[0-9.]+[a-z]*)\}\{([+-]?[0-9.]+[a-z]*)\}(?:\{([a-zA-Z]*)\})?$/
951
                  )
952
                {
953
                    $qids{$1} = 1;
954
                } else {
955
                    $unknown_qid = 1;
956
                }
957
            }
958
        }
959
        close(XYFILE);
960
        if (%qids) {
961
            push @errors_msg, map {
962
                "WARN: "
963
                  . sprintf(
964
                    __(
965
"please remove accentuated or non-standard characters from the following question ID: \"%s\""
966
                    ),
967
                    $_
968
                  )
969
                  . "\n"
970
            } ( sort { $a cmp $b } ( keys %qids ) );
971
        } elsif ($unknown_qid) {
972
            push @errors_msg,
973
              "WARN: "
974
              . __(
975
"some question IDs seems to have accentuated or non-standard characters. This may break future processings."
976
              ) . "\n";
977
        }
978
    }
979
    flush_errors();
980
981
    # 2) SOLUTION
982
983
    if ( $to_do{s} =~ /s/ ) {
984
        build_solution();
985
    } else {
986
        debug "Solution not requested: removing $out_corrige";
987
        unlink($out_corrige);
988
    }
989
990
    # 3) CATALOG
991
992
    if ( $to_do{s} =~ /c/ ) {
993
        build_catalog();
994
    } else {
995
        debug "Catalog not requested: removing $out_catalog";
996
        unlink($out_catalog);
997
    }
998
}
999
1000
############################################################################
1001
# MODE k: builds individual corrected answer sheets (exactly the same
1002
# sheets as for the students, but with correct answers ticked).
1003
############################################################################
1004
1005
if ( $to_do{k} ) {
1006
1007
    my $of = $out_corrige_indiv;
1008
    $of = $out_corrige            if ( !$of && !$to_do{s} );
1009
    $of = $prefix . "corrige.pdf" if ( !$of );
1010
1011
    if ( -f $of ) {
1012
        debug "Removing already existing file: $of";
1013
        unlink($of);
1014
    }
1015
1016
    @output_files = ($of);
1017
1018
    execute( command_opts => [ %global_opts, qw/CorrigeIndivExterne 1/ ] );
1019
    transfer( "$jobname.pdf", $of );
1020
    give_latex_errors( __ "individual solution" );
1021
}
1022
1023
############################################################################
1024
# MODE b: extracts the scoring strategy to the scoring database,
1025
# parsing the AUTOQCM[...] messages from the LaTeX output.
1026
############################################################################
1027
1028
if ( $to_do{b} ) {
1029
1030
    print "********** Making marks scale...\n";
1031
1032
    my %bs     = ();
1033
    my %titres = ();
1034
1035
    my $quest         = '';
1036
    my $rep           = '';
1037
    my $outside_quest = '';
1038
    my $etu           = 0;
1039
1040
    my $delta = 0;
1041
1042
    # Opens a connection with the database
1043
1044
    my $scoring = $data->module('scoring');
1045
    my $capture = $data->module('capture');
1046
1047
    # Launches the LaTeX engine
1048
1049
    my @opts = (qw/ScoringExterne 1 NoHyperRef 1/);
1050
1051
    if ( !$codedigit ) {
1052
1053
        # if not explicitly given, uses the same codedigit convention
1054
        # as recorded from the 'extract layout' phase
1055
        my $layout = $data->module('layout');
1056
        $codedigit = $layout->variable_transaction('build:codedigit');
1057
        $codedigit = 'dot' if ( !$codedigit );    # old AMC versions
1058
    }
1059
    if ($codedigit) {
1060
        push @opts, "codeDigitExterne", $codedigit;
1061
    }
1062
1063
    execute(
1064
        command_opts => [@opts],
1065
        once         => 1
1066
    );
1067
1068
    open( AMCLOG, "$jobname.amc" ) or die "Unable to open $jobname.amc : $!";
1069
1070
    my $qs                    = {};
1071
    my $qs0                   = {}; # memory for student 0 (when using AMCformS)
1072
    my $current_q             = {};
1073
    my $qs0_count             = 0;
1074
    my $finished_with_student = 0;
1075
1076
    # and parse the log...
1077
1078
    $scoring->begin_transaction('ScEx');
1079
    annotate_source_change($capture);
1080
    $scoring->clear_strategy;
1081
1082
  PARSELOG: while (<AMCLOG>) {
1083
        debug($_) if ($_);
1084
1085
        # AUTOQCM[TOTAL=N] tells that the total number of sheets is
1086
        # N. This will allow us to relay the progression of the
1087
        # process to the calling process.
1088
1089
        if (/AUTOQCM\[TOTAL=([\s0-9]+)\]/) {
1090
            my $t = $1;
1091
            $t =~ s/\s//g;
1092
            if ( $t > 0 ) {
1093
                $delta = 1 / $t;
1094
            } else {
1095
                print "*** TOTAL=$t ***\n";
1096
            }
1097
        }
1098
1099
        if (/AUTOQCM\[ETU=([0-9]+)\]/) {
1100
1101
            # save if student 0
1102
            $qs0 = $qs if ( $etu == 0 );
1103
1104
            # beginning of student sheet
1105
            $avance->progres($delta) if ( $etu ne '' );
1106
            $etu = $1;
1107
            print "Sheet $etu...\n";
1108
            debug "Sheet $etu...\n";
1109
            $qs                    = {};
1110
            $finished_with_student = 0;
1111
        }
1112
1113
        next PARSELOG if ($finished_with_student);
1114
1115
        if (/AUTOQCM\[FQ\]/) {
1116
1117
            # end of question: register it (or update it)
1118
            $scoring->new_question(
1119
                $etu,
1120
                $quest,
1121
                ( $current_q->{multiple} ? QUESTION_MULT : QUESTION_SIMPLE ),
1122
                $current_q->{indicative},
1123
                $current_q->{strategy}
1124
            );
1125
            $qs->{$quest}  = $current_q;
1126
            $outside_quest = $quest;
1127
            $quest         = '';
1128
            $rep           = '';
1129
        }
1130
1131
        if (/AUTOQCM\[Q=([0-9]+)\]/) {
1132
1133
            # beginning of question
1134
            $quest = $1;
1135
            $rep   = '';
1136
            if ( $qs->{$quest} ) {
1137
                $current_q = $qs->{$quest};
1138
            } else {
1139
                $current_q = {
1140
                    multiple   => 0,
1141
                    indicative => 0,
1142
                    strategy   => '',
1143
                };
1144
            }
1145
        }
1146
1147
        if (/AUTOQCM\[NUM=([0-9]+)=(.+)\]/) {
1148
1149
            # association question-number<->question-title
1150
            $scoring->question_title( $1, $2 ); 
1151
        }
1152
1153
        if (/AUTOQCM\[MULT\]/) {
1154
1155
            # this question is a multiple-style one
1156
            $current_q->{multiple} = 1;
1157
        }
1158
1159
        if (/AUTOQCM\[INDIC\]/) {
1160
1161
            # this question is an indicative one
1162
            $current_q->{indicative} = 1;
1163
        }
1164
1165
        if (/AUTOQCM\[REP=([0-9]+):([BM])\]/) {
1166
1167
            # answer
1168
            $rep = $1;
1169
            my $qq = $quest;
1170
            if ( $outside_quest && !$qq ) {
1171
                $qq = $outside_quest;
1172
                debug_and_stderr
1173
"WARNING: answer outside questions for student $etu (after question $qq)";
1174
            }
1175
            $scoring->new_answer( $etu, $qq, $rep, ( $2 eq 'B' ? 1 : 0 ), '' );
1176
        }
1177
1178
        # AUTOQCM[BR=N] tells that this student is a replicate of student N
1179
1180
        if (/AUTOQCM\[BR=([0-9]+)\]/) {
1181
            my $alias = $1;
1182
1183
            $scoring->replicate( $alias, $etu );
1184
            $etu                   = $alias;
1185
            $qs                    = $qs0 if ( $etu == 0 );
1186
            $finished_with_student = 1 if ( $qs0_count > 0 );
1187
            $qs0_count++;
1188
        }
1189
1190
        if (/AUTOQCM\[B=([^\]]+)\]/) {
1191
1192
            # scoring strategy string
1193
            if ($quest) {
1194
                if ($rep) {
1195
1196
                    # associated to an answer
1197
                    $scoring->add_answer_strategy( $etu, $quest, $rep, $1 );
1198
                } else {
1199
1200
                    # associated to a question
1201
                    $current_q->{strategy} = (
1202
                          $current_q->{strategy}
1203
                        ? $current_q->{strategy} . ','
1204
                        : ''
1205
                    ) . $1;
1206
                }
1207
            } else {
1208
1209
                # global scoring strategy, associated to a student if
1210
                # $etu>0, or to all students if $etu==0
1211
                $scoring->add_main_strategy( $etu, $1 );
1212
            }
1213
        }
1214
1215
        # AUTOQCM[BDS=string] gives us the default scoring stragety
1216
        # for simple questions
1217
        # AUTOQCM[BDM=string] gives us the default scoring stragety
1218
        # for multiple questions
1219
1220
        if (/AUTOQCM\[BD(S|M)=([^\]]+)\]/) {
1221
            $scoring->default_strategy(
1222
                ( $1 eq 'S' ? QUESTION_SIMPLE : QUESTION_MULT ), $2 );
1223
        }
1224
1225
        if (/AUTOQCM\[VAR:([0-9a-zA-Z.-]+)=([^\]]+)\]/) {
1226
1227
            # variables
1228
            my $name  = $1;
1229
            my $value = $2;
1230
            $name = 'postcorrect_flag' if ( $name eq 'postcorrect' );
1231
            $scoring->variable( $name, $value );
1232
        }
1233
    }
1234
    close(AMCLOG);
1235
1236
    $scoring->end_transaction('ScEx');
1237
}
1238
1239
relay_info_vars();
1240
1241
$avance->fin();