Moodle

Saving an existing question should not require the $form->categorymoveto parameter

Details

  • Type: Bug Bug
  • Status: Closed Closed
  • Priority: Minor Minor
  • Resolution: Fixed
  • Affects Version/s: 1.9.3
  • Fix Version/s: 1.9.5
  • Component/s: Questions
  • Labels:
    None
  • Affected Branches:
    MOODLE_19_STABLE
  • Fixed Branches:
    MOODLE_19_STABLE

Description

In the actual question/type/questiontype.php save_question function
the code is
if (!empty($question->id) && !empty($form->categorymoveto)) { // Question already exists
list($movetocategory, $movetocontextid) = explode(',', $form->categorymoveto);
if ($movetocategory != $question->category){ question_require_capability_on($question, 'move'); $question->category = $movetocategory; //don't need to test add permission of category we are moving question to. //Only categories that we have permission to add //a question to will get through the form cleaning code for the select box. }
// keep existing unique stamp code
$question->stamp = get_field('question', 'stamp', 'id', $question->id);
$question->modifiedby = $USER->id;
$question->timemodified = time();
if (!update_record('question', $question)) { error('Could not update question!'); }
} else { // Question is a new one
if (isset($form->categorymoveto)){ // Doing save as new question, and we have move rights. list($question->category, $notused) = explode(',', $form->categorymoveto); //don't need to test add permission of category we are moving question to. //Only categories that we have permission to add //a question to will get through the form cleaning code for the select box. } else { // Really a new question. list($question->category, $notused) = explode(',', $form->category); }
// Set the unique code
$question->stamp = make_unique_id_code();
$question->createdby = $USER->id;
$question->modifiedby = $USER->id;
$question->timecreated = time();
$question->timemodified = time();
if (!$question->id = insert_record('question', $question)) { print_object($question); error('Could not insert new question!'); }
}
If there is a call to save_question with an existing question i.e. !empty($question->id without the $form->categorymoveto parameter, this question will be considered as a new one.
This is the case when saving multianswers subquestions when these subquestions already exist i.e. when modifying a multianswer.
The multianswer case can be solved by adding a false categorymoveto parameter before calling the save_question.
However I think that the code should be modified so that it takes in account the case when the save_question is not coming from a question_edit_form with a valid $form->categorymoveto.

Issue Links

Activity

Hide
Tim Hunt added a comment -

The situation we have to be careful of is when the user had done save as new question.

In this case $question->id will be set, but we are still creating a new question. We need to find a reliable way of identifying this case.

I think this is a tricky bug. There have been related problems before like http://tracker.moodle.org/browse/MDL-14676. Can you prepare a patch, and then we can review it together. It would be nice to get this sorted out once and for all, but it is clearly very easy to cause regressions in this area. Thanks.

Show
Tim Hunt added a comment - The situation we have to be careful of is when the user had done save as new question. In this case $question->id will be set, but we are still creating a new question. We need to find a reliable way of identifying this case. I think this is a tricky bug. There have been related problems before like http://tracker.moodle.org/browse/MDL-14676. Can you prepare a patch, and then we can review it together. It would be nice to get this sorted out once and for all, but it is clearly very easy to cause regressions in this area. Thanks.
Hide
Pierre Pichet added a comment -

Ok.
On the top of my to-do list.
We should also take account the question/question.php code.

Show
Pierre Pichet added a comment - Ok. On the top of my to-do list. We should also take account the question/question.php code.
Hide
Pierre Pichet added a comment -

The convention used before all these capabilities was that
if the question->id != 0 means that this is an update
and if question->id == 0 means that we want a new question.

This is how the actual question/question.php works.
line
if (!empty($fromform->makecopy)) { $question->id = 0; // causes a new question to be created. $question->hidden = 0; // Copies should not be hidden }

Notice that the empty function is a good test for question->id != 0 .
As of PHP 4, The string value "0" is considered empty.
However As of PHP 5, objects with no properties are no longer considered empty.
Do this means that if question->id is not set, the test failed?

Show
Pierre Pichet added a comment - The convention used before all these capabilities was that if the question->id != 0 means that this is an update and if question->id == 0 means that we want a new question. This is how the actual question/question.php works. line if (!empty($fromform->makecopy)) { $question->id = 0; // causes a new question to be created. $question->hidden = 0; // Copies should not be hidden } Notice that the empty function is a good test for question->id != 0 . As of PHP 4, The string value "0" is considered empty. However As of PHP 5, objects with no properties are no longer considered empty. Do this means that if question->id is not set, the test failed?
Hide
Pierre Pichet added a comment - - edited

On testing with PHP 5.2.4
$wrapped->id = 0 ;
if (!empty($wrapped->id)) { echo "<p>wrapped id not empty </p>" ; }else { echo "<p>wrapped id == 0 </p>" ; }
gives wrapped id == 0 so OK

$wrapped->id = '' ;
if (!empty($wrapped->id)) { echo "<p>wrapped id not empty </p>" ; }else { echo "<p>wrapped id == '' </p>" ; }
gives wrapped id == '' so OK

unset($wrapped->id);
if (!empty($wrapped->id)) { echo "<p>wrapped id not empty </p>" ; }else { echo "<p>wrapped id empty </p>" ; }
gives wrapped id empty so OK

! empty($question->id) is the test to use .in PHP 5 and 4 .

So the code should go as
if (!empty($question->id) ) { // Question already exists // test category ...... // update ...... } else {// Question is a new one or a copy of an existing one (original id is not known) // test category .... // create a new question .... }

In the update part we have to test if the actual $questio->category is valid and different or not of the $form->categorymoveto
If categorymoveto is different from actual category we are moving a question and are assuming that we stay in the same context.
At first look the question.php code set this
$movecontext = optional_param('movecontext', 0, PARAM_BOOL);//switch to make question
//uneditable - form is displayed to edit category only
and
if ($id) {
$canview = question_has_capability_on($question, 'view');
if ($movecontext){
$question->formoptions->canedit = false;
$question->formoptions->canmove = (question_has_capability_on($question, 'move') && $contexts->have_cap('moodle/question:add'));
$question->formoptions->cansaveasnew = false;
$question->formoptions->repeatelements = false;
$question->formoptions->movecontext = true;
$formeditable = true;
question_require_capability_on($question, 'view');
i.e. canedit = false and cansaveasnew = false;
so no new question just updating and the file moving will have been done before calling save
The edit_question_form just work ( edit and save as new) in the same context.

more later ...

Show
Pierre Pichet added a comment - - edited On testing with PHP 5.2.4 $wrapped->id = 0 ; if (!empty($wrapped->id)) { echo "<p>wrapped id not empty </p>" ; }else { echo "<p>wrapped id == 0 </p>" ; } gives wrapped id == 0 so OK $wrapped->id = '' ; if (!empty($wrapped->id)) { echo "<p>wrapped id not empty </p>" ; }else { echo "<p>wrapped id == '' </p>" ; } gives wrapped id == '' so OK unset($wrapped->id); if (!empty($wrapped->id)) { echo "<p>wrapped id not empty </p>" ; }else { echo "<p>wrapped id empty </p>" ; } gives wrapped id empty so OK ! empty($question->id) is the test to use .in PHP 5 and 4 . So the code should go as if (!empty($question->id) ) { // Question already exists // test category ...... // update ...... } else {// Question is a new one or a copy of an existing one (original id is not known) // test category .... // create a new question .... } In the update part we have to test if the actual $questio->category is valid and different or not of the $form->categorymoveto If categorymoveto is different from actual category we are moving a question and are assuming that we stay in the same context. At first look the question.php code set this $movecontext = optional_param('movecontext', 0, PARAM_BOOL);//switch to make question //uneditable - form is displayed to edit category only and if ($id) { $canview = question_has_capability_on($question, 'view'); if ($movecontext){ $question->formoptions->canedit = false; $question->formoptions->canmove = (question_has_capability_on($question, 'move') && $contexts->have_cap('moodle/question:add')); $question->formoptions->cansaveasnew = false; $question->formoptions->repeatelements = false; $question->formoptions->movecontext = true; $formeditable = true; question_require_capability_on($question, 'view'); i.e. canedit = false and cansaveasnew = false; so no new question just updating and the file moving will have been done before calling save The edit_question_form just work ( edit and save as new) in the same context. more later ...
Hide
Tim Hunt added a comment -

empty($question->id) is a good test. And I would be very happy in that can reliably be used as the test for new vs. updated question, which seems to be the case. Keep up the good work.

Show
Tim Hunt added a comment - empty($question->id) is a good test. And I would be very happy in that can reliably be used as the test for new vs. updated question, which seems to be the case. Keep up the good work.
Hide
Pierre Pichet added a comment -

Function and Method Cross Reference 1.9
save_question()
Defined at:

  • /question/type/questiontype.php -> line 214
  • /question/type/multianswer/questiontype.php -> line 129
  • /question/type/random/questiontype.php -> line 48
  • /question/type/description/questiontype.php -> line 27
  • /question/type/datasetdependent/abstractqtype.php -> line 253

Referenced 15 times:

  • /mod/quiz/edit.php -> line 210
  • /question/type/multianswer/questiontype.php -> line 97
  • /question/type/multianswer/questiontype.php -> line 146
  • /question/type/random/questiontype.php -> line 55
  • /question/type/description/questiontype.php -> line 31
  • /question/type/datasetdependent/abstractqtype.php -> line 288
  • /question/type/datasetdependent/abstractqtype.php -> line 295
  • /question/type/datasetdependent/abstractqtype.php -> line 301
  • /question/question.php -> line 183
    And it could be defined in new questiontypes plug-ins.

So next work is to look on how it is defined and used in these places so to fix everything.

Show
Pierre Pichet added a comment - Function and Method Cross Reference 1.9 save_question() Defined at:
  • /question/type/questiontype.php -> line 214
  • /question/type/multianswer/questiontype.php -> line 129
  • /question/type/random/questiontype.php -> line 48
  • /question/type/description/questiontype.php -> line 27
  • /question/type/datasetdependent/abstractqtype.php -> line 253
Referenced 15 times:
  • /mod/quiz/edit.php -> line 210
  • /question/type/multianswer/questiontype.php -> line 97
  • /question/type/multianswer/questiontype.php -> line 146
  • /question/type/random/questiontype.php -> line 55
  • /question/type/description/questiontype.php -> line 31
  • /question/type/datasetdependent/abstractqtype.php -> line 288
  • /question/type/datasetdependent/abstractqtype.php -> line 295
  • /question/type/datasetdependent/abstractqtype.php -> line 301
  • /question/question.php -> line 183 And it could be defined in new questiontypes plug-ins.
So next work is to look on how it is defined and used in these places so to fix everything.
Hide
Pierre Pichet added a comment -

Developper docs will be a better place to continue this analysis

Show
Pierre Pichet added a comment - Developper docs will be a better place to continue this analysis
Hide
Pierre Pichet added a comment -

I have corrected the multianswer code for the actual version of questiontype.php save_question().
There was a necessary change due to the new code about MDL-14625 and a "patch" due to the fact that the save_question does not identify correctly what is a new question.
I prefer to do this before finishing the analysis.

Show
Pierre Pichet added a comment - I have corrected the multianswer code for the actual version of questiontype.php save_question(). There was a necessary change due to the new code about MDL-14625 and a "patch" due to the fact that the save_question does not identify correctly what is a new question. I prefer to do this before finishing the analysis.

People

Vote (0)
Watch (1)

Dates

  • Created:
    Updated:
    Resolved: