AMC-prepare.pl

DENIS Sébastien, 12/09/2018 03:27 pm

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