/** * Gets the data for the table * * @return array $data The array of data to show */ protected function get_data() { global $DB; $data = array(); $attempts = $this->session->getall_attempts(true); $userids = array(); foreach ($attempts as $attempt) { $userids[] = $attempt->userid; } // get user records to get the full name if (!empty($userids)) { list($useridsql, $params) = $DB->get_in_or_equal($userids); $sql = 'SELECT * FROM {user} WHERE id ' . $useridsql; $userrecs = $DB->get_records_sql($sql, $params); } else { $userrecs = array(); } foreach ($attempts as $attempt) { /** @var \mod_activequiz\activequiz_attempt $attempt */ $ditem = new \stdClass(); $ditem->attemptid = $attempt->id; $ditem->sessionid = $attempt->sessionid; if ($this->rtq->group_mode()) { $ditem->userid = $attempt->userid; $ditem->takenby = fullname($userrecs[$attempt->userid]); $ditem->groupname = $this->rtq->get_groupmanager()->get_group_name($attempt->forgroupid); } else { $ditem->userid = $attempt->userid; $userrec = $userrecs[$attempt->userid]; $ditem->username = fullname($userrec); } $ditem->attemptno = $attempt->attemptnum; $ditem->preview = $attempt->preview; $ditem->status = $attempt->getStatus(); $ditem->timestart = $attempt->timestart; $ditem->timefinish = $attempt->timefinish; $ditem->timemodified = $attempt->timemodified; $ditem->grade = number_format($this->rtq->get_grader()->calculate_attempt_grade($attempt), 2); $ditem->totalgrade = $this->rtq->getRTQ()->scale; $data[$attempt->id] = $ditem; } return $data; }
/** * Initializes quiz javascript and strings for javascript when on the * quiz view page, or the "quizstart" action * * @param \mod_activequiz\activequiz_attempt $attempt * @param \mod_activequiz\activequiz_session $session * @throws moodle_exception throws exception when invalid question on the attempt is found */ public function init_quiz_js($attempt, $session) { global $USER, $CFG; // include classList javascript to add the class List HTML5 for compatibility // below IE 10 $this->page->requires->js('/mod/activequiz/js/classList.js'); $this->page->requires->js('/mod/activequiz/js/core.js'); // add window.onload script manually to handle removing the loading mask echo html_writer::start_tag('script', array('type' => 'text/javascript')); echo <<<EOD (function preLoad(){ window.addEventListener('load', function(){activequiz.quiz_page_loaded();}, false); }()); EOD; echo html_writer::end_tag('script'); if ($this->rtq->is_instructor()) { $this->page->requires->js('/mod/activequiz/js/instructor.js'); } else { $this->page->requires->js('/mod/activequiz/js/student.js'); } // next set up a class to pass to js for js info $jsinfo = new stdClass(); $jsinfo->sesskey = sesskey(); $jsinfo->siteroot = $CFG->wwwroot; $jsinfo->rtqid = $this->rtq->getRTQ()->id; $jsinfo->sessionid = $session->get_session()->id; $jsinfo->attemptid = $attempt->id; $jsinfo->slots = $attempt->getSlots(); $jsinfo->isinstructor = $this->rtq->is_instructor() ? 'true' : 'false'; // manually create the questions stdClass as we can't support JsonSerializable yet $questions = array(); foreach ($attempt->get_questions() as $q) { /** @var \mod_activequiz\activequiz_question $q */ $question = new stdClass(); $question->id = $q->getId(); $question->questiontime = $q->getQuestionTime(); $question->tries = $q->getTries(); $question->question = $q->getQuestion(); $question->slot = $attempt->get_question_slot($q); // if the slot is false, throw exception for invalid question on quiz attempt if ($question->slot === false) { $a = new stdClass(); $a->questionname = $q->getQuestion()->name; throw new moodle_exception('invalidquestionattempt', 'mod_activequiz', '', $a, 'invalid slot when building questions array on quiz renderer'); } $questions[$question->slot] = $question; } $jsinfo->questions = $questions; // resuming quiz feature // this will check if the session has started already and print out $jsinfo->resumequiz = 'false'; if ($session->get_session()->status != 'notrunning') { $sessionstatus = $session->get_session()->status; $currentquestion = $session->get_session()->currentquestion; $nextstarttime = $session->get_session()->nextstarttime; if ($sessionstatus == 'running' && !empty($currentquestion)) { // we're in a currently running question $jsinfo->resumequiz = 'true'; $jsinfo->resumequizstatus = $sessionstatus; $jsinfo->resumequizcurrentquestion = $currentquestion; $nextQuestion = $this->rtq->get_questionmanager()->get_question_with_slot($session->get_session()->currentqnum, $attempt); if ($nextstarttime > time()) { // we're wating for question $jsinfo->resumequizaction = 'waitforquestion'; $jsinfo->resumequizdelay = $session->get_session()->nextstarttime - time(); $jsinfo->resumequizquestiontime = $nextQuestion->getQuestionTime(); } else { $jsinfo->resumequizaction = 'startquestion'; // how much time has elapsed since start time // first check if the question has a time limit if ($nextQuestion->getNoTime()) { $jsinfo->resumequizquestiontime = 0; } else { // otherwise figure out how much time is left $timeelapsed = time() - $nextstarttime; $timeLeft = $nextQuestion->getQuestionTime() - $timeelapsed; $jsinfo->resumequizquestiontime = $timeLeft; } } } else { if ($sessionstatus == 'reviewing' || $sessionstatus == 'endquestion') { // if we're reviewing, resume with quiz info of reviewing and just let // set interval capture next question start time $jsinfo->resumequiz = 'true'; $jsinfo->resumequizaction = 'reviewing'; $jsinfo->resumequizstatus = $sessionstatus; $jsinfo->resumequizcurrentquestion = $currentquestion; if ($attempt->lastquestion) { $jsinfo->lastquestion = 'true'; } else { $jsinfo->lastquestion = 'false'; } } } } // print jsinfo to javascript echo html_writer::start_tag('script', array('type' => 'text/javascript')); echo "rtqinitinfo = " . json_encode($jsinfo); echo html_writer::end_tag('script'); // add strings for js $this->page->requires->strings_for_js(array('waitforquestion', 'gatheringresults', 'feedbackintro', 'nofeedback', 'closingsession', 'sessionclosed', 'trycount', 'timertext', 'waitforrevewingend', 'show_correct_answer', 'hide_correct_answer', 'hidestudentresponses', 'showstudentresponses', 'hidenotresponded', 'shownotresponded'), 'activequiz'); $this->page->requires->strings_for_js(array('seconds'), 'moodle'); // finally allow question modifiers to add their own css/js $this->rtq->call_question_modifiers('add_js', null); }
/** * Handle's the page request * */ public function handle_request() { global $DB, $USER, $PAGE; // first check if there are questions or not. If there are no questions display that message instead, // regardless of action. if (count($this->RTQ->get_questionmanager()->get_questions()) === 0) { $this->pagevars['action'] = 'noquestions'; $this->pageurl->param('action', ''); // remove the action } switch ($this->pagevars['action']) { case 'noquestions': $this->RTQ->get_renderer()->view_header(); $this->RTQ->get_renderer()->no_questions($this->RTQ->is_instructor()); $this->RTQ->get_renderer()->view_footer(); break; case 'quizstart': // case for the quiz start landing page // set the quiz view page to the base layout for 1 column layout $PAGE->set_pagelayout('base'); if ($this->session->get_session() === false) { // redirect them to the default page with a quick message first $redirurl = clone $this->pageurl; $redirurl->remove_params('action'); redirect($redirurl, get_string('nosession', 'activequiz'), 5); } else { // this is here to help prevent race conditions for multiple group members trying to take the // quiz at the same time $cantakequiz = false; if ($this->RTQ->group_mode()) { if (!$this->RTQ->is_instructor() && $this->pagevars['group'] == 0) { print_error('invalidgroupid', 'mod_activequiz'); } // check if the user can take the quiz for the group if ($this->session->can_take_quiz_for_group($this->pagevars['group'])) { $cantakequiz = true; } } else { // if no group mode, user will always be able to take quiz $cantakequiz = true; } if ($cantakequiz) { if (!$this->session->init_attempts($this->RTQ->is_instructor(), $this->pagevars['group'], $this->pagevars['groupmembers'])) { print_error('cantinitattempts', 'activequiz'); } // set the session as running if ($this->RTQ->is_instructor() && $this->session->get_session()->status == 'notrunning') { $this->session->set_status('running'); } // get the current attempt an initialize the head contributions $attempt = $this->session->get_open_attempt(); $attempt->get_html_head_contributions(); $attempt->setStatus('inprogress'); // now show the quiz start landing page $this->RTQ->get_renderer()->view_header(true); $this->RTQ->get_renderer()->render_quiz($attempt, $this->session); $this->RTQ->get_renderer()->view_footer(); } else { $this->RTQ->get_renderer()->view_header(); $this->RTQ->get_renderer()->group_session_started(); $this->RTQ->get_renderer()->view_footer(); } } break; case 'selectgroupmembers': if (empty($this->pagevars['group'])) { $viewhome = clone $this->pageurl; $viewhome->remove_params('action'); redirect($viewhome, get_string('invalid_group_selected', 'activequiz'), 5); } else { $this->pageurl->param('group', $this->pagevars['group']); $groupselectform = new \mod_activequiz\forms\view\groupselectmembers($this->pageurl, array('rtq' => $this->RTQ, 'selectedgroup' => $this->pagevars['group'])); if ($data = $groupselectform->get_data()) { // basically we want to get all gm* fields $gmemnum = 1; $groupmembers = array(); $data = get_object_vars($data); while (isset($data['gm' . $gmemnum])) { if ($data['gm' . $gmemnum] != 0) { $groupmembers[] = $data['gm' . $gmemnum]; } $gmemnum++; } $this->pageurl->param('groupmembers', implode(',', $groupmembers)); $this->pageurl->param('action', 'quizstart'); // redirect to the quiz start page redirect($this->pageurl, null, 0); } else { $this->RTQ->get_renderer()->view_header(); $this->RTQ->get_renderer()->group_member_select($groupselectform); $this->RTQ->get_renderer()->view_footer(); } } break; default: // default is to show view to start quiz (for instructors/quiz controllers) or join quiz (for everyone else) // trigger event for course module viewed $event = \mod_activequiz\event\course_module_viewed::create(array('objectid' => $PAGE->cm->instance, 'context' => $PAGE->context)); $event->add_record_snapshot('course', $this->RTQ->getCourse()); $event->add_record_snapshot($PAGE->cm->modname, $this->RTQ->getRTQ()); $event->trigger(); // determine home display based on role if ($this->RTQ->is_instructor()) { $startsessionform = new \mod_activequiz\forms\view\start_session($this->pageurl); if ($data = $startsessionform->get_data()) { // create a new quiz session // first check to see if there are any open sessions // this shouldn't occur, but never hurts to check $sessions = $DB->get_records('activequiz_sessions', array('activequizid' => $this->RTQ->getRTQ()->id, 'sessionopen' => 1)); if (!empty($sessions)) { // error out with that there are existing sessions $this->RTQ->get_renderer()->setMessage(get_string('alreadyexisting_sessions', 'activequiz'), 'error'); $this->RTQ->get_renderer()->view_header(); $this->RTQ->get_renderer()->view_inst_home($startsessionform, $this->session->get_session()); $this->RTQ->get_renderer()->view_footer(); break; } else { if (!$this->session->create_session($data->sessionname)) { // error handling $this->RTQ->get_renderer()->setMessage(get_string('unabletocreate_session', 'activequiz'), 'error'); $this->RTQ->get_renderer()->view_header(); $this->RTQ->get_renderer()->view_inst_home($startsessionform, $this->session->get_session()); $this->RTQ->get_renderer()->view_footer(); break; // break out of the switch } } // redirect to the quiz start $quizstarturl = clone $this->pageurl; $quizstarturl->param('action', 'quizstart'); redirect($quizstarturl, null, 0); } else { $this->RTQ->get_renderer()->view_header(); $this->RTQ->get_renderer()->view_inst_home($startsessionform, $this->session->get_session()); $this->RTQ->get_renderer()->view_footer(); } } else { // check to see if the group already started a quiz $validgroups = array(); if ($this->RTQ->group_mode()) { // if there is already an attempt for this session for this group for this user don't allow them to start another $validgroups = $this->session->check_attempt_for_group(); if (empty($validgroups) && $validgroups !== false) { $this->RTQ->get_renderer()->view_header(); $this->RTQ->get_renderer()->group_session_started(); $this->RTQ->get_renderer()->view_footer(); break; } else { if ($validgroups === false) { $validgroups = array(); } } } $studentstartformparams = array('rtq' => $this->RTQ, 'validgroups' => $validgroups); $studentstartform = new \mod_activequiz\forms\view\student_start_form($this->pageurl, $studentstartformparams); if ($data = $studentstartform->get_data()) { $quizstarturl = clone $this->pageurl; $quizstarturl->param('action', 'quizstart'); // if data redirect to the quiz start url with the group selected if we're in group mode if ($this->RTQ->group_mode()) { $groupid = $data->group; $quizstarturl->param('group', $groupid); // check if the group attendance feature is enabled // if so redirect to the group member select form // don't send to group attendance form if an attempt is already started if ($this->RTQ->getRTQ()->groupattendance == 1 && !$this->session->get_open_attempt_for_current_user()) { $quizstarturl->param('action', 'selectgroupmembers'); } redirect($quizstarturl, null, 0); } else { redirect($quizstarturl, null, 0); } } else { // display student home. (form will display only if there is an active session $this->RTQ->get_renderer()->view_header(); $this->RTQ->get_renderer()->view_student_home($studentstartform, $this->session); $this->RTQ->get_renderer()->view_footer(); } } break; } }
/** * Get the session's grade * * For now this will always be the last attempt for the user * * @param \mod_activequiz\activequiz_session $session * @param int $userid The userid to get the grade for * @return array($forgroupid, $number) */ protected function get_session_grade($session, $userid) { // get all attempts for the specified userid that are closed and are not previews // also skip checking for groups as grading handles groups separately $attempts = $session->getall_attempts(false, 'closed', $userid, true); $attemptno = count($attempts); if ($attemptno === 0) { return array(0, 0); } // get the last attempt for the user $attemptgraded = $this->get_last_attempt($attempts); return array($attemptgraded->forgroupid, $this->calculate_attempt_grade($attemptgraded)); }
/** * Handles the incoming request * */ public function handle_request() { global $USER; switch ($this->action) { case 'startquiz': // only allow instructors to perform this action if ($this->RTQ->is_instructor()) { $firstquestion = $this->session->start_quiz(); $this->jsonlib->set('status', 'startedquiz'); $this->jsonlib->set('questionid', $firstquestion->get_slot()); $this->jsonlib->set('nextstarttime', $this->session->get_session()->nextstarttime); $this->jsonlib->set('notime', $firstquestion->getNoTime()); if ($firstquestion->getNoTime() == 0) { // this question has a time limit if ($firstquestion->getQuestionTime() == 0) { $questiontime = $this->RTQ->getRTQ()->defaultquestiontime; } else { $questiontime = $firstquestion->getQuestionTime(); } $this->jsonlib->set('questiontime', $questiontime); } else { $this->jsonlib->set('questiontime', 0); } $delay = $this->session->get_session()->nextstarttime - time(); $this->jsonlib->set('delay', $delay); $qattempt = $this->session->get_open_attempt(); $this->jsonlib->set('lastquestion', $qattempt->lastquestion ? 'true' : 'false'); $this->jsonlib->send_response(); } else { $this->jsonlib->send_error('invalidaction'); } break; case 'savequestion': // check if we're working on the current question for the session $currentquestion = $this->session->get_session()->currentquestion; $jscurrentquestion = required_param('questionid', PARAM_INT); if ($currentquestion != $jscurrentquestion) { $this->jsonlib->send_error('invalid question'); } // if we pass attempt to save the question $qattempt = $this->session->get_open_attempt(); // make sure the attempt belongs to the current user if ($qattempt->userid != $USER->id) { $this->jsonlib->send_error('invalid user'); } if ($qattempt->save_question()) { $this->jsonlib->set('status', 'success'); $this->jsonlib->set('feedback', $qattempt->get_question_feedback()); // next we need to send back the updated sequence check for javascript to update // the sequence check on the question form. this allows the question to be resubmitted again list($seqname, $seqvalue) = $qattempt->get_sequence_check($this->session->get_session()->currentqnum); $this->jsonlib->set('seqcheckname', $seqname); $this->jsonlib->set('seqcheckval', $seqvalue); $this->jsonlib->send_response(); } else { $this->jsonlib->send_error('unable to save question'); } break; case 'getresults': // only allow instructors to perform this action if ($this->RTQ->is_instructor()) { $this->session->set_status('reviewing'); // get the current question results $responses = $this->session->get_question_results(); $this->jsonlib->set('responses', $responses); $this->jsonlib->set('status', 'success'); $this->jsonlib->set('qtype', $this->RTQ->get_questionmanager()->get_questiontype_byqnum($this->session->get_session()->currentqnum)); $this->jsonlib->send_response(); } else { $this->jsonlib->send_error('invalidaction'); } break; case 'getcurrentresults': // case to get the results of the question currently going if ($this->RTQ->is_instructor()) { $responses = $this->session->get_question_results(); $this->jsonlib->set('responses', $responses); $this->jsonlib->set('status', 'success'); $this->jsonlib->set('qtype', $this->RTQ->get_questionmanager()->get_questiontype_byqnum($this->session->get_session()->currentqnum)); $this->jsonlib->send_response(); } else { $this->jsonlib->send_error('invalidaction'); } break; case 'getnotresponded': // only allow instructors to perform this action if ($this->RTQ->is_instructor()) { $notrespondedHTML = $this->session->get_not_responded(); $this->jsonlib->set('notresponded', $notrespondedHTML); $this->jsonlib->set('status', 'success'); $this->jsonlib->send_response(); } else { $this->jsonlib->send_error('invalidaction'); } break; case 'nextquestion': // only allow instructors to perform this action if ($this->RTQ->is_instructor()) { $nextquestion = $this->session->next_question(); $this->session->set_status('running'); $this->jsonlib->set('status', 'startedquestion'); $qattempt = $this->session->get_open_attempt(); $this->jsonlib->set('lastquestion', $qattempt->lastquestion ? 'true' : 'false'); $this->jsonlib->set('questionid', $nextquestion->get_slot()); $this->jsonlib->set('nextstarttime', $this->session->get_session()->nextstarttime); $this->jsonlib->set('notime', $nextquestion->getNoTime()); if ($nextquestion->getNoTime() == 0) { // this question has a time limit if ($nextquestion->getQuestionTime() == 0) { $questiontime = $this->RTQ->getRTQ()->defaultquestiontime; } else { $questiontime = $nextquestion->getQuestionTime(); } $this->jsonlib->set('questiontime', $questiontime); } else { $this->jsonlib->set('questiontime', 0); } $delay = $this->session->get_session()->nextstarttime - time(); $this->jsonlib->set('delay', $delay); $this->jsonlib->send_response(); } else { $this->jsonlib->send_error('invalidaction'); } break; case 'repollquestion': if ($this->RTQ->is_instructor()) { $repollquestion = $this->session->repoll_question(); $this->session->set_status('running'); $this->jsonlib->set('status', 'startedquestion'); $qattempt = $this->session->get_open_attempt(); $this->jsonlib->set('lastquestion', $qattempt->lastquestion ? 'true' : 'false'); $this->jsonlib->set('questionid', $repollquestion->get_slot()); $this->jsonlib->set('nextstarttime', $this->session->get_session()->nextstarttime); $this->jsonlib->set('notime', $repollquestion->getNoTime()); if ($repollquestion->getNoTime() == 0) { // this question has a time limit if ($repollquestion->getQuestionTime() == 0) { $questiontime = $this->RTQ->getRTQ()->defaultquestiontime; } else { $questiontime = $repollquestion->getQuestionTime(); } $this->jsonlib->set('questiontime', $questiontime); } else { $this->jsonlib->set('questiontime', 0); } $delay = $this->session->get_session()->nextstarttime - time(); $this->jsonlib->set('delay', $delay); $this->jsonlib->send_response(); } else { $this->jsonlib->send_error('invalidaction'); } break; case 'gotoquestion': if ($this->RTQ->is_instructor()) { $qnum = optional_param('qnum', '', PARAM_INT); if (empty($qnum)) { $this->jsonlib->send_error('invalid question number'); } if (!($question = $this->session->goto_question($qnum))) { $this->jsonlib->send_error('invalid question number'); } $this->session->set_status('running'); $this->jsonlib->set('status', 'startedquestion'); $qattempt = $this->session->get_open_attempt(); $this->jsonlib->set('lastquestion', $qattempt->lastquestion ? 'true' : 'false'); $this->jsonlib->set('questionid', $question->get_slot()); $this->jsonlib->set('nextstarttime', $this->session->get_session()->nextstarttime); $this->jsonlib->set('notime', $question->getNoTime()); if ($question->getNoTime() == 0) { // this question has a time limit if ($question->getQuestionTime() == 0) { $questiontime = $this->RTQ->getRTQ()->defaultquestiontime; } else { $questiontime = $question->getQuestionTime(); } $this->jsonlib->set('questiontime', $questiontime); } else { $this->jsonlib->set('questiontime', 0); } $delay = $this->session->get_session()->nextstarttime - time(); $this->jsonlib->set('delay', $delay); $this->jsonlib->send_response(); } else { $this->jsonlib->send_error('invalidaction'); } break; case 'endquestion': // update the session status to say that we're ending the question (this will in turn update students if ($this->RTQ->is_instructor()) { $this->session->end_question(); $this->jsonlib->set('status', 'success'); $this->jsonlib->send_response(); } else { $this->jsonlib->send_error('invalidaction'); } break; case 'getrightresponse': if ($this->RTQ->is_instructor()) { $rightresponsequestion = $this->session->get_question_right_response(); $this->jsonlib->set('rightanswer', $rightresponsequestion); $this->jsonlib->set('status', 'success'); $this->jsonlib->send_response(); } else { $this->jsonlib->send_error('invalidaction'); } break; case 'closesession': // only allow instructors to perform this action if ($this->RTQ->is_instructor()) { $this->session->end_session(); // next calculate and save grades if (!$this->RTQ->get_grader()->save_all_grades()) { $this->jsonlib->send_error('can\'t save grades'); } $this->jsonlib->set('status', 'success'); $this->jsonlib->send_response(); } else { $this->jsonlib->send_error('invalidaction'); } break; default: $this->jsonlib->send_error('invalidaction'); break; } }
/** * Given an ID of an instance of this module, * this function will permanently delete the instance * and any data that depends on it. * * @param int $id Id of the module instance * @return boolean Success/Failure **/ function activequiz_delete_instance($id) { global $DB, $CFG; require_once $CFG->dirroot . '/mod/activequiz/locallib.php'; require_once $CFG->libdir . '/questionlib.php'; require_once $CFG->dirroot . '/question/editlib.php'; try { // make sure the record exists $activequiz = $DB->get_record('activequiz', array('id' => $id), '*', MUST_EXIST); // go through each session and then delete them (also deletes all attempts for them) $sessions = $DB->get_records('activequiz_sessions', array('activequizid' => $activequiz->id)); foreach ($sessions as $session) { \mod_activequiz\activequiz_session::delete($session->id); } // delete all questions for this quiz $DB->delete_records('activequiz_questions', array('activequizid' => $activequiz->id)); // finally delete the activequiz object $DB->delete_records('activequiz', array('id' => $activequiz->id)); } catch (Exception $e) { return false; } return true; }