Set up a quiz with
- Allowed number of attempts set to 2 (or higher)
- Store results set to The best.

Start the quiz for the first time and get the best possible score.
Then start the quiz again, but this time get a lower score than the first attempt.

When finishing the quiz you'll now end up with a Drupal Access Denied page, note that this even happens when using user1 as it isn't actually a permissions error.

-----

Tracing everything back we'll end up in the following flow. The user finishes the quiz, but during the submit the result isn't stored into the database (because the users got a lower score than attempt 1 and we've told the quiz to only save the best result). The user is then redirected to the result screen, but hook_menu first sends the user passed the access check. And the access check tries to check if the user has access to the result page based on a database query which will result nothing.

----

As end user I would actually still expect to get a full result screen with all my results, but I can somewhat understand that such a thing would require some major adjustments to the code. But shouldn't we at least we redirect the user to a valid page (the start of the quiz in question?) with a message that his result wasn't stored since he scored lower than one of his previous attempts.

The current redirect comes from the following code in quiz/question_types/quiz_question/quiz_question.module (line 790)

function quiz_question_answering_form_finalize(&$form, &$form_state) {
  $quiz_result = quiz_result_load($_SESSION['quiz'][$form['#quiz']['nid']]['result_id']);
  $quiz = node_load($quiz_result->nid, $quiz_result->vid);

  // No more questions. Score quiz.
  $score = quiz_end_scoring($_SESSION['quiz'][$quiz->nid]['result_id']);

  if (empty($quiz->review_options['question']) || !array_filter($quiz->review_options['question']) || empty($form_state['feedback'])) {
    // Only redirect to question results if there is not question feedback.
    $form_state['redirect'] = "node/{$quiz->nid}/quiz-results/{$quiz_result->result_id}/view";
  }
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Da_Cloud created an issue. See original summary.

djdevin’s picture

There's a duplicate issue for that in the queue somewhere, I've seen it before...

The issue is as you described, we redirect to view the result, but that result was deleted.

In Quiz 4.x the entire result HTML was stuffed into the $_SESSION and then printed out. That's not the best idea but we don't have a solution right now.

We could delete the quiz result as soon as it is viewed, but it would result in a 404 on a page refresh. Maybe that's okay.

Da_Cloud’s picture

Adding the whole result to the SESSION doesn't really feel like a good solution, its quite a lot of information that you would need to place inside of it.

Saving these results to the database seems to be the best solution. All it would take is an extra field to store the status, and some mechanisme to clean it up. Getting a 404 on a page refresh isn't that big of a deal, we could always add a message to the initial result page stating that its a temporary page. Should the user then get a 404 its at least his "own" fault.

Fixing it this way might also be the best way to solve the problems with the current attempt count, which is somewhat related to this issue. In the above scenario your second attempt never gets saved, so you can keep on trying even though the quiz is set to a max of 2 attempts.

Da_Cloud’s picture

FileSize
1.99 KB

I've made a quick patch based on your solution. Meaning we don't directly delete the result, but instead mark it and delete it upon viewing the entity. The patch should not be committed as is, but is mainly created to test the current test cases as well as a check to see if I'm on the right track with this.

The patch only does two things right now. First of all in the function _quiz_maintain_results($quiz, $result_id) I've added a check to see if the current result_id is going to be deleted. If it is we undo the upcoming delete action and instead run a db_query to mark the result as "invalid". I've used the "is_invalid" field for now as it seems to be a deprecated field that so happens to sort of match the field we need.

Then in the function quiz_user_results($result_id) I've added a check to see if the result is marked as invalid. If it is we delete the result and show a message to the user warning him.

On my local environment this seems to do the trick of showing the result to the user while also directly removing the result from the database.

djdevin’s picture

I think that's a pretty good approach. That is_invalid column is not used anywhere, so we could definitely use it here.

Another idea might be to just keep all the results, and delete any results with "is_invalid" on cron.

Da_Cloud’s picture

Deling the results based on a cron would have my preference as well. Make everything more manageable without "random" delete action like in the patch, however I'm not quite sure how much work it would cost to do this. Since we'll be storing invalid results to the database we'll need to update all the reports to match the new behavior.

I'll need to look how much work updating all the reports will cost, but might be an idee to push the fix in two steps. Starting off with the current workaround by deleting the invalid result on view. Which we'll phase out later in favor of a cron once all the reports are reworked with the new state.

Da_Cloud’s picture

Assigned: Unassigned » Da_Cloud
Status: Active » Needs work
FileSize
4.57 KB

Added a patch that forms the groundwork for when we move everything to a cron based cleanup. Would still need a lot of work though.

I've added a setting to the settings form to set the time after which invalid records need to be deleted (with a default of 1 day). Not sure what the best default / available intervals would be, but giving adding some flexibility for advanced users seems a better idee than to simply delete everything on the first cron.

For the cron run I'm not entirely sure if its better to retrieve all the results and put them through a entity_delete() or if its better to delete them directly with a db_delete().

I've yet to look at all the code that needs to be updated, but to pick up on the list from the old issue:
- Attempt numbering
- Build on last
- Quiz resume
- Reports featuring results (for you personal report it might be nice to include in_valid results, but for the admin overview you want to exclude them)
- Writing testscript

Da_Cloud’s picture

Status: Needs work » Needs review
FileSize
15.59 KB

Status: Needs review » Needs work

The last submitted patch, 8: quiz-2838611-8.patch, failed testing.

Da_Cloud’s picture

The test result that failed seems to check if the store mechanisms are working by checking the around of results it can find in the database. So as excepted those will fail without properly rewriting them first (something that's yet to be done).

I'll rework the test cases later, as I've also found another issue with the current patch as well.

There is one major thing that could be worth mentioning. Once the results are reworked to be deleted on cron run we also have the possibility to fix #2687741: Allowed number of attempts fails when store results is different from all by using the query below for the cron delete mechanism. This way we prevent deleting quiz results for quizzes where the user has a limited amount of takes, thus directly fixing the count query that prevents the user from making more attempts.

  // Remove invalid quiz results.
  $rm_time = variable_get('quiz_remove_invalid_quiz_record', 86400);
  if ($rm_time) { // $time = 0 for never.
    $query = db_select('quiz_node_results', 'qnr');
    $query->fields('qnr', array('result_id'));
    $query->join('quiz_node_properties', 'qnp', 'qnr.vid = qnp.vid');
    // If the user has a limited amount of takes we don't delete invalid results
    $query->condition('qnp.takes', 0, '=');
    $query->condition('qnr.is_invalid', 1);
    $query->condition('qnr.time_end', REQUEST_TIME - $rm_time, '<=');
    $res = $query->execute();
    while ($result_id = $res->fetchField()) {
      $result_ids[$result_id] = $result_id;
    }
  }

However if we also hide the invalid results on the reports we end up with "hidden" records in the database. All I can think of to solve this issue is to add filters to the current result views to show/hide invalid results?

Da_Cloud’s picture

The following things have been done in the patch:

1) The function _quiz_maintain_results has been changed in the way that it only marks quiz_results as invalid. This goes for both QUIZ_KEEP_BEST and QUIZ_KEEP_LATEST.

2) quiz_cron() has been extended to remove all invalid quiz_results older than a certain amount of time (with the default being 1 day). As mentioned in the comment above I've also added the condition that the quiz must have unlimited takes. If it has a limited amount of takes we'll keep the results as to fix issue #2687741: Allowed number of attempts fails when store results is different from all.

3) I've attempted to update all reports/statistics to work with the is_invalid status.

