From 98f38217bb6de4ffa78f79e7371b5d8f53aaf98a Mon Sep 17 00:00:00 2001 From: jamiesensei Date: Fri, 11 Jul 2008 07:27:14 +0000 Subject: [PATCH] MDL-14216 "Improvements to regrade report - improved reporting of actions and dry run / regrade" --- lang/en_utf8/quiz_overview.php | 21 +- lib/questionlib.php | 64 ++- mod/quiz/lib.php | 11 +- mod/quiz/locallib.php | 15 +- mod/quiz/report/overview/db/install.xml | 21 + mod/quiz/report/overview/db/upgrade.php | 41 ++ mod/quiz/report/overview/overview_table.php | 106 +++-- .../report/overview/overviewsettings_form.php | 7 +- mod/quiz/report/overview/report.php | 428 ++++++++++++++++-- mod/quiz/report/overview/version.php | 10 + mod/quiz/report/reportlib.php | 49 +- theme/standard/styles_layout.css | 2 +- 12 files changed, 654 insertions(+), 121 deletions(-) create mode 100644 mod/quiz/report/overview/db/install.xml create mode 100644 mod/quiz/report/overview/db/upgrade.php create mode 100644 mod/quiz/report/overview/version.php diff --git a/lang/en_utf8/quiz_overview.php b/lang/en_utf8/quiz_overview.php index dddc39ab41..d7c1bd1e04 100644 --- a/lang/en_utf8/quiz_overview.php +++ b/lang/en_utf8/quiz_overview.php @@ -7,15 +7,23 @@ $string['allattemptscontributetograde'] = 'All attempts contribute to final grad $string['allstudents'] = 'Show all $a'; $string['attemptsonly'] = 'Show $a with attempts only'; $string['attemptsprepage'] = 'Attempts shown per page'; +$string['attemptprogress'] = 'Attempt $a->done of $a->todo'; $string['deleteselected'] = 'Delete selected attempts'; +$string['done'] = 'Done'; +$string['err_failedtorecalculateattemptgrades'] = 'Failed to recalculate attempt grades'; +$string['err_failedtodeleteregrades'] = 'Failed to delete calculated attempt grades'; $string['highlightinggraded'] = 'The user attempt that contributes to final grade is highlighted.'; +$string['needed'] = 'Needed'; +$string['noattemptstoregrade'] = 'No attempts need regrading'; $string['noattemptsonly'] = 'Show / download $a with no attempts only'; +$string['nogradepermission'] = 'You don\'t have permission to grade this quiz.'; $string['onlyoneattemptallowed'] = 'Only one attempt per user allowed on this quiz.'; $string['optallattempts'] = 'all attempts'; $string['optallstudents'] = 'all $a who have or have not attempted the quiz'; $string['optattemptsonly'] = '$a who have attempted the quiz'; $string['optnoattemptsonly'] = '$a who have not attempted the quiz'; -$string['optonlygradedattempts'] = 'only the attempt that is graded for each user ($a)'; +$string['optonlygradedattempts'] = 'that are graded for each user ($a)'; +$string['optonlyregradedattempts'] = 'that have been regraded / are marked as needing regrading'; $string['overview'] = 'Grades'; $string['overviewdownload'] = 'Overview download'; $string['overviewdownload'] = 'Overview download'; @@ -24,7 +32,18 @@ $string['pagesize'] = 'Page size'; $string['preferencespage'] = 'Preferences just for this page'; $string['preferencessave'] = 'Save preferences'; $string['preferencesuser'] = 'Your preferences for this report'; +$string['qprogress'] = 'Question $a->done of $a->todo'; +$string['regrade'] = 'Regrade'; +$string['regradeheader'] = 'Regrading'; +$string['regradeall'] = 'Regrade all'; +$string['regradeallgroup'] = 'Full regrade for group \'$a->groupname\''; +$string['regradealldry'] = 'Dryrun a full regrade'; +$string['regradealldrydo'] = 'Regrade attempts marked as needing regrading ($a)'; +$string['regradealldrydogroup'] = 'Regrade attempts ($a->countregradeneeded) marked as needing regrading in group \'$a->groupname\''; +$string['regradealldrygroup'] = 'Dryrun a full regrade for group \'$a->groupname\''; +$string['regradeselected'] = 'Regrade selected attempts'; $string['show'] = 'Show / download'; +$string['showattempts'] = 'Only show / download attempts'; $string['showinggraded'] = 'Showing only the attempt graded for each user.'; $string['showinggradedandungraded'] = 'Showing graded and ungraded attempts for each user. The one attempt for each user that is graded is highlighted. The grading method for this quiz is $a.'; $string['showdetailedmarks'] = 'Show / download marks for each question'; diff --git a/lib/questionlib.php b/lib/questionlib.php index ce91e500f3..9e7f56ad07 100644 --- a/lib/questionlib.php +++ b/lib/questionlib.php @@ -1233,8 +1233,10 @@ function question_get_feedback_class($fraction) { * @param object $attempt The attempt, in which the question needs to be regraded. * @param object $cmoptions * @param boolean $verbose Optional. Whether to print progress information or not. +* @param boolean $dryrun Optional. Whether to make changes to grades records +* or record that changes need to be made for a later regrade. */ -function regrade_question_in_attempt($question, $attempt, $cmoptions, $verbose=false) { +function regrade_question_in_attempt($question, $attempt, $cmoptions, $verbose=false, $dryrun=false) { global $DB; // load all states for this question in this attempt, ordered in sequence @@ -1280,43 +1282,67 @@ function regrade_question_in_attempt($question, $attempt, $cmoptions, $verbose=f // proceeding. if ($states[$j]->grade < 0) { $states[$j]->grade = 0; + $changed = true; } else if ($states[$j]->grade > $question->maxgrade) { $states[$j]->grade = $question->maxgrade; + $changed = true; + } - $error = question_process_comment($question, $replaystate, $attempt, - $replaystate->manualcomment, $states[$j]->grade); - if (is_string($error)) { - notify($error); + if (!$dryrun){ + $error = question_process_comment($question, $replaystate, $attempt, + $replaystate->manualcomment, $states[$j]->grade); + if (is_string($error)) { + notify($error); + } + } else { + $replaystate->grade = $states[$j]->grade; } } else { - // Reprocess (regrade) responses if (!question_process_responses($question, $replaystate, $action, $cmoptions, $attempt)) { $verbose && notify("Couldn't regrade state #{$state->id}!"); } + // We need rounding here because grades in the DB get truncated + // e.g. 0.33333 != 0.3333333, but we want them to be equal here + if ((round((float)$replaystate->raw_grade, 5) != round((float)$states[$j]->raw_grade, 5)) + or (round((float)$replaystate->penalty, 5) != round((float)$states[$j]->penalty, 5)) + or (round((float)$replaystate->grade, 5) != round((float)$states[$j]->grade, 5))) { + $changed = true; + } } + - // We need rounding here because grades in the DB get truncated - // e.g. 0.33333 != 0.3333333, but we want them to be equal here - if ((round((float)$replaystate->raw_grade, 5) != round((float)$states[$j]->raw_grade, 5)) - or (round((float)$replaystate->penalty, 5) != round((float)$states[$j]->penalty, 5)) - or (round((float)$replaystate->grade, 5) != round((float)$states[$j]->grade, 5))) { - $changed = true; - } $replaystate->id = $states[$j]->id; $replaystate->changed = true; $replaystate->update = true; // This will ensure that the existing database entry is updated rather than a new one created - save_question_session($question, $replaystate); + if (!$dryrun){ + save_question_session($question, $replaystate); + } } if ($changed) { - // TODO, call a method in quiz to do this, where 'quiz' comes from - // the question_attempts table. - $DB->update_record('quiz_attempts', $attempt); + if (!$dryrun){ + // TODO, call a method in quiz to do this, where 'quiz' comes from + // the question_attempts table. + $DB->update_record('quiz_attempts', $attempt); + } + } + if ($changed){ + $toinsert = new object(); + $toinsert->oldgrade = round((float)$states[count($states)-1]->grade, 5); + $toinsert->newgrade = round((float)$replaystate->grade, 5); + $toinsert->attemptid = $attempt->uniqueid; + $toinsert->questionid = $question->id; + //the grade saved is the old grade if the new grade is saved + //it is the new grade if this is a dry run. + $toinsert->regraded = $dryrun?0:1; + $toinsert->timemodified = time(); + $DB->insert_record('quiz_question_regrade', $toinsert); + return true; + } else { + return false; } - - return $changed; } return false; } diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index a8d16f57c0..530f48b875 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -242,7 +242,7 @@ function quiz_cron () { * @param string $status 'all', 'finished' or 'unfinished' to control * @return an array of all the user's attempts at this quiz. Returns an empty array if there are none. */ -function quiz_get_user_attempts($quizid, $userid, $status = 'finished', $includepreviews = false) { +function quiz_get_user_attempts($quizid, $userid=0, $status = 'finished', $includepreviews = false) { global $DB; $status_condition = array( 'all' => '', @@ -253,8 +253,15 @@ function quiz_get_user_attempts($quizid, $userid, $status = 'finished', $include if (!$includepreviews) { $previewclause = ' AND preview = 0'; } + $params=array($quizid); + if ($userid){ + $userclause = ' AND userid = ?'; + $params[]=$userid; + } else { + $userclause = ''; + } if ($attempts = $DB->get_records_select('quiz_attempts', - "quiz = ? AND userid = ?" . $previewclause . $status_condition[$status], array($quizid, $userid), + "quiz = ?" .$userclause. $previewclause . $status_condition[$status], $params, 'attempt ASC')) { return $attempts; } else { diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index 7215e741c3..be9209536c 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -516,9 +516,12 @@ function quiz_set_grade($newgrade, &$quiz) { * * @param object $quiz The quiz for which the best grade is to be calculated and then saved. * @param integer $userid The userid to calculate the grade for. Defaults to the current user. + * @param array $attempts The attempts of this user. Useful if you are + * looping through many users. Attempts can be fetched in one master query to + * avoid repeated querying. * @return boolean Indicates success or failure. */ -function quiz_save_best_grade($quiz, $userid = null) { +function quiz_save_best_grade($quiz, $userid = null, $attempts = array()) { global $DB; global $USER; @@ -526,10 +529,12 @@ function quiz_save_best_grade($quiz, $userid = null) { $userid = $USER->id; } - // Get all the attempts made by the user - if (!$attempts = quiz_get_user_attempts($quiz->id, $userid)) { - notify('Could not find any user attempts'); - return false; + if (!$attempts){ + // Get all the attempts made by the user + if (!$attempts = quiz_get_user_attempts($quiz->id, $userid)) { + notify('Could not find any user attempts'); + return false; + } } // Calculate the best grade diff --git a/mod/quiz/report/overview/db/install.xml b/mod/quiz/report/overview/db/install.xml new file mode 100644 index 0000000000..ab6834a4bb --- /dev/null +++ b/mod/quiz/report/overview/db/install.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + +
+
+
\ No newline at end of file diff --git a/mod/quiz/report/overview/db/upgrade.php b/mod/quiz/report/overview/db/upgrade.php new file mode 100644 index 0000000000..bee8b66ce1 --- /dev/null +++ b/mod/quiz/report/overview/db/upgrade.php @@ -0,0 +1,41 @@ +get_manager(); + + $result = true; + +//===== 1.9.0 upgrade line ======// + + if ($result && $oldversion < 2008062700) { + + /// Define table quiz_question_regrade to be created + $table = new xmldb_table('quiz_question_regrade'); + + /// Adding fields to table quiz_question_regrade + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null, null, null); + $table->add_field('questionid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null); + $table->add_field('attemptid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null); + $table->add_field('newgrade', XMLDB_TYPE_NUMBER, '10, 5', null, XMLDB_NOTNULL, null, null, null, null); + $table->add_field('oldgrade', XMLDB_TYPE_NUMBER, '10, 5', null, XMLDB_NOTNULL, null, null, null, null); + $table->add_field('regraded', XMLDB_TYPE_INTEGER, '4', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null); + + + /// Adding keys to table quiz_question_regrade + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + /// Conditionally launch create table for quiz_question_regrade + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + } + + + return $result; +} + +?> diff --git a/mod/quiz/report/overview/overview_table.php b/mod/quiz/report/overview/overview_table.php index 94a264a3c9..68d17253ad 100644 --- a/mod/quiz/report/overview/overview_table.php +++ b/mod/quiz/report/overview/overview_table.php @@ -72,10 +72,8 @@ class quiz_report_overview_table extends table_sql { if (!$this->is_downloading()) { if ($this->candelete) { // Start form - $strreallydel = addslashes_js(get_string('deleteattemptcheck','quiz')); echo '
'; - echo '
'; + echo ''; echo '
'; echo $this->reporturl->hidden_params_out(array(), 0, $this->displayoptions); echo '
'; @@ -87,15 +85,16 @@ class quiz_report_overview_table extends table_sql { if (!$this->is_downloading()) { // Print "Select all" etc. if ($this->candelete) { - echo ''; - echo '
'; + $strreallydel = addslashes_js(get_string('deleteattemptcheck','quiz')); + echo '
'; echo ''. get_string('selectall', 'quiz').' / '; echo ''. get_string('selectnone', 'quiz').' '; echo '  '; - echo ''; - echo '
'; + echo ''; + echo ''; + echo '
'; // Close form echo ''; echo ''; @@ -160,6 +159,22 @@ class quiz_report_overview_table extends table_sql { if ($attempt->timefinish) { $grade = quiz_rescale_grade($attempt->sumgrades, $this->quiz); if (!$this->is_downloading()) { + if (isset($this->regradedqs[$attempt->attemptuniqueid])){ + $newsumgrade = 0; + $oldsumgrade = 0; + foreach ($this->questions as $question){ + if (isset($this->regradedqs[$attempt->attemptuniqueid][$question->id])){ + $newsumgrade += $this->regradedqs[$attempt->attemptuniqueid][$question->id]->newgrade; + $oldsumgrade += $this->regradedqs[$attempt->attemptuniqueid][$question->id]->oldgrade; + } else { + $newsumgrade += $this->gradedstatesbyattempt[$attempt->attemptuniqueid][$question->id]->grade; + $oldsumgrade += $this->gradedstatesbyattempt[$attempt->attemptuniqueid][$question->id]->grade; + } + } + $newsumgrade = quiz_rescale_grade($newsumgrade, $this->quiz); + $oldsumgrade = quiz_rescale_grade($oldsumgrade, $this->quiz); + $grade = "$oldsumgrade
$newsumgrade"; + } $gradehtml = ''.$grade.''; if ($this->qmsubselect && $attempt->gradedattempt){ $gradehtml = '
'.$gradehtml.'
'; @@ -173,39 +188,35 @@ class quiz_report_overview_table extends table_sql { } } function other_cols($colname, $attempt){ - static $gradedstatesbyattempt = null; - if ($gradedstatesbyattempt === null){ - //get all the attempt ids we want to display on this page - //or to export for download. - if (!$this->is_downloading()) { - $attemptids = array(); - foreach ($this->rawdata as $attempt){ - if ($attempt->attemptuniqueid > 0){ - $attemptids[] = $attempt->attemptuniqueid; - } - } - $gradedstatesbyattempt = quiz_get_newgraded_states($attemptids, true, 'qs.id, qs.grade, qs.event, qs.question, qs.attempt'); - } else { - $gradedstatesbyattempt = quiz_get_newgraded_states($this->sql, true, 'qs.id, qs.grade, qs.event, qs.question, qs.attempt'); - } - } + if (preg_match('/^qsgrade([0-9]+)$/', $colname, $matches)){ $questionid = $matches[1]; $question = $this->questions[$questionid]; - $stateforqinattempt = $gradedstatesbyattempt[$attempt->attemptuniqueid][$questionid]; - if (question_state_is_graded($stateforqinattempt)) { - $grade = quiz_rescale_grade($stateforqinattempt->grade, $this->quiz); + if (isset($this->gradedstatesbyattempt[$attempt->attemptuniqueid][$questionid])){ + $stateforqinattempt = $this->gradedstatesbyattempt[$attempt->attemptuniqueid][$questionid]; } else { - $grade = '--'; + $stateforqinattempt = false; } - if (!$this->is_downloading()) { - $grade = $grade.'/'.quiz_rescale_grade($question->grade, $this->quiz); - return link_to_popup_window('/mod/quiz/reviewquestion.php?state='. - $stateforqinattempt->id.'&number='.$question->number, - 'reviewquestion', $grade, 450, 650, get_string('reviewresponse', 'quiz'), - 'none', true); + if ($stateforqinattempt && question_state_is_graded($stateforqinattempt)) { + $grade = quiz_rescale_grade($stateforqinattempt->grade, $this->quiz); + if (!$this->is_downloading()) { + if (isset($this->regradedqs[$attempt->attemptuniqueid][$questionid])){ + $gradefromdb = $grade; + $newgrade = quiz_rescale_grade($this->regradedqs[$attempt->attemptuniqueid][$questionid]->newgrade, $this->quiz); + $oldgrade = quiz_rescale_grade($this->regradedqs[$attempt->attemptuniqueid][$questionid]->oldgrade, $this->quiz); + + $grade = ''.$oldgrade.'
'. + $newgrade; + } + return link_to_popup_window('/mod/quiz/reviewquestion.php?state='. + $stateforqinattempt->id.'&number='.$question->number, + 'reviewquestion', $grade, 450, 650, get_string('reviewresponse', 'quiz'), + 'none', true); + } else { + return $grade; + } } else { - return $grade; + return '--'; } } else { return NULL; @@ -224,7 +235,15 @@ class quiz_report_overview_table extends table_sql { } } - + function col_regraded($attempt){ + if ($attempt->regraded == '') { + return ''; + } else if ($attempt->regraded == 0) { + return get_string('needed', 'quiz_overview'); + } else if ($attempt->regraded == 1) { + return get_string('done', 'quiz_overview'); + } + } function query_db($pagesize, $useinitialsbar=true){ // Add table joins so we can sort by question grade // unfortunately can't join all tables necessary to fetch all grades @@ -250,6 +269,23 @@ class quiz_report_overview_table extends table_sql { } } parent::query_db($pagesize, $useinitialsbar); + if ($this->detailedmarks){ + //get all the attempt ids we want to display on this page + //or to export for download. + if (!$this->is_downloading()) { + $attemptids = array(); + foreach ($this->rawdata as $attempt){ + if ($attempt->attemptuniqueid > 0){ + $attemptids[] = $attempt->attemptuniqueid; + } + } + $this->gradedstatesbyattempt = quiz_get_newgraded_states($attemptids, true, 'qs.id, qs.grade, qs.event, qs.question, qs.attempt'); + $this->regradedqs = quiz_get_regraded_qs($attemptids); + } else { + $this->gradedstatesbyattempt = quiz_get_newgraded_states($this->sql, true, 'qs.id, qs.grade, qs.event, qs.question, qs.attempt'); + $this->regradedqs = quiz_get_regraded_qs($this->sql); + } + } } } ?> diff --git a/mod/quiz/report/overview/overviewsettings_form.php b/mod/quiz/report/overview/overviewsettings_form.php index e250e0aa67..55e97198f5 100644 --- a/mod/quiz/report/overview/overviewsettings_form.php +++ b/mod/quiz/report/overview/overviewsettings_form.php @@ -27,10 +27,15 @@ class mod_quiz_report_overview_settings extends moodleform { $options[QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO] = get_string('optnoattemptsonly', 'quiz_overview', $studentsstring); } $mform->addElement('select', 'attemptsmode', get_string('show', 'quiz_overview'), $options); + + $showattemptsgrp = array(); if ($this->_customdata['qmsubselect']){ $gm = ''.quiz_get_grading_option_name($this->_customdata['quiz']->grademethod).''; - $mform->addElement('advcheckbox', 'qmfilter', get_string('show', 'quiz_overview'), get_string('optonlygradedattempts', 'quiz_overview', $gm), null, array(0,1)); + $showattemptsgrp[] =& $mform->createElement('advcheckbox', 'qmfilter', get_string('showattempts', 'quiz_overview'), get_string('optonlygradedattempts', 'quiz_overview', $gm), null, array(0,1)); } + + $showattemptsgrp[] =& $mform->createElement('advcheckbox', 'regradefilter', get_string('showattempts', 'quiz_overview'), get_string('optonlyregradedattempts', 'quiz_overview'), null, array(0,1)); + $mform->addGroup($showattemptsgrp, null, get_string('showattempts', 'quiz_overview'), '
', false); //------------------------------------------------------------------------------- $mform->addElement('header', 'preferencesuser', get_string('preferencesuser', 'quiz_overview')); diff --git a/mod/quiz/report/overview/report.php b/mod/quiz/report/overview/report.php index fad12b6e3d..972a82f122 100644 --- a/mod/quiz/report/overview/report.php +++ b/mod/quiz/report/overview/report.php @@ -6,7 +6,7 @@ * @author Martin Dougiamas, Tim Hunt and others. * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package quiz - *//** */ + */ require_once($CFG->libdir.'/tablelib.php'); require_once($CFG->dirroot.'/mod/quiz/report/overview/overviewsettings_form.php'); @@ -20,30 +20,57 @@ class quiz_overview_report extends quiz_default_report { function display($quiz, $cm, $course) { global $CFG, $COURSE, $DB; - $context = get_context_instance(CONTEXT_MODULE, $cm->id); + $this->context = get_context_instance(CONTEXT_MODULE, $cm->id); // Work out some display options - whether there is feedback, and whether scores should be shown. $hasfeedback = quiz_has_feedback($quiz->id) && $quiz->grade > 1.e-7 && $quiz->sumgrades > 1.e-7; $fakeattempt = new stdClass(); $fakeattempt->preview = false; $fakeattempt->timefinish = $quiz->timeopen; - $reviewoptions = quiz_get_reviewoptions($quiz, $fakeattempt, $context); + $reviewoptions = quiz_get_reviewoptions($quiz, $fakeattempt, $this->context); $showgrades = $quiz->grade && $quiz->sumgrades && $reviewoptions->scores; $download = optional_param('download', '', PARAM_ALPHA); + + /// find out current groups mode + $currentgroup = groups_get_activity_group($cm, true); + if (!$students = get_users_by_capability($this->context, 'mod/quiz:attempt','','','','','','',false)){ + $students = array(); + } else { + $students = array_keys($students); + } - if($attemptids = optional_param('attemptid', array(), PARAM_INT)) { - //attempts need to be deleted - require_capability('mod/quiz:deleteattempts', $context); - $attemptids = optional_param('attemptid', array(), PARAM_INT); - foreach($attemptids as $attemptid) { - add_to_log($course->id, 'quiz', 'delete attempt', 'report.php?id=' . $cm->id, - $attemptid, $cm->id); - quiz_delete_attempt($attemptid, $quiz); + if (empty($currentgroup)) { + // all users who can attempt quizzes + $allowed = $students; + $groupstudents = array(); + } else { + // all users who can attempt quizzes and who are in the currently selected group + if (!$groupstudents = get_users_by_capability($this->context, 'mod/quiz:attempt','','','','',$currentgroup,'',false)){ + $groupstudents = array(); + } else { + $groupstudents = array_keys($groupstudents); + } + $allowed = $groupstudents; + } + + if (empty($currentgroup)||$groupstudents) { + if (optional_param('delete', 0, PARAM_BOOL)){ + if($attemptids = optional_param('attemptid', array(), PARAM_INT)) { + //attempts need to be deleted + $this->delete_selected_attempts($quiz, $cm, $attemptids, $groupstudents); + //No need for a redirect, any attemptids that do not exist are ignored. + //So no problem if the user refreshes and tries to delete the same attempts + //twice. + } + } else if (optional_param('regrade', 0, PARAM_BOOL)){ + if($attemptids = optional_param('attemptid', array(), PARAM_INT)) { + $this->regrade_selected_attempts($quiz, $attemptids, $groupstudents); + //No need for a redirect, any attemptids that do not exist are ignored. + //So no problem if the user refreshes and tries to delete the same attempts + //twice. + } } - //No need for a redirect, any attemptids that do not exist are ignored. - //So no problem if the user refreshes and tries to delete the same attempts - //twice. } @@ -56,12 +83,11 @@ class quiz_overview_report extends quiz_default_report { $qmsubselect = quiz_report_qm_filter_select($quiz); - - /// find out current groups mode - $currentgroup = groups_get_activity_group($cm, true); - $mform = new mod_quiz_report_overview_settings($reporturl, array('qmsubselect'=> $qmsubselect, 'quiz'=>$quiz, 'currentgroup'=>$currentgroup)); if ($fromform = $mform->get_data()){ + $regradeall = false; + $regradealldry = false; + $regradealldrydo = false; $attemptsmode = $fromform->attemptsmode; if ($qmsubselect){ //control is not on the form if @@ -71,13 +97,22 @@ class quiz_overview_report extends quiz_default_report { } else { $qmfilter = 0; } + $regradefilter = $fromform->regradefilter; set_user_preference('quiz_report_overview_detailedmarks', $fromform->detailedmarks); set_user_preference('quiz_report_pagesize', $fromform->pagesize); $detailedmarks = $fromform->detailedmarks; $pagesize = $fromform->pagesize; } else { - $qmfilter = optional_param('qmfilter', 0, PARAM_INT); + $regradeall = optional_param('regradeall', 0, PARAM_BOOL); + $regradealldry = optional_param('regradealldry', 0, PARAM_BOOL); + $regradealldrydo = optional_param('regradealldrydo', 0, PARAM_BOOL); $attemptsmode = optional_param('attemptsmode', null, PARAM_INT); + if ($qmsubselect){ + $qmfilter = optional_param('qmfilter', 0, PARAM_INT); + } else { + $qmfilter = 0; + } + $regradefilter = optional_param('regradefilter', 0, PARAM_INT); if ($attemptsmode === null){ //default $attemptsmode = QUIZ_REPORT_ATTEMPTS_ALL; @@ -101,35 +136,16 @@ class quiz_overview_report extends quiz_default_report { } // We only want to show the checkbox to delete attempts // if the user has permissions and if the report mode is showing attempts. - $candelete = has_capability('mod/quiz:deleteattempts', $context) + $candelete = has_capability('mod/quiz:deleteattempts', $this->context) && ($attemptsmode!= QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO); $displayoptions = array(); $displayoptions['attemptsmode'] = $attemptsmode; $displayoptions['qmfilter'] = $qmfilter; + $displayoptions['regradefilter'] = $regradefilter; //work out the sql for this table. - if (!$students = get_users_by_capability($context, 'mod/quiz:attempt','','','','','','',false)){ - $students = array(); - } else { - $students = array_keys($students); - } - - if (empty($currentgroup)) { - // all users who can attempt quizzes - $allowed = $students; - $groupstudents = array(); - } else { - // all users who can attempt quizzes and who are in the currently selected group - if (!$groupstudents = get_users_by_capability($context, 'mod/quiz:attempt','','','','',$currentgroup,'',false)){ - $groupstudents = array(); - } else { - $groupstudents = array_keys($groupstudents); - } - $allowed = $groupstudents; - } - if ($detailedmarks) { $questions = quiz_report_load_questions($quiz); } else { @@ -143,12 +159,25 @@ class quiz_overview_report extends quiz_default_report { // Only print headers if not asked to download data $this->print_header_and_tabs($cm, $course, $quiz, "overview"); } - + + if ($regradeall){ + $this->regrade_all(false, $quiz, $groupstudents); + } else if ($regradealldry){ + $this->regrade_all(true, $quiz, $groupstudents); + } else if ($regradealldrydo){ + $this->regrade_all_needed($quiz, $groupstudents); + } + if ($regradeall || $regradealldry || $regradealldrydo){ + redirect($reporturl->out(false, $displayoptions), '', 5); + } + if ($groupmode = groups_get_activity_groupmode($cm)) { // Groups are being used if (!$table->is_downloading()) { groups_print_activity_menu($cm, $reporturl->out(false, $displayoptions)); } } + + // Print information on the number of existing attempts if (!$table->is_downloading()) { //do not print notices when downloading if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm, true, $currentgroup)) { @@ -168,6 +197,36 @@ class quiz_overview_report extends quiz_default_report { $mform->set_data($displayoptions +compact('detailedmarks', 'pagesize')); $mform->display(); } + + $countregradeneeded = $this->count_regrade_all_needed($quiz, $groupstudents); + //regrade buttons + if ($currentgroup){ + $a= new object(); + $a->groupname = groups_get_group_name($currentgroup); + $a->coursestudents = $COURSE->students; + $a->countregradeneeded = $countregradeneeded; + $regradealldrydolabel = get_string('regradealldrydogroup', 'quiz_overview', $a); + $regradealldrylabel = get_string('regradealldrygroup', 'quiz_overview', $a); + $regradealllabel = get_string('regradeallgroup', 'quiz_overview', $a); + } else { + $regradealldrydolabel = get_string('regradealldrydo', 'quiz_overview', $countregradeneeded); + $regradealldrylabel = get_string('regradealldry', 'quiz_overview'); + $regradealllabel = get_string('regradeall', 'quiz_overview'); + } + + + echo '
'; + echo '
'; + echo '
'; + echo $reporturl->hidden_params_out(array(), 0, $displayoptions); + echo ''; + echo ''; + if ($countregradeneeded){ + echo ''; + } + echo '
'; + echo '
'; + echo '
'; if (!$nostudents || ($attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL)){ // Print information on the grading method and whether we are displaying @@ -188,7 +247,7 @@ class quiz_overview_report extends quiz_default_report { ($qmsubselect?"($qmsubselect) AS gradedattempt, ":''). 'qa.uniqueid AS attemptuniqueid, qa.id AS attempt, u.id AS userid, u.idnumber, u.firstname, u.lastname, u.picture, '. 'qa.sumgrades, qa.timefinish, qa.timestart, qa.timefinish - qa.timestart AS duration '; - + // This part is the same for all cases - join users and quiz_attempts tables $from = '{user} u '; $from .= 'LEFT JOIN {quiz_attempts} qa ON qa.userid = u.id AND qa.quiz = :quizid'; @@ -197,7 +256,7 @@ class quiz_overview_report extends quiz_default_report { if ($qmsubselect && $qmfilter){ $from .= ' AND '.$qmsubselect; } - switch ($attemptsmode){ + switch ($attemptsmode){ case QUIZ_REPORT_ATTEMPTS_ALL: // Show all attempts, including students who are no longer in the course $where = 'qa.id IS NOT NULL AND qa.preview = 0'; @@ -223,9 +282,21 @@ class quiz_overview_report extends quiz_default_report { } $table->set_count_sql("SELECT COUNT(1) FROM $from WHERE $where", $params); - - - + + $sqlobject = new object; + $sqlobject->from = $from; + $sqlobject->where = $where; + $sqlobject->params = $params; + //test to see if there are any regraded attempts to be listed. + if (quiz_get_regraded_qs($sqlobject, 0, 1)){ + $regradedattempts = true; + } else { + $regradedattempts = false; + } + $fields .= ', COALESCE((SELECT MAX(qqr.regraded) FROM {quiz_question_regrade} qqr WHERE qqr.attemptid = qa.uniqueid),-1) AS regraded'; + if ($regradefilter){ + $where .= ' AND COALESCE((SELECT MAX(qqr.regraded) FROM {quiz_question_regrade} qqr WHERE qqr.attemptid = qa.uniqueid),-1) !=\'-1\''; + } $table->set_sql($fields, $from, $where, $params); // Define table columns @@ -270,10 +341,20 @@ class quiz_overview_report extends quiz_default_report { foreach ($questions as $id => $question) { // Ignore questions of zero length $columns[] = 'qsgrade'.$id; - $headers[] = '#'.$question->number; + $header = '#'.$question->number; + if (!$table->is_downloading()) { + $header .='
'; + } else { + $header .=' '; + } + $header .='--/'.quiz_rescale_grade($question->grade, $quiz); + $headers[] = $header; } } - + if ($regradedattempts){ + $columns[] = 'regraded'; + $headers[] = get_string('regrade', 'quiz_overview'); + } if ($showgrades) { $columns[] = 'sumgrades'; $headers[] = get_string('grade', 'quiz').'/'.$quiz->grade; @@ -291,7 +372,7 @@ class quiz_overview_report extends quiz_default_report { // Set up the table $table->define_baseurl($reporturl->out(false, $displayoptions)); - $table->collapsible(true); + $table->collapsible(false); $table->column_suppress('picture'); $table->column_suppress('fullname'); @@ -319,6 +400,255 @@ class quiz_overview_report extends quiz_default_report { } return true; } + /** + * @param bool changedb whether to change contents of state and grades + * tables. + */ + function regrade_all($dry, $quiz, $groupstudents){ + global $DB; + if (!has_capability('mod/quiz:grade', $this->context)) { + notify(get_string('regradenotallowed', 'quiz')); + return true; + } + // Fetch all attempts + if ($groupstudents){ + list($usql, $params) = $DB->get_in_or_equal($groupstudents); + $select = "userid $usql AND "; + } else { + $select = ''; + $params = array(); + } + $select .= "quiz = ? AND preview = 0"; + $params[] = $quiz->id; + if (!$attempts = $DB->get_records_select('quiz_attempts', $select, $params)) { + print_heading(get_string('noattempts', 'quiz')); + return true; + } + + $this->clear_regrade_table($quiz, $groupstudents); + + // Fetch all questions + $questions = question_load_questions(quiz_questions_in_quiz($quiz->questions), 'qqi.grade AS maxgrade, qqi.id AS instance', + '{quiz_question_instances} qqi ON qqi.quiz = ' . $quiz->id . ' AND q.id = qqi.question'); + + // Print heading + print_heading(get_string('regradingquiz', 'quiz', format_string($quiz->name))); + $qstodo = count($questions); + $qsdone = 0; + if ($qstodo > 1){ + $qpb = new progress_bar('qregradingbar', 500, true); + $qpb->update($qsdone, $qstodo, "Question $qsdone of $qstodo"); + } + $apb = new progress_bar('aregradingbar', 500, true); + + // Loop through all questions and all attempts and regrade while printing progress info + $attemptstodo = count($attempts); + foreach ($questions as $question) { + $attemptsdone = 0; + $apb->restart(); + echo '

'.get_string('regradingquestion', 'quiz', $question->name).'

'; + @flush();@ob_flush(); + foreach ($attempts as $attempt) { + set_time_limit(30); + $changed = regrade_question_in_attempt($question, $attempt, $quiz, true, $dry); + + $attemptsdone++; + $a = new object(); + $a->done = $attemptsdone; + $a->todo = $attemptstodo; + $apb->update($attemptsdone, $attemptstodo, get_string('attemptprogress', 'quiz_overview', $a)); + } + $qsdone++; + if (isset($qpb)){ + $a = new object(); + $a->done = $qsdone; + $a->todo = $qstodo; + $qpb->update($qsdone, $qstodo, get_string('qprogress', 'quiz_overview', $a)); + } + // the following makes sure that the output is sent immediately. + @flush();@ob_flush(); + } + + if (!$dry){ + $this->check_overall_grades($quiz, $groupstudents); + } + } + function count_regrade_all_needed($quiz, $groupstudents){ + global $DB; + // Fetch all attempts that need regrading + if ($groupstudents){ + list($usql, $params) = $DB->get_in_or_equal($groupstudents); + $where = "qa.userid $usql AND "; + } else { + $where = ''; + $params = array(); + } + $where .= "qa.quiz = ? AND qa.preview = 0 AND qa.uniqueid = qqr.attemptid AND qqr.regraded = 0"; + $params[] = $quiz->id; + return $DB->get_field_sql('SELECT COUNT(1) FROM {quiz_attempts} qa, {quiz_question_regrade} qqr WHERE '. $where, $params); + } + function regrade_all_needed($quiz, $groupstudents){ + global $DB; + if (!has_capability('mod/quiz:grade', $this->context)) { + notify(get_string('regradenotallowed', 'quiz')); + return; + } + // Fetch all attempts that need regrading + if ($groupstudents){ + list($usql, $params) = $DB->get_in_or_equal($groupstudents); + $where = "qa.userid $usql AND "; + } else { + $where = ''; + $params = array(); + } + $where .= "qa.quiz = ? AND qa.preview = 0 AND qa.uniqueid = qqr.attemptid AND qqr.regraded = 0"; + $params[] = $quiz->id; + if (!$attempts = $DB->get_records_sql('SELECT qa.*, qqr.questionid FROM {quiz_attempts} qa, {quiz_question_regrade} qqr WHERE '. $where, $params)) { + print_heading(get_string('noattemptstoregrade', 'quiz_overview')); + return true; + } + $this->clear_regrade_table($quiz, $groupstudents); + // Fetch all questions + $questions = question_load_questions(quiz_questions_in_quiz($quiz->questions), 'qqi.grade AS maxgrade, qqi.id AS instance', + '{quiz_question_instances} qqi ON qqi.quiz = ' . $quiz->id . ' AND q.id = qqi.question'); + + // Print heading + print_heading(get_string('regradingquiz', 'quiz', format_string($quiz->name))); + + $apb = new progress_bar('aregradingbar', 500, true); + + // Loop through all questions and all attempts and regrade while printing progress info + $attemptstodo = count($attempts); + $attemptsdone = 0; + @flush();@ob_flush(); + $attemptschanged = array(); + foreach ($attempts as $attempt) { + $question = $questions[$attempt->questionid]; + $changed = regrade_question_in_attempt($question, $attempt, $quiz, true); + if ($changed){ + $attemptschanged[] = $attempt->uniqueid; + $usersschanged[] = $attempt->userid; + } + if (!empty($apb)){ + $attemptsdone++; + $a = new object(); + $a->done = $attemptsdone; + $a->todo = $attemptstodo; + $apb->update($attemptsdone, $attemptstodo, get_string('attemptprogress', 'quiz_overview', $a)); + } + } + $this->check_overall_grades($quiz, array(), $attemptschanged); + } + + function clear_regrade_table($quiz, $groupstudents){ + global $DB; + // Fetch all attempts that need regrading + if ($groupstudents){ + list($usql, $params) = $DB->get_in_or_equal($groupstudents); + $where = "qa.userid $usql AND "; + } else { + $usql = ''; + $where = ''; + $params = array(); + } + $params[] = $quiz->id; + $delsql = 'DELETE FROM qqr USING {quiz_question_regrade} qqr, {quiz_attempts} qa WHERE qqr.attemptid = qa.uniqueid AND '; + if ($usql){ + $delsql .= "qa.userid $usql AND "; + } + $delsql .='qa.quiz=?'; + if (!$DB->execute($delsql, $params)){ + print_error('err_failedtodeleteregrades', 'quiz_overview'); + } + } + + function check_overall_grades($quiz, $userids=array(), $attemptids=array()){ + global $DB; + //recalculate $attempt->sumgrade + //already updated in regrade_question_in_attempt + $sql = "UPDATE {quiz_attempts} qa SET qa.sumgrades= " . + "(SELECT SUM(qs.grade) FROM {question_sessions} qns, {question_states} qs " . + "WHERE qns.newgraded = qs.id AND qns.attemptid = qa.uniqueid ) WHERE "; + $attemptsql=''; + if (!$attemptids){ + if ($userids){ + list($usql, $params) = $DB->get_in_or_equal($userids); + $attemptsql .= "qa.userid $usql AND "; + } else { + $params = array(); + } + $attemptsql .= "qa.quiz =? AND preview = 0"; + $params[] = $quiz->id; + } else { + list($asql, $params) = $DB->get_in_or_equal($attemptids); + $attemptsql .= "qa.uniqueid $asql"; + } + $sql .= $attemptsql; + if (!$DB->execute($sql, $params)){ + print_error('err_failedtorecalculateattemptgrades', 'quiz_overview'); + } + + // Update the overall quiz grades + if ($attemptids){ + //make sure we fetch all attempts for users to calculate grade. + //not just those that have changed. + $sql = "SELECT qa2.* FROM {quiz_attempts} qa2 WHERE qa2.userid IN (SELECT DISTINCT qa.userid FROM {quiz_attempts} qa WHERE $attemptsql)"; + } else { + $sql = "SELECT qa.* FROM {quiz_attempts} qa WHERE $attemptsql"; + } + if ($attempts = $DB->get_records_sql($sql, $params)) { + $attemptsbyuser = quiz_report_index_by_keys($attempts, array('userid', 'id')); + foreach($attemptsbyuser as $userid => $attemptsforuser) { + quiz_save_best_grade($quiz, $userid, $attemptsforuser); + } + } + } + function delete_selected_attempts($quiz, $cm, $attemptids, $groupstudents){ + global $DB, $COURSE; + require_capability('mod/quiz:deleteattempts', $this->context); + $attemptids = optional_param('attemptid', array(), PARAM_INT); + if ($groupstudents){ + list($usql, $params) = $DB->get_in_or_equal($groupstudents); + $where = "qa.userid $usql AND "; + } + foreach($attemptids as $attemptid) { + add_to_log($COURSE->id, 'quiz', 'delete attempt', 'report.php?id=' . $cm->id, + $attemptid, $cm->id); + quiz_delete_attempt($attemptid, $quiz); + } + } + function regrade_selected_attempts($quiz, $attemptids, $groupstudents){ + global $DB; + require_capability('mod/quiz:grade', $this->context); + if ($groupstudents){ + list($usql, $params) = $DB->get_in_or_equal($groupstudents); + $where = "qa.userid $usql AND "; + } else { + $params = array(); + $where = ''; + } + list($asql, $aparams) = $DB->get_in_or_equal($attemptids); + $where = "qa.id $asql AND "; + $params = array_merge($params, $aparams); + + $where .= "qa.quiz = ? AND qa.preview = 0"; + $params[] = $quiz->id; + if (!$attempts = $DB->get_records_sql('SELECT qa.* FROM {quiz_attempts} qa WHERE '. $where, $params)) { + print_error('noattemptstoregrade', 'quiz_overview'); + } + + // Fetch all questions + $questions = question_load_questions(quiz_questions_in_quiz($quiz->questions), 'qqi.grade AS maxgrade, qqi.id AS instance', + '{quiz_question_instances} qqi ON qqi.quiz = ' . $quiz->id . ' AND q.id = qqi.question'); + $updateoverallgrades = array(); + foreach($attempts as $attempt) { + foreach ($questions as $question){ + $changed = regrade_question_in_attempt($question, $attempt, $quiz, true); + } + $updateoverallgrades[] = $attempt->uniqueid; + } + $this->check_overall_grades($quiz, array(), $updateoverallgrades); + } } diff --git a/mod/quiz/report/overview/version.php b/mod/quiz/report/overview/version.php new file mode 100644 index 0000000000..a69690d619 --- /dev/null +++ b/mod/quiz/report/overview/version.php @@ -0,0 +1,10 @@ +version = 2008062700; // The (date) version of this module + +?> diff --git a/mod/quiz/report/reportlib.php b/mod/quiz/report/reportlib.php index 4a61787931..a65353ea9c 100644 --- a/mod/quiz/report/reportlib.php +++ b/mod/quiz/report/reportlib.php @@ -39,19 +39,52 @@ function quiz_get_newgraded_states($attemptidssql, $idxattemptq = true, $fields= return array(); } if ($idxattemptq){ - $gradedstatesbyattempt = array(); - foreach ($gradedstates as $gradedstate){ - if (!isset($gradedstatesbyattempt[$gradedstate->attempt])){ - $gradedstatesbyattempt[$gradedstate->attempt] = array(); - } - $gradedstatesbyattempt[$gradedstate->attempt][$gradedstate->question] = $gradedstate; - } - return $gradedstatesbyattempt; + return quiz_report_index_by_keys($gradedstates, array('attempt', 'question')); } else { return $gradedstates; } } +function quiz_report_index_by_keys($datum, $keys){ + if (!$datum){ + return $datum; + } + $key = array_shift($keys); + $datumkeyed = array(); + foreach ($datum as $data){ + if ($keys){ + $datumkeyed[$data->{$key}][]= $data; + } else { + $datumkeyed[$data->{$key}]= $data; + } + } + if ($keys){ + foreach ($datumkeyed as $datakey => $datakeyed){ + $datumkeyed[$datakey] = quiz_report_index_by_keys($datakeyed, $keys); + } + } + return $datumkeyed; +} +function quiz_get_regraded_qs($attemptidssql, $limitfrom=0, $limitnum=0){ + global $CFG, $DB; + if ($attemptidssql && is_array($attemptidssql)){ + list($asql, $params) = $DB->get_in_or_equal($attemptidssql); + $regradedqsql = "SELECT qqr.* FROM " . + "{quiz_question_regrade} qqr " . + "WHERE qqr.attemptid $asql"; + $regradedqs = $DB->get_records_sql($regradedqsql, $params, $limitfrom, $limitnum); + } else if ($attemptidssql && is_object($attemptidssql)){ + $regradedqsql = "SELECT qqr.* FROM " . + $attemptidssql->from.", ". + "{quiz_question_regrade} qqr " . + "WHERE qqr.attemptid = qa.uniqueid AND " . + $attemptidssql->where; + $regradedqs = $DB->get_records_sql($regradedqsql, $attemptidssql->params, $limitfrom, $limitnum); + } else { + return array(); + } + return quiz_report_index_by_keys($regradedqs, array('attemptid', 'questionid')); +} function quiz_get_average_grade_for_questions($quiz, $userids){ global $CFG, $DB; $qmfilter = quiz_report_qm_filter_select($quiz); diff --git a/theme/standard/styles_layout.css b/theme/standard/styles_layout.css index b2d1e898c7..6741f44198 100644 --- a/theme/standard/styles_layout.css +++ b/theme/standard/styles_layout.css @@ -3918,7 +3918,7 @@ table.quizreviewsummary td.cell { } #mod-quiz-report table#attempts, -#mod-quiz-report table#commands, +#mod-quiz-report div#commands, #mod-quiz-report table#itemanalysis { width: 80%; -- 2.39.5