diff --git a/mod/quiz/report/grading/db/access.php b/mod/quiz/report/grading/db/access.php index 859962fc94d..385e60dea4f 100644 --- a/mod/quiz/report/grading/db/access.php +++ b/mod/quiz/report/grading/db/access.php @@ -18,14 +18,15 @@ /** * Capability definitions for the quiz manual grading report. * - * @package quiz_grading + * @package quiz + * @subpackage grading * @copyright 2010 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$quizreport_grading_capabilities = array( +$capabilities = array( // Is the user allowed to see the student's real names while grading? - 'quizreport/grading:viewstudentnames' => array( + 'quiz/grading:viewstudentnames' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'legacy' => array( @@ -36,7 +37,7 @@ $quizreport_grading_capabilities = array( ), // Is the user allowed to see the student's idnumber while grading? - 'quizreport/grading:viewidnumber' => array( + 'quiz/grading:viewidnumber' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'legacy' => array( diff --git a/mod/quiz/report/grading/report.php b/mod/quiz/report/grading/report.php index 26ea18c1735..59c5f4d5365 100644 --- a/mod/quiz/report/grading/report.php +++ b/mod/quiz/report/grading/report.php @@ -50,7 +50,7 @@ class quiz_grading_report extends quiz_default_report { protected $context; function display($quiz, $cm, $course) { - global $CFG, $DB, $OUTPUT, $PAGE; + global $CFG, $DB, $PAGE; $this->quiz = $quiz; $this->cm = $cm; @@ -86,8 +86,8 @@ class quiz_grading_report extends quiz_default_report { // Check permissions $this->context = get_context_instance(CONTEXT_MODULE, $cm->id); require_capability('mod/quiz:grade', $this->context); - $shownames = has_capability('quizreport/grading:viewstudentnames', $this->context); - $showidnumbers = has_capability('quizreport/grading:viewidnumber', $this->context); + $shownames = has_capability('quiz/grading:viewstudentnames', $this->context); + $showidnumbers = has_capability('quiz/grading:viewidnumber', $this->context); // Validate order. if (!in_array($order, array('random', 'date', 'student', 'idnumber'))) { @@ -134,351 +134,415 @@ class quiz_grading_report extends quiz_default_report { return true; } - protected function get_attempts_query() { + protected function get_qubaids_condition() { global $DB; - $from = "FROM {$CFG->prefix}quiz_attempts quiza"; - $where = "quiza.quiz = {$this->cm->instance} AND quiza.preview = 0 AND quiza.timefinish <> 0"; + $where = "quiza.quiz = :mangrquizid AND + quiza.preview = 0 AND + quiza.timefinish <> 0"; + $params = array('mangrquizid' => $this->cm->instance); if ($this->currentgroup) { - $where .= ' AND quiza.userid IN (' . implode(',', array_keys($this->users)) . ')'; + list($usql, $uparam) = $DB->get_in_or_equal(array_keys($this->users), + SQL_PARAMS_NAMED, 'mangru00000'); + $where .= ' AND quiza.userid ' . $usql; + $params += $uparam; } - $sql = new stdClass; - $sql->from = $from; - $sql->where = $where; - $sql->usageidcolumn = 'quiza.uniqueid'; - - return $sql; + return new qubaid_join('{quiz_attempts} quiza', 'quiza.uniqueid', $where, $params); } - function temp() { // TODO - { - { - if ($allok) { - echo $OUTPUT->notification(get_string('changessaved', 'quiz'), 'notifysuccess'); - } else { - echo $OUTPUT->notification(get_string('changessavedwitherrors', 'quiz'), 'notifysuccess'); + protected function load_attempts_by_usage_ids($qubaids) { + global $DB; + + list($asql, $params) = $DB->get_in_or_equal($qubaids); + $params[] = $this->quiz->id; + + $attemptsbyid = $DB->get_records_sql(" + SELECT quiza.*, u.firstname, u.lastname, u.idnumber + FROM {quiz_attempts} quiza + JOIN {user} u ON u.id = quiza.userid + WHERE quiza.uniqueid $asql AND quiza.timefinish <> 0 AND quiza.quiz = ?", + $params); + + $attempts = array(); + foreach ($attemptsbyid as $attempt) { + $attempts[$attempt->uniqueid] = $attempt; + } + return $attempts; + } + + /** + * Get the URL of the front page of the report that lists all the questions. + * @param $includeauto if not given, use the current setting, otherwise, + * force a paricular value of includeauto in the URL. + * @return string the URL. + */ + protected function base_url() { + return new moodle_url('/mod/quiz/report.php', + array('id' => $this->cm->id, 'mode' => 'grading')); + } + + /** + * Get the URL of the front page of the report that lists all the questions. + * @param $includeauto if not given, use the current setting, otherwise, + * force a paricular value of includeauto in the URL. + * @return string the URL. + */ + protected function list_questions_url($includeauto = null) { + $url = $this->base_url(); + + $url->params($this->viewoptions); + + if (!is_null($includeauto)) { + $url->param('includeauto', $includeauto); + } + + return $url; + } + + /** + * @param integer $slot + * @param integer $questionid + * @param string $grade + * @param mixed $page = true, link to current page. false = omit page. + * number = link to specific page. + */ + protected function grade_question_url($slot, $questionid, $grade, $page = true) { + $url = $this->base_url(); + $url->params(array('slot' => $slot, 'qid' => $questionid, 'grade' => $grade)); + $url->params($this->viewoptions); + + $options = $this->viewoptions; + if (!$page) { + $url->remove_params('page'); + } else if (is_integer($page)) { + $url->param('page', $page); + } + + return $url; + } + + protected function format_count_for_table($counts, $type, $gradestring) { + $result = $counts->$type; + if ($counts->$type > 0) { + $result .= ' ' . html_writer::link($this->grade_question_url( + $counts->slot, $counts->questionid, $type), + get_string($gradestring, 'quiz_grading'), + array('class' => 'gradetheselink')); + } + return $result; + } + + protected function display_index($includeauto) { + global $OUTPUT; + + if ($groupmode = groups_get_activity_groupmode($this->cm)) { + // Groups are being used + groups_print_activity_menu($this->cm, $this->list_questions_url()); + } + + echo $OUTPUT->heading(get_string('questionsthatneedgrading', 'quiz_grading')); + if ($includeauto) { + $linktext = get_string('hideautomaticallygraded', 'quiz_grading'); + } else { + $linktext = get_string('alsoshowautomaticallygraded', 'quiz_grading'); + } + echo html_writer::tag('p', html_writer::link($this->list_questions_url(!$includeauto), + $linktext), array('class' => 'toggleincludeauto')); + + $statecounts = $this->get_question_state_summary(array_keys($this->questions)); + + $data = array(); + foreach ($statecounts as $counts) { + if ($counts->all == 0) { + continue; + } + if (!$includeauto && $counts->needsgrading == 0 && $counts->manuallygraded == 0) { + continue; + } + + $row = array(); + + $row[] = $this->questions[$counts->slot]->number; + + $row[] = format_string($counts->name); + + $row[] = $this->format_count_for_table($counts, 'needsgrading', 'grade'); + + $row[] = $this->format_count_for_table($counts, 'manuallygraded', 'updategrade'); + + if ($includeauto) { + $row[] = $this->format_count_for_table($counts, 'autograded', 'updategrade'); + } + + $row[] = $this->format_count_for_table($counts, 'all', 'gradeall'); + + $data[] = $row; + } + + if (empty($data)) { + echo $OUTPUT->heading(get_string('noquestionsfound', 'quiz_grading')); + return; + } + + $table = new html_table(); + $table->class = 'generaltable'; + $table->id = 'questionstograde'; + + $table->head[] = get_string('qno', 'quiz_grading'); + $table->head[] = get_string('questionname', 'quiz_grading'); + $table->head[] = get_string('tograde', 'quiz_grading'); + $table->head[] = get_string('alreadygraded', 'quiz_grading'); + if ($includeauto) { + $table->head[] = get_string('automaticallygraded', 'quiz_grading'); + } + $table->head[] = get_string('total', 'quiz_grading'); + + $table->data = $data; + echo html_writer::table($table); + } + + protected function display_grading_interface($slot, $questionid, $grade, + $pagesize, $page, $shownames, $showidnumbers, $order) { + global $OUTPUT; + + // Make sure there is something to do. + $statecounts = $this->get_question_state_summary(array($slot)); + + $counts = null; + foreach ($statecounts as $record) { + if ($record->questionid == $questionid) { + $counts = $record; + break; + } + } + + // If not, redirect back to the list. + if (!$counts || $counts->$grade == 0) { + redirect($this->list_questions_url(), get_string('alldoneredirecting', 'quiz_grading')); + } + + if ($pagesize * $page >= $counts->$grade) { + $page = 0; + } + + list($qubaids, $count) = $this->get_usage_ids_where_question_in_state( + $grade, $slot, $questionid, $order, $page, $pagesize); + $attempts = $this->load_attempts_by_usage_ids($qubaids); + + // Prepare the form. + $hidden = array( + 'id' => $this->cm->id, + 'mode' => 'grading', + 'slot' => $slot, + 'qid' => $questionid, + 'page' => $page, + ); + if (array_key_exists('includeauto', $this->viewoptions)) { + $hidden['includeauto'] = $this->viewoptions['includeauto']; + } + $mform = new quiz_grading_settings($hidden, $counts, $shownames, $showidnumbers); + + // Tell the form the current settings. + $settings = new stdClass; + $settings->grade = $grade; + $settings->pagesize = $pagesize; + $settings->order = $order; + $mform->set_data($settings); + + // Print the heading and form. + echo question_engine::initialise_js(); + + $a = new stdClass; + $a->number = $this->questions[$slot]->number; + $a->questionname = format_string($counts->name); + echo $OUTPUT->heading(get_string('gradingquestionx', 'quiz_grading', $a)); + echo html_writer::tag('p', html_writer::link($this->list_questions_url(), + get_string('backtothelistofquestions', 'quiz_grading')), + array('class' => 'mdl-align')); + + $mform->display(); + + // Paging info. + $a = new stdClass; + $a->from = $page * $pagesize + 1; + $a->to = min(($page + 1) * $pagesize, $count); + $a->of = $count; + echo $OUTPUT->heading(get_string('gradingattemptsxtoyofz', 'quiz_grading', $a), 3); + + if ($count > $pagesize && $order != 'random') { + echo $OUTPUT->paging_bar($count, $page, $pagesize, + $this->grade_question_url($slot, $questionid, $grade, false)); + } + + // Display the form with one section for each attempt. + $usehtmleditor = can_use_html_editor(); + $sesskey = sesskey(); + $qubaidlist = implode(',', $qubaids); + echo html_writer::start_tag('form', array('method' => 'post', + 'action' => $this->grade_question_url($slot, $questionid, $grade, $page), + 'class' => 'mform', 'id' => 'manualgradingform')) . + html_writer::start_tag('div') . + html_writer::input_hidden_params(new moodle_url('', array( + 'qubaids' => $qubaidlist, 'slots' => $slot, 'sesskey' => $sesskey))); + + foreach ($qubaids as $qubaid) { + $attempt = $attempts[$qubaid]; + $quba = question_engine::load_questions_usage_by_activity($qubaid); + $displayoptions = quiz_get_review_options($this->quiz, $attempt, $this->context); + $displayoptions->hide_all_feedback(); + $displayoptions->history = question_display_options::HIDDEN; + $displayoptions->manualcomment = question_display_options::EDITABLE; + + $heading = $this->get_question_heading($attempt, $shownames, $showidnumbers); + if ($heading) { + echo $OUTPUT->heading($heading, 4); + } + echo $quba->render_question($slot, $displayoptions, $this->questions[$slot]->number); + } + + echo html_writer::tag('div', html_writer::empty_tag('input', array( + 'type' => 'submit', 'value' => get_string('saveandnext', 'quiz_grading'))), + array('class' => 'mdl-align')) . + html_writer::end_tag('div') . html_writer::end_tag('form'); + } + + protected function get_question_heading($attempt, $shownames, $showidnumbers) { + $a = new stdClass; + $a->attempt = $attempt->attempt; + $a->fullname = fullname($attempt); + $a->idnumber = $attempt->idnumber; + + $showidnumbers &= !empty($attempt->idnumber); + + if ($shownames && $showidnumbers) { + return get_string('gradingattemptwithidnumber', 'quiz_grading', $a); + } else if ($shownames) { + return get_string('gradingattempt', 'quiz_grading', $a); + } else if ($showidnumbers) { + $a->fullname = $attempt->idnumber; + return get_string('gradingattempt', 'quiz_grading', $a); + } else { + return ''; + } + } + + protected function validate_submitted_marks() { + + $qubaids = optional_param('qubaids', null, PARAM_SEQUENCE); + if (!$qubaids) { + return false; + } + $qubaids = clean_param(explode(',', $qubaids), PARAM_INT); + + $slots = optional_param('slots', '', PARAM_SEQUENCE); + if (!$slots) { + $slots = array(); + } else { + $slots = explode(',', $slots); + } + + foreach ($qubaids as $qubaid) { + foreach ($slots as $slot) { + $prefix = 'q' . $qubaid . ':' . $slot . '_'; + $mark = optional_param($prefix . '-mark', null, PARAM_NUMBER); + $maxmark = optional_param($prefix . '-maxmark', null, PARAM_NUMBER); + $minfraction = optional_param($prefix . ':minfraction', null, PARAM_NUMBER); + if (!is_null($mark) && ($mark < $minfraction * $maxmark || $mark > $maxmark)) { + return false; } } } - $this->viewurl = new moodle_url('/mod/quiz/report.php', $viewoptions); - /// find out current groups mode - if ($groupmode = groups_get_activity_groupmode($this->cm)) { // Groups are being used - groups_print_activity_menu($this->cm, $this->viewurl->out(true, array('userid'=>0, 'attemptid'=>0))); - } - - if(empty($this->users)) { - if ($currentgroup){ - echo $OUTPUT->notification(get_string('nostudentsingroup')); - } else { - echo $OUTPUT->notification(get_string('nostudentsyet')); - } - return true; - } - $qattempts = quiz_get_total_qas_graded_and_ungraded($quiz, array_keys($gradeableqs), array_keys($this->users)); - if(empty($qattempts)) { - echo $OUTPUT->notification(get_string('noattemptstoshow', 'quiz')); - return true; - } - $qmenu = array(); - foreach ($gradeableqs as $qid => $questionformenu){ - $a= new stdClass(); - $a->number = $gradeableqs[$qid]->number; - $a->name = $gradeableqs[$qid]->name; - $a->gradedattempts =$qattempts[$qid]->gradedattempts; - $a->totalattempts =$qattempts[$qid]->totalattempts; - $a->openspan =''; - $a->closespan =''; - $qmenu[$qid]= get_string('questiontitle', 'quiz_grading', $a); - } - if (count($gradeableqs)!=1){ - $qurl = fullclone($this->viewurl); - $qurl->remove_params('questionid', 'attemptid', 'gradeall', 'gradeungraded', 'gradenextungraded'); - $menu = $OUTPUT->single_select($qurl, 'questionid', $qmenu, $questionid, array(''=>'choosedots'), 'questionid'); - echo '
'.$menu.'
'; - } - if (!$questionid){ - return true; - } - $a= new stdClass(); - $a->number = $question->number; - $a->name = $question->name; - $a->gradedattempts =$qattempts[$question->id]->gradedattempts; - $a->totalattempts =$qattempts[$question->id]->totalattempts; - $a->openspan =''; - $a->closespan =''; - echo $OUTPUT->heading(get_string('questiontitle', 'quiz_grading', $a)); - - // our 2 different views - - // the first view allows a user to select a question and - // displays the users who have answered the essay question - // and all of their attempts at answering the question - - // the second prints selected attempt answer(s) with a comment - // and grade form underneath them - - $ungraded = $qattempts[$questionid]->totalattempts- $qattempts[$questionid]->gradedattempts; - if ($gradenextungraded ||$gradeungraded || $gradeall || $userid || $attemptid){ - $this->print_questions_and_form($quiz, $question, $userid, $attemptid, $gradeungraded, $gradenextungraded, $ungraded); - } else { - $this->view_question($quiz, $question, $qattempts[$questionid]->totalattempts, $ungraded); - } return true; } - /** - * Prints a table with users and their attempts - * - * @return void - * @todo Add current grade to the table - * Finnish documenting - **/ - function view_question($quiz, $question, $totalattempts, $ungraded) { - global $CFG, $DB, $OUTPUT; + protected function process_submitted_data() { + global $DB; - $usercount = count($this->users); - - // set up table - $tablecolumns = array('picture', 'fullname', 'timefinish', 'grade'); - $tableheaders = array('', get_string('name'), get_string("completedon", "quiz"), ''); - - $table = new flexible_table('mod-quiz-report-grading'); - - $table->define_columns($tablecolumns); - $table->define_headers($tableheaders); - $table->define_baseurl($this->viewurl->out()); - - $table->sortable(true); - $table->initialbars($usercount>20); // will show initialbars if there are more than 20 users - $table->pageable(true); - $table->collapsible(true); - - $table->column_suppress('fullname'); - $table->column_suppress('picture'); - $table->column_suppress('grade'); - - $table->column_class('picture', 'picture'); - - // attributes in the table tag - $table->set_attribute('cellspacing', '0'); - $table->set_attribute('id', 'attempts'); - $table->set_attribute('class', 'generaltable generalbox'); - $table->set_attribute('align', 'center'); - //$table->set_attribute('width', '50%'); - - // get it ready! - $table->setup(); - - list($select, $from, $where, $params) = $this->attempts_sql($quiz->id, true, $question->id); - - list($twhere, $tparams) = $table->get_sql_where(); - if ($twhere) { - $where .= ' AND '.$twhere; //initial bar - $params = array_merge($params, $tparams); + $qubaids = optional_param('qubaids', null, PARAM_SEQUENCE); + if (!$qubaids) { + return; } - // sorting of the table - if ($sort = $table->get_sql_sort()) { - $sort = 'ORDER BY '.$sort; // seems like I would need to have u. or qa. infront of the ORDER BY attribues... but seems to work.. - } else { - // my default sort rule - $sort = 'ORDER BY u.firstname, u.lastname, qa.timefinish ASC'; + $qubaids = clean_param(explode(',', $qubaids), PARAM_INT); + $attempts = $this->load_attempts_by_usage_ids($qubaids); + + $transaction = $DB->start_delegated_transaction(); + foreach ($qubaids as $qubaid) { + $attempt = $attempts[$qubaid]; + $quba = question_engine::load_questions_usage_by_activity($qubaid); + $attemptobj = new quiz_attempt($attempt, $this->quiz, $this->cm, $this->course); + $attemptobj->process_all_actions(time()); } - - // set up the pagesize - $table->pagesize(QUIZ_REPORT_DEFAULT_PAGE_SIZE, $totalattempts); - - // get the attempts and process them - echo '
'; - if ($attempts = $DB->get_records_sql($select.$from.$where.$sort, $params, $table->get_page_start(), $table->get_page_size())) { - // grade all link - $links = "id&questionid=$question->id\">".get_string('gradeall', 'quiz_grading', $totalattempts).''; - if ($ungraded>0){ - $links .="
id&questionid=$question->id\">".get_string('gradeungraded', 'quiz_grading', $ungraded).''; - if ($ungraded>QUIZ_REPORT_DEFAULT_GRADING_PAGE_SIZE){ - $links .="
id&questionid=$question->id\">".get_string('gradenextungraded', 'quiz_grading', QUIZ_REPORT_DEFAULT_GRADING_PAGE_SIZE).''; - } - } - $table->add_data_keyed(array('grade'=> $links)); - $table->add_separator(); - foreach($attempts as $attempt) { - - $user = clone($attempt); - $user->id = $user->userid; - $picture = $OUTPUT->user_picture($user, array('courseid'=>$quiz->course)); - - // link to student profile - $userlink = "wwwroot/user/view.php?id=$attempt->userid&course=$quiz->course\">". - fullname($attempt, true).''; - - $gradedclass = question_state_is_graded($attempt)?' class="highlightgraded" ':''; - $gradedstring = question_state_is_graded($attempt)?(' '.get_string('graded','quiz_grading')):''; - - // link for the attempt - $attemptlink = "id&questionid=$question->id&attemptid=$attempt->attemptid\">". - userdate($attempt->timefinish, get_string('strftimedatetime')). - $gradedstring.''; - - // grade all attempts for this user - $gradelink = "id&questionid=$question->id&userid=$attempt->userid\">". - get_string('grade').''; - - $table->add_data( array($picture, $userlink, $attemptlink, $gradelink) ); - } - $table->add_separator(); - $table->add_data_keyed(array('grade'=> $links)); - } - // print everything here - $table->print_html(); - echo '
'; + $transaction->allow_commit(); } - /** - * Prints questions with comment and grade form underneath each question + * Load information about the number of attempts at various questions in each + * summarystate. * - * @return void - * @todo Finish documenting this function - **/ - function print_questions_and_form($quiz, $question, $userid, $attemptid, $gradeungraded, $gradenextungraded, $ungraded) { - global $CFG, $DB, $OUTPUT; - - $context = get_context_instance(CONTEXT_MODULE, $this->cm->id); - - $questions[$question->id] = &$question; - $usehtmleditor = can_use_html_editor(); - - list($select, $from, $where, $params) = $this->attempts_sql($quiz->id, false, $question->id, $userid, $attemptid, $gradeungraded, $gradenextungraded); - - $sort = 'ORDER BY u.firstname, u.lastname, qa.attempt ASC'; - - if ($gradenextungraded){ - $attempts = $DB->get_records_sql($select.$from.$where.$sort, $params, 0, QUIZ_REPORT_DEFAULT_GRADING_PAGE_SIZE); - } else { - $attempts = $DB->get_records_sql($select.$from.$where.$sort, $params); - } - if ($attempts){ - $firstattempt = current($attempts); - $fullname = fullname($firstattempt); - if ($gradeungraded) { // getting all ungraded attempts - echo $OUTPUT->heading(get_string('gradingungraded','quiz_grading', $ungraded), 3); - } else if ($gradenextungraded) { // getting next ungraded attempts - echo $OUTPUT->heading(get_string('gradingnextungraded','quiz_grading', QUIZ_REPORT_DEFAULT_GRADING_PAGE_SIZE), 3); - } else if ($userid){ - echo $OUTPUT->heading(get_string('gradinguser','quiz_grading', $fullname), 3); - } else if ($attemptid){ - $a = new stdClass(); - $a->fullname = $fullname; - $a->attempt = $firstattempt->attempt; - echo $OUTPUT->heading(get_string('gradingattempt','quiz_grading', $a), 3); - } else { - echo $OUTPUT->heading(get_string('gradingall','quiz_grading', count($attempts)), 3); - } - - // Display the form with one part for each selected attempt - - echo '
'. - ''. - ''. - ''. - ''; - - foreach ($attempts as $attempt) { - - // Load the state for this attempt (The questions array was created earlier) - $states = get_question_states($questions, $quiz, $attempt); - // The $states array is indexed by question id but because we are dealing - // with only one question there is only one entry in this array - $state = &$states[$question->id]; - - $options = quiz_get_review_options($quiz, $attempt, $context); - unset($options->questioncommentlink); - $options->readonly = 1; - - if (question_state_is_graded($state)) { - $gradedclass = 'main highlightgraded'; - $gradedstring = ' ' . get_string('graded','quiz_grading'); - } else { - $gradedclass = 'main'; - $gradedstring = ''; - } - $a = new stdClass(); - $a->fullname = fullname($attempt, true); - $a->attempt = $attempt->attempt; - - // print the user name, attempt count, the question, and some more hidden fields - echo '
'; - echo $OUTPUT->heading(get_string('gradingattempt', 'quiz_grading', $a) . $gradedstring, 3, $gradedclass); - - // Print the question, without showing any previous comment. - $copy = $state->manualcomment; - $state->manualcomment = ''; - $options->noeditlink = true; - print_question($question, $state, '', $quiz, $options); - - // The print the comment and grade fields, putting back the previous comment. - $state->manualcomment = $copy; - question_print_comment_fields($question, $state, 'manualgrades[' . $attempt->uniqueid . ']', - $quiz, get_string('manualgrading', 'quiz')); - - echo '
'; - } - echo '
'. - '
'; - } else { - echo $OUTPUT->notification(get_string('noattemptstoshow', 'quiz')); - } + * The results are returned as an two dimensional array $qubaid => $slot => $dataobject + * + * @param array $slots A list of slots for the questions you want to konw about. + * @return array The array keys are slot,qestionid. The values are objects with + * fields $slot, $questionid, $inprogress, $name, $needsgrading, $autograded, + * $manuallygraded and $all. + */ + protected function get_question_state_summary($slots) { + $dm = new question_engine_data_mapper(); + return $dm->load_questions_usages_question_state_summary( + $this->get_qubaids_condition(), $slots); } - function attempts_sql($quizid, $wantstateevent=false, $questionid=0, $userid=0, $attemptid=0, $gradeungraded=0, $gradenextungraded=0){ - global $CFG, $DB; - // this sql joins the attempts table and the user table - $select = 'SELECT qa.id AS attemptid, qa.uniqueid, qa.attempt, qa.timefinish, qa.preview, - u.id AS userid, u.firstname, u.lastname, u.picture, u.imagealt, u.email '; - if ($wantstateevent && $questionid){ - $select .= ', qs.event '; + /** + * Get a list of usage ids where the question with slot $slot, and optionally + * also with question id $questionid, is in summary state $summarystate. Also + * return the total count of such states. + * + * Only a subset of the ids can be returned by using $orderby, $limitfrom and + * $limitnum. A special value 'random' can be passed as $orderby, in which case + * $limitfrom is ignored. + * + * @param integer $slot The slot for the questions you want to konw about. + * @param integer $questionid (optional) Only return attempts that were of this specific question. + * @param string $summarystate 'all', 'needsgrading', 'autograded' or 'manuallygraded'. + * @param string $orderby 'random', 'date', 'student' or 'idnumber'. + * @param integer $page implements paging of the results. + * Ignored if $orderby = random or $pagesize is null. + * @param integer $pagesize implements paging of the results. null = all. + */ + function get_usage_ids_where_question_in_state($summarystate, $slot, + $questionid = null, $orderby = 'random', $page = 0, $pagesize = null) { + global $CFG; + $dm = new question_engine_data_mapper(); + + if ($pagesize && $orderby != 'random') { + $limitfrom = $page * $pagesize; + } else { + $limitfrom = 0; } - $from = 'FROM {user} u, ' . - '{quiz_attempts} qa '; + + $qubaids = $this->get_qubaids_condition(); + $params = array(); - - $from .= "LEFT JOIN {question_sessions} qns " . - "ON (qns.attemptid = qa.uniqueid AND qns.questionid = ?) "; - $params[] = $questionid; - $from .= "LEFT JOIN {question_states} qs " . - "ON (qs.id = qns.newest AND qs.question = ?) "; - $params[] = $questionid; - - list($usql, $u_params) = $DB->get_in_or_equal(array_keys($this->users)); - if ($gradenextungraded || $gradeungraded) { // get ungraded attempts - $where = "WHERE u.id $usql AND qs.event NOT IN (".QUESTION_EVENTS_GRADED.")"; - $params = array_merge($params, $u_params); - } else if ($userid) { // get all the attempts for a specific user - $where = 'WHERE u.id=?'; - $params[] = $userid; - } else if ($attemptid) { // get a specific attempt - $where = 'WHERE qa.id=? '; - $params[] = $attemptid; - } else { // get all user attempts - $where = "WHERE u.id $usql "; - $params = array_merge($params, $u_params); + if ($orderby == 'date') { + list($statetest, $params) = $dm->in_summary_state_test('manuallygraded', false, 'mangrstate'); + $orderby = "( + SELECT MAX(sortqas.timecreated) + FROM {question_attempt_steps} sortqas + WHERE sortqas.questionattemptid = qa.id + AND sortqas.state $statetest + )"; + } else if ($orderby == 'student' || $orderby == 'idnumber') { + $qubaids->from .= " JOIN {user} u ON quiza.userid = u.id "; + if ($orderby == 'student') { + $orderby = sql_fullname('u.firstname', 'u.lastname'); + } } - $where .= ' AND qs.event IN ('.QUESTION_EVENTS_CLOSED_OR_GRADED.')'; - - $where .= ' AND u.id = qa.userid AND qa.quiz = ?'; - $params[] = $quizid; - // ignore previews - $where .= ' AND preview = 0 '; - - $where .= ' AND qa.timefinish != 0 '; - - return array($select, $from, $where, $params); + return $dm->load_questions_usages_where_question_in_state($qubaids, $summarystate, + $slot, $questionid, $orderby, $params, $limitfrom, $pagesize); } - } - - diff --git a/mod/quiz/report/grading/version.php b/mod/quiz/report/grading/version.php index b9a0b85f791..9ff23c41924 100644 --- a/mod/quiz/report/grading/version.php +++ b/mod/quiz/report/grading/version.php @@ -23,4 +23,4 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$plugin->version = 2010070100; +$plugin->version = 2011021400; diff --git a/mod/quiz/report/reportlib.php b/mod/quiz/report/reportlib.php index 746b9dc1b6f..26c1658f51a 100644 --- a/mod/quiz/report/reportlib.php +++ b/mod/quiz/report/reportlib.php @@ -87,11 +87,11 @@ function quiz_report_unindex($datum) { * @param object $quiz the quiz. * @return array of slot => $question object with fields ->slot, ->id, ->maxmark, ->number, ->length. */ -function quiz_report_load_questions($quiz){ +function quiz_report_get_significant_questions($quiz) { global $DB; - $questionlist = quiz_questions_in_quiz($quiz->questions); - list($usql, $params) = $DB->get_in_or_equal(explode(',', $questionlist)); + $questionids = quiz_questions_in_quiz($quiz->questions); + list($usql, $params) = $DB->get_in_or_equal(explode(',', $questionids)); $params[] = $quiz->id; $questions = $DB->get_records_sql(" SELECT @@ -103,8 +103,8 @@ FROM {question} q JOIN {quiz_question_instances} qqi ON qqi.question = q.id WHERE - qqi.quiz = ? AND q.id $usql AND + qqi.quiz = ? AND length > 0", $params); $qsbyslot = array(); diff --git a/question/engine/datalib.php b/question/engine/datalib.php index dd670676820..4c8846f81ba 100644 --- a/question/engine/datalib.php +++ b/question/engine/datalib.php @@ -321,7 +321,7 @@ WHERE * $manuallygraded and $all. */ public function load_questions_usages_question_state_summary(qubaid_condition $qubaids, $slots) { - list($slottest, $params) = get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot0000'); + list($slottest, $params) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot0000'); $rs = $this->db->get_recordset_sql(" SELECT @@ -356,12 +356,8 @@ ORDER BY q.id ", $params + $qubaids->from_where_params()); - if (!$rs) { - throw new moodle_exception('errorloadingdata'); - } - $results = array(); - while ($row = rs_fetch_next_record($rs)) { + foreach ($rs as $row) { $index = $row->slot . ',' . $row->questionid; if (!array_key_exists($index, $results)) { @@ -380,7 +376,7 @@ ORDER BY $results[$index]->{$row->summarystate} = $row->numattempts; $results[$index]->all += $row->numattempts; } - rs_close($rs); + $rs->close(); return $results; } @@ -400,6 +396,7 @@ ORDER BY * @param integer $questionid (optional) Only return attempts that were of this specific question. * @param string $summarystate the summary state of interest, or 'all'. * @param string $orderby the column to order by. + * @param array $params any params required by any of the SQL fragments. * @param integer $limitfrom implements paging of the results. * Ignored if $orderby = random or $limitnum is null. * @param integer $limitnum implements paging of the results. null = all. @@ -407,16 +404,18 @@ ORDER BY */ public function load_questions_usages_where_question_in_state( qubaid_condition $qubaids, $summarystate, $slot, $questionid = null, - $orderby = 'random', $limitfrom = 0, $limitnum = null) { + $orderby = 'random', $params, $limitfrom = 0, $limitnum = null) { global $CFG; $extrawhere = ''; if ($questionid) { - $extrawhere .= ' AND qa.questionid = ' . $questionid; + $extrawhere .= ' AND qa.questionid = :questionid'; + $params['questionid'] = $questionid; } if ($summarystate != 'all') { - $test = $this->in_summary_state_test($summarystate); + list($test, $sparams) = $this->in_summary_state_test($summarystate); $extrawhere .= ' AND qas.state ' . $test; + $params += $sparams; } if ($orderby == 'random') { @@ -433,23 +432,25 @@ ORDER BY // 10,000 students in the coures, each doing 5 quiz attempts. That // is a 50,000 element int => int array, which PHP seems to use 5MB // memeory to store on a 64 bit server. - $qubaids = get_records_sql_menu(" + $params += $qubaids->from_where_params(); + $params['slot'] = $slot; + $qubaids = $this->db->get_records_sql_menu(" SELECT qa.questionusageid, 1 FROM {$qubaids->from_question_attempts('qa')} -JOIN {$CFG->prefix}question_attempt_steps qas ON +JOIN {question_attempt_steps} qas ON qas.id = {$this->latest_step_for_qa_subquery()} -JOIN {$CFG->prefix}question q ON q.id = qa.questionid +JOIN {question} q ON q.id = qa.questionid WHERE {$qubaids->where()} AND - qa.slot = $slot + qa.slot = :slot $extrawhere $sqlorderby - "); + ", $params); $qubaids = array_keys($qubaids); $count = count($qubaids); @@ -606,8 +607,6 @@ ORDER BY * @param question_attempt $qa the question attempt that has changed. */ public function update_question_attempt(question_attempt $qa) { - global $DB; - $record = new stdClass; $record->id = $qa->get_database_id(); $record->maxmark = $qa->get_max_mark(); @@ -725,11 +724,9 @@ ORDER BY * @param boolean $equal if false, do a NOT IN test. Default true. * @return string SQL fragment. */ - public function in_summary_state_test($summarystate, $equal = true) { + public function in_summary_state_test($summarystate, $equal = true, $prefix = 'summarystates') { $states = question_state::get_all_for_summary_state($summarystate); - list($sql, $params) = $this->db->get_in_or_equal($states, SQL_PARAMS_QM, 'param0000', $equal); - // TODO return $params; - return $sql; + return $this->db->get_in_or_equal($states, SQL_PARAMS_NAMED, $prefix . '00', $equal); } /** diff --git a/question/todo/diffstat.txt b/question/todo/diffstat.txt index 03a5041aafa..df28181e002 100644 --- a/question/todo/diffstat.txt +++ b/question/todo/diffstat.txt @@ -124,7 +124,7 @@ DONE lang/en_utf8/quiz_regrade.php | 7 - DONE mod/quiz/report/grading/db/access.php | 49 + DONE mod/quiz/report/grading/gradingsettings_form.php | 91 + DONE mod/quiz/report/grading/lang/en_utf8/quiz_grading.php | 60 + - mod/quiz/report/grading/report.php | 842 ++++--- +DONE mod/quiz/report/grading/report.php | 842 ++++--- DONE mod/quiz/report/grading/version.php | 26 + DONE lang/en_utf8/quiz_grading.php | 18 -