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