linker_moodle_2_1_up.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  1. <?php
  2. /**
  3. * Quiz-engine-specific part of the SLOODLE quiz linker.
  4. * For Moodle 2.1 or higher
  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. /**
  18. *
  19. * This file is based on the following files from Moodle Core:
  20. * startattempt.php handles starting an attempt.
  21. * processattempt.php handles processing input.
  22. * finishattempt.php handles closing the attempt.
  23. *
  24. * These are combined into one, because in SLOODLE we linked to have one object talk to a single linker.php.
  25. *
  26. */
  27. defined('SLOODLE_LINKER_SCRIPT') || die();
  28. /*
  29. SLOODLE-specific attemptlib with a sub-class of one of the core Moodle classes:
  30. See that file for an explanation of why we're doing this.
  31. */
  32. require_once(SLOODLE_DIRROOT.'/mod/quiz-1.0/'.'attemptlib.php');
  33. /**
  34. * The following is mostly copied from startattempt.php
  35. */
  36. $quizobj = quiz::create($cm->instance, $USER->id);
  37. //require_login($quizobj->get_courseid(), false, $quizobj->get_cm());
  38. // if no questions have been set up yet redirect to edit.php or display an error.
  39. if (!$quizobj->has_questions()) {
  40. $sloodle->response->quick_output(-12312, 'QUIZ', 'The quiz does not have any questions', FALSE);
  41. exit();
  42. }
  43. // Create an object to manage all the other (non-roles) access rules.
  44. $accessmanager = $quizobj->get_access_manager(time());
  45. if ($quizobj->is_preview_user() && $forcenew) {
  46. $accessmanager->current_attempt_finished();
  47. }
  48. // Check capabilities.
  49. // SLOODLE TODO: Replace this with a check to has_capability, and the relevant SLOODLE error code
  50. if (!$quizobj->is_preview_user()) {
  51. $quizobj->require_capability('mod/quiz:attempt');
  52. }
  53. // Check to see if a new preview was requested.
  54. if ($quizobj->is_preview_user() && $forcenew) {
  55. // To force the creation of a new preview, we set a finish time on the
  56. // current attempt (if any). It will then automatically be deleted below
  57. $DB->set_field('quiz_attempts', 'timefinish', time(),
  58. array('quiz' => $quizobj->get_quizid(), 'userid' => $USER->id));
  59. }
  60. // Look for an existing attempt.
  61. $attempts = quiz_get_user_attempts($quizobj->get_quizid(), $USER->id, 'all', true);
  62. $lastattempt = end($attempts);
  63. // If an in-progress attempt exists, check password then redirect to it.
  64. if ($lastattempt && !$lastattempt->timefinish) {
  65. $currentattemptid = $lastattempt->id;
  66. $messages = $accessmanager->prevent_access();
  67. } else {
  68. // Get number for the next or unfinished attempt
  69. if ($lastattempt && !$lastattempt->preview && !$quizobj->is_preview_user()) {
  70. $attemptnumber = $lastattempt->attempt + 1;
  71. } else {
  72. $lastattempt = false;
  73. $attemptnumber = 1;
  74. }
  75. $currentattemptid = null;
  76. $messages = $accessmanager->prevent_access() +
  77. $accessmanager->prevent_new_attempt(count($attempts), $lastattempt);
  78. }
  79. // Check access.
  80. if (!$quizobj->is_preview_user() && $messages) {
  81. $sloodle->response->quick_output(-12313, 'QUIZ', 'You do not have permission to use this quiz.', FALSE);
  82. exit();
  83. }
  84. $attemptid = null;
  85. if ($currentattemptid) {
  86. $attemptid = $currentattemptid;
  87. } else {
  88. // Delete any previous preview attempts belonging to this user.
  89. quiz_delete_previews($quizobj->get_quiz(), $USER->id);
  90. $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
  91. $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
  92. // API changes in 2.4. https://github.com/moodle/moodle/commit/8e771aed93eea08cc3e9410283f5354e02311281
  93. if ($CFG->version < 2012120303) {
  94. // Create the new attempt and initialize the question sessions
  95. $attempt = quiz_create_attempt($quizobj->get_quiz(), $attemptnumber, $lastattempt, time(),
  96. $quizobj->is_preview_user());
  97. } else {
  98. // Create the new attempt and initialize the question sessions
  99. $attempt = quiz_create_attempt($quizobj, $attemptnumber, $lastattempt, time(),
  100. $quizobj->is_preview_user());
  101. }
  102. if (!($quizobj->get_quiz()->attemptonlast && $lastattempt)) {
  103. // Starting a normal, new, quiz attempt.
  104. // Fully load all the questions in this quiz.
  105. $quizobj->preload_questions();
  106. $quizobj->load_questions();
  107. // Add them all to the $quba.
  108. $idstoslots = array();
  109. $questionsinuse = array_keys($quizobj->get_questions());
  110. foreach ($quizobj->get_questions() as $i => $questiondata) {
  111. if ($questiondata->qtype != 'random') {
  112. if (!$quizobj->get_quiz()->shuffleanswers) {
  113. $questiondata->options->shuffleanswers = false;
  114. }
  115. $question = question_bank::make_question($questiondata);
  116. } else {
  117. $question = question_bank::get_qtype('random')->choose_other_question(
  118. $questiondata, $questionsinuse, $quizobj->get_quiz()->shuffleanswers);
  119. if (is_null($question)) {
  120. $sloodle->response->quick_output(-1, 'QUIZ', 'The quiz did not have enough questions.', FALSE);
  121. exit();
  122. }
  123. }
  124. $idstoslots[$i] = $quba->add_question($question, $questiondata->maxmark);
  125. $questionsinuse[] = $question->id;
  126. }
  127. // Start all the questions.
  128. if ($attempt->preview) {
  129. $variantoffset = rand(1, 100);
  130. } else {
  131. $variantoffset = $attemptnumber;
  132. }
  133. $quba->start_all_questions(
  134. new question_variant_pseudorandom_no_repeats_strategy($variantoffset),
  135. time());
  136. // Update attempt layout.
  137. $newlayout = array();
  138. foreach (explode(',', $attempt->layout) as $qid) {
  139. if ($qid != 0) {
  140. $newlayout[] = $idstoslots[$qid];
  141. } else {
  142. $newlayout[] = 0;
  143. }
  144. }
  145. $attempt->layout = implode(',', $newlayout);
  146. } else {
  147. // Starting a subsequent attempt in each attempt builds on last mode.
  148. $oldquba = question_engine::load_questions_usage_by_activity($lastattempt->uniqueid);
  149. $oldnumberstonew = array();
  150. foreach ($oldquba->get_attempt_iterator() as $oldslot => $oldqa) {
  151. $newslot = $quba->add_question($oldqa->get_question(), $oldqa->get_max_mark());
  152. $quba->start_question_based_on($newslot, $oldqa);
  153. $oldnumberstonew[$oldslot] = $newslot;
  154. }
  155. // Update attempt layout.
  156. $newlayout = array();
  157. foreach (explode(',', $lastattempt->layout) as $oldslot) {
  158. if ($oldslot != 0) {
  159. $newlayout[] = $oldnumberstonew[$oldslot];
  160. } else {
  161. $newlayout[] = 0;
  162. }
  163. }
  164. $attempt->layout = implode(',', $newlayout);
  165. }
  166. // Save the attempt in the database.
  167. $transaction = $DB->start_delegated_transaction();
  168. question_engine::save_questions_usage_by_activity($quba);
  169. $attempt->uniqueid = $quba->get_id();
  170. $attempt->id = $DB->insert_record('quiz_attempts', $attempt);
  171. // Log the new attempt.
  172. if ($attempt->preview) {
  173. add_to_log($course->id, 'quiz', 'preview', 'view.php?id=' . $quizobj->get_cmid(),
  174. $quizobj->get_quizid(), $quizobj->get_cmid());
  175. } else {
  176. add_to_log($course->id, 'quiz', 'attempt', 'review.php?attempt=' . $attempt->id,
  177. $quizobj->get_quizid(), $quizobj->get_cmid());
  178. }
  179. // Trigger event
  180. $eventdata = new stdClass();
  181. $eventdata->component = 'mod_quiz';
  182. $eventdata->attemptid = $attempt->id;
  183. $eventdata->timestart = $attempt->timestart;
  184. $eventdata->userid = $attempt->userid;
  185. $eventdata->quizid = $quizobj->get_quizid();
  186. $eventdata->cmid = $quizobj->get_cmid();
  187. $eventdata->courseid = $quizobj->get_courseid();
  188. events_trigger('quiz_attempt_started', $eventdata);
  189. $transaction->allow_commit();
  190. $attemptid = $attempt->id;
  191. }
  192. /*
  193. SLOODLE: Edmund Edgar, 2012-05-15
  194. The following comes from attempt.php
  195. Some of this is probably duplicated with code originally from startattempt.php
  196. ...but we'll repeat it just in case
  197. ...as I'm not sure exactly what it all does.
  198. */
  199. $attemptobj = sloodle_quiz_attempt::create($attemptid);
  200. $quiz = $attemptobj->get_quiz();
  201. $slots = $attemptobj->get_slots('all');
  202. if (empty($slots)) {
  203. $sloodle->response->quick_output(-12312, 'QUIZ', 'The quiz does not have any questions', FALSE);
  204. exit();
  205. }
  206. $title = get_string('attempt', 'quiz', $attemptobj->get_attempt_number());
  207. //$PAGE->set_title(format_string($attemptobj->get_quiz_name()));
  208. //$PAGE->set_heading($attemptobj->get_course()->fullname);
  209. //$accessmanager->setup_attempt_page($PAGE);
  210. // Check that this attempt belongs to this user.
  211. if ($attemptobj->get_userid() != $USER->id) {
  212. $sloodle->response->quick_output(-12314, 'QUIZ', 'The attempt did not appear to belong to you.', FALSE);
  213. exit();
  214. }
  215. // Check capabilities and block settings
  216. if (!$attemptobj->is_preview_user()) {
  217. $attemptobj->require_capability('mod/quiz:attempt');
  218. } else {
  219. //navigation_node::override_active_url($attemptobj->start_attempt_url());
  220. $sloodle->response->quick_output(-12315, 'QUIZ', 'You do not have permission to take the quiz. Moodle quizzes are often only open to students, not teachers.', FALSE);
  221. exit();
  222. }
  223. // If the attempt is already closed, send them to the review page.
  224. if ($attemptobj->is_finished()) {
  225. // TODO SLOODLE Error
  226. //redirect($attemptobj->review_url(null, $page));
  227. $sloodle->response->quick_output(-12316, 'QUIZ', 'This quiz attempt has already finished.', FALSE);
  228. exit();
  229. }
  230. // Check the access rules.
  231. $accessmanager = $attemptobj->get_access_manager(time());
  232. $messages = $accessmanager->prevent_access();
  233. if (count($messages)) {
  234. $sloodle->response->quick_output(-12317, 'QUIZ', 'Access to this quiz is restricted.', FALSE);
  235. exit();
  236. }
  237. $output = $PAGE->get_renderer('mod_quiz');
  238. if (!$attemptobj->is_preview_user() && $messages) {
  239. // TODO: SLOODLE ERROR
  240. //print_error('attempterror', 'quiz', $attemptobj->view_url(),
  241. // $output->access_messages($messages));
  242. }
  243. add_to_log($attemptobj->get_courseid(), 'quiz', 'continue attempt',
  244. 'review.php?attempt=' . $attemptobj->get_attemptid(),
  245. $attemptobj->get_quizid(), $attemptobj->get_cmid());
  246. /*
  247. *
  248. * The following mostly comes from processattempt.php
  249. *
  250. */
  251. if ($isnotify) {
  252. $transaction = $DB->start_delegated_transaction();
  253. if ($questionids != '') {
  254. $questionids = explode(',', $questionids);
  255. }
  256. if (!$questionid = $questionids[0]) {
  257. $sloodle->response->quick_output(-12318, 'QUIZ', 'Question ID not set.', FALSE);
  258. exit();
  259. }
  260. if (!$quizresponse) {
  261. $sloodle->response->quick_output(-12319, 'QUIZ', 'Question response not set.', FALSE);
  262. exit();
  263. }
  264. // echo $attemptobj->get_question_attempt(1)->get_field_prefix();
  265. // exit;
  266. $responseoptionindex = -1;
  267. //$attemptobj->process_all_actions(time(), array('answer'=>$quizresponse));
  268. // We get passed the quiz ID, so we'll have to work through them to find the slot.
  269. // TOOD: It would probably make more sense to have the LSL script know which slot and send us it itself.
  270. foreach($slots as $slot) {
  271. $qa = $attemptobj->get_question_attempt($slot);
  272. $q = $qa->get_question();
  273. if ($q->id == $questionid) {
  274. // The linker script returns the option ID, but we need its index for process_all_actions_for_slot.
  275. $order = $q->get_order($qa);
  276. $responseoptionindex = array_search( $quizresponse, $order );
  277. if ($responseoptionindex < 0) {
  278. $sloodle->response->quick_output(-12318, 'QUIZ', 'Response index not found.', FALSE);
  279. exit();
  280. }
  281. $submitteddata = array('answer'=>$responseoptionindex);
  282. //print "processing for $slot, $questionid $responseoptionindex";
  283. $attemptobj->process_all_actions_for_slot($slot, $submitteddata, time());
  284. $transaction->allow_commit();
  285. break;
  286. }
  287. }
  288. // We have now finished processing form data
  289. // With post sloodle-2.0 quiz chairs, we should be told what happened to the score.
  290. // In theory we should be able to get this from the data we already have, but it seems complex...
  291. // The process_events allows us to award points if there is an instruction to do so in the object config.
  292. $scorechange = floatval(optional_param( 'scorechange', 0, PARAM_TEXT));
  293. //SloodleDebugLogger::log('DEBUG', "active object check");
  294. if (!is_null($sloodle->active_object)) {
  295. //SloodleDebugLogger::log('DEBUG', "quiz has an active object");
  296. $sloodle->process_interaction('answerquestion', $scorechange);
  297. //$transaction->allow_commit();
  298. }
  299. $output = array();
  300. } else {
  301. /*
  302. $pagequestions = explode(',', $pagelist);
  303. $lastquestion = count($pagequestions);
  304. // TODO SLOODLE: Recheck this
  305. // Only output quiz data if a question has not been requetsed
  306. */
  307. $output = array();
  308. // Can't see a way to access this - every bastard thing is protected...
  309. // Load all the questions and loop through them...
  310. $availablequestionids = array();
  311. foreach($slots as $slot) {
  312. $qa = $attemptobj->get_question_attempt($slot);
  313. $q = $qa->get_question();
  314. //var_dump($q);
  315. //var_dump($attemptobj->get_question_attempt($slot));
  316. //exit;
  317. //var_dump($q);
  318. //exit;
  319. //var_dump($attemptobj->get_behaviour());
  320. //exit;
  321. $availablequestionids[] = $q->id;
  322. $localqnum++;
  323. if ($limittoquestion == $q->id) {
  324. //echo "<hr>"; print_r($q); echo "<hr>";
  325. // Make sure the variables exist (avoids warnings!)
  326. if (!isset($q->single)) $q->single = 0;
  327. $shuffleanswers = 0;
  328. if (isset($q->options->shuffleanswers) && $q->options->shuffleanswers) $shuffleanswers = 1;
  329. $qtypestr = is_object($q->qtype) ? get_class($q->qtype) : $q->qtype;
  330. // Feed the LSL script the question type it expects.
  331. // If we don't recognise it we'll just send the script whatever we got
  332. // ...which it will probably just tell the user it doesn't support.
  333. $supported_qtypes = array(
  334. 'multichoice' => 'multichoice',
  335. 'qtype_multichoice' => 'multichoice',
  336. 'truefalse' => 'truefalse',
  337. 'qtype_truefalse' => 'truefalse'
  338. );
  339. $qtype = isset($supported_qtypes[$qtypestr]) ? $supported_qtypes[$qtypestr] : $qtypestr;
  340. $output[] = array(
  341. 'question',
  342. $localqnum, //$i, // The value in $i is equal to $q->id, rather than being sequential in the quiz
  343. $q->id,
  344. $q->parent,
  345. sloodle_clean_for_output($q->questiontext),
  346. $q->defaultgrade ? $q->defaultgrade : 1,
  347. $q->penalty,
  348. $qtype,
  349. $q->hidden,
  350. $q->maxgrade,
  351. $q->single,
  352. $shuffleanswers,
  353. 0 //$deferred // This variable doesn't seem to be mentioned anywhere else in the file
  354. );
  355. // Create an output array for our options (available answers) so that we can shuffle them later if necessary
  356. $outputoptions = array();
  357. // Go through each option
  358. $ordered_option_keys = $q->get_order($qa);
  359. $ops = $q->answers;
  360. $i=1;
  361. foreach($ordered_option_keys as $option_key) {
  362. $op = $ops[$option_key];
  363. if (!is_object($op)) {
  364. continue; // Ignore this if there are no options (Prevents nasty PHP notices!)
  365. }
  366. $feedback = sloodle_clean_for_output($op->feedback);
  367. // If the feedback is too long, substitute a placeholder.
  368. // The script will see that and grab the feedback in a separate request.
  369. if (strlen($feedback) > 512) {
  370. $feedback = "[[LONG]]";
  371. }
  372. $outputoptions[] = array(
  373. 'questionoption',
  374. $i,
  375. $op->id,
  376. $q->id,
  377. sloodle_clean_for_output($op->answer),
  378. $op->fraction,
  379. $feedback
  380. );
  381. $i++;
  382. }
  383. // Shuffle the options if necessary
  384. if ($shuffleanswers) shuffle($outputoptions);
  385. // Append our options to the main output array
  386. $output = array_merge($output, $outputoptions);
  387. }
  388. /*
  389. var_dump($attemptobj->quba->render_question($slot,
  390. $this->get_display_options_with_edit_link($reviewing, $slot, $thispageurl),
  391. $this->quba->get_question($slot)->_number));
  392. */
  393. }
  394. if ($limittoquestion == 0) {
  395. $output[] = array('quiz',$quiz->attempts,$quiz->name,$quiz->timelimit,$quiz->id,$lastquestion);
  396. $output[] = array('quizpages',1,1,join(',',$availablequestionids));
  397. }
  398. $secondsleft = ($quiz->timeclose ? $quiz->timeclose : 999999999999) - time();
  399. //if ($isteacher) {
  400. // // For teachers ignore the quiz closing time
  401. // $secondsleft = 999999999999;
  402. //}
  403. // If time limit is set include floating timer.
  404. if ($quiz->timelimit > 0) {
  405. $output[] = array('seconds left',$secondsleft);
  406. }
  407. }
  408. /*
  409. *
  410. * The following mostly comes from finishattempt.php
  411. *
  412. */
  413. if ($finishattempt) {
  414. $transaction = $DB->start_delegated_transaction();
  415. // Log the end of this attempt.
  416. add_to_log($attemptobj->get_courseid(), 'quiz', 'close attempt',
  417. 'review.php?attempt=' . $attemptobj->get_attemptid(),
  418. $attemptobj->get_quizid(), $attemptobj->get_cmid());
  419. // Update the quiz attempt record.
  420. try {
  421. // This seems to have changed somewhere between Moodle 2.1 and 2.3.
  422. // Try the old method first, then the new method if that fails.
  423. if (method_exists($attemptobj, 'finish_attempt')) {
  424. $attemptobj->finish_attempt(time());
  425. } else {
  426. $attemptobj->process_finish(time(), false);
  427. }
  428. /*
  429. } catch (question_out_of_sequence_exception $e) {
  430. print_error('submissionoutofsequencefriendlymessage', 'question',
  431. $attemptobj->attempt_url(null, $thispage));
  432. */
  433. } catch (Exception $e) {
  434. $sloodle->response->quick_output(-701, 'QUIZ', 'Closing quiz failed: '.$e->getMessage(), FALSE);
  435. exit();
  436. }
  437. // Send the user to the review page.
  438. $transaction->allow_commit();
  439. // redirect('review.php?attempt='.$attempt->id);
  440. //$sloodle->response->quick_output(-701, 'QUIZ', 'Got to finishattempt - but do not yet have Sloodle code to handle it.', FALSE);
  441. //exit();
  442. }
  443. $sloodle->response->set_status_code(1);
  444. $sloodle->response->set_status_descriptor('QUIZ');
  445. $sloodle->response->set_data($output);
  446. $sloodle->response->render_to_output();
  447. exit;
  448. ?>