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();
|