AMC-prepare.pl

DENIS Sébastien, 07/10/2023 09:47 pm

Download (35.9 kB)

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