AMC-prepare.pl

Alexis Bienvenüe, 09/19/2019 02:30 pm

Download (27.5 kB)

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