diff --git a/macros/core/scaffold.pl b/macros/core/scaffold.pl index bce37b3525..8ac9b64dee 100644 --- a/macros/core/scaffold.pl +++ b/macros/core/scaffold.pl @@ -44,17 +44,15 @@ =head1 DESCRIPTION Scaffold::Begin(); Section::Begin("Part 1: The first part"); - BEGIN_TEXT - This is the text for part 1. \(1+1\) = \{ans_rule\} - END_TEXT - ANS(Real(2)->cmp); + BEGIN_PGML + This is the text for part 1. [`1+1 =`] [_]{Real(2)} + END_PGML Section::End(); Section::Begin("Part 2: The second part"); - BEGIN_TEXT - This is text for the second part. \(2*2\) = \{ans_rule\} - END_TEXT - ANS(Real(4)->cmp); + BEGIN_PGML + This is text for the second part. [`2*2 =`] [_]{Real(4)} + END_PGML Section::End(); Scaffold::End(); @@ -75,7 +73,7 @@ =head1 DESCRIPTION answer blank to be considered correct by using the C option on the answer checker. For example: - ANS(Real(123)->cmp(scaffold_force => 1)); + [_]{ Real(123)->cmp(scaffold_force => 1) }; would mean that this answer would not have to be correct for the section to be considered correct. @@ -85,7 +83,7 @@ =head1 DESCRIPTION last one, if you wish. That material would always be showing, regardless of which sections are open. So, for example, you could put a data table in the area before the first section, so that it would be -visible throughtout the problem, no matter which section the student +visible through out the problem, no matter which section the student is working on. The C function accepts optional parameters that @@ -264,11 +262,9 @@ sub _scaffold_init { # Load style and javascript for opening and closing the scaffolds. ADD_CSS_FILE("js/Scaffold/scaffold.css"); ADD_JS_FILE("js/Scaffold/scaffold.js", 0, { defer => undef }); + return; } -# -# The Scaffoling package -# package Scaffold; our $forceOpen = $main::envir{forceScaffoldsOpen}; @@ -277,31 +273,30 @@ package Scaffold; our $isPTX = $main::displayMode eq "PTX"; our $afterAnswerDate = $main::envir{answersAvailable}; -our $scaffold; # the active scaffold (set by Begin() below) -my @scaffolds = (); # array of nested scaffolds -my $scaffold_no = 0; # each scaffold gets a unique number -my $scaffold_depth = 1; # each scaffold has a nesting depth +our $scaffold; # the active scaffold (set by Begin() below) +my @scaffolds; # array of nested scaffolds +my $scaffold_no = 0; # each scaffold gets a unique number +my $scaffold_depth = 1; # each scaffold has a nesting depth our $PG_ANSWERS_HASH = $main::PG->{PG_ANSWERS_HASH}; # where PG stores answer evaluators our $PG_OUTPUT = $main::PG->{OUTPUT_ARRAY}; # where PG stores the TEXT() output our $PREFIX = "$main::envir{QUIZ_PREFIX}Prob-$main::envir{probNum}"; +# Scaffold::Begin() is used to start a new scaffold section, passing +# it any options that need to be overridden (e.g. is_open, can_open, +# open_first_section, etc). # -# Scaffold::Begin() is used to start a new scaffold section, passing -# it any options that need to be overriden (e.g. is_open, can_open, -# open_first_section, etc). -# -# Problems can include more than one scaffold, if desired, -# and they can be nested. -# -# We save the current PG_OUTPUT, which will be put back durring -# the Scaffold::End() call. The sections use PG_OUTPUT to create -# their own text, which is added to the $scaffold->{output} -# during the Section::End() call. +# Problems can include more than one scaffold, if desired, +# and they can be nested. # +# We save the current PG_OUTPUT, which will be put back during +# the Scaffold::End() call. The sections use PG_OUTPUT to create +# their own text, which is added to the $scaffold->{output} +# during the Section::End() call. sub Begin { - my $self = Scaffold->new(@_); + my %options = @_; + my $self = Scaffold->new(%options); unshift(@scaffolds, $self); $scaffold = $self; $scaffold_depth++; @@ -311,47 +306,49 @@ sub Begin { return $self; } +# Scaffold::End() is used to end the scaffold. # -# Scaffold::End() is used to end the scaffold. -# -# This puts the scaffold into the page output -# and opens the sections that should be open. -# Then the next nested scaffold (if any) is poped off -# the stack and returned. -# +# This puts the scaffold into the page output and opens the sections that should be open. +# Then the next nested scaffold (if any) is popped off the stack and returned. sub End { Scaffold->Error("Scaffold::End() without a corresponding Scaffold::Begin") unless @scaffolds; Scaffold->Error("Scaffold ended with section was still open") if $self->{current_section}; my $self = $scaffold; - push(@{ $self->{output} }, splice(@$PG_OUTPUT, 0)); # collect any final non-section output - $self->hide_other_results(@{ $self->{open} }); # hide results of unnopened sections in the results table - push(@$PG_OUTPUT, @{ $self->{previous_output} }, @{ $self->{output} }) - ; # put back original output and scaffold output + + # collect any final non-section output + push(@{ $self->{output} }, splice(@$PG_OUTPUT, 0)); + + # hide results of unopened sections in the results table + main::add_content_post_processor(sub { + my ($problemContents, $headerContents) = @_; + return if $main::displayMode eq 'TeX'; + $self->hide_other_results($headerContents, @{ $self->{open} }); + }); + + # put back original output and scaffold output + push(@$PG_OUTPUT, @{ $self->{previous_output} }, @{ $self->{output} }); + delete $self->{previous_output}; - delete $self->{output}; # don't need these any more + delete $self->{output}; shift(@scaffolds); $scaffold = $scaffolds[0]; $scaffold_depth--; return $scaffold; } -# -# Report an error and die -# +# Report an error and die sub Error { my $self = shift; my $error = shift; die $error; } +# Create a new Scaffold object. # -# Create a new Scaffold object. -# -# Set the defaults for can_open, is_open, etc., but allow -# the author to override them. -# +# Set the defaults for can_open, is_open, etc., but allow +# the author to override them. sub new { - my $class = shift; + my ($class, %options) = @_; $class = ref($class) if ref($class); my $self = bless { can_open => "when_previous_correct", @@ -361,7 +358,7 @@ sub new { hardcopy_is_open => "always", # open all possible sections in hardcopy open_first_section => 1, # 0 means don't open any sections initially numbered => 0, # 1 means sections will be printed with their number - @_, + %options, number => ++$scaffold_no, # the number for this scaffold depth => $scaffold_depth, # the nesting depth for this scaffold sections => {}, # the sections within this scaffold @@ -373,11 +370,9 @@ sub new { return $self; } -# -# Add a section to the scaffold and give it a unique number (within -# the scaffold). Determine its label and save it as current_section -# so that we know which section is active. -# +# Add a section to the scaffold and give it a unique number (within +# the scaffold). Determine its label and save it as current_section +# so that we know which section is active. sub start_section { my $self = shift; my $section = shift; @@ -389,127 +384,84 @@ sub start_section { return $section; } -# -# Add the content from the current section into the scaffold's output -# and remove the current_section (so we can tell that no sectionis open). -# +# Add the content from the current section into the scaffold's output +# and remove the current_section (so we can tell that no section is open). sub end_section { my $self = shift; push(@{ $self->{output} }, splice(@{$PG_OUTPUT}, 0)); # save the section output delete $self->{current_section}; + return; } -# -# Record the answers for a section, and evaluate them, if non-empty, -# keeping the scores for future reference. -# +# Record the answers for a section. +# Scores are obtained when post processing is done. sub section_answers { - my $self = shift; - my %answers; - # - # MultiAnswer objects can set the answer hash score when the last answer is evaluated, - # so save the hashes and look up the scores after they have all been called. - # Essay answers never return as correct, so special case them, and provide a - # "scaffold_force" option in the AnswerHash that can be used to force Scaffold - # to consider the score to be 1 (bug in PGessaymacros.pl prevents us from using - # it for essay_cmp(), though). - # - push(@{ $self->{ans_names} }, @_); - foreach my $name (@_) { - my $input = $main::inputs_ref->{$name}; - my $evaluator = $PG_ANSWERS_HASH->{$name}->ans_eval; - Parser::Eval(sub { $answers{$name} = $evaluator->evaluate($input) }) if defined($input) && $input ne ""; - $answers{$name}{score} = 1 - if $answers{$name} && (($answers{$name}{type} || "") eq "essay" || $answers{$name}{"scaffold_force"}); - $evaluator->{rh_ans}{ans_message} = ""; - delete $evaluator->{rh_ans}{error_message}; - } - foreach my $name (@_) { $self->{scores}{$name} = $answers{$name}{score} if $answers{$name} } + my ($self, @section_answers) = @_; + push(@{ $self->{ans_names} }, @section_answers); + return; } -# -# Add the given sections to the list of sections to be openned -# for this scaffold -# +# Add the given sections to the list of sections to be opened for this scaffold. sub is_open { - my $self = shift; - push(@{ $self->{open} }, map { $_->{number} } @_) if @_; + my ($self, @open_sections) = @_; + push(@{ $self->{open} }, map { $_->{number} } @open_sections) if @open_sections; return $self->{open}; } -# -# Add CSS to dim the rows of the table that are not in the open -# section. (When a section is marked correct, the next section will -# be opened, so the correct answers will be dimmed, and the new -# section's blank rows will be active. That may be a downside to the -# dimming.) -# +# Add CSS to dim the rows of the table that are not in the open section and that are not correct. +# This is run in post processing after all of the section post processing has been completed. So at this point +# everything is known about the status of all answers in this scaffold. sub hide_other_results { - my $self = shift; - # - # Record the row for each answer evaluator, and - # mark which sections to show - # + my ($self, $headerContents, @openSections) = @_; + + # Record the row for each answer evaluator, and mark which sections to show. my %row; my $i = 2; - foreach my $name (keys %{$PG_ANSWERS_HASH}) { $row{$name} = $i; $i++ }; # record the rows for all answers + for (keys %{$PG_ANSWERS_HASH}) { $row{$_} = $i; ++$i }; # record the rows for all answers my %show; - map { $show{$_} = 1 } @_; - # - # Get the row numbers for the answers from OTHER sections - # - my @hide = (); - foreach $i (keys %{ $self->{sections} }) { - push(@hide, map { $row{$_} } @{ $self->{sections}{$i}{ans_names} }) if !$show{$i}; + map { $show{$_} = 1 } @openSections; + + # Get the row numbers for the answers from OTHER sections + my @hide; + for my $section (keys %{ $self->{sections} }) { + push(@hide, map { $row{$_} } @{ $self->{sections}{$section}{ans_names} }) + if !$show{$section} && !$self->{sections}{$section}{is_correct}; } - # - # Add styles that dim the hidden rows - # (the other possibility would be to use display:none) - # + + # Add styles that dim the hidden rows that are not correct (the other possibility would be to use display:none) if (@hide) { - my @styles = (map {".attemptResults > tbody > tr:nth-child($_) {opacity:.5}"} @hide); - main::HEADER_TEXT(''); + $headerContents->append_content(''); } -} -# -# Check if a scaffold is completely correct. -# (Must be called after the last section is ended.) -# -sub is_correct { - my $self = shift; - my $scores = $self->{scores}; - foreach my $name (@{ $self->{ans_names} }) { return 0 unless ($scores->{$name} || 0) >= 1 } - return 1; + return; } package Section; -# -# Shortcuts for Scaffold data -# +# Shortcuts for Scaffold data $PG_ANSWERS_HASH = $Scaffold::PG_ANSWERS_HASH; $PG_OUTPUT = $Scaffold::PG_OUTPUT; +# Section::Begin() is used to start a section in the scaffolding, +# passing it the name of the section and any options (e.g., can_open, +# is_open, etc.). # -# Section::Begin() is used to start a section in the scaffolding, -# passing it the name of the section and any options (e.g., can_open, -# is_open, etc.). -# -# The section is added to the scaffold, and the names of the answer -# blanks for previous sections are recorded, along with information -# about the answer blanks that have evaluators assigned (so we can -# see which answers belong to this section when it closes). -# +# The section is added to the scaffold, and the names of the answer +# blanks for previous sections are recorded, along with information +# about the answer blanks that have evaluators assigned (so we can +# see which answers belong to this section when it closes). sub Begin { + my ($section_name, %options) = @_; my $scaffold = $Scaffold::scaffold; Scaffold->Error("Sections must appear within a Scaffold") unless $scaffold; Scaffold->Error("Section::Begin() while a section is already open") if $scaffold->{current_section}; - my $self = $scaffold->start_section(Section->new(@_)); + my $self = $scaffold->start_section(Section->new($section_name, %options)); my $number = $self->{number}; my $number_at_depth = $number; # Convert the number (e.g. 2) into a depth-styled version (e.g. b, ii, B) - # Supports numbers up to 99 and depth up to 3 but then leaves in arabic + # Supports numbers up to 99 and depth up to 3 but then leaves in Arabic if ($scaffold->{depth} == 1 && $number <= 99) { $number_at_depth = ('a' .. 'cu')[ $number - 1 ]; } elsif ($scaffold->{depth} == 2 && $number <= 99) { @@ -538,19 +490,17 @@ sub Begin { $number_at_depth = ('A' .. 'CU')[ $number - 1 ]; } $self->{number_at_depth} = $number_at_depth; - $self->{previous_ans} = [ @{ $scaffold->{ans_names} } ], # copy of current list of answers in the scaffold - $self->{assigned_ans} = [ $self->assigned_ans ], # array indicating which answers have evaluators - return $self; + $self->{previous_ans} = [ @{ $scaffold->{ans_names} } ]; # copy of current list of answers in the scaffold + $self->{assigned_ans} = [ $self->assigned_ans ]; # array indicating which answers have evaluators + return $self; } +# Section::End() is used to end the active section. # -# Section::End() is used to end the active section. -# -# We get the names of the answer blanks that are in this section, -# then add the HTML around the section that is used by jQuery -# for showing/hiding the section, and finally tell the scaffold -# that the section is complete (it adds the content to its output). -# +# We get the names of the answer blanks that are in this section, +# then add the HTML around the section that is used by JavaScript +# for showing/hiding the section, and finally tell the scaffold +# that the section is complete (it adds the content to its output). sub End { my $scaffold = $Scaffold::scaffold; Scaffold->Error("Sections must appear within a Scaffold") unless $scaffold; @@ -560,19 +510,17 @@ sub End { $scaffold->section_answers(@{ $self->{ans_names} }); $self->add_container(); $scaffold->end_section(); + return; } +# Create a new Section object. # -# Create a new Section object. -# -# It takes default values for can_open, is_open, etc. -# from the active scaffold. These can be overridden -# by the author. -# +# It takes default values for can_open, is_open, etc. +# from the active scaffold. These can be overridden +# by the author. sub new { - my $class = shift; + my ($class, $name, %options) = @_; $class = ref($class) if ref($class); - my $name = shift; my $scaffold = $Scaffold::scaffold; my $self = bless { name => $name, @@ -581,23 +529,16 @@ sub new { after_AnswerDate_can_open => $scaffold->{after_AnswerDate_can_open}, is_open => $scaffold->{is_open}, hardcopy_is_open => $scaffold->{hardcopy_is_open}, - @_, + %options }, $class; return $self; } -# -# Adds the necessary HTML around the content of the section. -# -# First, determine the is_correct and can_open status and save them. -# Then check if the section is to be openned, and if so, add it -# to the open list of the scaffold. -# -# The $PG_OUTPUT variable holds just the contents of this section, -# so we unshift the openning tags onto the front, and push -# the closing tags onto the back. (This is added to the scaffold -# output when $scaffold->end_section() is called.) -# +# Adds the necessary HTML around the content of the section. Initially a temporary "scaffold-section" tag is added that +# wraps the content, and that is replaced with the correct HTML in post processing. The content is also removed in post +# processing if the scaffold can not be opened and is not correct. The $PG_OUTPUT variable holds just the contents of +# this section, so unshift the opening tags onto the front, and push the closing tags onto the back. (This is added to +# the scaffold output when $scaffold->end_section() is called.) sub add_container { my $self = shift; my $scaffold = $Scaffold::scaffold; @@ -605,63 +546,123 @@ sub add_container { my $name = $self->{name} // ''; my $title = ($name || $scaffold->{numbered}) ? $name : "Part $self->{number}:"; my $number = ($scaffold->{numbered} ? $self->{number_at_depth} . '.' : ''); - my ($iscorrect, $canopen, $isopen); - $iscorrect = $self->{is_correct} = $self->is_correct; - $canopen = $self->{can_open} = $self->can_open; - $isopen = $self->is_open; - - $scaffold->is_open($self) if $isopen; - splice(@$PG_OUTPUT, 0, scalar(@$PG_OUTPUT)) - if !($canopen || $iscorrect || $Scaffold::isPTX) || (!$isopen && $Scaffold::isHardcopy); unshift( @$PG_OUTPUT, - @{ - main::MODES( - HTML => [ - '
', - '
', - '
', - '', - '
', - qq{
}, - '
' - ], - TeX => ["\\par{\\bf $number $title}\\addtolength{\\leftskip}{15pt}\\par "], - PTX => $name ? [ "\n", "$name\n" ] : ["\n"], - ) - } + main::MODES( + HTML => qq{}, + TeX => + "\\par{\\bf $number $title}\\addtolength{\\leftskip}{15pt}\\par\n%scaffold-section-$label-start\n", + PTX => $name ? "\n$name\n" : "\n", + ) ); push( @$PG_OUTPUT, main::MODES( - HTML => '
', - TeX => "\\addtolength{\\leftskip}{-15pt}\\par ", + HTML => '', + TeX => "%scaffold-section-$label-end\n\\addtolength{\\leftskip}{-15pt}\\par ", PTX => "<\/task>\n", ) ); + + main::add_content_post_processor(sub { + my $problemContents = shift; + + # Nothing needs to be done for the PTX display mode. + return if $Scaffold::isPTX; + + # Essay answers never return as correct, so there is a special for case them. Also provide a "scaffold_force" + # option in the AnswerHash that can be used to force Scaffold to consider the score to be 1 (a bug in + # PGessaymacros.pl prevents it from working in essay_cmp() though -- it actually does work, both answer hashes + # defined in essay_cmp() need the setting though). + for (@{ $self->{ans_names} }) { + next unless defined $PG_ANSWERS_HASH->{$_}; + $scaffold->{scores}{$_} = $PG_ANSWERS_HASH->{$_}{ans_eval}{rh_ans}{score}; + $scaffold->{scores}{$_} = 1 + if ($PG_ANSWERS_HASH->{$_}{ans_eval}{rh_ans}{type} || '') eq 'essay' + || $PG_ANSWERS_HASH->{$_}{ans_eval}{rh_ans}{$_}{scaffold_force}; + } + + # Set the active scaffold to the scaffold for this section so that is_correct, can_open, + # and is_open methods use the correct one. + $Scaffold::scaffold = $scaffold; + + # Now, determine the is_correct and can_open status and save them. + # Then check if the section is to be opened, and if so, add it + # to the open list of the scaffold. + my $iscorrect = $self->{is_correct} = $self->is_correct; + my $canopen = $self->{can_open} = $self->can_open; + my $isopen = $self->is_open; + + $scaffold->is_open($self) if $isopen; + + if ($Scaffold::isHardcopy) { + $$problemContents =~ s/%scaffold-section-$label-start.*%scaffold-section-$label-end//ms + if !($canopen || $iscorrect) || !$isopen; + } else { + my $sectionElt = $problemContents->at(qq{scaffold-section[id="$label"]}); + return unless $sectionElt; + + $sectionElt->tag('div'); + delete $sectionElt->attr->{id}; + $sectionElt->attr(class => 'accordion'); + + $sectionElt->content('') if !($canopen || $iscorrect); + + $sectionElt->wrap_content( + Mojo::DOM->new_tag( + 'div', + class => 'accordion-item section-div', + sub { + Mojo::DOM->new_tag( + 'div', + id => $label, + class => 'accordion-collapse collapse' . ($isopen ? ' show' : ''), + 'aria-labelledby' => "$label-header", + sub { Mojo::DOM->new_tag('div', class => 'accordion-body') } + ); + } + )->to_string + ); + + $sectionElt->at('.accordion-item.section-div')->prepend_content( + Mojo::DOM->new_tag( + 'div', + class => 'accordion-header' + . ($iscorrect ? ' iscorrect' : ' iswrong') + . ($canopen ? ' canopen' : ' cannotopen'), + id => "$label-header", + sub { + Mojo::DOM->new_tag( + 'button', + class => 'accordion-button' . ($isopen ? '' : ' collapsed'), + type => 'button', + ( + $canopen + ? ( + data => { bs_target => "#$label", bs_toggle => 'collapse' }, + 'aria-controls' => $label + ) + : (tabindex => '-1') + ), + aria_expanded => ($isopen ? 'true' : 'false'), + sub { + Mojo::DOM->new_tag('span', class => 'section-number', $number) + . Mojo::DOM->new_tag('span', class => 'section-title', $title); + } + ); + } + )->to_string + ); + } + + return; + }); + + return; } -# -# Check if all the answers for this section are correct -# +# Check if all the answers for this section are correct sub is_correct { my $self = shift; my $scores = $Scaffold::scaffold->{scores}; @@ -669,10 +670,8 @@ sub is_correct { return 1; } -# -# Perform the can_open check for this section: -# If the author supplied code, use it, otherwise use the routine from Section::can_open. -# +# Perform the can_open check for this section: +# If the author supplied code, use it, otherwise use the routine from Section::can_open. sub can_open { my $self = shift; return 1 if $Scaffold::forceOpen; @@ -682,10 +681,8 @@ sub can_open { return &{$method}($self); } -# -# Peform the is_open check for this section: -# If the author supplied code, use it, otherwise use the routine from Section::is_open. -# +# Perform the is_open check for this section: +# If the author supplied code, use it, otherwise use the routine from Section::is_open. sub is_open { my $self = shift; return 1 if $Scaffold::forceOpen; @@ -696,28 +693,23 @@ sub is_open { return &{$method}($self); } -# -# Return a boolean array where a 1 means that answer blank has -# an answer evaluator assigned to it and 0 means not. -# +# Return a boolean array where a 1 means that answer blank has +# an answer evaluator assigned to it and 0 means not. sub assigned_ans { - my $self = shift; - my @answers = (); + my $self = shift; + my @answers; foreach my $name (keys %{$PG_ANSWERS_HASH}) { push(@answers, $PG_ANSWERS_HASH->{$name}->ans_eval ? 1 : 0); } return @answers; } -# -# Get the names of any of the original answer blanks that now have -# evaluators attached. -# +# Get the names of any of the original answer blanks that now have evaluators attached. sub new_answers { my $self = shift; my @assigned = @{ $self->{assigned_ans} }; # 0 if previously unassigned, 1 if assigned - my @answers = (); - my $i = 0; + my @answers; + my $i = 0; foreach my $name (keys %{$PG_ANSWERS_HASH}) { push(@answers, $name) if $PG_ANSWERS_HASH->{$name}->ans_eval && !$assigned[$i]; $i++; @@ -727,60 +719,49 @@ sub new_answers { } ######################################################################## -# -# Implements the possible values for the can_open option for scaffolds -# and sections -# +# Implements the possible values for the can_open option for scaffolds +# and sections + package Section::can_open; -# -# Always can be openned -# +# Always can be opened sub always { return 1 } -# -# Can be openned when all the answers from previous sections are correct -# + +# Can be opened when all the answers from previous sections are correct sub when_previous_correct { my $section = shift; my $scores = $Scaffold::scaffold->{scores}; foreach my $name (@{ $section->{previous_ans} }) { return 0 unless ($scores->{$name} || 0) >= 1 } return 1; } -# -# Can open when previous are correct but this one is not -# + +# Can open when previous are correct but this one is not sub first_incorrect { my $section = shift; return when_previous_correct($section) && !$section->{is_correct}; } -# -# Can open when incorrect -# + +# Can open when incorrect sub incorrect { my $section = shift; return !$section->{is_correct}; } -# -# Never can be openned -# + +# Never can be opened sub never { return 0 } ######################################################################## -# -# Implements the possible values for the is_open option for scaffolds -# and sections -# +# Implements the possible values for the is_open option for scaffolds +# and sections + package Section::is_open; -# -# Every section is open that can be -# +# Every section is open that can be sub always { return 1 } -# -# Every incorrect section is open that can be -# (unless it is the first one, and everything is blank, and -# the scaffold doesn't have open_first_section set) -# + +# Every incorrect section is open that can be +# (unless it is the first one, and everything is blank, and +# the scaffold doesn't have open_first_section set) sub incorrect { my $section = shift; my $scaffold = $Scaffold::scaffold; @@ -793,24 +774,21 @@ sub incorrect { } return 1; } -# -# The first incorrect section is open that can be -# + +# The first incorrect section is open that can be sub first_incorrect { my $section = shift; return Section::is_open::incorrect($section) && Section::can_open::when_previous_correct($section); } -# -# All correct sections and the first incorrect section -# are open (that are allowed to be open) -# + +# All correct sections and the first incorrect section +# are open (that are allowed to be open) sub correct_or_first_incorrect { my $section = shift; - return 1 if $section->{is_correct} || Section::is_open::first_incorrect($section); + return $section->{is_correct} || Section::is_open::first_incorrect($section); } -# -# No sections are open -# + +# No sections are open sub never { return 0 } 1;