4) The testcase that checks the purging mechanisme has been updated. Right now it will check both the amount of valid as invalid results, ending with a cron run to test if purging the results will work.

webservant316’s picture

@Da_Cloud is this patch working effectively in production for you?

@djdevin are you favoring this solution? I don't want to get into this if it is not the future. In my use case I would be open to a simpler solution....

for example in the function "quiz_access_results($quiz, $quiz_result = NULL)" my use case would be fine with checking to insure that the $quiz_result exists and if not then simply drupal_goto the last quiz result that does exist, probably the 'Best' quiz score saved. With this solution perhaps a message could be put on the page saying that the previous better result is displayed below instead of the current result. This solution would be a lot less intrusive, though a compromise.

webservant316’s picture

As an aside I had intended to flag all my quizzes to save only the 'Best' result, but a number of quizzes are flagged to save all. I am now wondering if I change the flag to save 'Best' only if the logic of quiz, course, and this patch will be able to handle changing the flag from 'all' to 'best'.

Da_Cloud’s picture

@webservant316 Currently I'm not running this patch on any production site. The project that required this functionality had a strict deadline, so alongside creating this patch we also used a different "fix" for the production site.

In the end we simply changed back the storage settings from "best" to "all" and used some custom views that would only show the best results to the user. This has some minor downsides (mainly being that end-users can still see his other results by manually typing the URLs), but for our project this wasn't a big issue.

webservant316’s picture

This solution is much simpler and works for me. Sorry if the patch is not done properly. Still learning how to do that.

webservant316’s picture

Also thanks for any input on this outstanding question...

As an aside I had intended to flag all my quizzes to save only the 'Best' result, but a number of quizzes are flagged to save all. I am now wondering if I change the flag to save 'Best' only if the logic of quiz, course, and this patch will be able to handle changing the flag from 'all' to 'best'.

My patch at #15 looks for the most recently dated quiz result and not the best result. So when changing the quiz flag from save all to save the best does the quiz module actually find the best quiz result and delete all the rest? If not my logic will not work in that case, though it will grab the most recently dated quiz result which is something at least.

webservant316’s picture

this small revision allows the simple patch to also chose the best result when there are multiple existing results.

IGNORE THIS PATCH, MADE WRONG

Status: Needs review » Needs work
webservant316’s picture

Answering my own question from #16 it appears that when changing the quiz flag from keep 'all' to keep 'best' that multiple existing results remain, until the student completes the quiz again at which time the logic to save the best result also deletes all the remaining results. So that is good. Thanks.

webservant316’s picture

