From 7f29a7dbe40d21dcbc5f0d21a399c2e6f7b3c98b Mon Sep 17 00:00:00 2001 From: jamiesensei Date: Sun, 20 Jul 2008 14:29:10 +0000 Subject: [PATCH] MDL-15631 "move detailled responses report out of contrib into main distribution" adding responses report to main distribution --- lang/en_utf8/quiz_responses.php | 16 +- mod/quiz/report/responses/report.php | 326 ++++++++++++++++++ mod/quiz/report/responses/responses_table.php | 225 ++++++++++++ .../responses/responsessettings_form.php | 43 +++ mod/quiz/report/responses/styles.css | 34 ++ 5 files changed, 635 insertions(+), 9 deletions(-) create mode 100644 mod/quiz/report/responses/report.php create mode 100644 mod/quiz/report/responses/responses_table.php create mode 100644 mod/quiz/report/responses/responsessettings_form.php create mode 100644 mod/quiz/report/responses/styles.css diff --git a/lang/en_utf8/quiz_responses.php b/lang/en_utf8/quiz_responses.php index bf2c049677..ca90ac34ed 100644 --- a/lang/en_utf8/quiz_responses.php +++ b/lang/en_utf8/quiz_responses.php @@ -1,11 +1,9 @@ - +$string['pagesize'] = 'Page Size'; +$string['reportresponses'] = 'Responses'; +$string['responses'] = 'Responses'; +$string['responsesdownload'] = 'Responses download'; +?> \ No newline at end of file diff --git a/mod/quiz/report/responses/report.php b/mod/quiz/report/responses/report.php new file mode 100644 index 0000000000..4ca2b9b1c6 --- /dev/null +++ b/mod/quiz/report/responses/report.php @@ -0,0 +1,326 @@ +libdir.'/tablelib.php'); +require_once($CFG->dirroot.'/mod/quiz/report/responses/responsessettings_form.php'); +require_once($CFG->dirroot.'/mod/quiz/report/responses/responses_table.php'); + +class quiz_responses_report extends quiz_default_report { + + /** + * Display the report. + */ + function display($quiz, $cm, $course) { + global $CFG, $COURSE, $DB; + + $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); + $showgrades = $quiz->grade && $quiz->sumgrades && $reviewoptions->scores; + + $download = optional_param('download', '', PARAM_ALPHA); + + 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); + } + //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. + } + + + $pageoptions = array(); + $pageoptions['id'] = $cm->id; + $pageoptions['q'] = $quiz->id; + $pageoptions['mode'] = 'responses'; + + $reporturl = new moodle_url($CFG->wwwroot.'/mod/quiz/report.php', $pageoptions); + $qmsubselect = quiz_report_qm_filter_select($quiz); + + + + /// find out current groups mode + $currentgroup = groups_get_activity_group($cm, true); + + $mform = new mod_quiz_report_responses_settings($reporturl, array('qmsubselect'=> $qmsubselect, 'quiz'=>$quiz, 'currentgroup'=>$currentgroup)); + if ($fromform = $mform->get_data()){ + $attemptsmode = $fromform->attemptsmode; + if ($qmsubselect){ + //control is not on the form if + //the grading method is not set + //to grade one attempt per user eg. for average attempt grade. + $qmfilter = $fromform->qmfilter; + } else { + $qmfilter = 0; + } + set_user_preference('quiz_report_pagesize', $fromform->pagesize); + $pagesize = $fromform->pagesize; + } else { + $qmfilter = optional_param('qmfilter', 0, PARAM_INT); + $attemptsmode = optional_param('attemptsmode', null, PARAM_INT); + if ($attemptsmode === null){ + //default + $attemptsmode = QUIZ_REPORT_ATTEMPTS_ALL; + } else if ($currentgroup){ + //default for when a group is selected + if ($attemptsmode === null || $attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL){ + $attemptsmode = QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH; + } + } else if (!$currentgroup && $course->id == SITEID) { + //force report on front page to show all, unless a group is selected. + $attemptsmode = QUIZ_REPORT_ATTEMPTS_ALL; + } + $pagesize = get_user_preferences('quiz_report_pagesize', 0); + } + if ($pagesize < 1) { + $pagesize = QUIZ_REPORT_DEFAULT_PAGE_SIZE; + } + // 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) + && ($attemptsmode!= QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO); + + + $displayoptions = array(); + $displayoptions['attemptsmode'] = $attemptsmode; + $displayoptions['qmfilter'] = $qmfilter; + + //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; + } + + $questions = quiz_report_load_questions($quiz); + + $table = new quiz_report_responses_table($quiz , $qmsubselect, $groupstudents, + $students, $questions, $candelete, $reporturl, $displayoptions); + $table->is_downloading($download, get_string('reportresponses','quiz_responses'), + "$COURSE->shortname ".format_string($quiz->name,true)); + if (!$table->is_downloading()) { + // Only print headers if not asked to download data + $meta = ''."\n"; + $this->print_header_and_tabs($cm, $course, $quiz, "responses", $meta); + } + + 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)) { + echo '
' . $strattemptnum . '
'; + } + } + $nostudents = false; + if (!$students){ + notify(get_string('nostudentsyet')); + $nostudents = true; + }else if ($currentgroup && !$groupstudents){ + notify(get_string('nostudentsingroup')); + $nostudents = true; + } + if (!$table->is_downloading()) { + // Print display options + $mform->set_data($displayoptions +compact('pagesize')); + $mform->display(); + } + + if (!$nostudents || ($attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL)){ + // Print information on the grading method and whether we are displaying + // + if (!$table->is_downloading()) { //do not print notices when downloading + if ($strattempthighlight = quiz_report_highlighting_grading_method($quiz, $qmsubselect, $qmfilter)) { + echo '
' . $strattempthighlight . '
'; + } + } + + + $showgrades = $quiz->grade && $quiz->sumgrades && $reviewoptions->scores; + $hasfeedback = quiz_has_feedback($quiz->id) && $quiz->grade > 1.e-7 && $quiz->sumgrades > 1.e-7; + + + // Construct the SQL + $fields = $DB->sql_concat('u.id', '\'#\'', 'COALESCE(qa.attempt, \'0\')').' AS uniqueid, '. + ($qmsubselect?"($qmsubselect) AS gradedattempt, ":''). + 'qa.uniqueid AS attemptuniqueid, qa.id AS attempt, u.id AS userid, u.idnumber, u.firstname,'. + ' u.lastname, u.institution, u.department, u.email, 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'; + $params = array('quizid' => $quiz->id); + + if ($qmsubselect && $qmfilter){ + $from .= ' AND '.$qmsubselect; + } + 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'; + break; + case QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH: + // Show only students with attempts + list($allowed_usql, $allowed_params) = $DB->get_in_or_equal($allowed, SQL_PARAMS_NAMED, 'u0000'); + $params += $allowed_params; + $where = "u.id $allowed_usql AND qa.preview = 0 AND qa.id IS NOT NULL"; + break; + case QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO: + // Show only students without attempts + list($allowed_usql, $allowed_params) = $DB->get_in_or_equal($allowed, SQL_PARAMS_NAMED, 'u0000'); + $params += $allowed_params; + $where = "u.id $allowed_usql AND qa.id IS NULL"; + break; + case QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS: + // Show all students with or without attempts + list($allowed_usql, $allowed_params) = $DB->get_in_or_equal($allowed, SQL_PARAMS_NAMED, 'u0000'); + $params += $allowed_params; + $where = "u.id $allowed_usql AND (qa.preview = 0 OR qa.preview IS NULL)"; + break; + } + + $table->set_count_sql("SELECT COUNT(1) FROM $from WHERE $where", $params); + + + + $table->set_sql($fields, $from, $where, $params); + + // Define table columns + $columns = array(); + $headers = array(); + + + if (!$table->is_downloading() && $candelete) { + $columns[]= 'checkbox'; + $headers[]= NULL; + } + + if (!$table->is_downloading() && $CFG->grade_report_showuserimage) { + $columns[]= 'picture'; + $headers[]= ''; + } + if (!$table->is_downloading()){ + $columns[]= 'fullname'; + $headers[]= get_string('name'); + } else { + $columns[]= 'lastname'; + $headers[]= get_string('lastname'); + $columns[]= 'firstname'; + $headers[]= get_string('firstname'); + } + + if ($CFG->grade_report_showuseridnumber) { + $columns[]= 'idnumber'; + $headers[]= get_string('idnumber'); + } + if ($table->is_downloading()){ + $columns[]= 'institution'; + $headers[]= get_string('institution'); + + $columns[]= 'department'; + $headers[]= get_string('department'); + + $columns[]= 'email'; + $headers[]= get_string('email'); + + $columns[]= 'timestart'; + $headers[]= get_string('startedon', 'quiz'); + + $columns[]= 'timefinish'; + $headers[]= get_string('timecompleted','quiz'); + + $columns[]= 'duration'; + $headers[]= get_string('attemptduration', 'quiz'); + } + + if ($showgrades) { + $columns[] = 'sumgrades'; + $headers[] = get_string('grade', 'quiz').'/'.$quiz->grade; + } + + // we want to display responses for all questions + foreach ($questions as $id => $question) { + // Ignore questions of zero length + $columns[] = 'qsanswer'.$id; + $headers[] = '#'.$question->number; + } + + if ($hasfeedback) { + $columns[] = 'feedbacktext'; + $headers[] = get_string('feedback', 'quiz'); + } + + // Load the question type specific information + if (!get_question_options($questions)) { + print_error('Could not load question options'); + } + + $table->define_columns($columns); + $table->define_headers($headers); + $table->sortable(true, 'uniqueid'); + + // Set up the table + $table->define_baseurl($reporturl->out(false, $displayoptions)); + + $table->collapsible(true); + + $table->column_suppress('picture'); + $table->column_suppress('fullname'); + $table->column_suppress('idnumber'); + + $table->no_sorting('feedbacktext'); + + $table->column_class('picture', 'picture'); + $table->column_class('lastname', 'bold'); + $table->column_class('firstname', 'bold'); + $table->column_class('fullname', 'bold'); + $table->column_class('sumgrades', 'bold'); + + $table->set_attribute('id', 'attempts'); + + $table->out($pagesize, true); + } + return true; + } + +} + + +?> \ No newline at end of file diff --git a/mod/quiz/report/responses/responses_table.php b/mod/quiz/report/responses/responses_table.php new file mode 100644 index 0000000000..4a13c86c48 --- /dev/null +++ b/mod/quiz/report/responses/responses_table.php @@ -0,0 +1,225 @@ +quiz = $quiz; + $this->qmsubselect = $qmsubselect; + $this->groupstudents = $groupstudents; + $this->students = $students; + $this->questions = $questions; + $this->candelete = $candelete; + $this->reporturl = $reporturl; + $this->displayoptions = $displayoptions; + } + function build_table(){ + global $CFG, $DB; + if ($this->rawdata) { + // Define some things we need later to process raw data from db. + $this->strtimeformat = get_string('strftimedatetime'); + parent::build_table(); + //end of adding data from attempts data to table / download + //now add averages at bottom of table : + $params = array($this->quiz->id); + + $this->add_separator(); + if ($this->is_downloading()){ + $namekey = 'lastname'; + } else { + $namekey = 'fullname'; + } + } + } + + function wrap_html_start(){ + if (!$this->is_downloading()) { + if ($this->candelete) { + // Start form + $strreallydel = addslashes_js(get_string('deleteattemptcheck','quiz')); + echo '
'; + echo '
'; + echo '
'; + echo $this->reporturl->hidden_params_out(array(), 0, $this->displayoptions); + echo '
'; + echo '
'; + } + } + } + function wrap_html_finish(){ + if (!$this->is_downloading()) { + // Print "Select all" etc. + if ($this->candelete) { + echo ''; + echo '
'; + echo ''. + get_string('selectall', 'quiz').' / '; + echo ''. + get_string('selectnone', 'quiz').' '; + echo '  '; + echo ''; + echo '
'; + // Close form + echo '
'; + echo '
'; + } + } + } + + + function col_checkbox($attempt){ + if ($attempt->attempt){ + return ''; + } else { + return ''; + } + } + + function col_picture($attempt){ + global $COURSE; + return print_user_picture($attempt->userid, $COURSE->id, $attempt->picture, false, true); + } + + + function col_timestart($attempt){ + if ($attempt->attempt) { + $startdate = userdate($attempt->timestart, $this->strtimeformat); + if (!$this->is_downloading()) { + return ''.$startdate.''; + } else { + return $startdate; + } + } else { + return '-'; + } + } + function col_timefinish($attempt){ + if ($attempt->attempt) { + if ($attempt->timefinish) { + $timefinish = userdate($attempt->timefinish, $this->strtimeformat); + if (!$this->is_downloading()) { + return ''.$timefinish.''; + } else { + return $timefinish; + } + } else { + return '-'; + } + } else { + return '-'; + } + } + + function col_duration($attempt){ + if ($attempt->timefinish) { + return format_time($attempt->duration); + } elseif ($attempt->timestart) { + return get_string('unfinished', 'quiz'); + } else { + return '-'; + } + } + function col_sumgrades($attempt){ + if ($attempt->timefinish) { + $grade = quiz_rescale_grade($attempt->sumgrades, $this->quiz); + if (!$this->is_downloading()) { + $gradehtml = ''.$grade.''; + if ($this->qmsubselect && $attempt->gradedattempt){ + $gradehtml = '
'.$gradehtml.'
'; + } + return $gradehtml; + } else { + return $grade; + } + } else { + return '-'; + } + } + function other_cols($colname, $attempt){ + static $gradedstatesbyattempt = null, $states =array(); + if ($gradedstatesbyattempt === null){ + //get all the attempt ids we want to display on this page + //or to export for download. + $attemptids = array(); + foreach ($this->rawdata as $attempt){ + if ($attempt->attemptuniqueid > 0){ + $attemptids[] = $attempt->attemptuniqueid; + $states[$attempt->attemptuniqueid] = get_question_states($this->questions, $this->quiz, $attempt); + } + } + $gradedstatesbyattempt = quiz_get_newgraded_states($attemptids, true, 'qs.id, qs.grade, qs.event, qs.question, qs.attempt'); + } + if (preg_match('/^qsanswer([0-9]+)$/', $colname, $matches)){ + $questionid = $matches[1]; + $question = $this->questions[$questionid]; + $stateforqinattempt = $gradedstatesbyattempt[$attempt->attemptuniqueid][$questionid]; + $responses = get_question_actual_response($question, $states[$attempt->attemptuniqueid][$questionid]); + $response = (!empty($responses)? implode(', ',$responses) : '-'); + $grade = $stateforqinattempt->grade; + if (!$this->is_downloading()) { + $format_options = new stdClass; + $format_options->para = false; + $format_options->noclean = true; + $format_options->newlines = false; + if ($grade<= 0) { + $qclass = 'uncorrect'; + } elseif ($grade == 1) { + $qclass = 'correct'; + } else { + $qclass = 'partialcorrect'; + } + return ''.format_text($response, FORMAT_MOODLE, $format_options).''; + } else { + return format_text($response, FORMAT_MOODLE, $format_options); + } + } else { + return NULL; + } + } + + function col_feedbacktext($attempt){ + if ($attempt->timefinish) { + if (!$this->is_downloading()) { + return quiz_report_feedback_for_grade(quiz_rescale_grade($attempt->sumgrades, $this->quiz), $this->quiz->id); + } else { + return strip_tags(quiz_report_feedback_for_grade(quiz_rescale_grade($attempt->sumgrades, $this->quiz), $this->quiz->id)); + } + } else { + return '-'; + } + + } + + function query_db($pagesize, $useinitialsbar=true){ + // Add table joins so we can sort by question answer + // unfortunately can't join all tables necessary to fetch all answers + // to get the state for one question per attempt row we must join two tables + // and there is a limit to how many joins you can have in one query. In MySQL it + // is 61. This means that when having more than 29 questions the query will fail. + // So we join just the tables needed to sort the attempts. + if($sort = $this->get_sql_sort()) { + $this->sql->from .= ' '; + $sortparts = explode(',', $sort); + $matches = array(); + foreach($sortparts as $sortpart) { + $sortpart = trim($sortpart); + if (preg_match('/^qsanswer([0-9]+)/', $sortpart, $matches)){ + $qid = intval($matches[1]); + $this->sql->fields .= ", qs$qid.grade AS qsgrade$qid, qs$qid.answer AS qsanswer$qid, qs$qid.event AS qsevent$qid, qs$qid.id AS qsid$qid"; + $this->sql->from .= "LEFT JOIN {question_sessions} qns$qid ON qns$qid.attemptid = qa.uniqueid AND qns$qid.questionid = :qid$qid "; + $this->sql->from .= "LEFT JOIN {question_states} qs$qid ON qs$qid.id = qns$qid.newgraded "; + $this->sql->params['qid'.$qid] = $qid; + } + } + } + parent::query_db($pagesize, $useinitialsbar); + } +} +?> \ No newline at end of file diff --git a/mod/quiz/report/responses/responsessettings_form.php b/mod/quiz/report/responses/responsessettings_form.php new file mode 100644 index 0000000000..c77e129378 --- /dev/null +++ b/mod/quiz/report/responses/responsessettings_form.php @@ -0,0 +1,43 @@ +libdir/formslib.php"; +class mod_quiz_report_responses_settings extends moodleform { + + function definition() { + global $COURSE; + $mform =& $this->_form; +//------------------------------------------------------------------------------- + $mform->addElement('header', 'preferencespage', get_string('preferencespage', 'quiz_overview')); + + if (!$this->_customdata['currentgroup']){ + $studentsstring = "'".$COURSE->students."'"; + } else { + $a = new object(); + $a->coursestudent = $COURSE->students; + $a->groupname = groups_get_group_name($this->_customdata['currentgroup']); + $studentsstring = get_string('studentingroup', 'quiz_overview', $a); + } + $options = array(); + if (!$this->_customdata['currentgroup']){ + $options[QUIZ_REPORT_ATTEMPTS_ALL] = get_string('optallattempts','quiz_overview'); + } + if ($this->_customdata['currentgroup'] || $COURSE->id != SITEID) { + $options[QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS] = get_string('optallstudents','quiz_overview', $studentsstring); + $options[QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH] = + get_string('optattemptsonly','quiz_overview', $studentsstring); + $options[QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO] = get_string('optnoattemptsonly', 'quiz_overview', $studentsstring); + } + $mform->addElement('select', 'attemptsmode', get_string('show', 'quiz_overview'), $options); + 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)); + } +//------------------------------------------------------------------------------- + $mform->addElement('header', 'preferencesuser', get_string('preferencesuser', 'quiz_overview')); + + $mform->addElement('text', 'pagesize', get_string('pagesize', 'quiz_overview')); + $mform->setType('pagesize', PARAM_INT); + + $this->add_action_buttons(false, get_string('preferencessave', 'quiz_overview')); + } +} +?> \ No newline at end of file diff --git a/mod/quiz/report/responses/styles.css b/mod/quiz/report/responses/styles.css new file mode 100644 index 0000000000..4ae9b26f62 --- /dev/null +++ b/mod/quiz/report/responses/styles.css @@ -0,0 +1,34 @@ +body#mod-quiz-report table#attempts { + margin: 20px auto; +} +body#mod-quiz-report table#attempts .header, +body#mod-quiz-report table#attempts .cell +{ + padding: 4px; +} +body#mod-quiz-report table#attempts .header .commands { + display: inline; +} +body#mod-quiz-report table#attempts td { + border-width: 1px; + border-style: solid; +} +body#mod-quiz-report table#attempts .header { + text-align: left; +} +body#mod-quiz-report table#attempts .numcol { + text-align: center; + vertical-align : middle !important; +} + +body#mod-quiz-report table#attempts .uncorrect { + background-color:#FF9090; +} + +body#mod-quiz-report table#attempts .correct { + background-color:#90FF90; +} + +body#mod-quiz-report table#attempts .partialcorrect { + background-color:#FFFF90; +} -- 2.39.5