-
-
Notifications
You must be signed in to change notification settings - Fork 79
Expand file tree
/
Copy pathAnswerHash.pm
More file actions
884 lines (694 loc) · 25.7 KB
/
Copy pathAnswerHash.pm
File metadata and controls
884 lines (694 loc) · 25.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
##########################################################################
## AnswerHash Package
##
## Provides a data structure for answer hashes. Currently just a wrapper
## for the hash, but that might change
=head1 NAME
AnswerHash.pm - a class to store student answers and the answer evaluator
=head1 DESCRIPTION - AnswerHash
This class stores information related to the student's answer. It is little more than a standard perl hash with
a special name, but it does have some access and manipulation methods.
Usage:
$rh_ans = AnswerHash->new();
=head2 SYNOPSIS
The answer hash class is guaranteed to contain the following instance variables:
score => $correctQ,
correct_ans => $originalCorrEqn,
student_ans => $modified_student_ans
original_student_ans => $original_student_answer,
ans_message => $PGanswerMessage,
type => 'typeString',
preview_text_string => $preview_text_string,
preview_latex_string => $preview_latex_string
$ans_hash->{score} -- a number between 0 and 1 indicating
whether the answer is correct. Fractions
allow the implementation of partial
credit for incorrect answers.
$ans_hash->{correct_ans} -- The correct answer, as supplied by the
instructor and then formatted. This can
be viewed by the student after the answer date.
$ans_hash->{student_ans} -- This is the student answer, after reformatting;
for example the answer might be forced
to capital letters for comparison with
the instructors answer. For a numerical
answer, it gives the evaluated answer.
This is displayed in the section reporting
the results of checking the student answers.
$ans_hash->{original_student_ans} -- This is the original student answer.
This is displayed on the preview page and may be used for
sticky answers.
$ans_hash->{ans_message} -- Any error message, or hint provided by
the answer evaluator.
This is also displayed in the section reporting
the results of checking the student answers.
$ans_hash->{type} -- A string indicating the type of answer evaluator.
This helps in preprocessing the student answer for errors.
Some examples:
'number_with_units'
'function'
'frac_number'
'arith_number'
$ans_hash->{preview_text_string} --
This typically shows how the student answer was parsed. It is
displayed on the preview page. For a student answer of 2sin(3x)
this would be 2*sin(3*x). For string answers it is typically the
same as $ans_hash{student_ans}.
$ans_hash->{preview_latex_string} --
THIS IS OPTIONAL. This is latex version of the student answer
which is used to show a typeset view on the answer on the preview
page. For a student answer of 2/3, this would be \frac{2}{3}.
'ans_message' => '', # null string
'preview_text_string' => undef,
'preview_latex_string' => undef,
'error_flag' => undef,
'error_message' => '',
=head2 METHODS
=cut
package AnswerHash;
use Exporter;
use PGUtil qw(not_null pretty_print);
use Mojo::JSON qw(encode_json);
# initialization fields
my %fields = (
'score' => undef,
'correct_ans' => undef,
'student_ans' => undef,
'ans_message' => undef,
'type' => undef,
'preview_text_string' => undef,
'preview_latex_string' => undef,
'original_student_ans' => undef
);
## Initializing constructor
=head3 AnswerHash->new
Usage
$rh_anshash = AnswerHash->new;
returns an object of type AnswerHash.
=cut
sub new {
my $class = shift @_;
my $self = {
'score' => 0,
'correct_ans' => 'No correct answer specified',
'student_ans' => undef,
'ans_message' => '',
'ans_label' => undef,
'type' => 'Undefined answer evaluator type',
'preview_text_string' => undef,
'preview_latex_string' => undef,
'original_student_ans' => undef,
'error_flag' => undef,
'error_message' => '',
}; # return a reference to a hash.
bless $self, $class;
$self->setKeys(@_);
return $self;
}
## IN: a hash
## Checks to make sure that the keys are valid,
## then sets their value
=head3 setKeys
Usage:
$rh_ans->setKeys(score=>1, student_answer => "yes");
Sets standard elements in the AnswerHash (the ones defined
above). Will give error if one attempts to set non-standard key
To set a non-standard element in a hash use
$rh_ans->{non-standard-key} = newValue;
There are no safety checks when using this method.
=cut
sub setKeys {
my $self = shift;
my %inits = @_;
foreach my $item (keys %inits) {
if (exists $fields{$item}) {
$self->{$item} = $inits{$item};
} else {
warn "AnswerHash cannot automatically initialize an item named $item";
}
}
}
# access methods
=head3 data
Usage:
$rh_ans->data('foo'); # set $rh_ans->{student_ans} = 'foo';
$student_input = $rh_ans->data(); # retrieve value of $rh_ans->{student_ans}
synonym for C<input>
=head3 input
Usage:
$rh_ans->input('foo') # sets $rh_ans->{student_ans} = 'foo';
$student_input = $rh_ans->input();
synonym for C<data>
=cut
sub data {
my $self = shift;
$self->input(@_);
}
sub input {
my $self = shift;
my $input = shift;
$self->{student_ans} = $input if defined($input);
$self->{student_ans};
}
=head3 score
Usage:
$rh_ans->score(1)
$score = $rh_ans->score();
Retrieve or set $rh_ans->{score}, the student's score on the problem.
=cut
sub score {
my $self = shift;
my $score = shift;
$self->{score} = $score || 0 if defined($score);
$self->{score};
}
=head3 stringify_hash
Usage:
$rh_ans->stringify_hash;
Turns all values in the hash into strings (so they won't cause trouble outside
the safe compartment). Hashes and arrays are converted into a JSON string.
=cut
sub stringify_hash {
my $self = shift;
Parser::Context->current(undef, $self->{correct_value}->context) if $self->{correct_value};
foreach my $key (keys %$self) {
my $ref = ref($self->{$key});
next if !$ref;
if ($ref eq "HASH" or $ref eq "ARRAY") {
$self->{$key} = encode_json($self->{$key});
} else {
$self->{$key} = "$self->{$key}";
}
}
}
# error methods
=head3 throw_error
Usage:
$rh_ans->throw_error("FLAG", "message");
FLAG is a distinctive word that describes the type of error.
Examples are EVAL for an evaluation error or "SYNTAX" for a syntax error.
The entry $rh_ans->{error_flag} is set to "FLAG".
The catch_error and clear_error methods use this entry.
message is a descriptive message for the end user, defining what error occurred.
=head3 catch_error
Usage:
$rh_ans->catch_error("FLAG2");
Returns true (1) if $rh_ans->{error_flag} equals "FLAG2", otherwise it returns
false (empty string).
=head3 clear_error
Usage:
$rh_ans->clear_error("FLAG2");
If $rh_ans->{error_flag} equals "FLAG2" then the {error_flag} entry is set to
the empty string as is the entry {error_message}
=head3 error_flag, error_message
Usage:
$flag = $rh_ans -> error_flag();
$message = $rh_ans -> error_message();
Retrieve or set the {error_flag} and {error_message} entries.
Use catch_error and throw_error where possible.
=cut
sub throw_error {
my $self = shift;
my $flag = shift;
my $message = shift;
$self->{error_message} .= " $message " if defined($message);
$self->{error_flag} = $flag if defined($flag);
$self->{error_flag};
}
sub catch_error {
my $self = shift;
my $flag = shift;
return ('') unless defined($self->{error_flag});
return $self->{error_flag} unless $flag; # empty input catches all errors.
return $self->{error_flag} if $self->{error_flag} eq $flag;
return ''; # nothing to catch
}
sub clear_error {
my $self = shift;
my $flag = shift;
if (defined($flag) and $flag =~ /\S/ and defined($self->{error_flag}) and $flag eq $self->{error_flag}) {
$self->{error_flag} = undef;
$self->{error_message} = undef;
}
$self;
}
sub error_flag {
my $self = shift;
my $flag = shift;
$self->{error_flag} = $flag if defined($flag);
$self->{error_flag};
}
sub error_message {
my $self = shift;
my $message = shift;
$self->{error_message} = $message if defined($message);
$self->{error_message};
}
# error print out method
# =head4 pretty_print
#
#
# Usage: $rh_ans -> pretty_print();
#
#
# Returns a string containing a representation of the AnswerHash as an HTML table.
#
# =cut
#
# sub pretty_print {
# my $r_input = shift;
# my $level = shift;
# $level = 4 unless defined($level);
# $level--;
# return '' unless $level > 0; # only print three levels of hashes (safety feature)
# my $out = '';
# if ( not ref($r_input) ) {
# $out = $r_input; # not a reference
# $out =~ s/</</g; # protect for HTML output
# } elsif (ref($r_input) =~/hash/i) {
# local($^W) = 0;
# $out .= "<TABLE border = \"2\" cellpadding = \"3\" BGCOLOR = \"#FFFFFF\">";
# foreach my $key (sort keys %$r_input ) {
# $out .= "<tr><TD> $key</TD><TD>=></td><td> ".pretty_print($r_input->{$key}, $level) . "</td></tr>";
# }
# $out .="</table>";
# } elsif (ref($r_input) eq 'ARRAY' ) {
# my @array = @$r_input;
# $out .= "( " ;
# while (@array) {
# $out .= pretty_print(shift @array, $level) . " , ";
# }
# $out .= " )";
# } elsif (ref($r_input) eq 'CODE') {
# $out = "$r_input";
# } else {
# $out = $r_input;
# $out =~ s/</</g; # protect for HTML output
# }
# $out;
# }
# action methods
=head3 OR
Usage:
$rh_ans->OR($rh_ans2);
Returns a new AnswerHash whose score is the maximum of the scores in $rh_ans and $rh_ans2.
The correct answers for the two hashes are combined with "OR".
The types are concatenated with "OR" as well.
Currently nothing is done with the error flags and messages.
=head3 AND
Usage:
$rh_ans->AND($rh_ans2);
Returns a new AnswerHash whose score is the minimum of the scores in $rh_ans and $rh_ans2.
The correct answers for the two hashes are combined with "AND".
The types are concatenated with "AND" as well.
Currently nothing is done with the error flags and messages.
=cut
sub OR {
my $self = shift;
my $rh_ans2 = shift;
my %options = @_;
return ($self) unless defined($rh_ans2) and ref($rh_ans2) eq 'AnswerHash';
my $out_hash = new AnswerHash;
# score is the maximum of the two scores
$out_hash->{score} = ($self->{score} < $rh_ans2->{score}) ? $rh_ans2->{score} : $self->{score};
$out_hash->{correct_ans} = join(" OR ", $self->{correct_ans}, $rh_ans2->{correct_ans});
$out_hash->{student_ans} = $self->{student_ans};
$out_hash->{type} = join(" OR ", $self->{type}, $rh_ans2->{type});
$out_hash->{preview_text_string} = join(" ", $self->{preview_text_string}, $rh_ans2->{preview_text_string});
$out_hash->{original_student_ans} = $self->{original_student_ans};
$out_hash;
}
sub AND {
my $self = shift;
my $rh_ans2 = shift;
my %options = @_;
my $out_hash = new AnswerHash;
# score is the minimum of the two scores
$out_hash->{score} = ($self->{score} > $rh_ans2->{score}) ? $rh_ans2->{score} : $self->{score};
$out_hash->{correct_ans} = join(" AND ", $self->{correct_ans}, $rh_ans2->{correct_ans});
$out_hash->{student_ans} = $self->{student_ans};
$out_hash->{type} = join(" AND ", $self->{type}, $rh_ans2->{type});
$out_hash->{preview_text_string} = join(" ", $self->{preview_text_string}, $rh_ans2->{preview_text_string});
$out_hash->{preview_latex_string} =
join(" \\quad ", $self->{preview_latex_string}, $rh_ans2->{preview_latex_string});
$out_hash->{original_student_ans} = $self->{original_student_ans};
$out_hash;
}
package AnswerEvaluator;
use Exporter;
use PGUtil qw(not_null pretty_print);
=head1 DESCRIPTION: AnswerEvaluator
This class organizes the construction of answer evaluator subroutines which check the
student's answer. By plugging filters into the answer evaluator class you can customize the way the
student's answer is normalized and checked. Our hope is that with properly designed filters, it will be
possible to reuse the filters in different combinations to obtain different answer evaluators,
thus greatly reducing the programming and maintenance required for constructing answer evaluators.
Usage:
$ans_eval = new AnswerEvaluator;
=head2 METHODS
=head3 new
Create a new AnswerEvaluator
Usage:
AnswerEvaluator->new();
=cut
sub new {
my $class = shift @_;
my $self = {
pre_filters => [ [ \&blank_prefilter ] ],
evaluators => [],
post_filters => [ [ \&blank_postfilter ] ],
debug => 0,
rh_ans => new AnswerHash,
};
bless $self, $class;
$self->rh_ans(@_); #initialize answer hash
return $self;
}
sub clone {
my $self = shift;
my $copy = bless {%$self}, ref $self;
return $copy;
}
# dereference_array_ans pretty prints an answer which is stored as an anonymous array.
sub dereference_array_ans {
my $self = shift;
my $rh_ans = shift;
$rh_ans->{_filter_name} = 'dereference_array_ans';
if (defined($rh_ans->{student_ans}) and ref($rh_ans->{student_ans}) eq 'ARRAY') {
$rh_ans->{student_ans} = "( " . join(" , ", @{ $rh_ans->{student_ans} }) . " ) ";
}
$rh_ans;
}
sub get_student_answer {
my $self = shift;
my $input = shift;
my %answer_options = @_;
my $display_input = $input;
$display_input =~ s/\0/\\0/g; # make null spacings visible
eval(q!main::DEBUG_MESSAGE( "Raw student answer is |$display_input|")!) if $self->{debug};
$input = '' unless defined($input);
if (ref($input) =~ /AnswerHash/) {
# in this case nothing needs to be done, since the student's answer is already in an answerhash.
# This is useful when an AnswerEvaluator is used as a filter in another answer evaluator.
} elsif ($input =~ /\0/) { # this case may occur with older versions of CGI??
my @input = split(/\0/, $input);
$self->{rh_ans}->{original_student_ans} = " ( " . join(", ", @input) . " ) ";
$input = \@input;
$self->{rh_ans}->{student_ans} = $input;
} elsif (ref($input) eq 'ARRAY') { # sometimes the answer may already be decoded into an array.
my @input = @$input;
$self->{rh_ans}->{original_student_ans} = " ( " . join(", ", @input) . " ) ";
$input = \@input;
$self->{rh_ans}->{student_ans} = $input;
} else {
$self->{rh_ans}->{original_student_ans} = $input;
$self->{rh_ans}->{student_ans} = $input;
}
$self->{rh_ans}->{ans_label} = $answer_options{ans_label} if defined($answer_options{ans_label});
$self->{rh_ans}->{_filter_name} = 'get_student_answer';
$input;
}
=head3 evaluate
Usage:
$answer_evaluator->evaluate($student_answer_string)
=cut
our $count; # used to keep track of where we are in queue
sub evaluate {
my $self = shift;
$self->get_student_answer(@_);
# dereference $self->{rh_ans};
my $rh_ans = $self->{rh_ans};
$rh_ans->{error_flag} = undef; #reset the error flags in case
$rh_ans->{done} = undef; #the answer evaluator is called twice
eval(q!main::DEBUG_MESSAGE( "<H3> Answer evaluator information: </H3>")!)
if defined($self->{debug})
and $self->{debug} > 0;
$self->print_result_if_debug('pre_filter', $rh_ans);
my @prefilters = @{ $self->{pre_filters} };
$count = 0; # the get student answer filter is counted as filter -1
foreach my $i (@prefilters) {
last if defined($rh_ans->{error_flag});
my @array = @$i;
# sanity check filter
#$self->debug_message("prefilter is ", join(" ", @array));
my $filter = shift(@array); # the array now contains the options for the filter
$rh_ans = &$filter($rh_ans, @array);
$self->print_result_if_debug('pre_filter', $rh_ans, @array);
}
my @evaluators = @{ $self->{evaluators} };
$count = 0;
foreach my $i (@evaluators) {
last if defined($rh_ans->{error_flag});
my @array = @$i;
my $evaluator = shift(@array); # the array now contains the options for the filter
$rh_ans = &$evaluator($rh_ans, @array);
$self->print_result_if_debug('evaluator', $rh_ans, @array);
}
my @post_filters = @{ $self->{post_filters} };
$count = 0; # blank filter catcher is filter 0
foreach my $i (@post_filters) {
last if defined($rh_ans->{done}) and $rh_ans->{done} == 1; # no further action needed
my @array = @$i;
my $filter = shift(@array); # the array now contains the options for the filter
$rh_ans = &$filter($rh_ans, @array);
$self->print_result_if_debug('post_filter', $rh_ans, @array);
}
$rh_ans = $self->dereference_array_ans($rh_ans);
# make sure that the student answer is not an array so that it is reported correctly in answer section.
eval(q!main::DEBUG_MESSAGE( `<h4>final result: </h4>`, pretty_print($rh_ans,'html'))!)
if defined($self->{debug})
and $self->{debug} > 0;
# re-reference $rh_ans;
$self->{rh_ans} = $rh_ans;
$rh_ans;
}
sub print_result_if_debug {
my $self = shift;
my $queue = shift; # the name of the queue we are in
my $rh_ans = shift;
my @options = @_; # this may not be even FIXME
unless (ref($rh_ans) eq 'AnswerHash') {
warn "$rh_ans is not an answerHash in queue $queue\n";
return;
}
if (defined($self->{debug}) and $self->{debug} > 0) {
$rh_ans->{rh_options} = \@options; #include the options in the debug information -- change to ra_options??
my $name = (defined($rh_ans->{_filter_name})) ? $rh_ans->{_filter_name} : 'unnamed';
eval(
q! main::DEBUG_MESSAGE( "\n $count. Result from queue $queue: name: \"$name\"n", pretty_print($rh_ans,'html',4))
!
);
++$count;
}
$rh_ans->{_filter_name} = undef;
}
# This next subroutine is for checking the instructor's answer and is not yet in use.
# sub correct_answer_evaluate {
# my $self = shift;
# $self-> {rh_ans} -> {correct_ans} = shift @_;
# my $rh_ans = $self ->{rh_ans};
# my @prefilters = @{$self -> {correct_answer_pre_filters}};
# my $count = -1; # the blank filter is counted as filter 0
# foreach my $i (@prefilters) {
# last if defined( $rh_ans->{error_flag} );
# my @array = @$i;
# my $filter = shift(@array); # the array now contains the options for the filter
# warn "before pre-filter: ",++$count, $self->{rh_ans}->pretty_print() if defined($self->{debug}) and $self->{debug}>0;
# $rh_ans = &$filter($rh_ans,@array);
# warn "Filter Name:", $rh_ans->{_filter_name},"<BR>\n" if $self->{debug}>0 and defined($rh_ans->{_filter_name})
# }
# my @evaluators = @{$self -> {correct_answer_evaluators} };
# $count = 0;
# foreach my $i ( @evaluators ) {
# last if defined($self->{rh_ans}->{error_flag});
# my @array = @$i;
# my $evaluator = shift(@array); # the array now contains the options for the filter
# warn "before evaluator: ",++$count, $self->{rh_ans}->pretty_print() if defined($self->{debug}) and $self->{debug}>0;
# $rh_ans = &$evaluator($rh_ans,@array);
# }
# my @post_filters = @{$self -> {correct_answer_post_filters} };
# $count = -1; # blank filter catcher is filter 0
# foreach my $i ( @post_filters ) {
# last if defined($rh_ans->{done}) and $rh_ans->{done} == 1; # no further action needed
# my @array = @$i;
# my $filter = shift(@array); # the array now contains the options for the filter
# warn "before post-filter: ",++$count, $self->{rh_ans}->pretty_print() if defined($self->{debug}) and $self->{debug}>0;
# $rh_ans = &$filter($rh_ans,@array);
# warn "Filter Name:", $rh_ans->{_filter_name},"<BR>\n" if $self->{debug}>0 and defined($rh_ans->{_filter_name})
# }
# $rh_ans = $self->dereference_array_ans($rh_ans);
# # make sure that the student answer is not an array so that it is reported correctly in answer section.
# warn "final result: ", $self->{rh_ans}->pretty_print() if defined($self->{debug}) and $self->{debug}>0;
# $self ->{rh_ans} = $rh_ans;
# $rh_ans;
# }
=head3 install_pre_filter
=head3 install_evaluator
=head3 install_post_filter
=cut
sub install_pre_filter {
my $self = shift;
if (@_ == 0) {
# do nothing if input is empty
} elsif ($_[0] eq 'reset' or $_[0] eq 'erase') {
$self->{pre_filters} = [];
} else {
push(@{ $self->{pre_filters} }, [@_]) if @_; #install pre_filter and it's options
}
@{ $self->{pre_filters} }; # return array of all pre_filters
}
sub install_evaluator {
my $self = shift;
if (@_ == 0) {
# do nothing if input is empty
} elsif ($_[0] eq 'reset' or $_[0] eq 'erase') {
$self->{evaluators} = [];
} else {
push(@{ $self->{evaluators} }, [@_]) if @_; #install evaluator and it's options
}
@{ $self->{'evaluators'} }; # return array of all evaluators
}
sub install_post_filter {
my $self = shift;
if (@_ == 0) {
# do nothing if input is empty
} elsif ($_[0] eq 'reset' or $_[0] eq 'erase') {
$self->{post_filters} = [];
} else {
push(@{ $self->{post_filters} }, [@_]) if @_; #install post_filter and it's options
}
@{ $self->{post_filters} }; # return array of all post_filters
}
## filters for checking the correctAnswer
sub install_correct_answer_pre_filter {
my $self = shift;
if (@_ == 0) {
# do nothing if input is empty
} elsif ($_[0] eq 'reset' or $_[0] eq 'erase') {
$self->{correct_answer_pre_filters} = [];
} else {
push(@{ $self->{correct_answer_pre_filters} }, [@_]) if @_; #install correct_answer_pre_filter and it's options
}
@{ $self->{correct_answer_pre_filters} }; # return array of all correct_answer_pre_filters
}
sub install_correct_answer_evaluator {
my $self = shift;
if (@_ == 0) {
# do nothing if input is empty
} elsif ($_[0] eq 'reset' or $_[0] eq 'erase') {
$self->{correct_answer_evaluators} = [];
} else {
push(@{ $self->{correct_answer_evaluators} }, [@_]) if @_; #install evaluator and it's options
}
@{ $self->{correct_answer_evaluators} }; # return array of all evaluators
}
sub install_correct_answer_post_filter {
my $self = shift;
if (@_ == 0) {
# do nothing if input is empty
} elsif ($_[0] eq 'reset' or $_[0] eq 'erase') {
$self->{correct_answer_post_filters} = [];
} else {
push(@{ $self->{correct_answer_post_filters} }, [@_]) if @_; #install post_filter and it's options
}
@{ $self->{correct_answer_post_filters} }; # return array of all post_filters
}
=head3 withPreFilter
Usage:
$answerHash->withPreFilter(filter[,options]);
Installs a prefilter (possibly with options), and returns the AnswerHash. This is so that you
can add a filter to a checker without having to save the checker in a variable, e.g.,
ANS(Real(10)->cmp->withPreFilter(...));
or
ANS(num_cmp(10)->withPreFilter(...));
=cut
sub withPreFilter {
my $self = shift;
$self->install_pre_filter(@_);
return $self;
}
=head3 withPostFilter
Usage:
$answerHash->withPostFilter(filter[,options]);
Installs a postfilter (possibly with options), and returns the AnswerHash. This is so that you
can add a filter to a checker without having to save the checker in a variable, e.g.,
ANS(Real(10)->cmp->withPostFilter(...));
or
ANS(num_cmp(10)->withPostFilter(...));
=cut
sub withPostFilter {
my $self = shift;
$self->install_post_filter(@_);
return $self;
}
sub ans_hash { #alias for rh_ans
my $self = shift;
$self->rh_ans(@_);
}
sub rh_ans {
my $self = shift;
my %in_hash = @_;
foreach my $key (keys %in_hash) {
$self->{rh_ans}->{$key} = $in_hash{$key};
}
$self->{rh_ans};
}
=head1 DESCRIPTION - Filters
A filter is a subroutine which takes one AnswerHash as an input, followed by
a hash of options.
Usage:
filter($ans_hash, option1 =>value1, option2=> value2 );
The filter performs some operations on the input AnswerHash and returns an
AnswerHash as output.
Many AnswerEvaluator objects are merely a sequence of filters placed into
three queues:
pre_filters: these normalize student input, prepare text and so forth
evaluators: these decide whether or not an answer is correct
post_filters: typically these clean up error messages or process errors
and generate error messages.
If a filter detects an error it can throw an error message using the C<$rh_ans->throw_error()>
method. This skips the AnswerHash by all remaining pre_filter C<$rh_ans->catch_error>,
decides how (
or whether) it is supposed to handle the error and then passes the result on
to the next post_filter.
Setting the flag C<$rh_ans->{done} = 1> will skip
the AnswerHash past the remaining post_filters.
=head2 blank_prefilter
=head2 blank_postfilter
=cut
######################################################
#
# Built in Filters
#
######################################################
sub blank_prefilter { # check for blanks
my $rh_ans = shift;
$rh_ans->{_filter_name} = 'blank_prefilter';
# undefined answers are BLANKS
(not defined($rh_ans->{student_ans})) && do {
$rh_ans->throw_error("BLANK", 'The answer is blank');
return ($rh_ans);
};
# answers which are arrays or hashes or some other object reference are NOT blanks
(ref($rh_ans->{student_ans})) && do { return ($rh_ans) };
# if the answer is a true variable consisting only of white space it is a BLANK
(($rh_ans->{student_ans}) !~ /\S/) && do {
$rh_ans->throw_error("BLANK", 'The answer is blank');
return ($rh_ans);
};
# If we get to here, we assume that the answer is not a blank. It is defined, not a reference
# and contains something other than whitespaces.
$rh_ans;
}
sub blank_postfilter {
my $rh_ans = shift;
$rh_ans->{_filter_name} = 'blank_postfilter';
return ($rh_ans) unless defined($rh_ans->{error_flag}) and $rh_ans->{error_flag} eq 'BLANK';
$rh_ans->{error_flag} = undef;
$rh_ans->{error_message} = '';
if (defined($rh_ans->{message_for_blank_answer})) {
$rh_ans->{ans_message} = $rh_ans->{message_for_blank_answer};
}
$rh_ans->{done} = 1; # no further checking is needed.
$rh_ans;
}
1;