question.class.php 74 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269
  1. <?php
  2. /* For licensing terms, see /license.txt */
  3. use Chamilo\CourseBundle\Entity\CQuizAnswer;
  4. /**
  5. * Class Question
  6. *
  7. * This class allows to instantiate an object of type Question
  8. *
  9. * @author Olivier Brouckaert, original author
  10. * @author Patrick Cool, LaTeX support
  11. * @author Julio Montoya <gugli100@gmail.com> lot of bug fixes
  12. * @author hubert.borderiou@grenet.fr - add question categories
  13. * @package chamilo.exercise
  14. */
  15. abstract class Question
  16. {
  17. public $id;
  18. public $question;
  19. public $description;
  20. public $weighting;
  21. public $position;
  22. public $type;
  23. public $level;
  24. public $picture;
  25. public $exerciseList; // array with the list of exercises which this question is in
  26. public $category_list;
  27. public $parent_id;
  28. public $category;
  29. public $isContent;
  30. public $course;
  31. public $feedback;
  32. public static $typePicture = 'new_question.png';
  33. public static $explanationLangVar = '';
  34. public $question_table_class = 'table table-striped';
  35. public $questionTypeWithFeedback;
  36. public $extra;
  37. public static $questionTypes = array(
  38. UNIQUE_ANSWER => array('unique_answer.class.php', 'UniqueAnswer'),
  39. MULTIPLE_ANSWER => array('multiple_answer.class.php', 'MultipleAnswer'),
  40. FILL_IN_BLANKS => array('fill_blanks.class.php', 'FillBlanks'),
  41. MATCHING => array('matching.class.php', 'Matching'),
  42. FREE_ANSWER => array('freeanswer.class.php', 'FreeAnswer'),
  43. ORAL_EXPRESSION => array('oral_expression.class.php', 'OralExpression'),
  44. HOT_SPOT => array('hotspot.class.php', 'HotSpot'),
  45. HOT_SPOT_DELINEATION => array('hotspot.class.php', 'HotspotDelineation'),
  46. MULTIPLE_ANSWER_COMBINATION => array('multiple_answer_combination.class.php', 'MultipleAnswerCombination'),
  47. UNIQUE_ANSWER_NO_OPTION => array('unique_answer_no_option.class.php', 'UniqueAnswerNoOption'),
  48. MULTIPLE_ANSWER_TRUE_FALSE => array('multiple_answer_true_false.class.php', 'MultipleAnswerTrueFalse'),
  49. MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE => array(
  50. 'multiple_answer_combination_true_false.class.php',
  51. 'MultipleAnswerCombinationTrueFalse'
  52. ),
  53. GLOBAL_MULTIPLE_ANSWER => array('global_multiple_answer.class.php', 'GlobalMultipleAnswer'),
  54. CALCULATED_ANSWER => array('calculated_answer.class.php', 'CalculatedAnswer'),
  55. UNIQUE_ANSWER_IMAGE => ['UniqueAnswerImage.php', 'UniqueAnswerImage'],
  56. DRAGGABLE => ['Draggable.php', 'Draggable'],
  57. MATCHING_DRAGGABLE => ['MatchingDraggable.php', 'MatchingDraggable'],
  58. //MEDIA_QUESTION => array('media_question.class.php' , 'MediaQuestion')
  59. ANNOTATION => ['Annotation.php', 'Annotation'],
  60. READING_COMPREHENSION => ['ReadingComprehension.php', 'ReadingComprehension']
  61. );
  62. /**
  63. * constructor of the class
  64. *
  65. * @author Olivier Brouckaert
  66. */
  67. public function __construct()
  68. {
  69. $this->id = 0;
  70. $this->question = '';
  71. $this->description = '';
  72. $this->weighting = 0;
  73. $this->position = 1;
  74. $this->picture = '';
  75. $this->level = 1;
  76. $this->category = 0;
  77. // This variable is used when loading an exercise like an scenario with
  78. // an special hotspot: final_overlap, final_missing, final_excess
  79. $this->extra = '';
  80. $this->exerciseList = array();
  81. $this->course = api_get_course_info();
  82. $this->category_list = array();
  83. $this->parent_id = 0;
  84. // See BT#12611
  85. $this->questionTypeWithFeedback = [
  86. MATCHING,
  87. MATCHING_DRAGGABLE,
  88. DRAGGABLE,
  89. FILL_IN_BLANKS,
  90. FREE_ANSWER,
  91. ORAL_EXPRESSION,
  92. CALCULATED_ANSWER,
  93. ANNOTATION
  94. ];
  95. }
  96. /**
  97. * @return int|null
  98. */
  99. public function getIsContent()
  100. {
  101. $isContent = null;
  102. if (isset($_REQUEST['isContent'])) {
  103. $isContent = intval($_REQUEST['isContent']);
  104. }
  105. return $this->isContent = $isContent;
  106. }
  107. /**
  108. * Reads question information from the data base
  109. *
  110. * @param int $id - question ID
  111. * @param int $course_id
  112. *
  113. * @return Question
  114. *
  115. * @author Olivier Brouckaert
  116. */
  117. public static function read($id, $course_id = null)
  118. {
  119. $id = intval($id);
  120. if (!empty($course_id)) {
  121. $course_info = api_get_course_info_by_id($course_id);
  122. } else {
  123. $course_info = api_get_course_info();
  124. }
  125. $course_id = $course_info['real_id'];
  126. if (empty($course_id) || $course_id == -1) {
  127. return false;
  128. }
  129. $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
  130. $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
  131. $sql = "SELECT *
  132. FROM $TBL_QUESTIONS
  133. WHERE c_id = $course_id AND id = $id ";
  134. $result = Database::query($sql);
  135. // if the question has been found
  136. if ($object = Database::fetch_object($result)) {
  137. $objQuestion = self::getInstance($object->type);
  138. if (!empty($objQuestion)) {
  139. $objQuestion->id = $id;
  140. $objQuestion->question = $object->question;
  141. $objQuestion->description = $object->description;
  142. $objQuestion->weighting = $object->ponderation;
  143. $objQuestion->position = $object->position;
  144. $objQuestion->type = $object->type;
  145. $objQuestion->picture = $object->picture;
  146. $objQuestion->level = (int) $object->level;
  147. $objQuestion->extra = $object->extra;
  148. $objQuestion->course = $course_info;
  149. $objQuestion->feedback = isset($object->feedback) ? $object->feedback : '';
  150. $objQuestion->category = TestCategory::getCategoryForQuestion($id);
  151. $tblQuiz = Database::get_course_table(TABLE_QUIZ_TEST);
  152. $sql = "SELECT DISTINCT q.exercice_id
  153. FROM $TBL_EXERCISE_QUESTION q
  154. INNER JOIN $tblQuiz e
  155. ON e.c_id = q.c_id AND e.id = q.exercice_id
  156. WHERE
  157. q.c_id = $course_id AND
  158. q.question_id = $id AND
  159. e.active >= 0";
  160. $result = Database::query($sql);
  161. // fills the array with the exercises which this question is in
  162. if ($result) {
  163. while ($obj = Database::fetch_object($result)) {
  164. $objQuestion->exerciseList[] = $obj->exercice_id;
  165. }
  166. }
  167. return $objQuestion;
  168. }
  169. }
  170. // question not found
  171. return false;
  172. }
  173. /**
  174. * returns the question ID
  175. *
  176. * @author Olivier Brouckaert
  177. *
  178. * @return integer - question ID
  179. */
  180. public function selectId()
  181. {
  182. return $this->id;
  183. }
  184. /**
  185. * returns the question title
  186. *
  187. * @author Olivier Brouckaert
  188. * @return string - question title
  189. */
  190. public function selectTitle()
  191. {
  192. if (!api_get_configuration_value('save_titles_as_html')) {
  193. return $this->question;
  194. }
  195. return Display::div($this->question, ['style' => 'display: inline-block;']);
  196. }
  197. /**
  198. * @param int $itemNumber
  199. * @return string
  200. */
  201. public function getTitleToDisplay($itemNumber)
  202. {
  203. $showQuestionTitleHtml = api_get_configuration_value('save_titles_as_html');
  204. $title = $showQuestionTitleHtml ? '' : '<strong>';
  205. $title .= $itemNumber.'. '.$this->selectTitle();
  206. $title .= $showQuestionTitleHtml ? '' : '</strong>';
  207. return Display::div(
  208. $title,
  209. ['class' => 'question_title']
  210. );
  211. }
  212. /**
  213. * returns the question description
  214. *
  215. * @author Olivier Brouckaert
  216. * @return string - question description
  217. */
  218. public function selectDescription()
  219. {
  220. return $this->description;
  221. }
  222. /**
  223. * returns the question weighting
  224. *
  225. * @author Olivier Brouckaert
  226. * @return integer - question weighting
  227. */
  228. public function selectWeighting()
  229. {
  230. return $this->weighting;
  231. }
  232. /**
  233. * returns the question position
  234. *
  235. * @author Olivier Brouckaert
  236. * @return integer - question position
  237. */
  238. public function selectPosition()
  239. {
  240. return $this->position;
  241. }
  242. /**
  243. * returns the answer type
  244. *
  245. * @author Olivier Brouckaert
  246. * @return integer - answer type
  247. */
  248. public function selectType()
  249. {
  250. return $this->type;
  251. }
  252. /**
  253. * returns the level of the question
  254. *
  255. * @author Nicolas Raynaud
  256. * @return integer - level of the question, 0 by default.
  257. */
  258. public function getLevel()
  259. {
  260. return $this->level;
  261. }
  262. /**
  263. * returns the picture name
  264. *
  265. * @author Olivier Brouckaert
  266. * @return string - picture name
  267. */
  268. public function selectPicture()
  269. {
  270. return $this->picture;
  271. }
  272. /**
  273. * @return string
  274. */
  275. public function selectPicturePath()
  276. {
  277. if (!empty($this->picture)) {
  278. return api_get_path(WEB_COURSE_PATH).$this->course['directory'].'/document/images/'.$this->getPictureFilename();
  279. }
  280. return '';
  281. }
  282. /**
  283. * @return int|string
  284. */
  285. public function getPictureId()
  286. {
  287. // for backward compatibility
  288. // when in field picture we had the filename not the document id
  289. if (preg_match("/quiz-.*/", $this->picture)) {
  290. return DocumentManager::get_document_id(
  291. $this->course,
  292. $this->selectPicturePath(),
  293. api_get_session_id()
  294. );
  295. }
  296. return $this->picture;
  297. }
  298. /**
  299. * @param int $courseId
  300. * @param int $sessionId
  301. * @return string
  302. */
  303. public function getPictureFilename($courseId = 0, $sessionId = 0)
  304. {
  305. $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
  306. $sessionId = empty($sessionId) ? api_get_session_id() : (int) $sessionId;
  307. if (empty($courseId)) {
  308. return '';
  309. }
  310. // for backward compatibility
  311. // when in field picture we had the filename not the document id
  312. if (preg_match("/quiz-.*/", $this->picture)) {
  313. return $this->picture;
  314. }
  315. $pictureId = $this->getPictureId();
  316. $courseInfo = $this->course;
  317. $documentInfo = DocumentManager::get_document_data_by_id(
  318. $pictureId,
  319. $courseInfo['code'],
  320. false,
  321. $sessionId
  322. );
  323. $documentFilename = '';
  324. if ($documentInfo) {
  325. // document in document/images folder
  326. $documentFilename = pathinfo(
  327. $documentInfo['path'],
  328. PATHINFO_BASENAME
  329. );
  330. }
  331. return $documentFilename;
  332. }
  333. /**
  334. * returns the array with the exercise ID list
  335. *
  336. * @author Olivier Brouckaert
  337. * @return array - list of exercise ID which the question is in
  338. */
  339. public function selectExerciseList()
  340. {
  341. return $this->exerciseList;
  342. }
  343. /**
  344. * returns the number of exercises which this question is in
  345. *
  346. * @author Olivier Brouckaert
  347. * @return integer - number of exercises
  348. */
  349. public function selectNbrExercises()
  350. {
  351. return sizeof($this->exerciseList);
  352. }
  353. /**
  354. * changes the question title
  355. *
  356. * @param string $title - question title
  357. *
  358. * @author Olivier Brouckaert
  359. */
  360. public function updateTitle($title)
  361. {
  362. $this->question = $title;
  363. }
  364. /**
  365. * @param int $id
  366. */
  367. public function updateParentId($id)
  368. {
  369. $this->parent_id = intval($id);
  370. }
  371. /**
  372. * changes the question description
  373. *
  374. * @param string $description - question description
  375. *
  376. * @author Olivier Brouckaert
  377. *
  378. */
  379. public function updateDescription($description)
  380. {
  381. $this->description = $description;
  382. }
  383. /**
  384. * changes the question weighting
  385. *
  386. * @param integer $weighting - question weighting
  387. *
  388. * @author Olivier Brouckaert
  389. */
  390. public function updateWeighting($weighting)
  391. {
  392. $this->weighting = $weighting;
  393. }
  394. /**
  395. * @param array $category
  396. *
  397. * @author Hubert Borderiou 12-10-2011
  398. *
  399. */
  400. public function updateCategory($category)
  401. {
  402. $this->category = $category;
  403. }
  404. /**
  405. * @param int $value
  406. *
  407. * @author Hubert Borderiou 12-10-2011
  408. */
  409. public function updateScoreAlwaysPositive($value)
  410. {
  411. $this->scoreAlwaysPositive = $value;
  412. }
  413. /**
  414. * @param int $value
  415. *
  416. * @author Hubert Borderiou 12-10-2011
  417. */
  418. public function updateUncheckedMayScore($value)
  419. {
  420. $this->uncheckedMayScore = $value;
  421. }
  422. /**
  423. * Save category of a question
  424. *
  425. * A question can have n categories if category is empty,
  426. * then question has no category then delete the category entry
  427. *
  428. * @param array $category_list
  429. *
  430. * @author Julio Montoya - Adding multiple cat support
  431. */
  432. public function saveCategories($category_list)
  433. {
  434. if (!empty($category_list)) {
  435. $this->deleteCategory();
  436. $TBL_QUESTION_REL_CATEGORY = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
  437. // update or add category for a question
  438. foreach ($category_list as $category_id) {
  439. $category_id = intval($category_id);
  440. $question_id = intval($this->id);
  441. $sql = "SELECT count(*) AS nb
  442. FROM $TBL_QUESTION_REL_CATEGORY
  443. WHERE
  444. category_id = $category_id
  445. AND question_id = $question_id
  446. AND c_id=".api_get_course_int_id();
  447. $res = Database::query($sql);
  448. $row = Database::fetch_array($res);
  449. if ($row['nb'] > 0) {
  450. // DO nothing
  451. } else {
  452. $sql = "INSERT INTO $TBL_QUESTION_REL_CATEGORY (c_id, question_id, category_id)
  453. VALUES (".api_get_course_int_id().", $question_id, $category_id)";
  454. Database::query($sql);
  455. }
  456. }
  457. }
  458. }
  459. /**
  460. * in this version, a question can only have 1 category
  461. * if category is 0, then question has no category then delete the category entry
  462. * @param int $categoryId
  463. * @return bool
  464. *
  465. * @author Hubert Borderiou 12-10-2011
  466. */
  467. public function saveCategory($categoryId)
  468. {
  469. $courseId = api_get_course_int_id();
  470. if (empty($courseId)) {
  471. return false;
  472. }
  473. if ($categoryId <= 0) {
  474. $this->deleteCategory();
  475. } else {
  476. // update or add category for a question
  477. $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
  478. $categoryId = intval($categoryId);
  479. $question_id = intval($this->id);
  480. $sql = "SELECT count(*) AS nb FROM $table
  481. WHERE
  482. question_id = $question_id AND
  483. c_id = ".$courseId;
  484. $res = Database::query($sql);
  485. $row = Database::fetch_array($res);
  486. if ($row['nb'] > 0) {
  487. $sql = "UPDATE $table
  488. SET category_id = $categoryId
  489. WHERE
  490. question_id = $question_id AND
  491. c_id = ".$courseId;
  492. Database::query($sql);
  493. } else {
  494. $sql = "INSERT INTO $table (c_id, question_id, category_id)
  495. VALUES (".$courseId.", $question_id, $categoryId)";
  496. Database::query($sql);
  497. }
  498. return true;
  499. }
  500. }
  501. /**
  502. * @author hubert borderiou 12-10-2011
  503. * delete any category entry for question id
  504. * delete the category for question
  505. */
  506. public function deleteCategory()
  507. {
  508. $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
  509. $question_id = intval($this->id);
  510. $sql = "DELETE FROM $table
  511. WHERE
  512. question_id = $question_id AND
  513. c_id = ".api_get_course_int_id();
  514. Database::query($sql);
  515. }
  516. /**
  517. * changes the question position
  518. *
  519. * @param integer $position - question position
  520. *
  521. * @author Olivier Brouckaert
  522. */
  523. public function updatePosition($position)
  524. {
  525. $this->position = $position;
  526. }
  527. /**
  528. * changes the question level
  529. *
  530. * @param integer $level - question level
  531. *
  532. * @author Nicolas Raynaud
  533. */
  534. public function updateLevel($level)
  535. {
  536. $this->level = $level;
  537. }
  538. /**
  539. * changes the answer type. If the user changes the type from "unique answer" to "multiple answers"
  540. * (or conversely) answers are not deleted, otherwise yes
  541. *
  542. * @param integer $type - answer type
  543. *
  544. * @author Olivier Brouckaert
  545. */
  546. public function updateType($type)
  547. {
  548. $TBL_REPONSES = Database::get_course_table(TABLE_QUIZ_ANSWER);
  549. $course_id = $this->course['real_id'];
  550. if (empty($course_id)) {
  551. $course_id = api_get_course_int_id();
  552. }
  553. // if we really change the type
  554. if ($type != $this->type) {
  555. // if we don't change from "unique answer" to "multiple answers" (or conversely)
  556. if (
  557. !in_array($this->type, array(UNIQUE_ANSWER, MULTIPLE_ANSWER)) ||
  558. !in_array($type, array(UNIQUE_ANSWER, MULTIPLE_ANSWER))
  559. ) {
  560. // removes old answers
  561. $sql = "DELETE FROM $TBL_REPONSES
  562. WHERE c_id = $course_id AND question_id = ".intval($this->id);
  563. Database::query($sql);
  564. }
  565. $this->type = $type;
  566. }
  567. }
  568. /**
  569. * Get default hot spot folder in documents
  570. * @return string
  571. */
  572. public function getHotSpotFolderInCourse()
  573. {
  574. if (empty($this->course) || empty($this->course['directory'])) {
  575. // Stop everything if course is not set.
  576. api_not_allowed();
  577. }
  578. $pictureAbsolutePath = api_get_path(SYS_COURSE_PATH).$this->course['directory'].'/document/images/';
  579. $picturePath = basename($pictureAbsolutePath);
  580. if (!is_dir($picturePath)) {
  581. create_unexisting_directory(
  582. $this->course,
  583. api_get_user_id(),
  584. 0,
  585. 0,
  586. 0,
  587. dirname($pictureAbsolutePath),
  588. '/'.$picturePath,
  589. $picturePath
  590. );
  591. }
  592. return $pictureAbsolutePath;
  593. }
  594. /**
  595. * adds a picture to the question
  596. *
  597. * @param string $picture - temporary path of the picture to upload
  598. *
  599. * @return boolean - true if uploaded, otherwise false
  600. *
  601. * @author Olivier Brouckaert
  602. */
  603. public function uploadPicture($picture)
  604. {
  605. $picturePath = $this->getHotSpotFolderInCourse();
  606. // if the question has got an ID
  607. if ($this->id) {
  608. $pictureFilename = self::generatePictureName();
  609. $img = new Image($picture);
  610. $img->send_image($picturePath.'/'.$pictureFilename, -1, 'jpg');
  611. $document_id = add_document(
  612. $this->course,
  613. '/images/'.$pictureFilename,
  614. 'file',
  615. filesize($picturePath.'/'.$pictureFilename),
  616. $pictureFilename
  617. );
  618. if ($document_id) {
  619. $this->picture = $document_id;
  620. if (!file_exists($picturePath.'/'.$pictureFilename)) {
  621. return false;
  622. }
  623. api_item_property_update(
  624. $this->course,
  625. TOOL_DOCUMENT,
  626. $document_id,
  627. 'DocumentAdded',
  628. api_get_user_id()
  629. );
  630. $this->resizePicture('width', 800);
  631. return true;
  632. }
  633. }
  634. return false;
  635. }
  636. /**
  637. * return the name for image use in hotspot question
  638. * to be unique, name is quiz-[utc unix timestamp].jpg
  639. * @param string $prefix
  640. * @param string $extension
  641. * @return string
  642. */
  643. public function generatePictureName($prefix = 'quiz-', $extension = 'jpg')
  644. {
  645. // image name is quiz-xxx.jpg in folder images/
  646. $utcTime = time();
  647. return $prefix.$utcTime.'.'.$extension;
  648. }
  649. /**
  650. * Resizes a picture || Warning!: can only be called after uploadPicture,
  651. * or if picture is already available in object.
  652. * @param string $Dimension - Resizing happens proportional according to given dimension: height|width|any
  653. * @param integer $Max - Maximum size
  654. *
  655. * @return boolean|null - true if success, false if failed
  656. *
  657. * @author Toon Keppens
  658. */
  659. private function resizePicture($Dimension, $Max)
  660. {
  661. // if the question has an ID
  662. if (!$this->id) {
  663. return false;
  664. }
  665. $picturePath = $this->getHotSpotFolderInCourse().'/'.$this->getPictureFilename();
  666. // Get dimensions from current image.
  667. $my_image = new Image($picturePath);
  668. $current_image_size = $my_image->get_image_size();
  669. $current_width = $current_image_size['width'];
  670. $current_height = $current_image_size['height'];
  671. if ($current_width < $Max && $current_height < $Max) {
  672. return true;
  673. } elseif ($current_height == '') {
  674. return false;
  675. }
  676. // Resize according to height.
  677. if ($Dimension == "height") {
  678. $resize_scale = $current_height / $Max;
  679. $new_width = ceil($current_width / $resize_scale);
  680. }
  681. // Resize according to width
  682. if ($Dimension == "width") {
  683. $new_width = $Max;
  684. }
  685. // Resize according to height or width, both should not be larger than $Max after resizing.
  686. if ($Dimension == "any") {
  687. if ($current_height > $current_width || $current_height == $current_width) {
  688. $resize_scale = $current_height / $Max;
  689. $new_width = ceil($current_width / $resize_scale);
  690. }
  691. if ($current_height < $current_width) {
  692. $new_width = $Max;
  693. }
  694. }
  695. $my_image->resize($new_width);
  696. $result = $my_image->send_image($picturePath);
  697. if ($result) {
  698. return true;
  699. }
  700. return false;
  701. }
  702. /**
  703. * deletes the picture
  704. *
  705. * @author Olivier Brouckaert
  706. * @return boolean - true if removed, otherwise false
  707. */
  708. public function removePicture()
  709. {
  710. $picturePath = $this->getHotSpotFolderInCourse();
  711. // if the question has got an ID and if the picture exists
  712. if ($this->id) {
  713. $picture = $this->picture;
  714. $this->picture = '';
  715. return @unlink($picturePath.'/'.$picture) ? true : false;
  716. }
  717. return false;
  718. }
  719. /**
  720. * Exports a picture to another question
  721. *
  722. * @author Olivier Brouckaert
  723. * @param integer $questionId - ID of the target question
  724. * @param array $courseInfo
  725. * @return boolean - true if copied, otherwise false
  726. */
  727. public function exportPicture($questionId, $courseInfo)
  728. {
  729. if (empty($questionId) || empty($courseInfo)) {
  730. return false;
  731. }
  732. $course_id = $courseInfo['real_id'];
  733. $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
  734. $destination_path = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document/images';
  735. $source_path = $this->getHotSpotFolderInCourse();
  736. // if the question has got an ID and if the picture exists
  737. if (!$this->id || empty($this->picture)) {
  738. return false;
  739. }
  740. $picture = $this->generatePictureName();
  741. if (file_exists($source_path.'/'.$this->picture)) {
  742. // for backward compatibility
  743. $result = @copy(
  744. $source_path.'/'.$this->picture,
  745. $destination_path.'/'.$picture
  746. );
  747. } else {
  748. $imageInfo = DocumentManager::get_document_data_by_id(
  749. $this->picture,
  750. $courseInfo['code']
  751. );
  752. if (file_exists($imageInfo['absolute_path'])) {
  753. $result = @copy(
  754. $imageInfo['absolute_path'],
  755. $destination_path.'/'.$picture
  756. );
  757. }
  758. }
  759. // If copy was correct then add to the database
  760. if (!$result) {
  761. return false;
  762. }
  763. $sql = "UPDATE $TBL_QUESTIONS SET
  764. picture = '".Database::escape_string($picture)."'
  765. WHERE c_id = $course_id AND id='".intval($questionId)."'";
  766. Database::query($sql);
  767. $documentId = add_document(
  768. $courseInfo,
  769. '/images/'.$picture,
  770. 'file',
  771. filesize($destination_path.'/'.$picture),
  772. $picture
  773. );
  774. if (!$documentId) {
  775. return false;
  776. }
  777. return api_item_property_update(
  778. $courseInfo,
  779. TOOL_DOCUMENT,
  780. $documentId,
  781. 'DocumentAdded',
  782. api_get_user_id()
  783. );
  784. }
  785. /**
  786. * Saves the picture coming from POST into a temporary file
  787. * Temporary pictures are used when we don't want to save a picture right after a form submission.
  788. * For example, if we first show a confirmation box.
  789. *
  790. * @author Olivier Brouckaert
  791. * @param string $picture - temporary path of the picture to move
  792. * @param string $pictureName - Name of the picture
  793. */
  794. public function setTmpPicture($picture, $pictureName)
  795. {
  796. $picturePath = $this->getHotSpotFolderInCourse();
  797. $pictureName = explode('.', $pictureName);
  798. $Extension = $pictureName[sizeof($pictureName) - 1];
  799. // saves the picture into a temporary file
  800. @move_uploaded_file($picture, $picturePath.'/tmp.'.$Extension);
  801. }
  802. /**
  803. * Set title
  804. * @param string $title
  805. */
  806. public function setTitle($title)
  807. {
  808. $this->question = $title;
  809. }
  810. /**
  811. * Sets extra info
  812. * @param string $extra
  813. */
  814. public function setExtra($extra)
  815. {
  816. $this->extra = $extra;
  817. }
  818. /**
  819. * updates the question in the data base
  820. * if an exercise ID is provided, we add that exercise ID into the exercise list
  821. *
  822. * @author Olivier Brouckaert
  823. * @param Exercise $exercise
  824. */
  825. public function save($exercise)
  826. {
  827. $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
  828. $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
  829. $em = Database::getManager();
  830. $exerciseId = $exercise->id;
  831. $id = $this->id;
  832. $question = $this->question;
  833. $description = $this->description;
  834. $weighting = $this->weighting;
  835. $position = $this->position;
  836. $type = $this->type;
  837. $picture = $this->picture;
  838. $level = $this->level;
  839. $extra = $this->extra;
  840. $c_id = $this->course['real_id'];
  841. $categoryId = $this->category;
  842. // question already exists
  843. if (!empty($id)) {
  844. $params = [
  845. 'question' => $question,
  846. 'description' => $description,
  847. 'ponderation' => $weighting,
  848. 'position' => $position,
  849. 'type' => $type,
  850. 'picture' => $picture,
  851. 'extra' => $extra,
  852. 'level' => $level,
  853. ];
  854. if ($exercise->questionFeedbackEnabled) {
  855. $params['feedback'] = $this->feedback;
  856. }
  857. Database::update(
  858. $TBL_QUESTIONS,
  859. $params,
  860. ['c_id = ? AND id = ?' => [$c_id, $id]]
  861. );
  862. $this->saveCategory($categoryId);
  863. if (!empty($exerciseId)) {
  864. api_item_property_update(
  865. $this->course,
  866. TOOL_QUIZ,
  867. $id,
  868. 'QuizQuestionUpdated',
  869. api_get_user_id()
  870. );
  871. }
  872. if (api_get_setting('search_enabled') == 'true') {
  873. if ($exerciseId != 0) {
  874. $this->search_engine_edit($exerciseId);
  875. } else {
  876. /**
  877. * actually there is *not* an user interface for
  878. * creating questions without a relation with an exercise
  879. */
  880. }
  881. }
  882. } else {
  883. // creates a new question
  884. $sql = "SELECT max(position)
  885. FROM $TBL_QUESTIONS as question,
  886. $TBL_EXERCISE_QUESTION as test_question
  887. WHERE
  888. question.id = test_question.question_id AND
  889. test_question.exercice_id = ".intval($exerciseId)." AND
  890. question.c_id = $c_id AND
  891. test_question.c_id = $c_id ";
  892. $result = Database::query($sql);
  893. $current_position = Database::result($result, 0, 0);
  894. $this->updatePosition($current_position + 1);
  895. $position = $this->position;
  896. $params = [
  897. 'c_id' => $c_id,
  898. 'question' => $question,
  899. 'description' => $description,
  900. 'ponderation' => $weighting,
  901. 'position' => $position,
  902. 'type' => $type,
  903. 'picture' => $picture,
  904. 'extra' => $extra,
  905. 'level' => $level
  906. ];
  907. if ($exercise->questionFeedbackEnabled) {
  908. $params['feedback'] = $this->feedback;
  909. }
  910. $this->id = Database::insert($TBL_QUESTIONS, $params);
  911. if ($this->id) {
  912. $sql = "UPDATE $TBL_QUESTIONS SET id = iid WHERE iid = {$this->id}";
  913. Database::query($sql);
  914. api_item_property_update(
  915. $this->course,
  916. TOOL_QUIZ,
  917. $this->id,
  918. 'QuizQuestionAdded',
  919. api_get_user_id()
  920. );
  921. // If hotspot, create first answer
  922. if ($type == HOT_SPOT || $type == HOT_SPOT_ORDER) {
  923. $quizAnswer = new CQuizAnswer();
  924. $quizAnswer
  925. ->setCId($c_id)
  926. ->setQuestionId($this->id)
  927. ->setAnswer('')
  928. ->setPonderation(10)
  929. ->setPosition(1)
  930. ->setHotspotCoordinates('0;0|0|0')
  931. ->setHotspotType('square');
  932. $em->persist($quizAnswer);
  933. $em->flush();
  934. $id = $quizAnswer->getIid();
  935. if ($id) {
  936. $quizAnswer
  937. ->setId($id)
  938. ->setIdAuto($id);
  939. $em->merge($quizAnswer);
  940. $em->flush();
  941. }
  942. }
  943. if ($type == HOT_SPOT_DELINEATION) {
  944. $quizAnswer = new CQuizAnswer();
  945. $quizAnswer
  946. ->setCId($c_id)
  947. ->setQuestionId($this->id)
  948. ->setAnswer('')
  949. ->setPonderation(10)
  950. ->setPosition(1)
  951. ->setHotspotCoordinates('0;0|0|0')
  952. ->setHotspotType('delineation');
  953. $em->persist($quizAnswer);
  954. $em->flush();
  955. $id = $quizAnswer->getIid();
  956. if ($id) {
  957. $quizAnswer
  958. ->setId($id)
  959. ->setIdAuto($id);
  960. $em->merge($quizAnswer);
  961. $em->flush();
  962. }
  963. }
  964. if (api_get_setting('search_enabled') == 'true') {
  965. if ($exerciseId != 0) {
  966. $this->search_engine_edit($exerciseId, true);
  967. } else {
  968. /**
  969. * actually there is *not* an user interface for
  970. * creating questions without a relation with an exercise
  971. */
  972. }
  973. }
  974. }
  975. }
  976. // if the question is created in an exercise
  977. if ($exerciseId) {
  978. // adds the exercise into the exercise list of this question
  979. $this->addToList($exerciseId, true);
  980. }
  981. }
  982. public function search_engine_edit(
  983. $exerciseId,
  984. $addQs = false,
  985. $rmQs = false
  986. ) {
  987. // update search engine and its values table if enabled
  988. if (api_get_setting('search_enabled') == 'true' && extension_loaded('xapian')) {
  989. $course_id = api_get_course_id();
  990. // get search_did
  991. $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
  992. if ($addQs || $rmQs) {
  993. //there's only one row per question on normal db and one document per question on search engine db
  994. $sql = 'SELECT * FROM %s
  995. WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_second_level=%s LIMIT 1';
  996. $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
  997. } else {
  998. $sql = 'SELECT * FROM %s
  999. WHERE course_code=\'%s\' AND tool_id=\'%s\'
  1000. AND ref_id_high_level=%s AND ref_id_second_level=%s LIMIT 1';
  1001. $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id);
  1002. }
  1003. $res = Database::query($sql);
  1004. if (Database::num_rows($res) > 0 || $addQs) {
  1005. require_once(api_get_path(LIBRARY_PATH).'search/ChamiloIndexer.class.php');
  1006. require_once(api_get_path(LIBRARY_PATH).'search/IndexableChunk.class.php');
  1007. $di = new ChamiloIndexer();
  1008. if ($addQs) {
  1009. $question_exercises = array((int) $exerciseId);
  1010. } else {
  1011. $question_exercises = array();
  1012. }
  1013. isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
  1014. $di->connectDb(null, null, $lang);
  1015. // retrieve others exercise ids
  1016. $se_ref = Database::fetch_array($res);
  1017. $se_doc = $di->get_document((int) $se_ref['search_did']);
  1018. if ($se_doc !== false) {
  1019. if (($se_doc_data = $di->get_document_data($se_doc)) !== false) {
  1020. $se_doc_data = unserialize($se_doc_data);
  1021. if (
  1022. isset($se_doc_data[SE_DATA]['type']) &&
  1023. $se_doc_data[SE_DATA]['type'] == SE_DOCTYPE_EXERCISE_QUESTION
  1024. ) {
  1025. if (
  1026. isset($se_doc_data[SE_DATA]['exercise_ids']) &&
  1027. is_array($se_doc_data[SE_DATA]['exercise_ids'])
  1028. ) {
  1029. foreach ($se_doc_data[SE_DATA]['exercise_ids'] as $old_value) {
  1030. if (!in_array($old_value, $question_exercises)) {
  1031. $question_exercises[] = $old_value;
  1032. }
  1033. }
  1034. }
  1035. }
  1036. }
  1037. }
  1038. if ($rmQs) {
  1039. while (($key = array_search($exerciseId, $question_exercises)) !== false) {
  1040. unset($question_exercises[$key]);
  1041. }
  1042. }
  1043. // build the chunk to index
  1044. $ic_slide = new IndexableChunk();
  1045. $ic_slide->addValue("title", $this->question);
  1046. $ic_slide->addCourseId($course_id);
  1047. $ic_slide->addToolId(TOOL_QUIZ);
  1048. $xapian_data = array(
  1049. SE_COURSE_ID => $course_id,
  1050. SE_TOOL_ID => TOOL_QUIZ,
  1051. SE_DATA => array(
  1052. 'type' => SE_DOCTYPE_EXERCISE_QUESTION,
  1053. 'exercise_ids' => $question_exercises,
  1054. 'question_id' => (int) $this->id
  1055. ),
  1056. SE_USER => (int) api_get_user_id(),
  1057. );
  1058. $ic_slide->xapian_data = serialize($xapian_data);
  1059. $ic_slide->addValue("content", $this->description);
  1060. //TODO: index answers, see also form validation on question_admin.inc.php
  1061. $di->remove_document((int) $se_ref['search_did']);
  1062. $di->addChunk($ic_slide);
  1063. //index and return search engine document id
  1064. if (!empty($question_exercises)) { // if empty there is nothing to index
  1065. $did = $di->index();
  1066. unset($di);
  1067. }
  1068. if ($did || $rmQs) {
  1069. // save it to db
  1070. if ($addQs || $rmQs) {
  1071. $sql = "DELETE FROM %s
  1072. WHERE course_code = '%s' AND tool_id = '%s' AND ref_id_second_level = '%s'";
  1073. $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
  1074. } else {
  1075. $sql = "DELETE FROM %S
  1076. WHERE
  1077. course_code = '%s'
  1078. AND tool_id = '%s'
  1079. AND tool_id = '%s'
  1080. AND ref_id_high_level = '%s'
  1081. AND ref_id_second_level = '%s'";
  1082. $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id);
  1083. }
  1084. Database::query($sql);
  1085. if ($rmQs) {
  1086. if (!empty($question_exercises)) {
  1087. $sql = "INSERT INTO %s (
  1088. id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did
  1089. )
  1090. VALUES (
  1091. NULL, '%s', '%s', %s, %s, %s
  1092. )";
  1093. $sql = sprintf(
  1094. $sql,
  1095. $tbl_se_ref,
  1096. $course_id,
  1097. TOOL_QUIZ,
  1098. array_shift($question_exercises),
  1099. $this->id,
  1100. $did
  1101. );
  1102. Database::query($sql);
  1103. }
  1104. } else {
  1105. $sql = "INSERT INTO %s (
  1106. id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did
  1107. )
  1108. VALUES (
  1109. NULL , '%s', '%s', %s, %s, %s
  1110. )";
  1111. $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id, $did);
  1112. Database::query($sql);
  1113. }
  1114. }
  1115. }
  1116. }
  1117. }
  1118. /**
  1119. * adds an exercise into the exercise list
  1120. *
  1121. * @author Olivier Brouckaert
  1122. * @param integer $exerciseId - exercise ID
  1123. * @param boolean $fromSave - from $this->save() or not
  1124. */
  1125. public function addToList($exerciseId, $fromSave = false)
  1126. {
  1127. $exerciseRelQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
  1128. $id = $this->id;
  1129. // checks if the exercise ID is not in the list
  1130. if (!in_array($exerciseId, $this->exerciseList)) {
  1131. $this->exerciseList[] = $exerciseId;
  1132. $new_exercise = new Exercise();
  1133. $new_exercise->read($exerciseId);
  1134. $count = $new_exercise->selectNbrQuestions();
  1135. $count++;
  1136. $sql = "INSERT INTO $exerciseRelQuestionTable (c_id, question_id, exercice_id, question_order)
  1137. VALUES ({$this->course['real_id']}, ".intval($id).", ".intval($exerciseId).", '$count')";
  1138. Database::query($sql);
  1139. // we do not want to reindex if we had just saved adnd indexed the question
  1140. if (!$fromSave) {
  1141. $this->search_engine_edit($exerciseId, true);
  1142. }
  1143. }
  1144. }
  1145. /**
  1146. * removes an exercise from the exercise list
  1147. *
  1148. * @author Olivier Brouckaert
  1149. * @param integer $exerciseId - exercise ID
  1150. * @return boolean - true if removed, otherwise false
  1151. */
  1152. public function removeFromList($exerciseId)
  1153. {
  1154. $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
  1155. $id = $this->id;
  1156. // searches the position of the exercise ID in the list
  1157. $pos = array_search($exerciseId, $this->exerciseList);
  1158. $course_id = api_get_course_int_id();
  1159. // exercise not found
  1160. if ($pos === false) {
  1161. return false;
  1162. } else {
  1163. // deletes the position in the array containing the wanted exercise ID
  1164. unset($this->exerciseList[$pos]);
  1165. //update order of other elements
  1166. $sql = "SELECT question_order
  1167. FROM $TBL_EXERCISE_QUESTION
  1168. WHERE
  1169. c_id = $course_id
  1170. AND question_id = ".intval($id)."
  1171. AND exercice_id = " . intval($exerciseId);
  1172. $res = Database::query($sql);
  1173. if (Database::num_rows($res) > 0) {
  1174. $row = Database::fetch_array($res);
  1175. if (!empty($row['question_order'])) {
  1176. $sql = "UPDATE $TBL_EXERCISE_QUESTION
  1177. SET question_order = question_order-1
  1178. WHERE
  1179. c_id = $course_id
  1180. AND exercice_id = ".intval($exerciseId)."
  1181. AND question_order > " . $row['question_order'];
  1182. Database::query($sql);
  1183. }
  1184. }
  1185. $sql = "DELETE FROM $TBL_EXERCISE_QUESTION
  1186. WHERE
  1187. c_id = $course_id
  1188. AND question_id = ".intval($id)."
  1189. AND exercice_id = " . intval($exerciseId);
  1190. Database::query($sql);
  1191. return true;
  1192. }
  1193. }
  1194. /**
  1195. * Deletes a question from the database
  1196. * the parameter tells if the question is removed from all exercises (value = 0),
  1197. * or just from one exercise (value = exercise ID)
  1198. *
  1199. * @author Olivier Brouckaert
  1200. * @param integer $deleteFromEx - exercise ID if the question is only removed from one exercise
  1201. */
  1202. public function delete($deleteFromEx = 0)
  1203. {
  1204. $course_id = api_get_course_int_id();
  1205. $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
  1206. $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
  1207. $TBL_REPONSES = Database::get_course_table(TABLE_QUIZ_ANSWER);
  1208. $TBL_QUIZ_QUESTION_REL_CATEGORY = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
  1209. $id = intval($this->id);
  1210. // if the question must be removed from all exercises
  1211. if (!$deleteFromEx) {
  1212. //update the question_order of each question to avoid inconsistencies
  1213. $sql = "SELECT exercice_id, question_order FROM $TBL_EXERCISE_QUESTION
  1214. WHERE c_id = $course_id AND question_id = ".intval($id)."";
  1215. $res = Database::query($sql);
  1216. if (Database::num_rows($res) > 0) {
  1217. while ($row = Database::fetch_array($res)) {
  1218. if (!empty($row['question_order'])) {
  1219. $sql = "UPDATE $TBL_EXERCISE_QUESTION
  1220. SET question_order = question_order-1
  1221. WHERE
  1222. c_id= $course_id
  1223. AND exercice_id = ".intval($row['exercice_id'])."
  1224. AND question_order > " . $row['question_order'];
  1225. Database::query($sql);
  1226. }
  1227. }
  1228. }
  1229. $sql = "DELETE FROM $TBL_EXERCISE_QUESTION
  1230. WHERE c_id = $course_id AND question_id = ".$id;
  1231. Database::query($sql);
  1232. $sql = "DELETE FROM $TBL_QUESTIONS
  1233. WHERE c_id = $course_id AND id = ".$id;
  1234. Database::query($sql);
  1235. $sql = "DELETE FROM $TBL_REPONSES
  1236. WHERE c_id = $course_id AND question_id = ".$id;
  1237. Database::query($sql);
  1238. // remove the category of this question in the question_rel_category table
  1239. $sql = "DELETE FROM $TBL_QUIZ_QUESTION_REL_CATEGORY
  1240. WHERE
  1241. c_id = $course_id AND
  1242. question_id = ".$id;
  1243. Database::query($sql);
  1244. api_item_property_update(
  1245. $this->course,
  1246. TOOL_QUIZ,
  1247. $id,
  1248. 'QuizQuestionDeleted',
  1249. api_get_user_id()
  1250. );
  1251. $this->removePicture();
  1252. } else {
  1253. // just removes the exercise from the list
  1254. $this->removeFromList($deleteFromEx);
  1255. if (api_get_setting('search_enabled') == 'true' && extension_loaded('xapian')) {
  1256. // disassociate question with this exercise
  1257. $this->search_engine_edit($deleteFromEx, false, true);
  1258. }
  1259. api_item_property_update(
  1260. $this->course,
  1261. TOOL_QUIZ,
  1262. $id,
  1263. 'QuizQuestionDeleted',
  1264. api_get_user_id()
  1265. );
  1266. }
  1267. }
  1268. /**
  1269. * Duplicates the question
  1270. *
  1271. * @author Olivier Brouckaert
  1272. * @param array $course_info Course info of the destination course
  1273. * @return false|string ID of the new question
  1274. */
  1275. public function duplicate($course_info = [])
  1276. {
  1277. if (empty($course_info)) {
  1278. $course_info = $this->course;
  1279. } else {
  1280. $course_info = $course_info;
  1281. }
  1282. $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
  1283. $TBL_QUESTION_OPTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
  1284. $question = $this->question;
  1285. $description = $this->description;
  1286. $weighting = $this->weighting;
  1287. $position = $this->position;
  1288. $type = $this->type;
  1289. $level = intval($this->level);
  1290. $extra = $this->extra;
  1291. // Using the same method used in the course copy to transform URLs
  1292. if ($this->course['id'] != $course_info['id']) {
  1293. $description = DocumentManager::replace_urls_inside_content_html_from_copy_course(
  1294. $description,
  1295. $this->course['code'],
  1296. $course_info['id']
  1297. );
  1298. $question = DocumentManager::replace_urls_inside_content_html_from_copy_course(
  1299. $question,
  1300. $this->course['code'],
  1301. $course_info['id']
  1302. );
  1303. }
  1304. $course_id = $course_info['real_id'];
  1305. // Read the source options
  1306. $options = self::readQuestionOption($this->id, $this->course['real_id']);
  1307. // Inserting in the new course db / or the same course db
  1308. $params = [
  1309. 'c_id' => $course_id,
  1310. 'question' => $question,
  1311. 'description' => $description,
  1312. 'ponderation' => $weighting,
  1313. 'position' => $position,
  1314. 'type' => $type,
  1315. 'level' => $level,
  1316. 'extra' => $extra
  1317. ];
  1318. $newQuestionId = Database::insert($TBL_QUESTIONS, $params);
  1319. if ($newQuestionId) {
  1320. $sql = "UPDATE $TBL_QUESTIONS
  1321. SET id = iid
  1322. WHERE iid = $newQuestionId";
  1323. Database::query($sql);
  1324. if (!empty($options)) {
  1325. // Saving the quiz_options
  1326. foreach ($options as $item) {
  1327. $item['question_id'] = $newQuestionId;
  1328. $item['c_id'] = $course_id;
  1329. unset($item['id']);
  1330. unset($item['iid']);
  1331. $id = Database::insert($TBL_QUESTION_OPTIONS, $item);
  1332. if ($id) {
  1333. $sql = "UPDATE $TBL_QUESTION_OPTIONS
  1334. SET id = iid
  1335. WHERE iid = $id";
  1336. Database::query($sql);
  1337. }
  1338. }
  1339. }
  1340. // Duplicates the picture of the hotspot
  1341. $this->exportPicture($newQuestionId, $course_info);
  1342. }
  1343. return $newQuestionId;
  1344. }
  1345. /**
  1346. * @return string
  1347. */
  1348. public function get_question_type_name()
  1349. {
  1350. $key = self::$questionTypes[$this->type];
  1351. return get_lang($key[1]);
  1352. }
  1353. /**
  1354. * @param string $type
  1355. * @return null
  1356. */
  1357. public static function get_question_type($type)
  1358. {
  1359. if ($type == ORAL_EXPRESSION && api_get_setting('enable_record_audio') !== 'true') {
  1360. return null;
  1361. }
  1362. return self::$questionTypes[$type];
  1363. }
  1364. /**
  1365. * @return array
  1366. */
  1367. public static function get_question_type_list()
  1368. {
  1369. if (api_get_setting('enable_record_audio') !== 'true') {
  1370. self::$questionTypes[ORAL_EXPRESSION] = null;
  1371. unset(self::$questionTypes[ORAL_EXPRESSION]);
  1372. }
  1373. if (api_get_setting('enable_quiz_scenario') !== 'true') {
  1374. self::$questionTypes[HOT_SPOT_DELINEATION] = null;
  1375. unset(self::$questionTypes[HOT_SPOT_DELINEATION]);
  1376. }
  1377. return self::$questionTypes;
  1378. }
  1379. /**
  1380. * Returns an instance of the class corresponding to the type
  1381. * @param integer $type the type of the question
  1382. * @return $this instance of a Question subclass (or of Questionc class by default)
  1383. */
  1384. public static function getInstance($type)
  1385. {
  1386. if (!is_null($type)) {
  1387. list($file_name, $class_name) = self::get_question_type($type);
  1388. if (!empty($file_name)) {
  1389. if (class_exists($class_name)) {
  1390. return new $class_name();
  1391. } else {
  1392. echo 'Can\'t instanciate class '.$class_name.' of type '.$type;
  1393. }
  1394. }
  1395. }
  1396. return null;
  1397. }
  1398. /**
  1399. * Creates the form to create / edit a question
  1400. * A subclass can redefine this function to add fields...
  1401. * @param FormValidator $form
  1402. * @param Exercise $exercise
  1403. */
  1404. public function createForm(&$form, $exercise)
  1405. {
  1406. echo '<style>
  1407. .media { display:none;}
  1408. </style>';
  1409. echo '<script>
  1410. // hack to hide http://cksource.com/forums/viewtopic.php?f=6&t=8700
  1411. function FCKeditor_OnComplete( editorInstance ) {
  1412. if (document.getElementById ( \'HiddenFCK\' + editorInstance.Name)) {
  1413. HideFCKEditorByInstanceName (editorInstance.Name);
  1414. }
  1415. }
  1416. function HideFCKEditorByInstanceName ( editorInstanceName ) {
  1417. if (document.getElementById ( \'HiddenFCK\' + editorInstanceName ).className == "HideFCKEditor" ) {
  1418. document.getElementById ( \'HiddenFCK\' + editorInstanceName ).className = "media";
  1419. }
  1420. }
  1421. </script>';
  1422. // question name
  1423. if (api_get_configuration_value('save_titles_as_html')) {
  1424. $editorConfig = ['ToolbarSet' => 'Minimal'];
  1425. $form->addHtmlEditor(
  1426. 'questionName',
  1427. get_lang('Question'),
  1428. false,
  1429. false,
  1430. $editorConfig,
  1431. true
  1432. );
  1433. } else {
  1434. $form->addElement('text', 'questionName', get_lang('Question'));
  1435. }
  1436. $form->addRule('questionName', get_lang('GiveQuestion'), 'required');
  1437. // default content
  1438. $isContent = isset($_REQUEST['isContent']) ? intval($_REQUEST['isContent']) : null;
  1439. // Question type
  1440. $answerType = isset($_REQUEST['answerType']) ? intval($_REQUEST['answerType']) : null;
  1441. $form->addElement('hidden', 'answerType', $answerType);
  1442. // html editor
  1443. $editorConfig = array(
  1444. 'ToolbarSet' => 'TestQuestionDescription',
  1445. 'Height' => '150'
  1446. );
  1447. if (!api_is_allowed_to_edit(null, true)) {
  1448. $editorConfig['UserStatus'] = 'student';
  1449. }
  1450. $form->addButtonAdvancedSettings('advanced_params');
  1451. $form->addElement('html', '<div id="advanced_params_options" style="display:none">');
  1452. $form->addHtmlEditor(
  1453. 'questionDescription',
  1454. get_lang('QuestionDescription'),
  1455. false,
  1456. false,
  1457. $editorConfig
  1458. );
  1459. // hidden values
  1460. $my_id = isset($_REQUEST['myid']) ? intval($_REQUEST['myid']) : null;
  1461. $form->addElement('hidden', 'myid', $my_id);
  1462. if ($this->type != MEDIA_QUESTION) {
  1463. // Advanced parameters
  1464. $select_level = self::get_default_levels();
  1465. $form->addElement('select', 'questionLevel', get_lang('Difficulty'), $select_level);
  1466. // Categories
  1467. $tabCat = TestCategory::getCategoriesIdAndName();
  1468. $form->addElement('select', 'questionCategory', get_lang('Category'), $tabCat);
  1469. global $text;
  1470. switch ($this->type) {
  1471. case UNIQUE_ANSWER:
  1472. $buttonGroup = array();
  1473. $buttonGroup[] = $form->addButtonSave($text, 'submitQuestion', true);
  1474. $buttonGroup[] = $form->addButton(
  1475. 'convertAnswer',
  1476. get_lang('ConvertToMultipleAnswer'),
  1477. 'dot-circle-o',
  1478. 'default',
  1479. null,
  1480. null,
  1481. null,
  1482. true
  1483. );
  1484. $form->addGroup($buttonGroup);
  1485. break;
  1486. case MULTIPLE_ANSWER:
  1487. $buttonGroup = array();
  1488. $buttonGroup[] = $form->addButtonSave($text, 'submitQuestion', true);
  1489. $buttonGroup[] = $form->addButton(
  1490. 'convertAnswer',
  1491. get_lang('ConvertToUniqueAnswer'),
  1492. 'check-square-o',
  1493. 'default',
  1494. null,
  1495. null,
  1496. null,
  1497. true
  1498. );
  1499. $form->addGroup($buttonGroup);
  1500. break;
  1501. }
  1502. //Medias
  1503. //$course_medias = self::prepare_course_media_select(api_get_course_int_id());
  1504. //$form->addElement('select', 'parent_id', get_lang('AttachToMedia'), $course_medias);
  1505. }
  1506. $form->addElement('html', '</div>');
  1507. if (!isset($_GET['fromExercise'])) {
  1508. switch ($answerType) {
  1509. case 1:
  1510. $this->question = get_lang('DefaultUniqueQuestion');
  1511. break;
  1512. case 2:
  1513. $this->question = get_lang('DefaultMultipleQuestion');
  1514. break;
  1515. case 3:
  1516. $this->question = get_lang('DefaultFillBlankQuestion');
  1517. break;
  1518. case 4:
  1519. $this->question = get_lang('DefaultMathingQuestion');
  1520. break;
  1521. case 5:
  1522. $this->question = get_lang('DefaultOpenQuestion');
  1523. break;
  1524. case 9:
  1525. $this->question = get_lang('DefaultMultipleQuestion');
  1526. break;
  1527. }
  1528. }
  1529. if (!is_null($exercise)) {
  1530. if ($exercise->questionFeedbackEnabled && $this->showFeedback($exercise)) {
  1531. $form->addTextarea('feedback', get_lang('FeedbackIfNotCorrect'));
  1532. }
  1533. }
  1534. // default values
  1535. $defaults = array();
  1536. $defaults['questionName'] = $this->question;
  1537. $defaults['questionDescription'] = $this->description;
  1538. $defaults['questionLevel'] = $this->level;
  1539. $defaults['questionCategory'] = $this->category;
  1540. $defaults['feedback'] = $this->feedback;
  1541. // Came from he question pool
  1542. if (isset($_GET['fromExercise'])) {
  1543. $form->setDefaults($defaults);
  1544. }
  1545. if (!empty($_REQUEST['myid'])) {
  1546. $form->setDefaults($defaults);
  1547. } else {
  1548. if ($isContent == 1) {
  1549. $form->setDefaults($defaults);
  1550. }
  1551. }
  1552. }
  1553. /**
  1554. * function which process the creation of questions
  1555. * @param FormValidator $form
  1556. * @param Exercise $exercise
  1557. */
  1558. public function processCreation($form, $exercise)
  1559. {
  1560. $this->updateTitle($form->getSubmitValue('questionName'));
  1561. $this->updateDescription($form->getSubmitValue('questionDescription'));
  1562. $this->updateLevel($form->getSubmitValue('questionLevel'));
  1563. $this->updateCategory($form->getSubmitValue('questionCategory'));
  1564. $this->setFeedback($form->getSubmitValue('feedback'));
  1565. //Save normal question if NOT media
  1566. if ($this->type != MEDIA_QUESTION) {
  1567. $this->save($exercise);
  1568. // modify the exercise
  1569. $exercise->addToList($this->id);
  1570. $exercise->update_question_positions();
  1571. }
  1572. }
  1573. /**
  1574. * abstract function which creates the form to create / edit the answers of the question
  1575. * @param FormValidator $form
  1576. */
  1577. abstract public function createAnswersForm($form);
  1578. /**
  1579. * abstract function which process the creation of answers
  1580. * @param FormValidator $form
  1581. * @param Exercise $exercise
  1582. */
  1583. abstract public function processAnswersCreation($form, $exercise);
  1584. /**
  1585. * Displays the menu of question types
  1586. *
  1587. * @param Exercise $objExercise
  1588. */
  1589. public static function display_type_menu($objExercise)
  1590. {
  1591. $feedback_type = $objExercise->feedback_type;
  1592. $exerciseId = $objExercise->id;
  1593. // 1. by default we show all the question types
  1594. $question_type_custom_list = self::get_question_type_list();
  1595. if (!isset($feedback_type)) {
  1596. $feedback_type = 0;
  1597. }
  1598. if ($feedback_type == 1) {
  1599. //2. but if it is a feedback DIRECT we only show the UNIQUE_ANSWER type that is currently available
  1600. $question_type_custom_list = array(
  1601. UNIQUE_ANSWER => self::$questionTypes[UNIQUE_ANSWER],
  1602. HOT_SPOT_DELINEATION => self::$questionTypes[HOT_SPOT_DELINEATION]
  1603. );
  1604. } else {
  1605. unset($question_type_custom_list[HOT_SPOT_DELINEATION]);
  1606. }
  1607. echo '<div class="well">';
  1608. echo '<ul class="question_menu">';
  1609. foreach ($question_type_custom_list as $i => $a_type) {
  1610. // include the class of the type
  1611. require_once $a_type[0];
  1612. // get the picture of the type and the langvar which describes it
  1613. $img = $explanation = '';
  1614. eval('$img = '.$a_type[1].'::$typePicture;');
  1615. eval('$explanation = get_lang('.$a_type[1].'::$explanationLangVar);');
  1616. echo '<li>';
  1617. echo '<div class="icon-image">';
  1618. $icon = '<a href="admin.php?'.api_get_cidreq().'&newQuestion=yes&answerType='.$i.'">'.
  1619. Display::return_icon($img, $explanation, null, ICON_SIZE_BIG).'</a>';
  1620. if ($objExercise->force_edit_exercise_in_lp === false) {
  1621. if ($objExercise->exercise_was_added_in_lp == true) {
  1622. $img = pathinfo($img);
  1623. $img = $img['filename'].'_na.'.$img['extension'];
  1624. $icon = Display::return_icon($img, $explanation, null, ICON_SIZE_BIG);
  1625. }
  1626. }
  1627. echo $icon;
  1628. echo '</div>';
  1629. echo '</li>';
  1630. }
  1631. echo '<li>';
  1632. echo '<div class="icon_image_content">';
  1633. if ($objExercise->exercise_was_added_in_lp == true) {
  1634. echo Display::return_icon('database_na.png', get_lang('GetExistingQuestion'), null, ICON_SIZE_BIG);
  1635. } else {
  1636. if ($feedback_type == 1) {
  1637. echo $url = "<a href=\"question_pool.php?".api_get_cidreq()."&type=1&fromExercise=$exerciseId\">";
  1638. } else {
  1639. echo $url = '<a href="question_pool.php?'.api_get_cidreq().'&fromExercise='.$exerciseId.'">';
  1640. }
  1641. echo Display::return_icon('database.png', get_lang('GetExistingQuestion'), null, ICON_SIZE_BIG);
  1642. }
  1643. echo '</a>';
  1644. echo '</div></li>';
  1645. echo '</ul>';
  1646. echo '</div>';
  1647. }
  1648. /**
  1649. * @param int $question_id
  1650. * @param string $name
  1651. * @param int $course_id
  1652. * @param int $position
  1653. * @return false|string
  1654. */
  1655. public static function saveQuestionOption($question_id, $name, $course_id, $position = 0)
  1656. {
  1657. $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
  1658. $params['question_id'] = intval($question_id);
  1659. $params['name'] = $name;
  1660. $params['position'] = $position;
  1661. $params['c_id'] = $course_id;
  1662. $result = self::readQuestionOption($question_id, $course_id);
  1663. $last_id = Database::insert($table, $params);
  1664. if ($last_id) {
  1665. $sql = "UPDATE $table SET id = iid WHERE iid = $last_id";
  1666. Database::query($sql);
  1667. }
  1668. return $last_id;
  1669. }
  1670. /**
  1671. * @param int $question_id
  1672. * @param int $course_id
  1673. */
  1674. public static function deleteAllQuestionOptions($question_id, $course_id)
  1675. {
  1676. $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
  1677. Database::delete(
  1678. $table,
  1679. array(
  1680. 'c_id = ? AND question_id = ?' => array(
  1681. $course_id,
  1682. $question_id
  1683. )
  1684. )
  1685. );
  1686. }
  1687. /**
  1688. * @param int $id
  1689. * @param array $params
  1690. * @param int $course_id
  1691. * @return bool|int
  1692. */
  1693. public static function updateQuestionOption($id, $params, $course_id)
  1694. {
  1695. $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
  1696. $result = Database::update(
  1697. $table,
  1698. $params,
  1699. array('c_id = ? AND id = ?' => array($course_id, $id))
  1700. );
  1701. return $result;
  1702. }
  1703. /**
  1704. * @param int $question_id
  1705. * @param int $course_id
  1706. * @return array
  1707. */
  1708. static function readQuestionOption($question_id, $course_id)
  1709. {
  1710. $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
  1711. $result = Database::select(
  1712. '*',
  1713. $table,
  1714. array(
  1715. 'where' => array(
  1716. 'c_id = ? AND question_id = ?' => array(
  1717. $course_id,
  1718. $question_id
  1719. )
  1720. ),
  1721. 'order' => 'id ASC'
  1722. )
  1723. );
  1724. return $result;
  1725. }
  1726. /**
  1727. * Shows question title an description
  1728. *
  1729. * @param Exercise $exercise
  1730. * @param int $counter
  1731. * @param array $score
  1732. * @return string HTML string with the header of the question (before the answers table)
  1733. */
  1734. public function return_header($exercise, $counter = null, $score = [])
  1735. {
  1736. $counter_label = '';
  1737. if (!empty($counter)) {
  1738. $counter_label = intval($counter);
  1739. }
  1740. $score_label = get_lang('Wrong');
  1741. $class = 'error';
  1742. if ($score['pass'] == true) {
  1743. $score_label = get_lang('Correct');
  1744. $class = 'success';
  1745. }
  1746. if ($this->type == FREE_ANSWER || $this->type == ORAL_EXPRESSION) {
  1747. $score['revised'] = isset($score['revised']) ? $score['revised'] : false;
  1748. if ($score['revised'] == true) {
  1749. $score_label = get_lang('Revised');
  1750. $class = '';
  1751. } else {
  1752. $score_label = get_lang('NotRevised');
  1753. $class = 'warning';
  1754. $weight = float_format($score['weight'], 1);
  1755. $score['result'] = " ? / ".$weight;
  1756. }
  1757. }
  1758. // display question category, if any
  1759. $header = TestCategory::returnCategoryAndTitle($this->id);
  1760. $show_media = null;
  1761. if ($show_media) {
  1762. $header .= $this->show_media_content();
  1763. }
  1764. $header .= Display::page_subheader2($counter_label.". ".$this->question);
  1765. $header .= ExerciseLib::getQuestionRibbon($class, $score_label, $score['result']);
  1766. if ($this->type != READING_COMPREHENSION) {
  1767. // Do not show the description (the text to read) if the question is of type READING_COMPREHENSION
  1768. $header .= Display::div($this->description, array('class' => 'question_description'));
  1769. } else {
  1770. if ($score['pass'] == true) {
  1771. $message = Display::div(
  1772. sprintf(
  1773. get_lang('ReadingQuestionCongratsSpeedXReachedForYWords'),
  1774. ReadingComprehension::$speeds[$this->level],
  1775. $this->getWordsCount()
  1776. )
  1777. );
  1778. } else {
  1779. $message = Display::div(
  1780. sprintf(
  1781. get_lang('ReadingQuestionCongratsSpeedXNotReachedForYWords'),
  1782. ReadingComprehension::$speeds[$this->level],
  1783. $this->getWordsCount()
  1784. )
  1785. );
  1786. }
  1787. $header .= $message.'<br />';
  1788. }
  1789. if (isset($score['pass']) && $score['pass'] === false) {
  1790. if ($this->showFeedback($exercise)) {
  1791. $header .= $this->returnFormatFeedback();
  1792. }
  1793. }
  1794. return $header;
  1795. }
  1796. /**
  1797. * Create a question from a set of parameters
  1798. * @param int Quiz ID
  1799. * @param string Question name
  1800. * @param int Maximum result for the question
  1801. * @param int Type of question (see constants at beginning of question.class.php)
  1802. * @param int Question level/category
  1803. * @param string $quiz_id
  1804. */
  1805. public function create_question(
  1806. $quiz_id,
  1807. $question_name,
  1808. $question_description = '',
  1809. $max_score = 0,
  1810. $type = 1,
  1811. $level = 1
  1812. ) {
  1813. $course_id = api_get_course_int_id();
  1814. $tbl_quiz_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
  1815. $tbl_quiz_rel_question = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
  1816. $quiz_id = intval($quiz_id);
  1817. $max_score = (float) $max_score;
  1818. $type = intval($type);
  1819. $level = intval($level);
  1820. // Get the max position
  1821. $sql = "SELECT max(position) as max_position
  1822. FROM $tbl_quiz_question q
  1823. INNER JOIN $tbl_quiz_rel_question r
  1824. ON
  1825. q.id = r.question_id AND
  1826. exercice_id = $quiz_id AND
  1827. q.c_id = $course_id AND
  1828. r.c_id = $course_id";
  1829. $rs_max = Database::query($sql);
  1830. $row_max = Database::fetch_object($rs_max);
  1831. $max_position = $row_max->max_position + 1;
  1832. $params = [
  1833. 'c_id' => $course_id,
  1834. 'question' => $question_name,
  1835. 'description' => $question_description,
  1836. 'ponderation' => $max_score,
  1837. 'position' => $max_position,
  1838. 'type' => $type,
  1839. 'level' => $level,
  1840. ];
  1841. $question_id = Database::insert($tbl_quiz_question, $params);
  1842. if ($question_id) {
  1843. $sql = "UPDATE $tbl_quiz_question SET id = iid WHERE iid = $question_id";
  1844. Database::query($sql);
  1845. // Get the max question_order
  1846. $sql = "SELECT max(question_order) as max_order
  1847. FROM $tbl_quiz_rel_question
  1848. WHERE c_id = $course_id AND exercice_id = $quiz_id ";
  1849. $rs_max_order = Database::query($sql);
  1850. $row_max_order = Database::fetch_object($rs_max_order);
  1851. $max_order = $row_max_order->max_order + 1;
  1852. // Attach questions to quiz
  1853. $sql = "INSERT INTO $tbl_quiz_rel_question (c_id, question_id, exercice_id, question_order)
  1854. VALUES($course_id, $question_id, $quiz_id, $max_order)";
  1855. Database::query($sql);
  1856. }
  1857. return $question_id;
  1858. }
  1859. /**
  1860. * @return array the image filename of the question type
  1861. */
  1862. public function get_type_icon_html()
  1863. {
  1864. $type = $this->selectType();
  1865. $tabQuestionList = self::get_question_type_list(); // [0]=file to include [1]=type name
  1866. require_once $tabQuestionList[$type][0];
  1867. $img = $explanation = null;
  1868. eval('$img = '.$tabQuestionList[$type][1].'::$typePicture;');
  1869. eval('$explanation = get_lang('.$tabQuestionList[$type][1].'::$explanationLangVar);');
  1870. return array($img, $explanation);
  1871. }
  1872. /**
  1873. * Get course medias
  1874. * @param int course id
  1875. * @param integer $course_id
  1876. *
  1877. * @return array
  1878. */
  1879. static function get_course_medias(
  1880. $course_id,
  1881. $start = 0,
  1882. $limit = 100,
  1883. $sidx = "question",
  1884. $sord = "ASC",
  1885. $where_condition = array()
  1886. ) {
  1887. $table_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
  1888. $default_where = array('c_id = ? AND parent_id = 0 AND type = ?' => array($course_id, MEDIA_QUESTION));
  1889. $result = Database::select(
  1890. '*',
  1891. $table_question,
  1892. array(
  1893. 'limit' => " $start, $limit",
  1894. 'where' => $default_where,
  1895. 'order' => "$sidx $sord"
  1896. )
  1897. );
  1898. return $result;
  1899. }
  1900. /**
  1901. * Get count course medias
  1902. * @param int course id
  1903. *
  1904. * @return int
  1905. */
  1906. static function get_count_course_medias($course_id)
  1907. {
  1908. $table_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
  1909. $result = Database::select(
  1910. 'count(*) as count',
  1911. $table_question,
  1912. array(
  1913. 'where' => array(
  1914. 'c_id = ? AND parent_id = 0 AND type = ?' => array(
  1915. $course_id,
  1916. MEDIA_QUESTION,
  1917. ),
  1918. )
  1919. ),
  1920. 'first'
  1921. );
  1922. if ($result && isset($result['count'])) {
  1923. return $result['count'];
  1924. }
  1925. return 0;
  1926. }
  1927. /**
  1928. * @param int $course_id
  1929. * @return array
  1930. */
  1931. public static function prepare_course_media_select($course_id)
  1932. {
  1933. $medias = self::get_course_medias($course_id);
  1934. $media_list = array();
  1935. $media_list[0] = get_lang('NoMedia');
  1936. if (!empty($medias)) {
  1937. foreach ($medias as $media) {
  1938. $media_list[$media['id']] = empty($media['question']) ? get_lang('Untitled') : $media['question'];
  1939. }
  1940. }
  1941. return $media_list;
  1942. }
  1943. /**
  1944. * @return integer[]
  1945. */
  1946. public static function get_default_levels()
  1947. {
  1948. $select_level = array(
  1949. 1 => 1,
  1950. 2 => 2,
  1951. 3 => 3,
  1952. 4 => 4,
  1953. 5 => 5
  1954. );
  1955. return $select_level;
  1956. }
  1957. /**
  1958. * @return string
  1959. */
  1960. public function show_media_content()
  1961. {
  1962. $html = '';
  1963. if ($this->parent_id != 0) {
  1964. $parent_question = self::read($this->parent_id);
  1965. $html = $parent_question->show_media_content();
  1966. } else {
  1967. $html .= Display::page_subheader($this->selectTitle());
  1968. $html .= $this->selectDescription();
  1969. }
  1970. return $html;
  1971. }
  1972. /**
  1973. * Swap between unique and multiple type answers
  1974. * @return UniqueAnswer|MultipleAnswer
  1975. */
  1976. public function swapSimpleAnswerTypes()
  1977. {
  1978. $oppositeAnswers = array(
  1979. UNIQUE_ANSWER => MULTIPLE_ANSWER,
  1980. MULTIPLE_ANSWER => UNIQUE_ANSWER
  1981. );
  1982. $this->type = $oppositeAnswers[$this->type];
  1983. Database::update(
  1984. Database::get_course_table(TABLE_QUIZ_QUESTION),
  1985. array('type' => $this->type),
  1986. array('c_id = ? AND id = ?' => array($this->course['real_id'], $this->id))
  1987. );
  1988. $answerClasses = array(
  1989. UNIQUE_ANSWER => 'UniqueAnswer',
  1990. MULTIPLE_ANSWER => 'MultipleAnswer'
  1991. );
  1992. $swappedAnswer = new $answerClasses[$this->type];
  1993. foreach ($this as $key => $value) {
  1994. $swappedAnswer->$key = $value;
  1995. }
  1996. return $swappedAnswer;
  1997. }
  1998. /**
  1999. * @param array $score
  2000. * @return bool
  2001. */
  2002. public function isQuestionWaitingReview($score)
  2003. {
  2004. $isReview = false;
  2005. if (!empty($score['comments']) || $score['score'] > 0) {
  2006. $isReview = true;
  2007. }
  2008. return $isReview;
  2009. }
  2010. /**
  2011. * @param string $value
  2012. */
  2013. public function setFeedback($value)
  2014. {
  2015. $this->feedback = $value;
  2016. }
  2017. /**
  2018. * @param Exercise $exercise
  2019. * @return bool
  2020. */
  2021. public function showFeedback($exercise)
  2022. {
  2023. return
  2024. in_array($this->type, $this->questionTypeWithFeedback) &&
  2025. $exercise->feedback_type != EXERCISE_FEEDBACK_TYPE_EXAM;
  2026. }
  2027. /**
  2028. * @return string
  2029. */
  2030. public function returnFormatFeedback()
  2031. {
  2032. return '<br /><br />'.Display::return_message($this->feedback, 'normal', false);
  2033. }
  2034. }