upload_exercise.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. <?php
  2. /* For licensing terms, see /license.txt */
  3. use ChamiloSession as Session;
  4. /**
  5. * Upload quiz: This script shows the upload quiz feature.
  6. */
  7. $help_content = 'exercise_upload';
  8. require_once __DIR__.'/../inc/global.inc.php';
  9. api_protect_course_script(true);
  10. $is_allowed_to_edit = api_is_allowed_to_edit(null, true);
  11. $debug = false;
  12. $origin = api_get_origin();
  13. if (!$is_allowed_to_edit) {
  14. api_not_allowed(true);
  15. }
  16. $this_section = SECTION_COURSES;
  17. $htmlHeadXtra[] = "<script>
  18. $(function(){
  19. $('#user_custom_score').click(function() {
  20. $('#options').toggle();
  21. });
  22. });
  23. </script>";
  24. // Action handling
  25. lp_upload_quiz_action_handling();
  26. $interbreadcrumb[] = [
  27. 'url' => 'exercise.php?'.api_get_cidreq(),
  28. 'name' => get_lang('Tests'),
  29. ];
  30. Display::display_header(get_lang('Import quiz from Excel'), 'Exercises');
  31. echo '<div class="actions">';
  32. echo lp_upload_quiz_actions();
  33. echo '</div>';
  34. // the main content
  35. lp_upload_quiz_main();
  36. function lp_upload_quiz_actions()
  37. {
  38. $return = '<a href="exercise.php?'.api_get_cidreq().'">'.
  39. Display::return_icon(
  40. 'back.png',
  41. get_lang('BackToTestsList'),
  42. '',
  43. ICON_SIZE_MEDIUM
  44. ).'</a>';
  45. return $return;
  46. }
  47. function lp_upload_quiz_main()
  48. {
  49. $lp_id = isset($_GET['lp_id']) ? (int) $_GET['lp_id'] : null;
  50. $form = new FormValidator(
  51. 'upload',
  52. 'POST',
  53. api_get_self().'?'.api_get_cidreq().'&lp_id='.$lp_id,
  54. '',
  55. ['enctype' => 'multipart/form-data']
  56. );
  57. $form->addElement('header', get_lang('Import quiz from Excel'));
  58. $form->addElement('file', 'user_upload_quiz', get_lang('File upload'));
  59. $link = '<a href="../exercise/quiz_template.xls">'.
  60. Display::return_icon('export_excel.png', get_lang('Download the Excel Template')).get_lang('Download the Excel Template').'</a>';
  61. $form->addElement('label', '', $link);
  62. $table = new HTML_Table(['class' => 'table']);
  63. $tableList = [
  64. UNIQUE_ANSWER => get_lang('Multiple choice'),
  65. MULTIPLE_ANSWER => get_lang('Multiple answers'),
  66. FILL_IN_BLANKS => get_lang('Fill blanks or form'),
  67. MATCHING => get_lang('Matching'),
  68. FREE_ANSWER => get_lang('Open question'),
  69. GLOBAL_MULTIPLE_ANSWER => get_lang('Global multiple answer'),
  70. ];
  71. $table->setHeaderContents(0, 0, get_lang('Question type'));
  72. $table->setHeaderContents(0, 1, '#');
  73. $row = 1;
  74. foreach ($tableList as $key => $label) {
  75. $table->setCellContents($row, 0, $label);
  76. $table->setCellContents($row, 1, $key);
  77. $row++;
  78. }
  79. $table = $table->toHtml();
  80. $form->addElement('label', get_lang('Question type'), $table);
  81. $form->addElement(
  82. 'checkbox',
  83. 'user_custom_score',
  84. null,
  85. get_lang('Use custom score for all questions'),
  86. ['id' => 'user_custom_score']
  87. );
  88. $form->addElement('html', '<div id="options" style="display:none">');
  89. $form->addElement('text', 'correct_score', get_lang('Correct score'));
  90. $form->addElement('text', 'incorrect_score', get_lang('Incorrect score'));
  91. $form->addElement('html', '</div>');
  92. $form->addRule('user_upload_quiz', get_lang('Required field'), 'required');
  93. $form->addProgress();
  94. $form->addButtonUpload(get_lang('Upload'), 'submit_upload_quiz');
  95. $form->display();
  96. }
  97. /**
  98. * Handles a given Excel spreadsheets as in the template provided.
  99. */
  100. function lp_upload_quiz_action_handling()
  101. {
  102. if (!isset($_POST['submit_upload_quiz'])) {
  103. return;
  104. }
  105. $_course = api_get_course_info();
  106. if (empty($_course)) {
  107. return false;
  108. }
  109. $courseId = $_course['real_id'];
  110. // Get the extension of the document.
  111. $path_info = pathinfo($_FILES['user_upload_quiz']['name']);
  112. // Check if the document is an Excel document
  113. if (!in_array($path_info['extension'], ['xls', 'xlsx'])) {
  114. return;
  115. }
  116. // Variables
  117. $numberQuestions = 0;
  118. $question = [];
  119. $scoreList = [];
  120. $feedbackTrueList = [];
  121. $feedbackFalseList = [];
  122. $questionDescriptionList = [];
  123. $noNegativeScoreList = [];
  124. $questionTypeList = [];
  125. $answerList = [];
  126. $quizTitle = '';
  127. $objPHPExcel = PHPExcel_IOFactory::load($_FILES['user_upload_quiz']['tmp_name']);
  128. $objPHPExcel->setActiveSheetIndex(0);
  129. $worksheet = $objPHPExcel->getActiveSheet();
  130. $highestRow = $worksheet->getHighestRow(); // e.g. 10
  131. $highestColumn = $worksheet->getHighestColumn(); // e.g 'F'
  132. $correctScore = isset($_POST['correct_score']) ? $_POST['correct_score'] : null;
  133. $incorrectScore = isset($_POST['incorrect_score']) ? $_POST['incorrect_score'] : null;
  134. $useCustomScore = isset($_POST['user_custom_score']) ? true : false;
  135. for ($row = 1; $row <= $highestRow; $row++) {
  136. $cellTitleInfo = $worksheet->getCellByColumnAndRow(0, $row);
  137. $cellDataInfo = $worksheet->getCellByColumnAndRow(1, $row);
  138. $cellScoreInfo = $worksheet->getCellByColumnAndRow(2, $row);
  139. $title = $cellTitleInfo->getValue();
  140. switch ($title) {
  141. case 'Quiz':
  142. $quizTitle = $cellDataInfo->getValue();
  143. break;
  144. case 'Question':
  145. $question[] = $cellDataInfo->getValue();
  146. // Search cell with Answer title
  147. $answerRow = $row;
  148. $continue = true;
  149. $answerIndex = 0;
  150. while ($continue) {
  151. $answerRow++;
  152. $answerInfoTitle = $worksheet->getCellByColumnAndRow(0, $answerRow);
  153. $answerInfoData = $worksheet->getCellByColumnAndRow(1, $answerRow);
  154. $answerInfoExtra = $worksheet->getCellByColumnAndRow(2, $answerRow);
  155. $answerInfoTitle = $answerInfoTitle->getValue();
  156. if (strpos($answerInfoTitle, 'Answer') !== false) {
  157. $answerList[$numberQuestions][$answerIndex]['data'] = $answerInfoData->getValue();
  158. $answerList[$numberQuestions][$answerIndex]['extra'] = $answerInfoExtra->getValue();
  159. } else {
  160. $continue = false;
  161. }
  162. $answerIndex++;
  163. // To avoid loops
  164. if ($answerIndex > 60) {
  165. $continue = false;
  166. }
  167. }
  168. // Search cell with question type
  169. $answerRow = $row;
  170. $continue = true;
  171. $questionTypeIndex = 0;
  172. while ($continue) {
  173. $answerRow++;
  174. $questionTypeTitle = $worksheet->getCellByColumnAndRow(0, $answerRow);
  175. $questionTypeExtra = $worksheet->getCellByColumnAndRow(2, $answerRow);
  176. $title = $questionTypeTitle->getValue();
  177. if ($title == 'QuestionType') {
  178. $questionTypeList[$numberQuestions] = $questionTypeExtra->getValue();
  179. $continue = false;
  180. }
  181. if ($title == 'Question') {
  182. $continue = false;
  183. }
  184. // To avoid loops
  185. if ($questionTypeIndex > 60) {
  186. $continue = false;
  187. }
  188. $questionTypeIndex++;
  189. }
  190. // Detect answers
  191. $numberQuestions++;
  192. break;
  193. case 'Score':
  194. $scoreList[] = $cellScoreInfo->getValue();
  195. break;
  196. case 'NoNegativeScore':
  197. $noNegativeScoreList[] = $cellScoreInfo->getValue();
  198. break;
  199. case 'Category':
  200. $categoryList[] = $cellDataInfo->getValue();
  201. break;
  202. case 'FeedbackTrue':
  203. $feedbackTrueList[] = $cellDataInfo->getValue();
  204. break;
  205. case 'FeedbackFalse':
  206. $feedbackFalseList[] = $cellDataInfo->getValue();
  207. break;
  208. case 'EnrichQuestion':
  209. $questionDescriptionList[] = $cellDataInfo->getValue();
  210. break;
  211. }
  212. }
  213. $propagateNegative = 0;
  214. if ($useCustomScore && !empty($incorrectScore)) {
  215. if ($incorrectScore < 0) {
  216. $propagateNegative = 1;
  217. }
  218. }
  219. if ($quizTitle != '') {
  220. // Variables
  221. $type = 2;
  222. $random = $active = $results = $max_attempt = $expired_time = 0;
  223. // Make sure feedback is enabled (3 to disable), otherwise the fields
  224. // added to the XLS are not shown, which is confusing
  225. $feedback = 0;
  226. // Quiz object
  227. $exercise = new Exercise();
  228. $exercise->updateTitle($quizTitle);
  229. $exercise->updateExpiredTime($expired_time);
  230. $exercise->updateType($type);
  231. $exercise->setRandom($random);
  232. $exercise->active = $active;
  233. $exercise->updateResultsDisabled($results);
  234. $exercise->updateAttempts($max_attempt);
  235. $exercise->updateFeedbackType($feedback);
  236. $exercise->updatePropagateNegative($propagateNegative);
  237. $quiz_id = $exercise->save();
  238. if ($quiz_id) {
  239. // Import questions.
  240. for ($i = 0; $i < $numberQuestions; $i++) {
  241. // Question name
  242. $questionTitle = $question[$i];
  243. $myAnswerList = isset($answerList[$i]) ? $answerList[$i] : [];
  244. $description = isset($questionDescriptionList[$i]) ? $questionDescriptionList[$i] : '';
  245. $categoryId = null;
  246. if (isset($categoryList[$i]) && !empty($categoryList[$i])) {
  247. $categoryName = $categoryList[$i];
  248. $categoryId = TestCategory::get_category_id_for_title($categoryName, $courseId);
  249. if (empty($categoryId)) {
  250. $category = new TestCategory();
  251. $category->name = $categoryName;
  252. $categoryId = $category->save();
  253. }
  254. }
  255. $question_description_text = '<p></p>';
  256. if (!empty($description)) {
  257. // Question description.
  258. $question_description_text = "<p>$description</p>";
  259. }
  260. // Unique answers are the only question types available for now
  261. // through xls-format import
  262. $question_id = null;
  263. if (isset($questionTypeList[$i]) && $questionTypeList[$i] != '') {
  264. $detectQuestionType = (int) $questionTypeList[$i];
  265. } else {
  266. $detectQuestionType = detectQuestionType($myAnswerList);
  267. }
  268. /** @var Question $answer */
  269. switch ($detectQuestionType) {
  270. case FREE_ANSWER:
  271. $answer = new FreeAnswer();
  272. break;
  273. case GLOBAL_MULTIPLE_ANSWER:
  274. $answer = new GlobalMultipleAnswer();
  275. break;
  276. case MULTIPLE_ANSWER:
  277. $answer = new MultipleAnswer();
  278. break;
  279. case FILL_IN_BLANKS:
  280. $answer = new FillBlanks();
  281. $question_description_text = '';
  282. break;
  283. case MATCHING:
  284. $answer = new Matching();
  285. break;
  286. case UNIQUE_ANSWER:
  287. default:
  288. $answer = new UniqueAnswer();
  289. break;
  290. }
  291. if ($questionTitle != '') {
  292. $question_id = $answer->create_question(
  293. $quiz_id,
  294. $questionTitle,
  295. $question_description_text,
  296. 0, // max score
  297. $answer->type
  298. );
  299. if (!empty($categoryId)) {
  300. TestCategory::addCategoryToQuestion(
  301. $categoryId,
  302. $question_id,
  303. $courseId
  304. );
  305. }
  306. }
  307. switch ($detectQuestionType) {
  308. case GLOBAL_MULTIPLE_ANSWER:
  309. case MULTIPLE_ANSWER:
  310. case UNIQUE_ANSWER:
  311. $total = 0;
  312. if (is_array($myAnswerList) && !empty($myAnswerList) && !empty($question_id)) {
  313. $id = 1;
  314. $objAnswer = new Answer($question_id, $courseId);
  315. $globalScore = isset($scoreList[$i]) ? $scoreList[$i] : null;
  316. // Calculate the number of correct answers to divide the
  317. // score between them when importing from CSV
  318. $numberRightAnswers = 0;
  319. foreach ($myAnswerList as $answer_data) {
  320. if (strtolower($answer_data['extra']) == 'x') {
  321. $numberRightAnswers++;
  322. }
  323. }
  324. foreach ($myAnswerList as $answer_data) {
  325. $answerValue = $answer_data['data'];
  326. $correct = 0;
  327. $score = 0;
  328. if (strtolower($answer_data['extra']) == 'x') {
  329. $correct = 1;
  330. $score = isset($scoreList[$i]) ? $scoreList[$i] : 0;
  331. $comment = isset($feedbackTrueList[$i]) ? $feedbackTrueList[$i] : '';
  332. } else {
  333. $comment = isset($feedbackFalseList[$i]) ? $feedbackFalseList[$i] : '';
  334. $floatVal = (float) $answer_data['extra'];
  335. if (is_numeric($floatVal)) {
  336. $score = $answer_data['extra'];
  337. }
  338. }
  339. if ($useCustomScore) {
  340. if ($correct) {
  341. $score = $correctScore;
  342. } else {
  343. $score = $incorrectScore;
  344. }
  345. }
  346. // Fixing scores:
  347. switch ($detectQuestionType) {
  348. case GLOBAL_MULTIPLE_ANSWER:
  349. if ($correct) {
  350. $score = abs($scoreList[$i]);
  351. } else {
  352. if (isset($noNegativeScoreList[$i]) && $noNegativeScoreList[$i] == 'x') {
  353. $score = 0;
  354. } else {
  355. $score = -abs($scoreList[$i]);
  356. }
  357. }
  358. $score /= $numberRightAnswers;
  359. break;
  360. case UNIQUE_ANSWER:
  361. break;
  362. case MULTIPLE_ANSWER:
  363. if (!$correct) {
  364. //$total = $total - $score;
  365. }
  366. break;
  367. }
  368. $objAnswer->createAnswer(
  369. $answerValue,
  370. $correct,
  371. $comment,
  372. $score,
  373. $id
  374. );
  375. $total += (float) $score;
  376. $id++;
  377. }
  378. $objAnswer->save();
  379. $questionObj = Question::read(
  380. $question_id,
  381. $_course
  382. );
  383. if ($questionObj) {
  384. switch ($detectQuestionType) {
  385. case GLOBAL_MULTIPLE_ANSWER:
  386. $questionObj->updateWeighting($globalScore);
  387. break;
  388. case UNIQUE_ANSWER:
  389. case MULTIPLE_ANSWER:
  390. default:
  391. $questionObj->updateWeighting($total);
  392. break;
  393. }
  394. $questionObj->save($exercise);
  395. }
  396. }
  397. break;
  398. case FREE_ANSWER:
  399. $globalScore = isset($scoreList[$i]) ? $scoreList[$i] : null;
  400. $questionObj = Question::read($question_id, $_course);
  401. if ($questionObj) {
  402. $questionObj->updateWeighting($globalScore);
  403. $questionObj->save($exercise);
  404. }
  405. break;
  406. case FILL_IN_BLANKS:
  407. $fillInScoreList = [];
  408. $size = [];
  409. $globalScore = 0;
  410. foreach ($myAnswerList as $data) {
  411. $score = isset($data['extra']) ? $data['extra'] : 0;
  412. $globalScore += $score;
  413. $fillInScoreList[] = $score;
  414. $size[] = 200;
  415. }
  416. $scoreToString = implode(',', $fillInScoreList);
  417. $sizeToString = implode(',', $size);
  418. //<p>Texte long avec les [mots] à [remplir] mis entre [crochets]</p>::10,10,10:200.36363999999998,200,200:0@'
  419. $answerValue = $description.'::'.$scoreToString.':'.$sizeToString.':0@';
  420. $objAnswer = new Answer($question_id, $courseId);
  421. $objAnswer->createAnswer(
  422. $answerValue,
  423. '', //$correct,
  424. '', //$comment,
  425. $globalScore,
  426. 1
  427. );
  428. $objAnswer->save();
  429. $questionObj = Question::read($question_id, $_course);
  430. if ($questionObj) {
  431. $questionObj->updateWeighting($globalScore);
  432. $questionObj->save($exercise);
  433. }
  434. break;
  435. case MATCHING:
  436. $globalScore = isset($scoreList[$i]) ? $scoreList[$i] : null;
  437. $position = 1;
  438. $objAnswer = new Answer($question_id, $courseId);
  439. foreach ($myAnswerList as $data) {
  440. $option = isset($data['extra']) ? $data['extra'] : '';
  441. $objAnswer->createAnswer($option, 0, '', 0, $position);
  442. $position++;
  443. }
  444. $counter = 1;
  445. foreach ($myAnswerList as $data) {
  446. $value = isset($data['data']) ? $data['data'] : '';
  447. $position++;
  448. $objAnswer->createAnswer(
  449. $value,
  450. $counter,
  451. ' ',
  452. $globalScore,
  453. $position
  454. );
  455. $counter++;
  456. }
  457. $objAnswer->save();
  458. $questionObj = Question::read($question_id, $_course);
  459. if ($questionObj) {
  460. $questionObj->updateWeighting($globalScore);
  461. $questionObj->save($exercise);
  462. }
  463. break;
  464. }
  465. }
  466. }
  467. $lpObject = Session::read('lpobject');
  468. if (!empty($lpObject)) {
  469. /** @var learnpath $oLP */
  470. $oLP = UnserializeApi::unserialize('lp', $lpObject);
  471. if (is_object($oLP)) {
  472. if ((empty($oLP->cc)) || $oLP->cc != api_get_course_id()) {
  473. $oLP = null;
  474. Session::erase('oLP');
  475. Session::erase('lpobject');
  476. } else {
  477. Session::write('oLP', $oLP);
  478. }
  479. }
  480. }
  481. if (isset($_SESSION['oLP']) && isset($_GET['lp_id'])) {
  482. $previous = $_SESSION['oLP']->select_previous_item_id();
  483. $parent = 0;
  484. // Add a Quiz as Lp Item
  485. $_SESSION['oLP']->add_item(
  486. $parent,
  487. $previous,
  488. TOOL_QUIZ,
  489. $quiz_id,
  490. $quizTitle,
  491. ''
  492. );
  493. // Redirect to home page for add more content
  494. header('Location: ../lp/lp_controller.php?'.api_get_cidreq().'&action=add_item&type=step&lp_id='.intval($_GET['lp_id']));
  495. exit;
  496. } else {
  497. // header('location: exercise.php?' . api_get_cidreq());
  498. echo '<script>window.location.href = "'.api_get_path(WEB_CODE_PATH).'exercise/admin.php?'.api_get_cidreq().'&exerciseId='.$quiz_id.'&session_id='.api_get_session_id().'"</script>';
  499. }
  500. }
  501. }
  502. /**
  503. * @param array $answers_data
  504. *
  505. * @return int
  506. */
  507. function detectQuestionType($answers_data)
  508. {
  509. $correct = 0;
  510. $isNumeric = false;
  511. if (empty($answers_data)) {
  512. return FREE_ANSWER;
  513. }
  514. foreach ($answers_data as $answer_data) {
  515. if (strtolower($answer_data['extra']) == 'x') {
  516. $correct++;
  517. } else {
  518. if (is_numeric($answer_data['extra'])) {
  519. $isNumeric = true;
  520. }
  521. }
  522. }
  523. if ($correct == 1) {
  524. $type = UNIQUE_ANSWER;
  525. } else {
  526. if ($correct > 1) {
  527. $type = MULTIPLE_ANSWER;
  528. } else {
  529. $type = FREE_ANSWER;
  530. }
  531. }
  532. if ($type == MULTIPLE_ANSWER) {
  533. if ($isNumeric) {
  534. $type = MULTIPLE_ANSWER;
  535. } else {
  536. $type = GLOBAL_MULTIPLE_ANSWER;
  537. }
  538. }
  539. return $type;
  540. }
  541. if ($origin != 'learnpath') {
  542. //so we are not in learnpath tool
  543. Display::display_footer();
  544. }