From 5f2be65910eab9101baf7b4bb8409d98d23609fa Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Sat, 5 Aug 2023 17:19:52 -0500 Subject: [PATCH 1/2] Rework the persistence hash. This replaces all of the previous persistence hash methods with a single `peristent_data` method. This method both saves to and retrieves from the persistence hash. The first (or second including self in PGcore) parameter is the key in the persistence hash. If the optional second (or third including self in PGcore) parameter is not given then the value of the key for the given label in the hash will be returned. If the second parameter is given then the value of the key in the hash will be saved or updated. Note that if third parameter is given but is undefined then the key will be deleted from the hash. Anything that can be JSON encoded can be stored. Any frontend should save the persistence hash in some way, and send it back each time the problem is rendered. The `store_persistent_data`, `update_persistent_data`, and `get_peristent_data` methods from before still exist in PG.pl (but were removed from PGcore.pl) and they all just call the new `persistent_data` method. However, they should be considered deprecated, and are only left for backwards compatability. Note that the `store_persistent_data` method no longer warns if one tries to set the data for a key in the hash that is already set. There was no reason for that warning and probably is why the `update_persistent_data` method was created that does exactly the same thing except that it doesn't warn and it doesn't return the label that was passed in (why do that anyway?). The whole mechanism was rather poorly designed to begin with. --- lib/PGcore.pm | 36 +++++++++++++++--------------------- lib/WeBWorK/PG.pm | 4 ---- macros/PG.pl | 34 +++++++++++++++++++++++++++++++--- 3 files changed, 46 insertions(+), 28 deletions(-) diff --git a/lib/PGcore.pm b/lib/PGcore.pm index ade9a9fb8..7fa398ff5 100755 --- a/lib/PGcore.pm +++ b/lib/PGcore.pm @@ -75,7 +75,6 @@ sub new { # Holds other data, besides answers, which persists during a session and beyond. PERSISTENCE_HASH => $envir->{PERSISTENCE_HASH} // {}, # Main data, received from DB - PERSISTENCE_HASH_UPDATED => {}, # Keys whose updated values should be saved by the DB answer_name_count => 0, implicit_named_answer_stack => [], implicit_answer_eval_stack => [], @@ -83,10 +82,10 @@ sub new { KEPT_EXTRA_ANSWERS => [], ANSWER_PREFIX => 'AnSwEr', ARRAY_PREFIX => 'ArRaY', - vec_num => 0, # for distinguishing matrices + vec_num => 0, # for distinguishing matrices QUIZ_PREFIX => $envir->{QUIZ_PREFIX}, PG_VERSION => $ENV{PG_VERSION}, - PG_ACTIVE => 1, # toggle to zero to stop processing + PG_ACTIVE => 1, # toggle to zero to stop processing submittedAnswers => 0, # have any answers been submitted? is this the first time this session? PG_session_persistence_hash => {}, # stores data from one invoction of the session to the next. PG_original_problem_seed => 0, @@ -477,25 +476,20 @@ sub extend_ans_group { # modifies the group type return $label; } -sub store_persistent_data { # will store strings only (so far) - my ($self, $label, @values) = @_; - if (defined($self->{PERSISTENCE_HASH}->{$label})) { - warn "can' overwrite $label in persistent data"; - } else { - $self->{PERSISTENCE_HASH_UPDATED}{$label} = 1; - $self->{PERSISTENCE_HASH}{$label} = join("", @values); +# Save to or retrieve data from the persistence hash. The $label parameter is the key in the persistence hash. If the +# $value parameter is not given then the value of the $label key in the hash will be returned. If the $value parameter +# is given then the value of the $label key in the hash will be saved or updated. Note that if the $value parameter is +# given but is undefined then the $label key will be deleted from the hash. Anything that can be JSON encoded can be +# stored. +sub persistent_data { + my ($self, $label, $value) = @_; + if (@_ > 2) { + if (defined $value) { + $self->{PERSISTENCE_HASH}{$label} = $value; + } else { + delete $self->{PERSISTENCE_HASH}{$label}; + } } - $label; -} - -sub update_persistent_data { # will store strings only (so far) - my ($self, $label, @values) = @_; - $self->{PERSISTENCE_HASH_UPDATED}{$label} = 1; - $self->{PERSISTENCE_HASH}{$label} = join("", @values); -} - -sub get_persistent_data { - my ($self, $label) = @_; return $self->{PERSISTENCE_HASH}{$label}; } diff --git a/lib/WeBWorK/PG.pm b/lib/WeBWorK/PG.pm index 2aedd86d7..030753c90 100644 --- a/lib/WeBWorK/PG.pm +++ b/lib/WeBWorK/PG.pm @@ -264,10 +264,6 @@ sub defineProblemEnvironment ($pg_envir, $options = {}, $image_generator = undef showMessages => $options->{showMessages} // 1, showCorrectAnswers => $options->{showCorrectAnswers} // 0, - # The next has marks what data was updated and needs to be saved - # by the front end. - PERSISTENCE_HASH_UPDATED => {}, - inputs_ref => $options->{inputs_ref}, (map { $_ => $ansEvalDefaults->{$_} } keys %$ansEvalDefaults), diff --git a/macros/PG.pl b/macros/PG.pl index 07cce8ae9..c0bfa930b 100644 --- a/macros/PG.pl +++ b/macros/PG.pl @@ -545,19 +545,47 @@ sub ANS_NUM_TO_NAME { $PG->new_label(@_); } +=head2 persistent_data + +Save to or retrieve data from the persistence hash. The persistence hash is data +that will persist for this problem. It is saved when answers are submitted, and +can be retrieved and used within a problem. + + persistent_data($label); + persistent_data($label, $value); + +The C<$label> parameter is the key in the persistence hash. If the C<$value> +parameter is not given then the value of the C<$label> key in the hash will be +returned. If the C<$value> parameter is given then the value of the C<$label> +key in the hash will be saved or updated. Note that if the C<$value> parameter +is given but is undefined then the C<$label> key will be deleted from the hash. +Anything that can be JSON encoded can be stored. + +=cut + +sub persistent_data { + my ($label, @value) = @_; + return $PG->persistent_data($label, @value); +} + +# The store_persistent_data, update_persistent_data, and get_persistent_data methods are deprecated and are only still +# here for backward compatability. Use the persistent_data method instead which can do everything these three methods +# can do. Note that if you use the persistent_data method, then you will need to join the values as strings if you want +# that. Even better pass the persistent_data method an array reference containing the values so you can avoid the hassle +# of splitting the values when they are retrieved. sub store_persistent_data { my ($label, @values) = @_; - $PG->store_persistent_data($label, @values); + return $PG->persistent_data($label, join('', @values)); } sub update_persistent_data { my ($label, @values) = @_; - $PG->update_persistent_data($label, @values); + return $PG->persistent_data($label, join('', @values)); } sub get_persistent_data { my ($label) = @_; - return $PG->get_persistent_data($label); + return $PG->persistent_data($label); } sub add_content_post_processor { From 3a91b76c0e5c08b6f2b71244e3a8bbc1a134b83e Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Mon, 16 Dec 2024 16:26:42 -0600 Subject: [PATCH 2/2] Add a scaffold `preview_can_change_state` option that determines behavior when preview is used. The default value is 1, and in that case scaffolds will continue to behave as they currently do. If this option is set to 0, then when an answer preview occurs, the scaffold state will remain the same as before the preview occured. Opened scaffolds will stay open, closed scaffolds will stay closed, and scaffolds that could't be opened still can't be opened. Note this refers to the initial open/closed state when the problem loads, and does not respect opening or closing of scaffolds (that can be opened or closed) by the user. This uses the persistence hash to store the state. Note that state is really the scores for all answers in scaffold sections. It is important to note that state is usually not saved by the frontend until an answer submission occurs (although it is saved in a hidden form field for instructors), and if there is no state, then it assumes the problem is in its initial state, and uses scores of 0 for all answers. --- macros/core/scaffold.pl | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/macros/core/scaffold.pl b/macros/core/scaffold.pl index f69b58c33..df34ff125 100644 --- a/macros/core/scaffold.pl +++ b/macros/core/scaffold.pl @@ -184,6 +184,16 @@ =head1 DESCRIPTION have the student open it by hand before anwering the questions. In this case, set this value to 0 (it is 1 by default). +=item C 0 or 1 >> + +This determines if scaffold state can be changed when a preview occurs +(i.e., when the "Preview My Answers" button is used). If this is 0, +then when a preview occurs any scaffold sections that were open before +the preview will remain open, and any scaffold sections that were closed +before the preview will remain closed. If this is 1, then the rules +described above will be applied using the scores of the answers in the +parts. This is 1 by default. + =item C 0 or 1 >>> This determines whether each section is automatically numbered before @@ -353,6 +363,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 + preview_can_change_state => 1, %options, number => ++$scaffold_no, # the number for this scaffold depth => $scaffold_depth, # the nesting depth for this scaffold @@ -536,16 +547,24 @@ sub add_container { # Nothing needs to be done for the PTX display mode. return if $Scaffold::isPTX; + my $scaffoldScores = main::persistent_data('_scaffold_scores') // {}; + # Provide a "scaffold_force" option in the AnswerHash that can be used to force # Scaffold to consider the score to be 1. This is used by PGessaymacros.pl. + # Also, if answers are being previewed and the preview_can_change_state option is 0, then use the scores saved + # in the persistent data hash form the last answer submission (if there is no data for an answer, then the + # anwser is considered blank). for (@{ $self->{ans_names} }) { next unless defined $PG_ANSWERS_HASH->{$_}; - $scaffold->{scores}{$_} = - $PG_ANSWERS_HASH->{$_}{ans_eval}{rh_ans}{scaffold_force} - ? 1 + $scaffold->{scores}{$_} = $scaffoldScores->{$_} = + $PG_ANSWERS_HASH->{$_}{ans_eval}{rh_ans}{scaffold_force} ? 1 + : !$scaffold->{preview_can_change_state} && $PG_ANSWERS_HASH->{$_}{ans_eval}{rh_ans}{isPreview} + ? $scaffoldScores->{$_} : $PG_ANSWERS_HASH->{$_}{ans_eval}{rh_ans}{score}; } + main::persistent_data(_scaffold_scores => $scaffoldScores); + # 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;