if (!($course = $DB->get_record("course", array('id' => $offlinequiz->course)))) { print_error("The course with id {$offlinequiz->course} that the offlinequiz with id {$offlinequiz->id} belongs to is missing"); } if (!($cm = get_coursemodule_from_instance("offlinequiz", $offlinequiz->id, $course->id))) { print_error("The course module for the offlinequiz with id {$offlinequiz->id} is missing"); } // Can only grade finished results. if ($result->status != 'complete') { print_error('resultnotcomplete', 'offlinequiz'); } // Check login and permissions. require_login($course->id, false, $cm); $context = context_module::instance($cm->id); require_capability('mod/offlinequiz:grade', $context); // Load the questions needed by page. if (!($quba = question_engine::load_questions_usage_by_activity($result->usageid))) { print_error('Could not load question usage'); } $slotquestion = $quba->get_question($slot); // Print the page header. $PAGE->set_pagelayout('popup'); echo $OUTPUT->header(); echo $OUTPUT->heading(format_string($slotquestion->name)); // Process any data that was submitted. if (data_submitted() && confirm_sesskey()) { if (optional_param('submit', false, PARAM_BOOL)) { // Set the mark in the quba's slot. $transaction = $DB->start_delegated_transaction(); $quba->process_all_actions(time()); question_engine::save_questions_usage_by_activity($quba); $transaction->allow_commit();
protected function display_grading_interface($slot, $questionid, $grade, $pagesize, $page, $shownames, $showidnumbers, $order, $counts) { global $OUTPUT; 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_form($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), 3); 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. $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'); }
/** * Constructor assuming we already have the necessary data loaded. * * @param object $attempt the row of the quiz_attempts table. * @param object $quiz the quiz object for this attempt and user. * @param object $cm the course_module object for this quiz. * @param object $course the row from the course table for the course we belong to. * @param bool $loadquestions (optional) if true, the default, load all the details * of the state of each question. Else just set up the basic details of the attempt. */ public function __construct($attempt, $quiz, $cm, $course, $loadquestions = true) { $this->attempt = $attempt; $this->quizobj = new quiz($quiz, $cm, $course); if (!$loadquestions) { return; } $this->quba = question_engine::load_questions_usage_by_activity($this->attempt->uniqueid); $this->determine_layout(); $this->number_questions(); }
/** * Constructor assuming we already have the necessary data loaded. * * @param object $attempt the row of the quiz_attempts table. * @param object $quiz the quiz object for this attempt and user. * @param object $cm the course_module object for this quiz. * @param object $course the row from the course table for the course we belong to. * @param bool $loadquestions (optional) if true, the default, load all the details * of the state of each question. Else just set up the basic details of the attempt. */ public function __construct($attempt, $quiz, $cm, $course, $loadquestions = true) { global $DB; $this->attempt = $attempt; $this->quizobj = new quiz($quiz, $cm, $course); if (!$loadquestions) { return; } $this->quba = question_engine::load_questions_usage_by_activity($this->attempt->uniqueid); $this->slots = $DB->get_records('quiz_slots', array('quizid' => $this->get_quizid()), 'slot', 'slot, requireprevious, questionid'); $this->sections = array_values($DB->get_records('quiz_sections', array('quizid' => $this->get_quizid()), 'firstslot')); $this->link_sections_and_slots(); $this->determine_layout(); $this->number_questions(); }
/** * Constructor assuming we already have the necessary data loaded. * * @param object $attempt the row of the reader_attempts table. * @param object $reader the reader object for this attempt and user. * @param object $cm the course_module object for this reader. * @param object $course the row from the course table for the course we belong to. */ public function __construct($attempt, $reader, $cm, $course) { $this->attempt = $attempt; $this->readerobj = reader::create($reader->id, $attempt->userid, $attempt->quizid); $this->quba = question_engine::load_questions_usage_by_activity($this->attempt->uniqueid); $this->determine_layout(); $this->number_questions(); }
/** * (non-PHPdoc) * * @see offlinequiz_default_report::display() */ public function display($offlinequiz, $cm, $course) { global $CFG, $OUTPUT, $SESSION, $DB; // Define some strings. $strtimeformat = get_string('strftimedatetime'); $letterstr = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ'; offlinequiz_load_useridentification(); $offlinequizconfig = get_config('offlinequiz'); // Deal with actions. $action = optional_param('action', '', PARAM_ACTION); // Only print headers if not asked to download data or delete data. if (!($download = optional_param('download', null, PARAM_TEXT)) && !$action == 'delete') { $this->print_header_and_tabs($cm, $course, $offlinequiz, 'overview'); echo $OUTPUT->box_start('linkbox'); echo $OUTPUT->heading(format_string($offlinequiz->name)); echo $OUTPUT->heading(get_string('results', 'offlinequiz')); require_once $CFG->libdir . '/grouplib.php'; echo groups_print_activity_menu($cm, $CFG->wwwroot . '/mod/offlinequiz/report.php?id=' . $cm->id . '&mode=overview', true); echo $OUTPUT->box_end(); echo '<br/>'; } $context = context_module::instance($cm->id); $systemcontext = context_system::instance(); // Set table options. $noresults = optional_param('noresults', 0, PARAM_INT); $pagesize = optional_param('pagesize', 10, PARAM_INT); $groupid = optional_param('group', 0, PARAM_INT); if ($pagesize < 1) { $pagesize = 10; } $answerletters = 'abcdefghijklmnopqrstuvwxyz'; if ($action == 'delete' && confirm_sesskey()) { $selectedresultids = array(); $params = (array) data_submitted(); foreach ($params as $key => $value) { if (preg_match('!^s([0-9]+)$!', $key, $matches)) { $selectedresultids[] = $matches[1]; } } if ($selectedresultids) { foreach ($selectedresultids as $resultid) { if ($resultid && ($todelete = $DB->get_record('offlinequiz_results', array('id' => $resultid)))) { offlinequiz_delete_result($resultid, $context); // Log this event. $params = array('objectid' => $resultid, 'relateduserid' => $todelete->userid, 'context' => context_module::instance($cm->id), 'other' => array('mode' => 'overview')); $event = \mod_offlinequiz\event\attempt_deleted::create($params); $event->trigger(); // Change the status of all related pages with error 'resultexists' to 'suspended'. $user = $DB->get_record('user', array('id' => $todelete->userid)); $group = $DB->get_record('offlinequiz_groups', array('id' => $todelete->offlinegroupid)); $sql = "SELECT id\n FROM {offlinequiz_scanned_pages}\n WHERE offlinequizid = :offlinequizid\n AND userkey = :userkey\n AND groupnumber = :groupnumber\n AND status = 'error'\n AND (error = 'resultexists' OR error = 'differentresultexists')"; $params = array('offlinequizid' => $offlinequiz->id, 'userkey' => $user->{$offlinequizconfig->ID_field}, 'groupnumber' => $group->number); $otherpages = $DB->get_records_sql($sql, $params); foreach ($otherpages as $page) { $DB->set_field('offlinequiz_scanned_pages', 'status', 'suspended', array('id' => $page->id)); $DB->set_field('offlinequiz_scanned_pages', 'error', '', array('id' => $page->id)); } } } offlinequiz_grade_item_update($offlinequiz, 'reset'); offlinequiz_update_grades($offlinequiz); redirect(new moodle_url('/mod/offlinequiz/report.php', array('mode' => 'overview', 'id' => $cm->id, 'noresults' => $noresults, 'pagesize' => $pagesize))); } } // Now check if asked download of data. if ($download) { $filename = clean_filename("{$course->shortname} " . format_string($offlinequiz->name, true)); $sort = ''; } // Fetch the group data. $groups = $DB->get_records('offlinequiz_groups', array('offlinequizid' => $offlinequiz->id), 'number', '*', 0, $offlinequiz->numgroups); // Define table columns. $tablecolumns = array('checkbox', 'picture', 'fullname', $offlinequizconfig->ID_field, 'timestart', 'offlinegroupid', 'sumgrades'); $tableheaders = array('<input type="checkbox" name="toggle" onClick="if (this.checked) {select_all_in(\'DIV\',null,\'tablecontainer\');} else {deselect_all_in(\'DIV\',null,\'tablecontainer\');}"/>', '', get_string('fullname'), get_string($offlinequizconfig->ID_field), get_string('importedon', 'offlinequiz'), get_string('group'), get_string('grade', 'offlinequiz')); $checked = array(); // Get participants list. $withparticipants = false; if ($lists = $DB->get_records('offlinequiz_p_lists', array('offlinequizid' => $offlinequiz->id))) { $withparticipants = true; $tablecolumns[] = 'checked'; $tableheaders[] = get_string('present', 'offlinequiz'); foreach ($lists as $list) { $participants = $DB->get_records('offlinequiz_participants', array('listid' => $list->id)); foreach ($participants as $participant) { $checked[$participant->userid] = $participant->checked; } } } if (!$download) { // Set up the table. $params = array('offlinequiz' => $offlinequiz, 'noresults' => $noresults, 'pagesize' => $pagesize); $table = new offlinequiz_results_table('mod-offlinequiz-report-overview-report', $params); $table->define_columns($tablecolumns); $table->define_headers($tableheaders); $table->define_baseurl($CFG->wwwroot . '/mod/offlinequiz/report.php?mode=overview&id=' . $cm->id . '&noresults=' . $noresults . '&pagesize=' . $pagesize); $table->sortable(true); $table->no_sorting('checkbox'); if ($withparticipants) { $table->no_sorting('checked'); } $table->column_suppress('picture'); $table->column_suppress('fullname'); $table->column_class('picture', 'picture'); $table->column_class($offlinequizconfig->ID_field, 'userkey'); $table->column_class('timestart', 'timestart'); $table->column_class('offlinegroupid', 'offlinegroupid'); $table->column_class('sumgrades', 'sumgrades'); $table->set_attribute('cellpadding', '2'); $table->set_attribute('id', 'attempts'); $table->set_attribute('class', 'generaltable generalbox'); // Start working -- this is necessary as soon as the niceties are over. $table->setup(); } else { if ($download == 'ODS') { require_once "{$CFG->libdir}/odslib.class.php"; $filename .= ".ods"; // Creating a workbook. $workbook = new MoodleODSWorkbook("-"); // Sending HTTP headers. $workbook->send($filename); // Creating the first worksheet. $sheettitle = get_string('reportoverview', 'offlinequiz'); $myxls = $workbook->add_worksheet($sheettitle); // Format types. $format = $workbook->add_format(); $format->set_bold(0); $formatbc = $workbook->add_format(); $formatbc->set_bold(1); $formatbc->set_align('center'); $formatb = $workbook->add_format(); $formatb->set_bold(1); $formaty = $workbook->add_format(); $formaty->set_bg_color('yellow'); $formatc = $workbook->add_format(); $formatc->set_align('center'); $formatr = $workbook->add_format(); $formatr->set_bold(1); $formatr->set_color('red'); $formatr->set_align('center'); $formatg = $workbook->add_format(); $formatg->set_bold(1); $formatg->set_color('green'); $formatg->set_align('center'); // Here starts workshhet headers. $headers = array(get_string($offlinequizconfig->ID_field), get_string('firstname'), get_string('lastname'), get_string('importedon', 'offlinequiz'), get_string('group'), get_string('grade', 'offlinequiz')); if (!empty($withparticipants)) { $headers[] = get_string('present', 'offlinequiz'); } $colnum = 0; foreach ($headers as $item) { $myxls->write(0, $colnum, $item, $formatbc); $colnum++; } $rownum = 1; } else { if ($download == 'Excel') { require_once "{$CFG->libdir}/excellib.class.php"; $filename .= ".xls"; // Creating a workbook. $workbook = new MoodleExcelWorkbook("-"); // Sending HTTP headers. $workbook->send($filename); // Creating the first worksheet. $sheettitle = get_string('results', 'offlinequiz'); $myxls = $workbook->add_worksheet($sheettitle); // Format types. $format = $workbook->add_format(); $format->set_bold(0); $formatbc = $workbook->add_format(); $formatbc->set_bold(1); $formatbc->set_align('center'); $formatb = $workbook->add_format(); $formatb->set_bold(1); $formaty = $workbook->add_format(); $formaty->set_bg_color('yellow'); $formatc = $workbook->add_format(); $formatc->set_align('center'); $formatr = $workbook->add_format(); $formatr->set_bold(1); $formatr->set_color('red'); $formatr->set_align('center'); $formatg = $workbook->add_format(); $formatg->set_bold(1); $formatg->set_color('green'); $formatg->set_align('center'); // Here starts worksheet headers. $headers = array(get_string($offlinequizconfig->ID_field), get_string('firstname'), get_string('lastname'), get_string('importedon', 'offlinequiz'), get_string('group'), get_string('grade', 'offlinequiz')); if (!empty($withparticipants)) { $headers[] = get_string('present', 'offlinequiz'); } $colnum = 0; foreach ($headers as $item) { $myxls->write(0, $colnum, $item, $formatbc); $colnum++; } $rownum = 1; } else { if ($download == 'CSV') { $filename .= ".csv"; header("Content-Encoding: UTF-8"); header("Content-Type: text/csv; charset=utf-8"); header("Content-Disposition: attachment; filename=\"{$filename}\""); header("Expires: 0"); header("Cache-Control: must-revalidate,post-check=0,pre-check=0"); header("Pragma: public"); echo ""; // UTF-8 BOM. $headers = get_string($offlinequizconfig->ID_field) . ", " . get_string('fullname') . ", " . get_string('importedon', 'offlinequiz') . ", " . get_string('group') . ", " . get_string('grade', 'offlinequiz'); if (!empty($withparticipants)) { $headers .= ", " . get_string('present', 'offlinequiz'); } echo $headers . " \n"; } else { if ($download == 'CSVplus1' || $download == 'CSVpluspoints') { $filename .= ".csv"; header("Content-Encoding: UTF-8"); header("Content-Type: text/csv; charset=utf-8"); header("Content-Disposition: attachment; filename=\"{$filename}\""); header("Expires: 0"); header("Cache-Control: must-revalidate,post-check=0,pre-check=0"); header("Pragma: public"); echo ""; // UTF-8 BOM. // Print the table headers. echo get_string('firstname') . ',' . get_string('lastname') . ',' . get_string($offlinequizconfig->ID_field) . ',' . get_string('group'); $maxquestions = offlinequiz_get_maxquestions($offlinequiz, $groups); for ($i = 0; $i < $maxquestions; $i++) { echo ', ' . get_string('question') . ' ' . ($i + 1); } echo "\n"; // Print the correct answer bit-strings. foreach ($groups as $group) { if ($group->templateusageid) { $quba = question_engine::load_questions_usage_by_activity($group->templateusageid); $slots = $quba->get_slots(); echo ', ,' . get_string('correct', 'offlinequiz'); echo ',' . $group->number; foreach ($slots as $slot) { $slotquestion = $quba->get_question($slot); $qtype = $slotquestion->get_type_name(); if ($qtype == 'multichoice' || $qtype == 'multichoiceset') { $attempt = $quba->get_question_attempt($slot); $order = $slotquestion->get_order($attempt); // Order of the answers. $tempstr = ","; $letters = array(); $counter = 0; foreach ($order as $key => $answerid) { $fraction = $DB->get_field('question_answers', 'fraction', array('id' => $answerid)); if ($fraction > 0) { $letters[] = $answerletters[$counter]; } $counter++; } if (empty($letters)) { $tempstr .= '99'; } else { $tempstr .= implode('/', $letters); } echo $tempstr; } } echo "\n"; } } } } } } } $coursecontext = context_course::instance($course->id); $contextids = $coursecontext->get_parent_context_ids(true); // Construct the SQL // First get roleids for students from leagcy. if (!($roles = get_roles_with_capability('mod/offlinequiz:attempt', CAP_ALLOW, $systemcontext))) { error("No roles with capability 'moodle/offlinequiz:attempt' defined in system context"); } $roleids = array(); foreach ($roles as $role) { $roleids[] = $role->id; } $rolelist = implode(',', $roleids); $select = "SELECT " . $DB->sql_concat('u.id', "'#'", "COALESCE(qa.usageid, 0)") . " AS uniqueid,\n qa.id AS resultid, u.id, qa.usageid, qa.offlinegroupid, qa.status,\n u.id AS userid, u.firstname, u.lastname,\n u.alternatename, u.middlename, u.firstnamephonetic, u.lastnamephonetic,\n u.picture, u." . $offlinequizconfig->ID_field . ",\n qa.sumgrades, qa.timefinish, qa.timestart, qa.timefinish - qa.timestart AS duration "; $result = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'ctx'); list($contexttest, $cparams) = $result; list($roletest, $rparams) = $DB->get_in_or_equal($roleids, SQL_PARAMS_NAMED, 'role'); $from = "FROM {user} u\n JOIN {role_assignments} ra ON ra.userid = u.id\n LEFT JOIN {offlinequiz_results} qa ON u.id = qa.userid AND qa.offlinequizid = :offlinequizid\n "; $where = " WHERE ra.contextid {$contexttest} AND ra.roleid {$roletest} "; $params = array('offlinequizid' => $offlinequiz->id); $params = array_merge($params, $cparams, $rparams); if ($groupid) { $from .= " JOIN {groups_members} gm ON gm.userid = u.id "; $where .= " AND gm.groupid = :groupid "; $params['groupid'] = $groupid; } if (empty($noresults)) { $where = $where . " AND qa.userid IS NOT NULL\n AND qa.status = 'complete'"; // Show ONLY students with results. } else { if ($noresults == 1) { // The value noresults = 1 means only no results, so make the left join ask for only records // where the right is null (no results). $where .= ' AND qa.userid IS NULL'; // Show ONLY students without results. } else { if ($noresults == 3) { // We want all results, also the partial ones. $from = "FROM {user} u\n JOIN {offlinequiz_results} qa ON u.id = qa.userid "; $where = " WHERE qa.offlinequizid = :offlinequizid"; } } } // The value noresults = 2 means we want all students, with or without results. $countsql = 'SELECT COUNT(DISTINCT(u.id)) ' . $from . $where; if (!$download) { // Count the records NOW, before funky question grade sorting messes up $from. $totalinitials = $DB->count_records_sql($countsql, $params); // Add extra limits due to initials bar. list($ttest, $tparams) = $table->get_sql_where(); if (!empty($ttest)) { $where .= ' AND ' . $ttest; $countsql .= ' AND ' . $ttest; $params = array_merge($params, $tparams); } $total = $DB->count_records_sql($countsql, $params); // Add extra limits due to sorting by question grade. $sort = $table->get_sql_sort(); // Fix some wired sorting. if (empty($sort)) { $sort = ' ORDER BY u.lastname'; } else { $sort = ' ORDER BY ' . $sort; } $table->pagesize($pagesize, $total); } // Fetch the results. if (!$download) { $results = $DB->get_records_sql($select . $from . $where . $sort, $params, $table->get_page_start(), $table->get_page_size()); } else { $results = $DB->get_records_sql($select . $from . $where . $sort, $params); } // Build table rows. if (!$download) { $table->initialbars(true); } if (!empty($results) || !empty($noresults)) { foreach ($results as $result) { $user = $DB->get_record('user', array('id' => $result->userid)); $picture = $OUTPUT->user_picture($user, array('courseid' => $course->id)); if (!empty($result->resultid)) { $checkbox = '<input type="checkbox" name="s' . $result->resultid . '" value="' . $result->resultid . '" />'; } else { $checkbox = ''; } if (!empty($result) && (empty($result->resultid) || $result->timefinish == 0)) { $resultdate = '-'; } else { $resultdate = userdate($result->timefinish, $strtimeformat); } if (!empty($result) && $result->offlinegroupid) { $groupletter = $letterstr[$groups[$result->offlinegroupid]->number]; } else { $groupletter = '-'; } $userlink = '<a href="' . $CFG->wwwroot . '/user/view.php?id=' . $result->userid . '&course=' . $course->id . '">' . fullname($result) . '</a>'; if (!$download) { $row = array($checkbox, $picture, $userlink, $result->{$offlinequizconfig->ID_field}, $resultdate, $groupletter); } else { $row = array($result->{$offlinequizconfig->ID_field}, $result->firstname, $result->lastname, $resultdate, $groupletter); } if (!empty($result) && $result->offlinegroupid) { $outputgrade = format_float($result->sumgrades / $groups[$result->offlinegroupid]->sumgrades * $offlinequiz->grade, $offlinequiz->decimalpoints); } else { $outputgrade = '-'; } if (!$download) { if ($result->status == 'partial') { $row[] = get_string('partial', 'offlinequiz'); } else { if ($result->sumgrades === null) { $row[] = '-'; } else { $row[] = '<a href="review.php?q=' . $offlinequiz->id . '&resultid=' . $result->resultid . '">' . $outputgrade . '</a>'; } } if ($withparticipants) { $row[] = !empty($checked[$result->userid]) ? "<img src=\"{$CFG->wwwroot}/mod/offlinequiz/pix/tick.gif\" alt=\"" . get_string('ischecked', 'offlinequiz') . "\">" : "<img src=\"{$CFG->wwwroot}/mod/offlinequiz/pix/cross.gif\" alt=\"" . get_string('isnotchecked', 'offlinequiz') . "\">"; } } else { if ($download != 'CSVplus1' || $download == 'CSVpluspoints') { $row[] = $result->sumgrades === null ? '-' : $outputgrade; if ($withparticipants) { if (array_key_exists($result->userid, $checked)) { $row[] = $checked[$result->userid] ? get_string('ok') : '-'; } else { $row[] = '-'; } } } } if (!$download) { $table->add_data($row); } else { if ($download == 'Excel' or $download == 'ODS') { $colnum = 0; foreach ($row as $item) { $myxls->write($rownum, $colnum, $item, $format); $colnum++; } $rownum++; } else { if ($download == 'CSV') { $text = implode(',', $row); echo $text . "\n"; } else { if ($download == 'CSVplus1' || $download == 'CSVpluspoints') { $text = $row[1] . ',' . $row[2] . ',' . $row[0] . ',' . $groups[$result->offlinegroupid]->number; if ($pages = $DB->get_records('offlinequiz_scanned_pages', array('resultid' => $result->resultid), 'pagenumber ASC')) { foreach ($pages as $page) { if ($page->status == 'ok' || $page->status == 'submitted') { $choices = $DB->get_records('offlinequiz_choices', array('scannedpageid' => $page->id), 'slotnumber, choicenumber'); $counter = 0; $oldslot = -1; $letters = array(); foreach ($choices as $choice) { if ($oldslot == -1) { $oldslot = $choice->slotnumber; } else { if ($oldslot != $choice->slotnumber) { if (empty($letters)) { $text .= ',99'; } else { $text .= ',' . implode('/', $letters); } $counter = 0; $oldslot = $choice->slotnumber; $letters = array(); } } if ($choice->value == 1) { $letters[] = $answerletters[$counter]; } $counter++; } if (empty($letters)) { $text .= ',99'; } else { $text .= ',' . implode('/', $letters); } } } } echo $text . "\n"; if ($download == 'CSVpluspoints') { $text = $row[1] . ',' . $row[2] . ',' . $row[0] . ',' . $groups[$result->offlinegroupid]->number; $quba = question_engine::load_questions_usage_by_activity($result->usageid); $slots = $quba->get_slots(); foreach ($slots as $slot) { $slotquestion = $quba->get_question($slot); $attempt = $quba->get_question_attempt($slot); $text .= ',' . format_float($attempt->get_mark(), $offlinequiz->decimalpoints, false); } echo $text . "\n"; } } } } } } // End foreach ($results... } else { if (!$download) { $table->print_initials_bar(); } } if (!$download) { // Print table. $table->finish_html(); if (!empty($results)) { echo '<form id="downloadoptions" action="report.php" method="get">'; echo ' <input type="hidden" name="id" value="' . $cm->id . '" />'; echo ' <input type="hidden" name="q" value="' . $offlinequiz->id . '" />'; echo ' <input type="hidden" name="mode" value="overview" />'; echo ' <input type="hidden" name="noheader" value="yes" />'; echo ' <table class="boxaligncenter"><tr><td>'; $options = array('Excel' => get_string('excelformat', 'offlinequiz'), 'ODS' => get_string('odsformat', 'offlinequiz'), 'CSV' => get_string('csvformat', 'offlinequiz'), 'CSVplus1' => get_string('csvplus1format', 'offlinequiz'), 'CSVpluspoints' => get_string('csvpluspointsformat', 'offlinequiz')); print_string('downloadresultsas', 'offlinequiz'); echo "</td><td>"; echo html_writer::select($options, 'download', '', false); echo ' <input type="submit" value="' . get_string('download') . '" />'; echo ' <script type="text/javascript">' . "\n<!--\n" . 'document.getElementById("noscriptmenuaction").style.display = "none";' . "\n-->\n" . '</script>'; echo " </td>\n"; echo "<td>"; echo "</td>\n"; echo '</tr></table></form>'; } } else { if ($download == 'Excel' || $download == 'ODS') { $workbook->close(); exit; } else { if ($download == 'CSV' || $download == 'CSVplus1' || $download == 'CSVpluspoints') { exit; } } } // Print display options. echo '<div class="controls">'; echo '<form id="options" action="report.php" method="get">'; echo '<div class=centerbox>'; echo '<p>' . get_string('displayoptions', 'offlinequiz') . ': </p>'; echo '<input type="hidden" name="id" value="' . $cm->id . '" />'; echo '<input type="hidden" name="q" value="' . $offlinequiz->id . '" />'; echo '<input type="hidden" name="mode" value="overview" />'; echo '<input type="hidden" name="detailedmarks" value="0" />'; echo '<table id="overview-options" class="boxaligncenter">'; echo '<tr align="left">'; echo '<td><label for="pagesize">' . get_string('pagesizeparts', 'offlinequiz') . '</label></td>'; echo '<td><input type="text" id="pagesize" name="pagesize" size="3" value="' . $pagesize . '" /></td>'; echo '</tr>'; echo '<tr align="left">'; echo '<td colspan="2">'; $options = array(); $options[] = get_string('attemptsonly', 'offlinequiz'); $options[] = get_string('noattemptsonly', 'offlinequiz'); $options[] = get_string('allstudents', 'offlinequiz'); $options[] = get_string('allresults', 'offlinequiz'); echo html_writer::select($options, 'noresults', $noresults, ''); echo '</td></tr>'; echo '<tr><td colspan="2" align="center">'; echo '<input type="submit" value="' . get_string('go') . '" />'; echo '</td></tr></table>'; echo '</div>'; echo '</form>'; echo '</div>'; echo "\n"; return true; }
/** * Called via pluginfile.php -> question_pluginfile to serve files belonging to * a question in a question_attempt when that attempt is a preview. * * @param object $course course settings object * @param object $context context object * @param string $component the name of the component we are serving files for. * @param string $filearea the name of the file area. * @param int $qubaid the question_usage this image belongs to. * @param int $slot the relevant slot within the usage. * @param array $args the remaining bits of the file path. * @param bool $forcedownload whether the user must be forced to download the file. * @return bool false if file not found, does not return if found - justsend the file */ function question_preview_question_pluginfile($course, $context, $component, $filearea, $qubaid, $slot, $args, $forcedownload) { global $USER, $DB, $CFG; $quba = question_engine::load_questions_usage_by_activity($qubaid); if (!question_has_capability_on($quba->get_question($slot), 'use')) { send_file_not_found(); } $options = new question_display_options(); $options->feedback = question_display_options::VISIBLE; $options->numpartscorrect = question_display_options::VISIBLE; $options->generalfeedback = question_display_options::VISIBLE; $options->rightanswer = question_display_options::VISIBLE; $options->manualcomment = question_display_options::VISIBLE; $options->history = question_display_options::VISIBLE; if (!$quba->check_file_access($slot, $options, $component, $filearea, $args, $forcedownload)) { send_file_not_found(); } $fs = get_file_storage(); $relativepath = implode('/', $args); $fullpath = "/{$context->id}/{$component}/{$filearea}/{$relativepath}"; if (!($file = $fs->get_file_by_hash(sha1($fullpath))) or $file->is_directory()) { send_file_not_found(); } send_stored_file($file, 0, 0, $forcedownload); }
/** * Regrade a particular offlinequiz result. Either for real ($dryrun = false), or * as a pretend regrade to see which fractions would change. The outcome is * stored in the offlinequiz_overview_regrades table. * * Note, $result is not upgraded in the database. The caller needs to do that. * However, $result->sumgrades is updated, if this is not a dry run. * * @param object $result the offlinequiz result to regrade. * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real. * @param array $slots if null, regrade all questions, otherwise, just regrade * the quetsions with those slots. */ protected function regrade_result($result, $questions, $dryrun = false, $slots = null) { global $DB; $transaction = $DB->start_delegated_transaction(); $quba = question_engine::load_questions_usage_by_activity($result->usageid); if (is_null($slots)) { $slots = $quba->get_slots(); } $changed = false; $finished = true; foreach ($slots as $slot) { $qqr = new stdClass(); $qqr->oldfraction = $quba->get_question_fraction($slot); $slotquestion = $quba->get_question($slot); $newmaxmark = $questions[$slotquestion->id]->maxmark; $quba->regrade_question($slot, $finished, $newmaxmark); $qqr->newfraction = $quba->get_question_fraction($slot); if (abs($qqr->oldfraction - $qqr->newfraction) > 1.0E-7) { $changed = true; } } if (!$dryrun) { question_engine::save_questions_usage_by_activity($quba); } $transaction->allow_commit(); return $changed; }
/** * Processes a set of Scantron-formatted responses, creating a quiz attempt, as though the user had entered these answers into Moodle directly. * * @param array $set An associative array of data read off of a Scantron form. Known to work for the scantron form 223127; likely works for others. * @param bool $overwrite If set, imported responses will be allowed to overwrite existsing quiz attempts with the same unique id (QUBA id). * @param bool $allow_cross_user If set, allows a quiz attempt to move from one user to another (i.e. if the student had entered in the wrong ID number.) */ protected function enter_scantron_responses($set, $overwrite = false, $allow_cross_user = false, $finish = true, $error_if_not_first = false, $force_first = false) { global $DB; //if no usage ID has been specified, throw an exception if (!array_key_exists('Special Codes', $set)) { if (array_key_exists('Student Name', $set) || array_key_exists('ID', $set)) { throw new quiz_papercopy_invalid_usage_id_exception(); } else { throw new quiz_papercopy_benign_row_exception(); } } //get the usage ID from the Special Codes field on the scantron $usage_id = intval($set['Special Codes']); //get the ID for the attempt that would be created $new_id = $this->user_id_from_scantron($set); //if we need this to be the first attempt, check for an existing attempt by this user at the current quiz if ($error_if_not_first || $force_first) { //if an attempt exists with both this user_id and quiz ID $existing_attempt = $DB->get_record('quiz_attempts', array('userid' => $new_id, 'quiz' => $this->quiz->id)); //if this isn't allowed to be a subsequent attempt, throw an exception if ($error_if_not_first) { throw new quiz_papercopy_not_first_attempt_when_required(); } elseif ($force_first) { $DB->delete_records('quiz_attempts', array('userid' => $new_id, 'quiz' => $this->quiz->id)); } } //check for any attempt that uses this usage $existing_record = $DB->get_record('quiz_attempts', array('uniqueid' => $usage_id), 'userid', IGNORE_MISSING); //if one exists, handle the overwrite cases if ($existing_record) { //if we're trying to assign the same record to a different usage, and we haven't explicitly allowed cross-user overwrites, throw an exception if ($new_id != $existing_record->userid && !$allow_cross_user) { throw new quiz_papercopy_conflicting_users_exception(); } //if overwrite is enabled, remove the existing attempt if ($overwrite) { $DB->delete_records('quiz_attempts', array('uniqueid' => $usage_id)); } else { throw new quiz_papercopy_attempt_exists_exception(); } } try { //get a usage object from the Special Codes usage ID $usage = question_engine::load_questions_usage_by_activity($usage_id); } catch (coding_exception $e) { //if we couldn't load that usage, throw an "invalid usage id" error throw new quiz_papercopy_invalid_usage_id_exception(); } //get an associative array, which indicates the order in which questions were $slots = $usage->get_slots(); //enter the student's answers for each of the questions foreach ($slots as $slot) { $letter2number = array('A' => 1, 'B' => 2, 'C' => 3, 'D' => 4); if (array_key_exists($set['Question' . $slot], $letter2number)) { $set['Question' . $slot] = $letter2number[$set['Question' . $slot]]; } $usage->process_action($slot, array('answer' => $set['Question' . $slot] - 1)); } //set the attempt's owner to reflect the student who filled out the scantron $target_user = $this->user_id_from_scantron($set); //create a new attempt object, if requested, immediately close it, grading the attempt // $attempt = $this->build_attempt_from_usage($usage, $target_user, $finish, true); $attempt = $this->build_attempt_from_usage($usage, $target_user, true, true); //return the user's grade and id, on success return array('grade' => $attempt->sumgrades, 'user' => $attempt->userid); }
/** * Retrieves a template question usage for an offline group. Creates a new template if there is none. * While creating question usage it shuffles the group questions if shuffleanswers is created. * * @param object $offlinequiz * @param object $group * @param object $context * @return question_usage_by_activity */ function offlinequiz_get_group_template_usage($offlinequiz, $group, $context) { global $CFG, $DB; if (!empty($group->templateusageid) && $group->templateusageid > 0) { $templateusage = question_engine::load_questions_usage_by_activity($group->templateusageid); } else { $questionids = offlinequiz_get_group_question_ids($offlinequiz, $group->id); if ($offlinequiz->shufflequestions) { $offlinequiz->groupid = $group->id; $questionids = offlinequiz_shuffle_questions($questionids); } // We have to use our own class s.t. we can use the clone function to create results. $templateusage = offlinequiz_make_questions_usage_by_activity('mod_offlinequiz', $context); $templateusage->set_preferred_behaviour('immediatefeedback'); if (!$questionids) { print_error(get_string('noquestionsfound', 'offlinequiz'), 'view.php?q=' . $offlinequiz->id); } // Gets database raw data for the questions. $questiondata = question_load_questions($questionids); // Get the question instances for initial markmarks. $sql = "SELECT questionid, maxmark\n FROM {offlinequiz_group_questions}\n WHERE offlinequizid = :offlinequizid\n AND offlinegroupid = :offlinegroupid "; $groupquestions = $DB->get_records_sql($sql, array('offlinequizid' => $offlinequiz->id, 'offlinegroupid' => $group->id)); foreach ($questionids as $questionid) { if ($questionid) { // Convert the raw data of multichoice questions to a real question definition object. if (!$offlinequiz->shuffleanswers) { $questiondata[$questionid]->options->shuffleanswers = false; } $question = question_bank::make_question($questiondata[$questionid]); // We only add multichoice questions which are needed for grading. if ($question->get_type_name() == 'multichoice' || $question->get_type_name() == 'multichoiceset') { $templateusage->add_question($question, $groupquestions[$question->id]->maxmark); } } } // Create attempts for all questions (fixes order of the answers if shuffleanswers is active). $templateusage->start_all_questions(); // Save the template question usage to the DB. question_engine::save_questions_usage_by_activity($templateusage); // Save the templateusage-ID in the offlinequiz_groups table. $group->templateusageid = $templateusage->get_id(); $DB->set_field('offlinequiz_groups', 'templateusageid', $group->templateusageid, array('id' => $group->id)); } // End else. return $templateusage; }
/** * Construct the class. if a dbattempt object is passed in set it, otherwise initialize empty class * * @param questionmanager $questionmanager * @param \stdClass * @param \context_module $context */ public function __construct($questionmanager, $dbattempt = null, $context = null) { $this->questionmanager = $questionmanager; $this->context = $context; // if empty create new attempt if (empty($dbattempt)) { $this->attempt = new \stdClass(); // create a new quba since we're creating a new attempt $this->quba = \question_engine::make_questions_usage_by_activity('mod_activequiz', $this->questionmanager->getRTQ()->getContext()); $this->quba->set_preferred_behaviour('immediatefeedback'); $attemptlayout = $this->questionmanager->add_questions_to_quba($this->quba); // add the attempt layout to this instance $this->attempt->qubalayout = implode(',', $attemptlayout); } else { // else load it up in this class instance $this->attempt = $dbattempt; $this->quba = \question_engine::load_questions_usage_by_activity($this->attempt->questionengid); } }
/** * Checks whether a given result is complete, i.e. all contributing scanned pages have been submitted. * Updates the result in the DB if it is complete. Also updates the scanned pages that were duplicates from * 'doublepage' to 'resultexists' * * @param object $offlinequiz * @param object $group * @param object $result * @return boolean */ function offlinequiz_check_result_completed($offlinequiz, $group, $result) { global $DB; $resultpages = $DB->get_records_sql("SELECT *\n FROM {offlinequiz_scanned_pages}\n WHERE resultid = :resultid\n AND status = 'submitted'", array('resultid' => $result->id)); if (count($resultpages) == $group->numberofpages) { $transaction = $DB->start_delegated_transaction(); $quba = question_engine::load_questions_usage_by_activity($result->usageid); $quba->finish_all_questions(time()); $totalmark = $quba->get_total_mark(); question_engine::save_questions_usage_by_activity($quba); $result->sumgrades = $totalmark; $result->status = 'complete'; $result->timestart = time(); $result->timefinish = time(); $result->timemodified = time(); $DB->update_record('offlinequiz_results', $result); $transaction->allow_commit(); offlinequiz_update_grades($offlinequiz, $result->userid); // Change the error of all submitted pages of the result to '' (was 'missingpages' before). foreach ($resultpages as $page) { $DB->set_field('offlinequiz_scanned_pages', 'error', '', array('id' => $page->id)); } // Change the status of all double pages of the user to 'resultexists'. $offlinequizconfig = get_config('offlinequiz'); $user = $DB->get_record('user', array('id' => $result->userid)); $sql = "SELECT id\n FROM {offlinequiz_scanned_pages}\n WHERE offlinequizid = :offlinequizid\n AND userkey = :userkey\n AND groupnumber = :groupnumber\n AND error = 'doublepage'"; $params = array('offlinequizid' => $offlinequiz->id, 'userkey' => $user->{$offlinequizconfig->ID_field}, 'groupnumber' => $group->number); $doublepages = $DB->get_records_sql($sql, $params); foreach ($doublepages as $page) { $DB->set_field('offlinequiz_scanned_pages', 'error', 'resultexists', array('id' => $page->id)); $DB->set_field('offlinequiz_scanned_pages', 'resultid', 0, array('id' => $page->id)); } return true; } return false; }
public function update_all_results_and_logs($offlinequiz) { global $DB, $CFG; $this->prevent_timeout(); // Now we have to migrate offlinequiz_attempts to offlinequiz_results because // we need the new result IDs for the scannedpages. // Get all attempts that have already been migrated to the new question engine. $attempts = $DB->get_records('offlinequiz_attempts', array('offlinequiz' => $offlinequiz->id, 'needsupgradetonewqe' => 0, 'sheet' => 0)); $groups = $DB->get_records('offlinequiz_groups', array('offlinequizid' => $offlinequiz->id), 'number', '*', 0, $offlinequiz->numgroups); list($maxquestions, $maxanswers, $formtype, $questionsperpage) = offlinequiz_get_question_numbers($offlinequiz, $groups); $transaction = $DB->start_delegated_transaction(); foreach ($attempts as $attempt) { $group = $DB->get_record('offlinequiz_groups', array('offlinequizid' => $offlinequiz->id, 'number' => $attempt->groupid)); $attemptlog = $DB->get_record('offlinequiz_i_log', array('offlinequiz' => $offlinequiz->id, 'attempt' => $attempt->id, 'page' => 0)); $result = new StdClass(); $result->offlinequizid = $offlinequiz->id; if ($group) { $result->offlinegroupid = $group->id; } $teacherid = $attemptlog->importadmin; if (empty($teacherid)) { $teacherid = 2; } $result->userid = $attempt->userid; $result->sumgrades = $attempt->sumgrades; $result->usageid = $attempt->uniqueid; $result->teacherid = $teacherid; $result->offlinegroupid = $group->id; $result->status = 'complete'; $result->timestart = $attempt->timestart; $result->timefinish = $attempt->timefinish; $result->timemodified = $attempt->timemodified; if (!($oldresult = $DB->get_record('offlinequiz_results', array('offlinequizid' => $result->offlinequizid, 'userid' => $result->userid)))) { $result->id = $DB->insert_record('offlinequiz_results', $result); } else { $result->id = $oldresult->id; $DB->update_record('offlinequiz_results', $result); } // Save the resultid, s.t. we can still reconstruct the data later. $DB->set_field('offlinequiz_attempts', 'resultid', $result->id, array('id' => $attempt->id)); if ($quba = question_engine::load_questions_usage_by_activity($result->usageid)) { $quba->finish_all_questions(); $slots = $quba->get_slots(); // Get all the page logs that have contributed to the attempt. if ($group->numberofpages == 1) { $pagelogs = array($attemptlog); } else { $sql = "SELECT *\n FROM {offlinequiz_i_log}\n WHERE offlinequiz = :offlinequizid\n AND attempt = :attemptid\n AND page > 0"; $params = array('offlinequizid' => $offlinequiz->id, 'attemptid' => $attempt->id); $pagelogs = $DB->get_records_sql($sql, $params); } foreach ($pagelogs as $pagelog) { $rawdata = $pagelog->rawdata; $scannedpage = new StdClass(); $scannedpage->offlinequizid = $offlinequiz->id; $scannedpage->resultid = $result->id; $scannedpage->filename = $this->get_pic_name($rawdata); $scannedpage->groupnumber = $this->get_group($rawdata); $scannedpage->userkey = $this->get_user_name($rawdata); if ($group->numberofpages == 1) { $scannedpage->pagenumber = 1; } else { $scannedpage->pagenumber = $pagelog->page; } $scannedpage->time = $pagelog->time ? $pagelog->time : time(); $scannedpage->status = 'submitted'; $scannedpage->error = ''; $scannedpage->id = $DB->insert_record('offlinequiz_scanned_pages', $scannedpage); $itemdata = $this->get_item_data($rawdata); $items = explode(',', $itemdata); if (!empty($items)) { // Determine the slice of slots we are interested in. // we start at the top of the page (e.g. 0, 96, etc). $startindex = min(($scannedpage->pagenumber - 1) * $questionsperpage, count($slots)); // We end on the bottom of the page or when the questions are gone (e.g., 95, 105). $endindex = min($scannedpage->pagenumber * $questionsperpage, count($slots)); $questioncounter = 0; for ($slotindex = $startindex; $slotindex < $endindex; $slotindex++) { $slot = $slots[$slotindex]; if (array_key_exists($questioncounter, $items)) { $item = $items[$questioncounter]; for ($key = 0; $key < strlen($item); $key++) { $itemchoice = substr($item, $key, 1); $choice = new stdClass(); $choice->scannedpageid = $scannedpage->id; $choice->slotnumber = $slot; $choice->choicenumber = $key; if ($itemchoice == '1') { $choice->value = 1; } else { if ($itemchoice == '0') { $choice->value = 0; } else { $choice->value = -1; } } $choice->id = $DB->insert_record('offlinequiz_choices', $choice); } } $questioncounter++; } } $rawcorners = explode(',', $pagelog->corners); if (!empty($rawcorners) && count($rawcorners) > 8) { for ($i = 0; $i < count($rawcorners); $i++) { if ($rawcorners[$i] < 0) { $rawcorners[$i] = 0; } if ($rawcorners[$i] > 2000) { $rawcorners[$i] = 2000; } } $corners = array(); $corners[0] = new oq_point($rawcorners[1], $rawcorners[2]); $corners[1] = new oq_point($rawcorners[3], $rawcorners[4]); $corners[2] = new oq_point($rawcorners[5], $rawcorners[6]); $corners[3] = new oq_point($rawcorners[7], $rawcorners[8]); offlinequiz_save_page_corners($scannedpage, $corners); } } } } $DB->set_field('offlinequiz', 'needsilogupgrade', 0, array('id' => $offlinequiz->id)); $transaction->allow_commit(); // We start a new transaction for the remaining i_log entries. $otherlogs = $DB->get_records('offlinequiz_i_log', array('offlinequiz' => $offlinequiz->id, 'attempt' => 0)); $transaction = $DB->start_delegated_transaction(); foreach ($otherlogs as $pagelog) { list($status, $error) = $this->get_status_and_error($pagelog->error); $rawdata = $pagelog->rawdata; $groupnumber = $this->get_group($rawdata); $intgroup = intval($groupnumber); if ($intgroup > 0 && $intgroup <= $offlinequiz->numgroups) { $groupnumber = $intgroup; } else { $groupnumber = 0; } $scannedpage = new StdClass(); $scannedpage->offlinequizid = $offlinequiz->id; $scannedpage->filename = $this->get_pic_name($rawdata); $scannedpage->groupnumber = $groupnumber; $scannedpage->userkey = $this->get_user_name($rawdata); $scannedpage->pagenumber = $pagelog->page; if ($pagelog->time) { $scannedpage->time = $pagelog->time; } else { $scannedpage->time = time(); } $scannedpage->status = $status; $scannedpage->error = $error; $scannedpage->id = $DB->insert_record('offlinequiz_scanned_pages', $scannedpage); // We do not migrate itemdata for the scanned pages with error. // We do store the corners though. $rawcorners = explode(',', $pagelog->corners); if (!empty($rawcorners) && count($rawcorners) > 8) { $corners = array(); $corners[0] = new oq_point($rawcorners[1], $rawcorners[2]); $corners[1] = new oq_point($rawcorners[3], $rawcorners[4]); $corners[2] = new oq_point($rawcorners[5], $rawcorners[6]); $corners[3] = new oq_point($rawcorners[7], $rawcorners[8]); offlinequiz_save_page_corners($scannedpage, $corners); } } $transaction->allow_commit(); return true; }
private static function quiz_attempt_stepone($attempt, $lastattempt, $quizobj, $timenow, $quba, $attemptnumber) { //global $CFG, $DB; if (!($quizobj->get_quiz()->attemptonlast && $lastattempt)) { // Starting a normal, new, quiz attempt. // Fully load all the questions in this quiz. $quizobj->preload_questions(); $quizobj->load_questions(); // Add them all to the $quba. $questionsinuse = array_keys($quizobj->get_questions()); self::quiz_question_process($attempt, $quizobj, $quba, $questionsinuse, $attemptnumber, $timenow); } else { // Starting a subsequent attempt in each attempt builds on last mode. $oldquba = question_engine::load_questions_usage_by_activity($lastattempt->uniqueid); $oldnumberstonew = array(); foreach ($oldquba->get_attempt_iterator() as $oldslot => $oldqa) { $newslot = $quba->add_question($oldqa->get_question(), $oldqa->get_max_mark()); $quba->start_question_based_on($newslot, $oldqa); $oldnumberstonew[$oldslot] = $newslot; } } }
// Get and validate display options. $maxvariant = $question->get_num_variants(); $options = new question_preview_options($question); $options->load_user_defaults(); $options->set_from_request(); $PAGE->set_url(question_preview_url($id, $options->behaviour, $options->maxmark, $options)); // Get and validate exitsing preview, or start a new one. $previewid = optional_param('previewid', 0, PARAM_INT); if ($previewid) { if (!isset($SESSION->question_previews[$previewid])) { print_error('notyourpreview', 'question'); } try { $quba = question_engine::load_questions_usage_by_activity($previewid); } catch (Exception $e) { print_error('submissionoutofsequencefriendlymessage', 'question', question_preview_url($question->id, $options->behaviour, $options->maxmark, $options), null, $e); } $slot = $quba->get_first_question_number(); $usedquestion = $quba->get_question($slot); if ($usedquestion->id != $question->id) { print_error('questionidmismatch', 'question'); } $question = $usedquestion; $options->variant = $quba->get_variant($slot); } else { $quba = question_engine::make_questions_usage_by_activity('core_question_preview',
/** * Makes an attempt for one student in this quiz, answering all the questions. * * @param $student * @param $quiz * @return int How many questions answers were made. */ public function make_student_quiz_atttempt($student, $quiz) { $submissioncount = 0; $attempt = $this->start_quiz_attempt($quiz->id, $student->id); $quba = question_engine::load_questions_usage_by_activity($attempt->uniqueid); // This bit strips out bits of quiz_attempt::process_submitted_actions() // Simulates data from the form. // TODO iterate over the questions. $formdata = array('answer' => 'Sample essay answer text', 'answerformat' => FORMAT_MOODLE); $slot = 1; // Only 1 question so far. $quba->process_action($slot, $formdata, time()); $submissioncount++; question_engine::save_questions_usage_by_activity($quba); $this->end_quiz_attmept($attempt); return $submissioncount; }
protected function load_quba(moodle_database $db = null) { $this->quba = question_engine::load_questions_usage_by_activity($this->quba->get_id(), $db); }
/** * Regrade a particular quiz attempt. Either for real ($dryrun = false), or * as a pretend regrade to see which fractions would change. The outcome is * stored in the quiz_overview_regrades table. * * Note, $attempt is not upgraded in the database. The caller needs to do that. * However, $attempt->sumgrades is updated, if this is not a dry run. * * @param object $attempt the quiz attempt to regrade. * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real. * @param array $slots if null, regrade all questions, otherwise, just regrade * the quetsions with those slots. */ protected function regrade_attempt($attempt, $dryrun = false, $slots = null) { global $DB; // Need more time for a quiz with many questions. set_time_limit(300); $transaction = $DB->start_delegated_transaction(); $quba = question_engine::load_questions_usage_by_activity($attempt->uniqueid); if (is_null($slots)) { $slots = $quba->get_slots(); } $finished = $attempt->state == quiz_attempt::FINISHED; foreach ($slots as $slot) { $qqr = new stdClass(); $qqr->oldfraction = $quba->get_question_fraction($slot); $quba->regrade_question($slot, $finished); $qqr->newfraction = $quba->get_question_fraction($slot); if (abs($qqr->oldfraction - $qqr->newfraction) > 1.0E-7) { $qqr->questionusageid = $quba->get_id(); $qqr->slot = $slot; $qqr->regraded = empty($dryrun); $qqr->timemodified = time(); $DB->insert_record('quiz_overview_regrades', $qqr, false); } } if (!$dryrun) { question_engine::save_questions_usage_by_activity($quba); } $transaction->allow_commit(); // Really, PHP should not need this hint, but without this, we just run out of memory. $quba = null; $transaction = null; gc_collect_cycles(); }
$variantoffset = $attemptnumber; } $quba->start_all_questions(new question_variant_pseudorandom_no_repeats_strategy($variantoffset), $timenow); // Update attempt layout. $newlayout = array(); foreach (explode(',', $attempt->layout) as $qid) { if ($qid != 0) { $newlayout[] = $idstoslots[$qid]; } else { $newlayout[] = 0; } } $attempt->layout = implode(',', $newlayout); } else { // Starting a subsequent attempt in each attempt builds on last mode. $oldquba = question_engine::load_questions_usage_by_activity($lastattempt->uniqueid); $oldnumberstonew = array(); foreach ($oldquba->get_attempt_iterator() as $oldslot => $oldqa) { $newslot = $quba->add_question($oldqa->get_question(), $oldqa->get_max_mark()); $quba->start_question_based_on($newslot, $oldqa); $oldnumberstonew[$oldslot] = $newslot; } // Update attempt layout. $newlayout = array(); foreach (explode(',', $lastattempt->layout) as $oldslot) { if ($oldslot != 0) { $newlayout[] = $oldnumberstonew[$oldslot]; } else { $newlayout[] = 0; } }
/** * Start a subsequent new attempt, in each attempt builds on last mode. * * @param question_usage_by_activity $quba this question usage * @param object $attempt this attempt * @param object $lastattempt last attempt * @return object modified attempt object * */ function quiz_start_attempt_built_on_last($quba, $attempt, $lastattempt) { $oldquba = question_engine::load_questions_usage_by_activity($lastattempt->uniqueid); $oldnumberstonew = array(); foreach ($oldquba->get_attempt_iterator() as $oldslot => $oldqa) { $newslot = $quba->add_question($oldqa->get_question(), $oldqa->get_max_mark()); $quba->start_question_based_on($newslot, $oldqa); $oldnumberstonew[$oldslot] = $newslot; } // Update attempt layout. $newlayout = array(); foreach (explode(',', $lastattempt->layout) as $oldslot) { if ($oldslot != 0) { $newlayout[] = $oldnumberstonew[$oldslot]; } else { $newlayout[] = 0; } } $attempt->layout = implode(',', $newlayout); return $attempt; }
/** * We create two usages, each with two questions, a short-answer marked * out of 5, and and essay marked out of 10. We just start these attempts. * * Then we change the max mark for the short-answer question in one of the * usages to 20, using a qubaid_list, and verify. * * Then we change the max mark for the essay question in the other * usage to 2, using a qubaid_join, and verify. */ public function test_set_max_mark_in_attempts() { // Set up some things the tests will need. $this->resetAfterTest(); $dm = new question_engine_data_mapper(); // Create the questions. $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); $sa = $generator->create_question('shortanswer', null, array('category' => $cat->id)); $essay = $generator->create_question('essay', null, array('category' => $cat->id)); // Create the first usage. $q = question_bank::load_question($sa->id); $this->start_attempt_at_question($q, 'interactive', 5); $q = question_bank::load_question($essay->id); $this->start_attempt_at_question($q, 'interactive', 10); $this->finish(); $this->save_quba(); $usage1id = $this->quba->get_id(); // Create the second usage. $this->quba = question_engine::make_questions_usage_by_activity('unit_test', context_system::instance()); $q = question_bank::load_question($sa->id); $this->start_attempt_at_question($q, 'interactive', 5); $this->process_submission(array('answer' => 'fish')); $q = question_bank::load_question($essay->id); $this->start_attempt_at_question($q, 'interactive', 10); $this->finish(); $this->save_quba(); $usage2id = $this->quba->get_id(); // Test set_max_mark_in_attempts with a qubaid_list. $usagestoupdate = new qubaid_list(array($usage1id)); $dm->set_max_mark_in_attempts($usagestoupdate, 1, 20.0); $quba1 = question_engine::load_questions_usage_by_activity($usage1id); $quba2 = question_engine::load_questions_usage_by_activity($usage2id); $this->assertEquals(20, $quba1->get_question_max_mark(1)); $this->assertEquals(10, $quba1->get_question_max_mark(2)); $this->assertEquals(5, $quba2->get_question_max_mark(1)); $this->assertEquals(10, $quba2->get_question_max_mark(2)); // Test set_max_mark_in_attempts with a qubaid_join. $usagestoupdate = new qubaid_join('{question_usages} qu', 'qu.id', 'qu.id = :usageid', array('usageid' => $usage2id)); $dm->set_max_mark_in_attempts($usagestoupdate, 2, 2.0); $quba1 = question_engine::load_questions_usage_by_activity($usage1id); $quba2 = question_engine::load_questions_usage_by_activity($usage2id); $this->assertEquals(20, $quba1->get_question_max_mark(1)); $this->assertEquals(10, $quba1->get_question_max_mark(2)); $this->assertEquals(5, $quba2->get_question_max_mark(1)); $this->assertEquals(2, $quba2->get_question_max_mark(2)); // Test the nothing to do case. $usagestoupdate = new qubaid_join('{question_usages} qu', 'qu.id', 'qu.id = :usageid', array('usageid' => -1)); $dm->set_max_mark_in_attempts($usagestoupdate, 2, 2.0); $quba1 = question_engine::load_questions_usage_by_activity($usage1id); $quba2 = question_engine::load_questions_usage_by_activity($usage2id); $this->assertEquals(20, $quba1->get_question_max_mark(1)); $this->assertEquals(10, $quba1->get_question_max_mark(2)); $this->assertEquals(5, $quba2->get_question_max_mark(1)); $this->assertEquals(2, $quba2->get_question_max_mark(2)); }
protected function process_submitted_data() { global $DB; $qubaids = optional_param('qubaids', null, PARAM_SEQUENCE); if (!$qubaids) { return; } $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()); } $transaction->allow_commit(); }
/** * Regrade a particular quiz attempt. Either for real ($dryrun = false), or * as a pretend regrade to see which fractions would change. The outcome is * stored in the quiz_overview_regrades table. * * Note, $attempt is not upgraded in the database. The caller needs to do that. * However, $attempt->sumgrades is updated, if this is not a dry run. * * @param object $attempt the quiz attempt to regrade. * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real. * @param array $slots if null, regrade all questions, otherwise, just regrade * the quetsions with those slots. */ protected function regrade_attempt($attempt, $dryrun = false, $slots = null) { global $DB; $transaction = $DB->start_delegated_transaction(); $quba = question_engine::load_questions_usage_by_activity($attempt->uniqueid); if (is_null($slots)) { $slots = $quba->get_slots(); } $finished = $attempt->timefinish > 0; foreach ($slots as $slot) { $qqr = new stdClass(); $qqr->oldfraction = $quba->get_question_fraction($slot); $quba->regrade_question($slot, $finished); $qqr->newfraction = $quba->get_question_fraction($slot); if (abs($qqr->oldfraction - $qqr->newfraction) > 1.0E-7) { $qqr->questionusageid = $quba->get_id(); $qqr->slot = $slot; $qqr->regraded = empty($dryrun); $qqr->timemodified = time(); $DB->insert_record('quiz_overview_regrades', $qqr, false); } } if (!$dryrun) { question_engine::save_questions_usage_by_activity($quba); } $transaction->allow_commit(); }
protected function print_quba($quba_id, $batch_mode, $include_barcodes = true, $include_intro = true) { //get the default question display information $options = new question_display_options_pdf(); //get a question usage object from the database $usage = question_engine::load_questions_usage_by_activity($quba_id); //get an associative array, which indicates the questions which should be rendered $slots = $usage->get_slots(); //if we're not _only_ outputting a key, output the core of the quesiton if ($batch_mode !== quiz_papercopy_batch_mode::KEY_ONLY) { //start a new copy with the given margins echo html_writer::start_tag('page', array('backtop' => '9mm', 'backbottom' => '0mm', 'backleft' => '0mm', 'backright' => '8mm')); if ($include_barcodes) { //echo html_writer::start_tag('page_header'); echo self::render_barcode($quba_id); //echo html_writer::end_tag('page_header'); } else { echo html_writer::tag('div', $quba_id, array('class' => 'qubaid')); } //bookmark, for easy access from a PDF viewer echo html_writer::tag('bookmark', '', array('title' => get_string('copynumber', 'quiz_papercopy', $quba_id), 'level' => '0')); //print the quiz's introduction if (!empty($this->quiz->intro) && $include_intro) { $introtext = $this->quiz->intro; $introtext = file_rewrite_pluginfile_urls($this->quiz->intro, 'pluginfile.php', $this->context->id, 'mod_quiz', 'intro', null); echo html_writer::tag('div', self::insert_ids($introtext, $quba_id), array('class' => 'introduction')); } //output each question foreach ($slots as $slot => $question) { $qbuf = $usage->render_question($question, $options, $slot + 1); //if the core PDF renderer has purification turned off, purify the question locally if (core_pdf_renderer::$do_not_purify) { $qbuf = core_pdf_renderer::clean_with_htmlpurify($qbuf); } echo $qbuf; } echo html_writer::end_tag('page'); } //if a key has been requested, output it as well if ($batch_mode == quiz_papercopy_batch_mode::KEY_ONLY || $batch_mode == quiz_papercopy_batch_mode::WITH_KEY) { //start a new copy with the given margins echo html_writer::start_tag('page', array('backtop' => '9mm', 'backbottom' => '0mm', 'backleft' => '0mm', 'backright' => '8mm')); //start of page headers if ($include_barcodes) { echo html_writer::start_tag('page_header'); echo html_writer::start_tag('div', array('align' => 'center')); echo html_writer::tag('barcode', '', array('value' => $id, 'style' => 'width: 40mm; height: 7mm;', 'label' => 'label')); echo html_writer::end_tag('div'); echo html_writer::end_tag('page_header'); } //bookmark, for easy access from a PDF viewer echo html_writer::tag('bookmark', '', array('title' => get_string('answerkeynumber', 'quiz_papercopy', $quba_id), 'level' => $batch_mode !== quiz_papercopy_batch_mode::KEY_ONLY)); //print the quiz's introduction echo html_writer::tag('p', get_string('answerkeyfortestid', 'quiz_papercopy', $quba_id), array('style' => 'font-weight:bold;')); echo html_writer::start_tag('table'); //output each question foreach ($slots as $slot => $question) { echo html_writer::start_tag('tr'); //echo the answer key contents echo html_writer::tag('td', $slot + 1 . '. ', array('style' => 'padding-right: 10px;')); echo html_writer::tag('td', $usage->get_right_answer_summary($question)); echo html_writer::end_tag('tr'); } echo html_writer::start_tag('table'); echo html_writer::end_tag('page'); } }