linker_moodle_2_0_down.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. <?php
  2. /**
  3. * Quiz-engine-specific part of the SLOODLE quiz linker.
  4. * For Moodle 2.0 or lower
  5. *
  6. * Allows in-world objects to interact with Moodle quizzes.
  7. * Part of the Sloodle project (www.sloodle.org).
  8. *
  9. * @package sloodlequiz
  10. * @copyright Copyright (c) 2006-8 Sloodle (various contributors)
  11. * @license http://www.gnu.org/licenses/gpl-3.0.html GNU GPL v3
  12. *
  13. * @contributor (various Moodle authors including Tim Hunt)
  14. * @contributor Edmund Edgar
  15. * @contributor Peter R. Bloomfield
  16. */
  17. // Get number for the next or unfinished attempt
  18. if(!$attemptnumber = (int)sloodle_get_field_sql_params('SELECT MAX(attempt)+1 FROM ' .
  19. "{$CFG->prefix}quiz_attempts WHERE quiz = ? AND " .
  20. "userid = ? AND timefinish > 0 AND preview != 1", array($quiz->id, $USER->id))) {
  21. $attemptnumber = 1;
  22. }
  23. $strattemptnum = get_string('attempt', 'quiz', $attemptnumber);
  24. $strquizzes = get_string("modulenameplural", "quiz");
  25. // course id: $course->id
  26. // course id: $course->id
  27. // course name: $quiz->id
  28. // attempts: $quiz->attempts
  29. if ($limittoquestion == 0) $output[] = array('course',$course->id,$course->fullname);
  30. $numberofpreviousattempts = sloodle_count_records_select_params('quiz_attempts', "quiz = ? AND " .
  31. "userid = ? AND timefinish > 0 AND preview != 1", array($quiz->id, $USER->id));
  32. if ($quiz->attempts and $numberofpreviousattempts >= $quiz->attempts) {
  33. $sloodle->response->quick_output(-10301, 'QUIZ', 'You do not have any attempts left', FALSE);
  34. exit();
  35. }
  36. /// Check subnet access
  37. if ($quiz->subnet and !address_in_subnet(getremoteaddr(), $quiz->subnet)) {
  38. $sloodle->response->quick_output(-1, 'MISC', 'A subnet error occurred', FALSE);
  39. exit();
  40. }
  41. /// Check password access
  42. if ($quiz->password) {
  43. $sloodle->response->quick_output(-10302, 'QUIZ', 'Quiz requires password - not supported by Sloodle', FALSE);
  44. exit();
  45. }
  46. if ($quiz->delay1 or $quiz->delay2) {
  47. //quiz enforced time delay
  48. if ($attempts = quiz_get_user_attempts($quiz->id, $USER->id)) {
  49. $numattempts = count($attempts);
  50. } else {
  51. $numattempts = 0;
  52. }
  53. $timenow = time();
  54. $lastattempt_obj = sloodle_get_record_select_params('quiz_attempts', "quiz = ? AND attempt = ? AND userid = ?", array($quiz->id, $numattempts, $USER->id), 'timefinish');
  55. if ($lastattempt_obj) {
  56. $lastattempt = $lastattempt_obj->timefinish;
  57. }
  58. if ($numattempts == 1 && $quiz->delay1) {
  59. if ($timenow - $quiz->delay1 < $lastattempt) {
  60. $sloodle->response->quick_output(-701, 'QUIZ', 'You need to wait until the time delay has expired.', FALSE);
  61. exit();
  62. }
  63. } else if($numattempts > 1 && $quiz->delay2) {
  64. if ($timenow - $quiz->delay2 < $lastattempt) {
  65. $sloodle->response->quick_output(-701, 'QUIZ', 'You need to wait until the time delay has expired.', FALSE);
  66. exit();
  67. }
  68. }
  69. }
  70. // sloodle_prim_render_output($output);
  71. // exit;
  72. /// Load attempt or create a new attempt if there is no unfinished one
  73. $attempt = sloodle_get_record('quiz_attempts', 'quiz', $quiz->id,
  74. 'userid', $USER->id, 'timefinish', 0);
  75. $newattempt = false;
  76. if (!$attempt) {
  77. $newattempt = true;
  78. // Start a new attempt and initialize the question sessions
  79. $attempt = quiz_create_attempt($quiz, $attemptnumber);
  80. // If this is an attempt by a teacher mark it as a preview
  81. // Save the attempt
  82. if (!$attempt->timestart) { // shouldn't really happen, just for robustness
  83. $attempt->timestart = time();
  84. }
  85. if (!$attempt->timemodified) { // shouldn't really happen, just for robustness
  86. $attempt->timemodified = time();
  87. }
  88. if (!$attempt->id = sloodle_insert_record('quiz_attempts', $attempt)) {
  89. $sloodle->response->quick_output(-701, 'QUIZ', 'Could not create new attempt.', FALSE);
  90. exit();
  91. }
  92. // make log entries
  93. add_to_log($course->id, 'quiz', 'attempt',
  94. "review.php?attempt=$attempt->id",
  95. "$quiz->id", $cm->id);
  96. } else {
  97. // log continuation of attempt only if some time has lapsed
  98. if (($timestamp - $attempt->timemodified) > 600) { // 10 minutes have elapsed
  99. add_to_log($course->id, 'quiz', 'continue attemp', // this action used to be called 'continue attempt' but the database field has only 15 characters
  100. "review.php?attempt=$attempt->id",
  101. "$quiz->id", $cm->id);
  102. }
  103. }
  104. if (!$attempt->timestart) { // shouldn't really happen, just for robustness
  105. $attempt->timestart = time();
  106. }
  107. /// Load all the questions and states needed by this script
  108. ///// SLOODLE MODIFICATION /////
  109. // For SLOODLE to work properly, it needs the entire list of questions at all times.
  110. // It ignores the "page" structure of a Moodle quiz.
  111. $questionlist = quiz_questions_in_quiz($attempt->layout);
  112. $pagelist = $questionlist;
  113. ///// END SLOODLE MODIFICATION /////
  114. $questionlistids = explode(',', $questionlist);
  115. if ($questionids != '') {
  116. $questionids = explode(',', $questionids);
  117. }
  118. // add all questions that are on the submitted form
  119. if ($questionids && (count($questionids) > 0) ) {
  120. $questionlistids = array_merge($questionlistids, $questionids);
  121. }
  122. $params = array($quiz->id);
  123. $questioninstr = '';
  124. $delim = '';
  125. foreach($questionlistids as $qlid) {
  126. $params[] = $qlid;
  127. $questioninstr .= $delim.'?';
  128. $delim = ',';
  129. }
  130. if ( !$questionlistids || (count($questionlistids) == 0) ) {
  131. $sloodle->response->quick_output(-10303, 'QUIZ', 'No questions found.', FALSE);
  132. exit();
  133. }
  134. $sql = "SELECT q.*, i.grade AS maxgrade, i.id AS instance".
  135. " FROM {$CFG->prefix}question q,".
  136. " {$CFG->prefix}quiz_question_instances i".
  137. " WHERE i.quiz = ? AND q.id = i.question".
  138. " AND q.id IN ($questioninstr) ;";
  139. // Load the questions
  140. $questions = sloodle_get_records_sql_params($sql, $params);
  141. if ( !$questions || (count($questions) == 0) ) {
  142. $sloodle->response->quick_output(-10303, 'QUIZ', 'No questions found.'.$sql.join(':',$params), FALSE);
  143. exit;
  144. $sloodle->response->quick_output(-10303, 'QUIZ', 'No questions found.', FALSE);
  145. exit();
  146. }
  147. // Load the question type specific information
  148. if (!get_question_options($questions)) {
  149. $sloodle->response->quick_output(-10303, 'QUIZ', 'Could not load question options.', FALSE);
  150. exit();
  151. }
  152. // Restore the question sessions to their most recent states
  153. // creating new sessions where required
  154. if (!$states = get_question_states($questions, $quiz, $attempt)) {
  155. $sloodle->response->quick_output(-701, 'QUIZ', 'Could not restore questions sessions.', FALSE);
  156. exit();
  157. }
  158. // Save all the newly created states
  159. if ($newattempt) {
  160. foreach ($questions as $i => $question) {
  161. save_question_session($questions[$i], $states[$i]);
  162. }
  163. }
  164. // If the new attempt is to be based on a previous attempt copy responses over
  165. if ($newattempt and $attempt->attempt > 1 and $quiz->attemptonlast and !$attempt->preview) {
  166. // Find the previous attempt
  167. if (!$lastattemptid = sloodle_get_field('quiz_attempts', 'uniqueid', 'quiz', $attempt->quiz, 'userid', $attempt->userid, 'attempt', $attempt->attempt-1)) {
  168. $sloodle->response->quick_output(-701, 'QUIZ', 'Could not find previous attempt to build on.', FALSE);
  169. exit();
  170. }
  171. // For each question find the responses from the previous attempt and save them to the new session
  172. foreach ($questions as $i => $question) {
  173. // Load the last graded state for the question
  174. $statefields = 'n.questionid as question, s.*, n.sumpenalty';
  175. $sql = "SELECT $statefields".
  176. " FROM {$CFG->prefix}question_states s,".
  177. " {$CFG->prefix}question_sessions n".
  178. " WHERE s.id = n.newgraded".
  179. " AND n.attemptid = ?".
  180. " AND n.questionid = ?";
  181. if (!$laststate = sloodle_get_record_sql_params($sql, array($lastattemptid, $i))) {
  182. // Only restore previous responses that have been graded
  183. continue;
  184. }
  185. // Restore the state so that the responses will be restored
  186. restore_question_state($questions[$i], $laststate);
  187. // prepare the previous responses for new processing
  188. $action = new stdClass;
  189. $action->responses = $laststate->responses;
  190. $action->timestamp = $laststate->timestamp;
  191. $action->event = QUESTION_EVENTOPEN;
  192. // Process these responses ...
  193. question_process_responses($questions[$i], $states[$i], $action, $quiz, $attempt);
  194. // Fix for Bug #5506: When each attempt is built on the last one,
  195. // preserve the options from any previous attempt.
  196. if ( isset($laststate->options) ) {
  197. $states[$i]->options = $laststate->options;
  198. }
  199. // ... and save the new states
  200. save_question_session($questions[$i], $states[$i]);
  201. }
  202. }
  203. /// Process form data /////////////////////////////////////////////////
  204. if ($isnotify) {
  205. SloodleDebugLogger::log('DEBUG', "in is notify");
  206. $responses = (object)$_REQUEST; // GET version of data_submitted (see lib/weblib) used in original web version
  207. // set the default event. This can be overruled by individual buttons.
  208. $event = (array_key_exists('markall', $responses)) ? QUESTION_EVENTSUBMIT :
  209. ($finishattempt ? QUESTION_EVENTCLOSE : QUESTION_EVENTSAVE);
  210. SloodleDebugLogger::log('DEBUG', "checked finish attempt");
  211. // Unset any variables we know are not responses
  212. unset($responses->id);
  213. unset($responses->q);
  214. unset($responses->oldpage);
  215. unset($responses->newpage);
  216. unset($responses->review);
  217. unset($responses->questionids);
  218. unset($responses->saveattempt); // responses get saved anway
  219. unset($responses->finishattempt); // same as $finishattempt
  220. unset($responses->markall);
  221. unset($responses->forcenewattempt);
  222. // extract responses
  223. // $actions is an array indexed by the questions ids
  224. $actions = question_extract_responses($questions, $responses, $event);
  225. SloodleDebugLogger::log('DEBUG', "extracted");
  226. // Process each question in turn
  227. foreach($questionids as $i) {
  228. if (!isset($actions[$i])) {
  229. $actions[$i]->responses = array('' => '');
  230. }
  231. $actions[$i]->timestamp = $timestamp;
  232. question_process_responses($questions[$i], $states[$i], $actions[$i], $quiz, $attempt);
  233. save_question_session($questions[$i], $states[$i]);
  234. }
  235. $attempt->timemodified = $timestamp;
  236. // We have now finished processing form data
  237. // With post sloodle-2.0 quiz chairs, we should be told what happened to the score.
  238. // In theory we should be able to get this from the data we already have, but it seems complex...
  239. // The process_events allows us to award points if there is an instruction to do so in the object config.
  240. $scorechange = floatval(optional_param( 'scorechange', 0, PARAM_TEXT));
  241. //SloodleDebugLogger::log('DEBUG', "active object check");
  242. if (!is_null($sloodle->active_object)) {
  243. //SloodleDebugLogger::log('DEBUG', "quiz has an active object");
  244. $sloodle->process_interaction('answerquestion', $scorechange);
  245. /*
  246. if ($scorechange > 0) {
  247. } else if ($scorechange < 0) {
  248. //$sloodle->active_object->process_events( 'SloodleModuleAwards', 'answerincorrect', 1, $sloodle->user->get_user_id() );
  249. }
  250. */
  251. // TODO: Maybe we should set a side effect code here?
  252. } else {
  253. SloodleDebugLogger::log('DEBUG', "quiz has no active object");
  254. }
  255. } else {
  256. SloodleDebugLogger::log('DEBUG', 'quiz is not notify');
  257. }
  258. /// Finish attempt if requested
  259. if ($finishattempt) {
  260. // Set the attempt to be finished
  261. $attempt->timefinish = $timestamp;
  262. // Find all the questions for this attempt for which the newest
  263. // state is not also the newest graded state
  264. if ($closequestions = sloodle_get_records_select_params('question_sessions',
  265. "attemptid = ? AND newest != newgraded", array($attempt->uniqueid),'', 'questionid, questionid')) {
  266. // load all the questions
  267. $closequestionlist = implode(',', array_keys($closequestions));
  268. $params = array($quiz->id);
  269. $instr = '';
  270. $delim = '';
  271. foreach(array_keys($closequestions) as $cq) {
  272. $params[] = $cq;
  273. $instr .= $delim.'?';
  274. $delim = ',';
  275. }
  276. $sql = "SELECT q.*, i.grade AS maxgrade, i.id AS instance".
  277. " FROM {$CFG->prefix}question q,".
  278. " {$CFG->prefix}quiz_question_instances i".
  279. " WHERE i.quiz = ? AND q.id = i.question".
  280. " AND q.id IN ($instr)";
  281. if (!$closequestions = sloodle_get_records_sql_params($sql, $params)) {
  282. $sloodle->response->quick_output(-10303, 'QUIZ', 'Questions missing.', FALSE);
  283. exit();
  284. }
  285. // Load the question type specific information
  286. if (!get_question_options($closequestions)) {
  287. $sloodle->response->quick_output(-10303, 'QUIZ', 'Could not load question options.', FALSE);
  288. exit();
  289. }
  290. // Restore the question sessions
  291. if (!$closestates = get_question_states($closequestions, $quiz, $attempt)) {
  292. $sloodle->response->quick_output(-701, 'QUIZ', 'Could not restore question sessions.', FALSE);
  293. exit();
  294. }
  295. foreach($closequestions as $key => $question) {
  296. $action->event = QUESTION_EVENTCLOSE;
  297. $action->responses = $closestates[$key]->responses;
  298. $action->timestamp = $closestates[$key]->timestamp;
  299. question_process_responses($question, $closestates[$key], $action, $quiz, $attempt);
  300. save_question_session($question, $closestates[$key]);
  301. }
  302. }
  303. add_to_log($course->id, 'quiz', 'close attempt',
  304. "review.php?attempt=$attempt->id",
  305. "$quiz->id", $cm->id);
  306. }
  307. /// Update the quiz attempt and the overall grade for the quiz
  308. if ((isset($responses) && $responses) || $finishattempt) {
  309. if (!sloodle_update_record('quiz_attempts', $attempt)) {
  310. $sloodle->response->quick_output(-701, 'QUIZ', 'Failed to save current quiz attempt.', FALSE);
  311. exit();
  312. }
  313. if (($attempt->attempt > 1 || $attempt->timefinish > 0) and !$attempt->preview) {
  314. quiz_save_best_grade($quiz);
  315. }
  316. }
  317. /// Check access to quiz page
  318. // check the quiz times
  319. //TODO: Figure out what this does...
  320. if ($timestamp < $quiz->timeopen || ($quiz->timeclose and $timestamp > $quiz->timeclose)) {
  321. $sloodle->response->quick_output(-701, 'QUIZ', 'Quiz not available.', FALSE);
  322. exit();
  323. }
  324. if ($finishattempt) {
  325. // redirect('review.php?attempt='.$attempt->id);
  326. //$sloodle->response->quick_output(-701, 'QUIZ', 'Got to finishattempt - but do not yet have Sloodle code to handle it.', FALSE);
  327. //exit();
  328. }
  329. /// Print the quiz page ////////////////////////////////////////////////////////
  330. if (!$isnotify) {
  331. $pagequestions = explode(',', $pagelist);
  332. $lastquestion = count($pagequestions);
  333. // Only output quiz data if a question has not been requetsed
  334. if ($limittoquestion == 0) {
  335. $output[] = array('quiz',$quiz->attempts,$quiz->name,$quiz->timelimit,$quiz->id,$lastquestion);
  336. $output[] = array('quizpages',quiz_number_of_pages($attempt->layout),$page,$pagelist);
  337. }
  338. // We will keep track of question numbers local to this quiz
  339. $localqnum = 0;
  340. /// Print all the questions
  341. $number = quiz_first_questionnumber($attempt->layout, $pagelist);
  342. foreach ($pagequestions as $i) {
  343. $options = quiz_get_renderoptions($quiz->review, $states[$i]);
  344. // Print the question
  345. // var_dump($questions[$i]);
  346. $q = $questions[$i];
  347. $localqnum++;
  348. if ($limittoquestion == $q->id) {
  349. //echo "<hr>"; print_r($q); echo "<hr>";
  350. // Make sure the variables exist (avoids warnings!)
  351. if (!isset($q->single)) $q->single = 0;
  352. $shuffleanswers = 0;
  353. if (isset($q->options->shuffleanswers) && $q->options->shuffleanswers) $shuffleanswers = 1;
  354. $imgUrl = "{$CFG->wwwroot}/pluginfile.php/".$q->options->id."/question/questiontext/".$attempt->id."/".$q ->id."/".$q ->id;
  355. $imgUrl .=substr ( sloodle_extract_first_image_url($q->questiontext), 14);
  356. $output[] = array(
  357. 'question',
  358. $localqnum, //$i, // The value in $i is equal to $q->id, rather than being sequential in the quiz
  359. $q->id,
  360. $q->parent,
  361. sloodle_clean_for_output($q->questiontext),
  362. $q->defaultgrade,
  363. $q->penalty,
  364. $q->qtype,
  365. $q->hidden,
  366. $q->maxgrade,
  367. $q->single,
  368. $shuffleanswers,
  369. 0, //$deferred // This variable doesn't seem to be mentioned anywhere else in the file
  370. $imgUrl
  371. );
  372. // Create an output array for our options (available answers) so that we can shuffle them later if necessary
  373. $outputoptions = array();
  374. // Go through each option
  375. $ops = $q->options;
  376. foreach($ops as $opkey=>$op) {
  377. if (!is_array($op)) continue; // Ignore this if there are no options (Prevents nasty PHP notices!)
  378. foreach($op as $ok=>$ov) {
  379. $answer_imgUrl = "{$CFG->wwwroot}/pluginfile.php/".$ov->id."/question/questiontext/".$attempt->id."/".$ov->question."/".$ov->question;
  380. $answer_imgUrl .=substr ( sloodle_extract_first_image_url($ov->answer), 14);
  381. $feedback_imgUrl = "{$CFG->wwwroot}/pluginfile.php/".$ov->id."/question/questiontext/".$attempt->id."/".$ov->question."/".$ov->question;
  382. $feedback_imgUrl .=substr ( sloodle_extract_first_image_url($ov->feedback), 14);
  383. $outputoptions[] = array(
  384. 'questionoption',
  385. $i,
  386. $ov->id,
  387. $ov->question,
  388. sloodle_clean_for_output($ov->answer),
  389. $ov->fraction,
  390. sloodle_clean_for_output($ov->feedback),
  391. $answer_imgUrl,
  392. $feedback_imgUrl
  393. );
  394. }
  395. }
  396. // Shuffle the options if necessary
  397. if ($shuffleanswers) shuffle($outputoptions);
  398. // Append our options to the main output array
  399. $output = array_merge($output, $outputoptions);
  400. }
  401. //print_question($questions[$i], $states[$i], $number, $quiz, $options);
  402. save_question_session($questions[$i], $states[$i]);
  403. $number += $questions[$i]->length;
  404. }
  405. $secondsleft = ($quiz->timeclose ? $quiz->timeclose : 999999999999) - time();
  406. //if ($isteacher) {
  407. // // For teachers ignore the quiz closing time
  408. // $secondsleft = 999999999999;
  409. //}
  410. // If time limit is set include floating timer.
  411. if ($quiz->timelimit > 0) {
  412. $output[] = array('seconds left',$secondsleft);
  413. }
  414. }
  415. $sloodle->response->set_status_code(1);
  416. $sloodle->response->set_status_descriptor('QUIZ');
  417. $sloodle->response->set_data($output);
  418. $sloodle->response->render_to_output();
  419. exit;
  420. ?>