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