Oops #17 is a patch after the patch. Improved patch attached again from the baseline.

Again this patch is a much simpler solution and chooses the best existing quiz result for display when multiple quiz results exist and the quiz save result is set to 'best'.

webservant316’s picture

I had stepped away from my efforts with the course module and just returned to remind myself of this issue and further test my solution in #17 above. The patch is working for me as is. Would appreciate any comments toward a long term solution.

djdevin’s picture

Patch in #20 redirects during a menu access check which you we do not want to do. See https://drupal.stackexchange.com/questions/108966/would-redirecting-user...

The patch in #11 has a better approach and would be the long term solution.

webservant316’s picture

Good point about the redirect during the access check. I revised my custom patch in production as follows. Works the same way, but better. Maybe this will be useful in the long term solution. A default fallback seems useful.

djdevin’s picture

Patch in #23 breaks post-quiz feedback since it loads the best result instead of the one the user just finished.

We're not trying to just avoid the page not found, we are trying to display the result the user finished but either discard it or mark it as invalid so that only the best result is retained.

Please work on the approach in #11 which accounts for this situation by deleting the result only after the user has viewed it.

webservant316’s picture

oh, I see. I will come back around to this.

I was probably hesitant to get into #11 myself because that solution will require many report views to be fixed and tested. I wasn't sure I understood all that was involved there. Do you know enough about patch #11 to know what modifications are needed to reports and how much is already done?

djdevin’s picture

Really the only modification is excluding quiz results that are marked as "invalid" as they're going to get removed later.

This was an already existing schema field that already existed in Quiz for this functionality, but was never used. So now we get to use it!

webservant316’s picture

Ok, thanks.

One question about my patch in #23. Seems like this patch could still be useful as a fail safe option to display a quiz result in the case that the path is called and the quiz id is not found for whatever reason. Thoughts?

Da_Cloud’s picture

Theoretically speaking the only way that the code in patch #11 couldn't return a result page is when the results couldn't get stored into the database. Seeming how that would be a critical failure somewhere else in the code I personally don't think we'll need a fail save mechanism.

As far as the status of patch #11. Its been quite a while since it was made, but I'm pretty confident its as good as finished and mainly just needs to be reviewed. I had already updated the test scripts to match the new purging mechanism and as far as I could see all db_queries and default views where updated to use the new "Invalid" status.

The only main issue would be custom views made by people themselves as there is no real way to create an upgrade path for those. Not including the "invalid" check on a view doesn't break anything, but it could end up showing quiz results that shouldn't be shown.

djdevin’s picture

Assigned: Da_Cloud » Unassigned
Status: Needs work » Needs review

Yes, agreed. Likewise we wouldn't load a different quiz if node/123 was not found. If an admin or user requests a specific result it should either return the result or trigger a 404 or some other action. Loading up a previous result will be confusing as it was not requested and the user would be seeing outdated responses.

The only way I could see that happening is if someone bookmarked their result, but again...it's gone, so we should treat it like any other resource.

The patch in #11 looks great, since it is hefty I wanted to let it sit for a little bit for review.

Unfortunately a different patch was submitted in #20 which sent this ticket back to "Needs work" instead of "Needs review" and it fell off of my list...I will get it committed soon.

We have some custom views as well, but like you said they would just show results that are going to get deleted on the next scheduler run.

"Keep best" was broken anyway so I don't even think we have any quizzes set up like that.

djdevin’s picture

The only comments I have about the patch in #11 is that we should probably convert those queries to db_select() since we're modifying them.

Da_Cloud’s picture

Changes compared to the patch from #11

  1. I've added an additional case in the test script that will check if the purging only happens after the given purge time period.
  2. All queries within the functions that we touch have been updated to Dynamic queries (db_select & db_update).
  3. For all the functions that we touch I've updated the coding standards to match the Drupal standard.
  4. Found an additional query which needed to be updated with the "new" is_invalid status.

Status: Needs review » Needs work

The last submitted patch, 31: quiz-rework_purging_mechanism-2838611-31.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Da_Cloud’s picture

  • djdevin committed 3ce4b17 on 7.x-5.x authored by Da_Cloud
    Issue #2838611 by Da_Cloud: Access denied upon completing a quiz when...
djdevin’s picture

Status: Needs review » Fixed

Thanks, great changes in here.

We can probably replace the old quiz_remove_partial_quiz_record_value() in the admin settings with the Duration module, but I'll save that for another issue.

This is pretty silly:

function quiz_remove_partial_quiz_record_value() {
  return array(
    '0' => t('Never'),
    '86400' => t('1 Day'),
    '172800' => t('2 Days'),
    '259200' => t('3 Days'),
    '345600' => t('4 Days'),
[etc]
webservant316’s picture

I haven't tested the improved patch yet, but seems like it could also be useful to post a simple message with the temporary results saying something like

"Your Quiz result is found below, however, only the best result is saved here."

combined with the following when the most recent result is the best result...

"Congrats! Your best Quiz result is found below, and saved for future reference."

Hmm this would need consideration because the Quiz node also allows the expression of pass and fail messages.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.