question.class.php 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179
  1. <?php // $Id: question.class.php 22257 2009-07-20 17:50:09Z juliomontoya $
  2. /* For licensing terms, see /license.txt */
  3. /**
  4. * File containing the Question class.
  5. * @package chamilo.exercise
  6. * @author Olivier Brouckaert
  7. * @version $Id: question.class.php 22257 2009-07-20 17:50:09Z juliomontoya $
  8. */
  9. if(!class_exists('Question')):
  10. // answer types
  11. define('UNIQUE_ANSWER', 1);
  12. define('MULTIPLE_ANSWER', 2);
  13. define('FILL_IN_BLANKS', 3);
  14. define('MATCHING', 4);
  15. define('FREE_ANSWER', 5);
  16. define('HOT_SPOT', 6);
  17. define('HOT_SPOT_ORDER', 7);
  18. define('HOT_SPOT_DELINEATION', 8);
  19. define('MULTIPLE_ANSWER_COMBINATION', 9);
  20. /**
  21. QUESTION CLASS
  22. *
  23. * This class allows to instantiate an object of type Question
  24. *
  25. * @author Olivier Brouckaert, original author
  26. * @author Patrick Cool, LaTeX support
  27. */
  28. abstract class Question
  29. {
  30. public $id;
  31. public $question;
  32. public $description;
  33. public $weighting;
  34. public $position;
  35. public $type;
  36. public $level;
  37. public $picture;
  38. public $exerciseList; // array with the list of exercises which this question is in
  39. private $isContent;
  40. static $typePicture = 'new_question.png';
  41. static $explanationLangVar = '';
  42. static $questionTypes = array(
  43. UNIQUE_ANSWER => array('unique_answer.class.php' , 'UniqueAnswer'),
  44. MULTIPLE_ANSWER => array('multiple_answer.class.php' , 'MultipleAnswer'),
  45. FILL_IN_BLANKS => array('fill_blanks.class.php' , 'FillBlanks'),
  46. MATCHING => array('matching.class.php' , 'Matching'),
  47. FREE_ANSWER => array('freeanswer.class.php' , 'FreeAnswer'),
  48. HOT_SPOT => array('hotspot.class.php' , 'HotSpot'),
  49. MULTIPLE_ANSWER_COMBINATION => array('multiple_answer_combination.class.php' , 'MultipleAnswerCombination'),
  50. );
  51. /**
  52. * constructor of the class
  53. *
  54. * @author - Olivier Brouckaert
  55. */
  56. function Question() {
  57. $this->id=0;
  58. $this->question='';
  59. $this->description='';
  60. $this->weighting=0;
  61. $this->position=1;
  62. $this->picture='';
  63. $this->level = 1;
  64. $this->exerciseList=array();
  65. }
  66. public function getIsContent() {
  67. $isContent = intval($_REQUEST['isContent']);
  68. return $this->isContent = $isContent;
  69. }
  70. /**
  71. * Reads question informations from the data base
  72. *
  73. * @author - Olivier Brouckaert
  74. * @param - integer $id - question ID
  75. * @return - boolean - true if question exists, otherwise false
  76. */
  77. static function read($id)
  78. {
  79. global $_course;
  80. $TBL_EXERCICES = Database::get_course_table(TABLE_QUIZ_TEST);
  81. $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
  82. $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
  83. $sql="SELECT question,description,ponderation,position,type,picture,level FROM $TBL_QUESTIONS WHERE id='".Database::escape_string($id)."'";
  84. $result=Database::query($sql);
  85. // if the question has been found
  86. if($object=Database::fetch_object($result))
  87. {
  88. $objQuestion = Question::getInstance($object->type);
  89. $objQuestion->id = $id;
  90. $objQuestion->question = $object->question;
  91. $objQuestion->description = $object->description;
  92. $objQuestion->weighting = $object->ponderation;
  93. $objQuestion->position = $object->position;
  94. $objQuestion->type = $object->type;
  95. $objQuestion->picture = $object->picture;
  96. $objQuestion->level = (int) $object->level;
  97. $sql="SELECT exercice_id FROM $TBL_EXERCICE_QUESTION WHERE question_id='".intval($id)."'";
  98. $result=Database::query($sql);
  99. // fills the array with the exercises which this question is in
  100. while($object=Database::fetch_object($result)) {
  101. $objQuestion->exerciseList[]=$object->exercice_id;
  102. }
  103. return $objQuestion;
  104. }
  105. // question not found
  106. return false;
  107. }
  108. /**
  109. * returns the question ID
  110. *
  111. * @author - Olivier Brouckaert
  112. * @return - integer - question ID
  113. */
  114. function selectId()
  115. {
  116. return $this->id;
  117. }
  118. /**
  119. * returns the question title
  120. *
  121. * @author - Olivier Brouckaert
  122. * @return - string - question title
  123. */
  124. function selectTitle()
  125. {
  126. $this->question=text_filter($this->question);
  127. return $this->question;
  128. }
  129. /**
  130. * returns the question description
  131. *
  132. * @author - Olivier Brouckaert
  133. * @return - string - question description
  134. */
  135. function selectDescription()
  136. {
  137. $this->description=text_filter($this->description);
  138. return $this->description;
  139. }
  140. /**
  141. * returns the question weighting
  142. *
  143. * @author - Olivier Brouckaert
  144. * @return - integer - question weighting
  145. */
  146. function selectWeighting()
  147. {
  148. return $this->weighting;
  149. }
  150. /**
  151. * returns the question position
  152. *
  153. * @author - Olivier Brouckaert
  154. * @return - integer - question position
  155. */
  156. function selectPosition()
  157. {
  158. return $this->position;
  159. }
  160. /**
  161. * returns the answer type
  162. *
  163. * @author - Olivier Brouckaert
  164. * @return - integer - answer type
  165. */
  166. function selectType()
  167. {
  168. return $this->type;
  169. }
  170. /**
  171. * returns the level of the question
  172. *
  173. * @author - Nicolas Raynaud
  174. * @return - integer - level of the question, 0 by default.
  175. */
  176. function selectLevel()
  177. {
  178. return $this->level;
  179. }
  180. /**
  181. * returns the picture name
  182. *
  183. * @author - Olivier Brouckaert
  184. * @return - string - picture name
  185. */
  186. function selectPicture()
  187. {
  188. return $this->picture;
  189. }
  190. /**
  191. * returns the array with the exercise ID list
  192. *
  193. * @author - Olivier Brouckaert
  194. * @return - array - list of exercise ID which the question is in
  195. */
  196. function selectExerciseList()
  197. {
  198. return $this->exerciseList;
  199. }
  200. /**
  201. * returns the number of exercises which this question is in
  202. *
  203. * @author - Olivier Brouckaert
  204. * @return - integer - number of exercises
  205. */
  206. function selectNbrExercises()
  207. {
  208. return sizeof($this->exerciseList);
  209. }
  210. /**
  211. * changes the question title
  212. *
  213. * @author - Olivier Brouckaert
  214. * @param - string $title - question title
  215. */
  216. function updateTitle($title)
  217. {
  218. $this->question=$title;
  219. }
  220. /**
  221. * changes the question description
  222. *
  223. * @author - Olivier Brouckaert
  224. * @param - string $description - question description
  225. */
  226. function updateDescription($description)
  227. {
  228. $this->description=$description;
  229. }
  230. /**
  231. * changes the question weighting
  232. *
  233. * @author - Olivier Brouckaert
  234. * @param - integer $weighting - question weighting
  235. */
  236. function updateWeighting($weighting)
  237. {
  238. $this->weighting=$weighting;
  239. }
  240. /**
  241. * changes the question position
  242. *
  243. * @author - Olivier Brouckaert
  244. * @param - integer $position - question position
  245. */
  246. function updatePosition($position)
  247. {
  248. $this->position=$position;
  249. }
  250. /**
  251. * changes the question level
  252. *
  253. * @author - Nicolas Raynaud
  254. * @param - integer $level - question level
  255. */
  256. function updateLevel($level)
  257. {
  258. $this->level=$level;
  259. }
  260. /**
  261. * changes the answer type. If the user changes the type from "unique answer" to "multiple answers"
  262. * (or conversely) answers are not deleted, otherwise yes
  263. *
  264. * @author - Olivier Brouckaert
  265. * @param - integer $type - answer type
  266. */
  267. function updateType($type)
  268. {
  269. global $TBL_REPONSES;
  270. // if we really change the type
  271. if($type != $this->type)
  272. {
  273. // if we don't change from "unique answer" to "multiple answers" (or conversely)
  274. if(!in_array($this->type,array(UNIQUE_ANSWER,MULTIPLE_ANSWER)) || !in_array($type,array(UNIQUE_ANSWER,MULTIPLE_ANSWER)))
  275. {
  276. // removes old answers
  277. $sql="DELETE FROM $TBL_REPONSES WHERE question_id='".Database::escape_string($this->id)."'";
  278. Database::query($sql);
  279. }
  280. $this->type=$type;
  281. }
  282. }
  283. /**
  284. * adds a picture to the question
  285. *
  286. * @author - Olivier Brouckaert
  287. * @param - string $Picture - temporary path of the picture to upload
  288. * @param - string $PictureName - Name of the picture
  289. * @return - boolean - true if uploaded, otherwise false
  290. */
  291. function uploadPicture($Picture,$PictureName)
  292. {
  293. global $picturePath, $_course, $_user;
  294. if (!file_exists($picturePath)) {
  295. if (mkdir($picturePath, api_get_permissions_for_new_directories())) {
  296. // document path
  297. $documentPath = api_get_path(SYS_COURSE_PATH) . $_course['path'] . "/document";
  298. $path = str_replace($documentPath,'',$picturePath);
  299. $title_path = basename($picturePath);
  300. $doc_id = add_document($_course, $path, 'folder', 0,$title_path);
  301. api_item_property_update($_course, TOOL_DOCUMENT, $doc_id, 'FolderCreated', $_user['user_id']);
  302. }
  303. }
  304. // if the question has got an ID
  305. if($this->id) {
  306. $extension = pathinfo($PictureName, PATHINFO_EXTENSION);
  307. $this->picture='quiz-'.$this->id.'.jpg';
  308. if($extension == 'gif' || $extension == 'png') {
  309. $o_img = new image($Picture);
  310. $o_img->send_image('JPG',$picturePath.'/'.$this->picture);
  311. $document_id = add_document($_course, '/images/'.$this->picture, 'file', filesize($picturePath.'/'.$this->picture),$this->picture);
  312. }
  313. else
  314. {
  315. move_uploaded_file($Picture,$picturePath.'/'.$this->picture)?true:false;
  316. }
  317. $document_id = add_document($_course, '/images/'.$this->picture, 'file', filesize($picturePath.'/'.$this->picture),$this->picture);
  318. if($document_id) {
  319. return api_item_property_update($_course, TOOL_DOCUMENT, $document_id, 'DocumentAdded', $_user['user_id']);
  320. }
  321. }
  322. return false;
  323. }
  324. /**
  325. * Resizes a picture || Warning!: can only be called after uploadPicture, or if picture is already available in object.
  326. *
  327. * @author - Toon Keppens
  328. * @param - string $Dimension - Resizing happens proportional according to given dimension: height|width|any
  329. * @param - integer $Max - Maximum size
  330. * @return - boolean - true if success, false if failed
  331. */
  332. function resizePicture($Dimension, $Max)
  333. {
  334. global $picturePath;
  335. // if the question has an ID
  336. if($this->id) {
  337. // Get dimensions from current image.
  338. $current_img = imagecreatefromjpeg($picturePath.'/'.$this->picture);
  339. $current_image_size = getimagesize($picturePath.'/'.$this->picture);
  340. $current_height = imagesy($current_img);
  341. $current_width = imagesx($current_img);
  342. if($current_image_size[0] < $Max && $current_image_size[1] <$Max)
  343. return true;
  344. elseif($current_height == "")
  345. return false;
  346. // Resize according to height.
  347. if ($Dimension == "height")
  348. {
  349. $resize_scale = $current_height / $Max;
  350. $new_height = $Max;
  351. $new_width = ceil($current_width / $resize_scale);
  352. }
  353. // Resize according to width
  354. if ($Dimension == "width")
  355. {
  356. $resize_scale = $current_width / $Max;
  357. $new_width = $Max;
  358. $new_height = ceil($current_height / $resize_scale);
  359. }
  360. // Resize according to height or width, both should not be larger than $Max after resizing.
  361. if ($Dimension == "any")
  362. {
  363. if ($current_height > $current_width || $current_height == $current_width)
  364. {
  365. $resize_scale = $current_height / $Max;
  366. $new_height = $Max;
  367. $new_width = ceil($current_width / $resize_scale);
  368. }
  369. if ($current_height < $current_width)
  370. {
  371. $resize_scale = $current_width / $Max;
  372. $new_width = $Max;
  373. $new_height = ceil($current_height / $resize_scale);
  374. }
  375. }
  376. // Create new image
  377. $new_img = imagecreatetruecolor($new_width, $new_height);
  378. $bgColor = imagecolorallocate($new_img, 255,255,255);
  379. imagefill($new_img , 0,0 , $bgColor);
  380. // Resize image
  381. imagecopyresized($new_img, $current_img, 0, 0, 0, 0, $new_width, $new_height, $current_width, $current_height);
  382. // Write image to file
  383. $result = imagejpeg($new_img, $picturePath.'/'.$this->picture, 100);
  384. // Delete temperory images, clear memory
  385. imagedestroy($current_img);
  386. imagedestroy($new_img);
  387. if ($result)
  388. {
  389. return true;
  390. }
  391. else
  392. {
  393. return false;
  394. }
  395. }
  396. }
  397. /**
  398. * deletes the picture
  399. *
  400. * @author - Olivier Brouckaert
  401. * @return - boolean - true if removed, otherwise false
  402. */
  403. function removePicture()
  404. {
  405. global $picturePath;
  406. // if the question has got an ID and if the picture exists
  407. if($this->id)
  408. {
  409. $picture=$this->picture;
  410. $this->picture='';
  411. return @unlink($picturePath.'/'.$picture)?true:false;
  412. }
  413. return false;
  414. }
  415. /**
  416. * Exports a picture to another question
  417. *
  418. * @author - Olivier Brouckaert
  419. * @param - integer $questionId - ID of the target question
  420. * @return - boolean - true if copied, otherwise false
  421. */
  422. function exportPicture($questionId)
  423. {
  424. global $TBL_QUESTIONS, $picturePath;
  425. // if the question has got an ID and if the picture exists
  426. if($this->id && !empty($this->picture))
  427. {
  428. $picture=explode('.',$this->picture);
  429. $Extension=$picture[sizeof($picture)-1];
  430. $picture='quiz-'.$questionId.'.'.$Extension;
  431. $sql="UPDATE $TBL_QUESTIONS SET picture='".Database::escape_string($picture)."' WHERE id='".Database::escape_string($questionId)."'";
  432. Database::query($sql);
  433. return @copy($picturePath.'/'.$this->picture,$picturePath.'/'.$picture)?true:false;
  434. }
  435. return false;
  436. }
  437. /**
  438. * saves the picture coming from POST into a temporary file
  439. * Temporary pictures are used when we don't want to save a picture right after a form submission.
  440. * For example, if we first show a confirmation box.
  441. *
  442. * @author - Olivier Brouckaert
  443. * @param - string $Picture - temporary path of the picture to move
  444. * @param - string $PictureName - Name of the picture
  445. */
  446. function setTmpPicture($Picture,$PictureName)
  447. {
  448. global $picturePath;
  449. $PictureName=explode('.',$PictureName);
  450. $Extension=$PictureName[sizeof($PictureName)-1];
  451. // saves the picture into a temporary file
  452. @move_uploaded_file($Picture,$picturePath.'/tmp.'.$Extension);
  453. }
  454. /**
  455. Sets the title
  456. */
  457. public function setTitle($title)
  458. {
  459. $this->question = $title;
  460. }
  461. /**
  462. * Moves the temporary question "tmp" to "quiz-$questionId"
  463. * Temporary pictures are used when we don't want to save a picture right after a form submission.
  464. * For example, if we first show a confirmation box.
  465. *
  466. * @author - Olivier Brouckaert
  467. * @return - boolean - true if moved, otherwise false
  468. */
  469. function getTmpPicture()
  470. {
  471. global $picturePath;
  472. // if the question has got an ID and if the picture exists
  473. if($this->id)
  474. {
  475. if(file_exists($picturePath.'/tmp.jpg'))
  476. {
  477. $Extension='jpg';
  478. }
  479. elseif(file_exists($picturePath.'/tmp.gif'))
  480. {
  481. $Extension='gif';
  482. }
  483. elseif(file_exists($picturePath.'/tmp.png'))
  484. {
  485. $Extension='png';
  486. }
  487. $this->picture='quiz-'.$this->id.'.'.$Extension;
  488. return @rename($picturePath.'/tmp.'.$Extension,$picturePath.'/'.$this->picture)?true:false;
  489. }
  490. return false;
  491. }
  492. /**
  493. * updates the question in the data base
  494. * if an exercise ID is provided, we add that exercise ID into the exercise list
  495. *
  496. * @author - Olivier Brouckaert
  497. * @param - integer $exerciseId - exercise ID if saving in an exercise
  498. */
  499. function save($exerciseId=0) {
  500. global $_course,$_user;
  501. $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
  502. $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
  503. $id=$this->id;
  504. $question=$this->question;
  505. $description=$this->description;
  506. $weighting=$this->weighting;
  507. $position=$this->position;
  508. $type=$this->type;
  509. $picture=$this->picture;
  510. $level=$this->level;
  511. // question already exists
  512. if(!empty($id)) {
  513. $sql="UPDATE $TBL_QUESTIONS SET
  514. question ='".Database::escape_string($question)."',
  515. description ='".Database::escape_string($description)."',
  516. ponderation ='".Database::escape_string($weighting)."',
  517. position ='".Database::escape_string($position)."',
  518. type ='".Database::escape_string($type)."',
  519. picture ='".Database::escape_string($picture)."',
  520. level ='".Database::escape_string($level)."'
  521. WHERE id='".Database::escape_string($id)."'";
  522. Database::query($sql);
  523. if(!empty($exerciseId)) {
  524. api_item_property_update($_course, TOOL_QUIZ, $id,'QuizQuestionUpdated',$_user['user_id']);
  525. }
  526. if (api_get_setting('search_enabled')=='true') {
  527. if ($exerciseId != 0) {
  528. $this -> search_engine_edit($exerciseId);
  529. } else {
  530. /**
  531. * actually there is *not* an user interface for
  532. * creating questions without a relation with an exercise
  533. */
  534. }
  535. }
  536. } else {
  537. // creates a new question
  538. $sql="SELECT max(position) FROM $TBL_QUESTIONS as question, $TBL_EXERCICE_QUESTION as test_question WHERE question.id=test_question.question_id AND test_question.exercice_id='".Database::escape_string($exerciseId)."'";
  539. $result=Database::query($sql);
  540. $current_position=Database::result($result,0,0);
  541. $this -> updatePosition($current_position+1);
  542. $position = $this -> position;
  543. $sql="INSERT INTO $TBL_QUESTIONS(question,description,ponderation,position,type,picture,level) VALUES(
  544. '".Database::escape_string($question)."',
  545. '".Database::escape_string($description)."',
  546. '".Database::escape_string($weighting)."',
  547. '".Database::escape_string($position)."',
  548. '".Database::escape_string($type)."',
  549. '".Database::escape_string($picture)."',
  550. '".Database::escape_string($level)."'
  551. )";
  552. Database::query($sql);
  553. $this->id=Database::insert_id();
  554. api_item_property_update($_course, TOOL_QUIZ, $this->id,'QuizQuestionAdded',$_user['user_id']);
  555. // If hotspot, create first answer
  556. if ($type == HOT_SPOT || $type == HOT_SPOT_ORDER) {
  557. $TBL_ANSWERS = Database::get_course_table(TABLE_QUIZ_ANSWER);
  558. $sql="INSERT INTO $TBL_ANSWERS (`id` , `question_id` , `answer` , `correct` , `comment` , `ponderation` , `position` , `hotspot_coordinates` , `hotspot_type` ) VALUES ('1', '".Database::escape_string($this->id)."', '', NULL , '', '10' , '1', '0;0|0|0', 'square')";
  559. Database::query($sql);
  560. }
  561. if (api_get_setting('search_enabled')=='true') {
  562. if ($exerciseId != 0) {
  563. $this -> search_engine_edit($exerciseId, TRUE);
  564. } else {
  565. /**
  566. * actually there is *not* an user interface for
  567. * creating questions without a relation with an exercise
  568. */
  569. }
  570. }
  571. }
  572. // if the question is created in an exercise
  573. if($exerciseId) {
  574. /*
  575. $sql = 'UPDATE '.Database::get_course_table(TABLE_LP_ITEM).'
  576. SET max_score = '.intval($weighting).'
  577. WHERE item_type = "'.TOOL_QUIZ.'"
  578. AND path='.intval($exerciseId);
  579. Database::query($sql);
  580. */
  581. // adds the exercise into the exercise list of this question
  582. $this->addToList($exerciseId, TRUE);
  583. }
  584. }
  585. function search_engine_edit($exerciseId, $addQs=FALSE, $rmQs=FALSE) {
  586. // update search engine and its values table if enabled
  587. if (api_get_setting('search_enabled')=='true' && extension_loaded('xapian')) {
  588. $course_id = api_get_course_id();
  589. // get search_did
  590. $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
  591. if ($addQs || $rmQs) {
  592. //there's only one row per question on normal db and one document per question on search engine db
  593. $sql = 'SELECT * FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_second_level=%s LIMIT 1';
  594. $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
  595. } else {
  596. $sql = 'SELECT * FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level=%s LIMIT 1';
  597. $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id);
  598. }
  599. $res = Database::query($sql);
  600. if (Database::num_rows($res) > 0 || $addQs) {
  601. require_once(api_get_path(LIBRARY_PATH) . 'search/DokeosIndexer.class.php');
  602. require_once(api_get_path(LIBRARY_PATH) . 'search/IndexableChunk.class.php');
  603. $di = new DokeosIndexer();
  604. if ($addQs) {
  605. $question_exercises = array((int)$exerciseId);
  606. } else {
  607. $question_exercises = array();
  608. }
  609. isset($_POST['language'])? $lang=Database::escape_string($_POST['language']): $lang = 'english';
  610. $di->connectDb(NULL, NULL, $lang);
  611. // retrieve others exercise ids
  612. $se_ref = Database::fetch_array($res);
  613. $se_doc = $di->get_document((int)$se_ref['search_did']);
  614. if ($se_doc !== FALSE) {
  615. if ( ($se_doc_data=$di->get_document_data($se_doc)) !== FALSE ) {
  616. $se_doc_data = unserialize($se_doc_data);
  617. if (isset($se_doc_data[SE_DATA]['type']) && $se_doc_data[SE_DATA]['type'] == SE_DOCTYPE_EXERCISE_QUESTION) {
  618. if (isset($se_doc_data[SE_DATA]['exercise_ids']) && is_array($se_doc_data[SE_DATA]['exercise_ids'])) {
  619. foreach ($se_doc_data[SE_DATA]['exercise_ids'] as $old_value) {
  620. if (!in_array($old_value, $question_exercises)) {
  621. $question_exercises[] = $old_value;
  622. }
  623. }
  624. }
  625. }
  626. }
  627. }
  628. if ($rmQs) {
  629. while ( ($key=array_search($exerciseId, $question_exercises)) !== FALSE) {
  630. unset($question_exercises[$key]);
  631. }
  632. }
  633. // build the chunk to index
  634. $ic_slide = new IndexableChunk();
  635. $ic_slide->addValue("title", $this->question);
  636. $ic_slide->addCourseId($course_id);
  637. $ic_slide->addToolId(TOOL_QUIZ);
  638. $xapian_data = array(
  639. SE_COURSE_ID => $course_id,
  640. SE_TOOL_ID => TOOL_QUIZ,
  641. SE_DATA => array('type' => SE_DOCTYPE_EXERCISE_QUESTION, 'exercise_ids' => $question_exercises, 'question_id' => (int)$this->id),
  642. SE_USER => (int)api_get_user_id(),
  643. );
  644. $ic_slide->xapian_data = serialize($xapian_data);
  645. $ic_slide->addValue("content", $this->description);
  646. //TODO: index answers, see also form validation on question_admin.inc.php
  647. $di->remove_document((int)$se_ref['search_did']);
  648. $di->addChunk($ic_slide);
  649. //index and return search engine document id
  650. if (!empty($question_exercises)) { // if empty there is nothing to index
  651. $did = $di->index();
  652. unset($di);
  653. }
  654. if ($did || $rmQs) {
  655. // save it to db
  656. if ($addQs || $rmQs) {
  657. $sql = 'DELETE FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_second_level=\'%s\'';
  658. $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
  659. } else {
  660. $sql = 'DELETE FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=\'%s\' AND ref_id_second_level=\'%s\'';
  661. $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id);
  662. }
  663. Database::query($sql);
  664. if ($rmQs) {
  665. if (!empty($question_exercises)) {
  666. $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did)
  667. VALUES (NULL , \'%s\', \'%s\', %s, %s, %s)';
  668. $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, array_shift($question_exercises), $this->id, $did);
  669. Database::query($sql);
  670. }
  671. } else {
  672. $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did)
  673. VALUES (NULL , \'%s\', \'%s\', %s, %s, %s)';
  674. $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id, $did);
  675. Database::query($sql);
  676. }
  677. }
  678. }
  679. }
  680. }
  681. /**
  682. * adds an exercise into the exercise list
  683. *
  684. * @author - Olivier Brouckaert
  685. * @param - integer $exerciseId - exercise ID
  686. * @param - boolean $fromSave - comming from $this->save() or not
  687. */
  688. function addToList($exerciseId, $fromSave = FALSE) {
  689. $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
  690. $id = $this->id;
  691. // checks if the exercise ID is not in the list
  692. if (!in_array($exerciseId,$this->exerciseList)) {
  693. $this->exerciseList[]=$exerciseId;
  694. $new_exercise = new Exercise();
  695. $new_exercise->read($exerciseId);
  696. $count = $new_exercise->selectNbrQuestions();
  697. $count++;
  698. $sql="INSERT INTO $TBL_EXERCICE_QUESTION (question_id, exercice_id, question_order) VALUES ('".Database::escape_string($id)."','".Database::escape_string($exerciseId)."', '$count' )";
  699. Database::query($sql);
  700. // we do not want to reindex if we had just saved adnd indexed the question
  701. if (!$fromSave) {
  702. $this->search_engine_edit($exerciseId, TRUE);
  703. }
  704. }
  705. }
  706. /**
  707. * removes an exercise from the exercise list
  708. *
  709. * @author - Olivier Brouckaert
  710. * @param - integer $exerciseId - exercise ID
  711. * @return - boolean - true if removed, otherwise false
  712. */
  713. function removeFromList($exerciseId) {
  714. global $TBL_EXERCICE_QUESTION;
  715. $id=$this->id;
  716. // searches the position of the exercise ID in the list
  717. $pos=array_search($exerciseId,$this->exerciseList);
  718. // exercise not found
  719. if($pos === false) {
  720. return false;
  721. } else {
  722. // deletes the position in the array containing the wanted exercise ID
  723. unset($this->exerciseList[$pos]);
  724. //update order of other elements
  725. $sql = "SELECT question_order FROM $TBL_EXERCICE_QUESTION WHERE question_id='".Database::escape_string($id)."' AND exercice_id='".Database::escape_string($exerciseId)."'";
  726. $res = Database::query($sql);
  727. if (Database::num_rows($res)>0) {
  728. $row = Database::fetch_array($res);
  729. if (!empty($row['question_order'])) {
  730. $sql = "UPDATE $TBL_EXERCICE_QUESTION SET question_order = question_order-1 WHERE exercice_id='".Database::escape_string($exerciseId)."' AND question_order > ".$row['question_order'];
  731. $res = Database::query($sql);
  732. }
  733. }
  734. $sql="DELETE FROM $TBL_EXERCICE_QUESTION WHERE question_id='".Database::escape_string($id)."' AND exercice_id='".Database::escape_string($exerciseId)."'";
  735. Database::query($sql);
  736. return true;
  737. }
  738. }
  739. /**
  740. * deletes a question from the database
  741. * the parameter tells if the question is removed from all exercises (value = 0),
  742. * or just from one exercise (value = exercise ID)
  743. *
  744. * @author - Olivier Brouckaert
  745. * @param - integer $deleteFromEx - exercise ID if the question is only removed from one exercise
  746. */
  747. function delete($deleteFromEx=0) {
  748. global $_course,$_user;
  749. $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
  750. $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
  751. $TBL_REPONSES = Database::get_course_table(TABLE_QUIZ_ANSWER);
  752. $id=$this->id;
  753. // if the question must be removed from all exercises
  754. if(!$deleteFromEx)
  755. {
  756. //update the question_order of each question to avoid inconsistencies
  757. $sql = "SELECT exercice_id, question_order FROM $TBL_EXERCICE_QUESTION WHERE question_id='".Database::escape_string($id)."'";
  758. $res = Database::query($sql);
  759. if (Database::num_rows($res)>0) {
  760. while ($row = Database::fetch_array($res)) {
  761. if (!empty($row['question_order'])) {
  762. $sql = "UPDATE $TBL_EXERCICE_QUESTION SET question_order = question_order-1 WHERE exercice_id='".Database::escape_string($row['exercice_id'])."' AND question_order > ".$row['question_order'];
  763. $res = Database::query($sql);
  764. }
  765. }
  766. }
  767. $sql="DELETE FROM $TBL_EXERCICE_QUESTION WHERE question_id='".Database::escape_string($id)."'";
  768. Database::query($sql);
  769. $sql="DELETE FROM $TBL_QUESTIONS WHERE id='".Database::escape_string($id)."'";
  770. Database::query($sql);
  771. $sql="DELETE FROM $TBL_REPONSES WHERE question_id='".Database::escape_string($id)."'";
  772. Database::query($sql);
  773. api_item_property_update($_course, TOOL_QUIZ, $id,'QuizQuestionDeleted',$_user['user_id']);
  774. $this->removePicture();
  775. // resets the object
  776. $this->Question();
  777. }
  778. // just removes the exercise from the list
  779. else
  780. {
  781. $this->removeFromList($deleteFromEx);
  782. if (api_get_setting('search_enabled')=='true' && extension_loaded('xapian')) {
  783. // disassociate question with this exercise
  784. $this -> search_engine_edit($deleteFromEx, FALSE, TRUE);
  785. }
  786. api_item_property_update($_course, TOOL_QUIZ, $id,'QuizQuestionDeleted',$_user['user_id']);
  787. }
  788. }
  789. /**
  790. * Duplicates the question
  791. *
  792. * @author - Olivier Brouckaert
  793. * @return - integer - ID of the new question
  794. */
  795. function duplicate() {
  796. global $TBL_QUESTIONS, $picturePath;
  797. $question = $this->question;
  798. $description = $this->description;
  799. $weighting = $this->weighting;
  800. $position = $this->position;
  801. $type = $this->type;
  802. $level = intval($this->level);
  803. $sql="INSERT INTO $TBL_QUESTIONS(question, description, ponderation, position, type, level ) VALUES('".Database::escape_string($question)."','".Database::escape_string($description)."','".Database::escape_string($weighting)."','".Database::escape_string($position)."','".Database::escape_string($type)."' ,'".Database::escape_string($level)."')";
  804. Database::query($sql);
  805. $id=Database::insert_id();
  806. // duplicates the picture
  807. $this->exportPicture($id);
  808. return $id;
  809. }
  810. /**
  811. * Returns an instance of the class corresponding to the type
  812. * @param integer $type the type of the question
  813. * @return an instance of a Question subclass (or of Questionc class by default)
  814. */
  815. static function getInstance ($type) {
  816. if (!is_null($type)) {
  817. list($file_name,$class_name) = self::$questionTypes[$type];
  818. include_once($file_name);
  819. if (class_exists($class_name)) {
  820. return new $class_name();
  821. } else {
  822. echo 'Can\'t instanciate class '.$class_name.' of type '.$type;
  823. return null;
  824. }
  825. }
  826. }
  827. /**
  828. * Creates the form to create / edit a question
  829. * A subclass can redifine this function to add fields...
  830. * @param FormValidator $form the formvalidator instance (by reference)
  831. */
  832. function createForm (&$form,$fck_config=0)
  833. {
  834. echo ' <style>
  835. div.row div.label{ width: 5%; }
  836. div.row div.formw{ width: 92%; }
  837. .media { display:none;}
  838. </style>';
  839. echo '<script>
  840. // hack to hide http://cksource.com/forums/viewtopic.php?f=6&t=8700
  841. function FCKeditor_OnComplete( editorInstance )
  842. {
  843. if (document.getElementById ( \'HiddenFCK\' + editorInstance.Name )) {
  844. HideFCKEditorByInstanceName (editorInstance.Name);
  845. }
  846. }
  847. function HideFCKEditorByInstanceName ( editorInstanceName ) {
  848. if (document.getElementById ( \'HiddenFCK\' + editorInstanceName ).className == "HideFCKEditor" ) {
  849. document.getElementById ( \'HiddenFCK\' + editorInstanceName ).className = "media";
  850. }
  851. }
  852. function show_media()
  853. {
  854. var my_display = document.getElementById(\'HiddenFCKquestionDescription\').style.display;
  855. if(my_display== \'none\' || my_display == \'\') {
  856. document.getElementById(\'HiddenFCKquestionDescription\').style.display = \'block\';
  857. document.getElementById(\'media_icon\').innerHTML=\'&nbsp;<img style="vertical-align: middle;" src="../img/looknfeelna.png" alt="" />&nbsp;'.get_lang('EnrichQuestion').'\';
  858. } else {
  859. document.getElementById(\'HiddenFCKquestionDescription\').style.display = \'none\';
  860. document.getElementById(\'media_icon\').innerHTML=\'&nbsp;<img style="vertical-align: middle;" src="../img/looknfeel.png" alt="" />&nbsp;'.get_lang('EnrichQuestion').'\';
  861. }
  862. }
  863. </script>';
  864. $renderer = $form->defaultRenderer();
  865. $form->addElement('html','<div class="form">');
  866. // question name
  867. $form->addElement('text','questionName','<span class="form_required">*</span> '.get_lang('Question'),'size="80"');
  868. //$form->applyFilter('questionName','html_filter');
  869. //$radios_results_enabled[] = $form->createElement('static', null, null, null);
  870. //$test=FormValidator :: createElement ('text', 'questionName');
  871. //$radios_results_enabled[]=$test;
  872. // question level
  873. $select_level = array (1,2,3,4,5);
  874. //$radios_results_enabled[] =
  875. foreach($select_level as $val) {
  876. $radios_results_enabled[] = FormValidator :: createElement ('radio', null, null,$val,$val);
  877. }
  878. $form->addGroup($radios_results_enabled,'questionLevel',get_lang('Difficulty'));
  879. $renderer->setElementTemplate('<div class="row"><div class="label">{label}</div><div class="formw" >{element}</div></div>','questionName');
  880. $renderer->setElementTemplate('<div class="row"><div class="label">{label}</div><div class="formw">{element}</div></div>','questionLevel');
  881. $form->addRule('questionName', get_lang('GiveQuestion'), 'required');
  882. // default content
  883. $isContent = intval($_REQUEST['isContent']);
  884. // question type
  885. $answerType= intval($_REQUEST['answerType']);
  886. $form->addElement('hidden','answerType',$_REQUEST['answerType']);
  887. // html editor
  888. $editor_config = array('ToolbarSet' => 'TestQuestionDescription', 'Width' => '100%', 'Height' => '150');
  889. if(is_array($fck_config)){
  890. $editor_config = array_merge($editor_config, $fck_config);
  891. }
  892. if(!api_is_allowed_to_edit(null,true)) $editor_config['UserStatus'] = 'student';
  893. $form -> addElement('html','<div class="row">
  894. <div class="label"></div>
  895. <div class="formw" style="height:50px">
  896. <a href="javascript://" onclick=" return show_media()"> <span id="media_icon"> <img style="vertical-align: middle;" src="../img/looknfeel.png" alt="" />&nbsp;'.get_lang('EnrichQuestion').'</span></a>
  897. </div>
  898. </div>');
  899. $form -> addElement ('html','<div class="HideFCKEditor" id="HiddenFCKquestionDescription" >');
  900. $form->add_html_editor('questionDescription', get_lang('langQuestionDescription'), false, false, $editor_config);
  901. $form -> addElement ('html','</div>');
  902. $renderer->setElementTemplate('<div class="row"><div class="label">{label}</div><div class="formw">{element}</div></div>','questionDescription');
  903. // hidden values
  904. $form->addElement('hidden','myid',$_REQUEST['myid']);
  905. switch($answerType) {
  906. case 1: $this->question = get_lang('langDefaultUniqueQuestion'); break;
  907. case 2: $this->question = get_lang('langDefaultMultipleQuestion'); break;
  908. case 3: $this->question = get_lang('langDefaultFillBlankQuestion'); break;
  909. case 4: $this->question = get_lang('langDefaultMathingQuestion'); break;
  910. case 5: $this->question = get_lang('langDefaultOpenQuestion'); break;
  911. case 9: $this->question = get_lang('langDefaultMultipleQuestion'); break;
  912. }
  913. $form->addElement('html','</div>');
  914. // default values
  915. $defaults = array();
  916. $defaults['questionName'] = $this -> question;
  917. $defaults['questionDescription'] = $this -> description;
  918. $defaults['questionLevel'] = $this -> level;
  919. if (!empty($_REQUEST['myid'])) {
  920. $form -> setDefaults($defaults);
  921. } else {
  922. if ($isContent == 1) {
  923. $form -> setDefaults($defaults);
  924. }
  925. }
  926. }
  927. /**
  928. * function which process the creation of questions
  929. * @param FormValidator $form the formvalidator instance
  930. * @param Exercise $objExercise the Exercise instance
  931. */
  932. function processCreation ($form, $objExercise) {
  933. $this -> updateTitle($form->getSubmitValue('questionName'));
  934. $this -> updateDescription($form->getSubmitValue('questionDescription'));
  935. $this -> updateLevel($form->getSubmitValue('questionLevel'));
  936. $this -> save($objExercise -> id);
  937. // modify the exercise
  938. $objExercise->addToList($this -> id);
  939. $objExercise->update_question_positions();
  940. }
  941. /**
  942. * abstract function which creates the form to create / edit the answers of the question
  943. * @param the formvalidator instance
  944. */
  945. abstract function createAnswersForm ($form);
  946. /**
  947. * abstract function which process the creation of answers
  948. * @param the formvalidator instance
  949. */
  950. abstract function processAnswersCreation ($form);
  951. /**
  952. * Displays the menu of question types
  953. */
  954. static function display_type_menu ($feedbacktype = 0)
  955. {
  956. global $exerciseId;
  957. // 1. by default we show all the question types
  958. $question_type_custom_list = self::$questionTypes;
  959. if (!isset($feedbacktype)) $feedbacktype=0;
  960. if ($feedbacktype==1) {
  961. //2. but if it is a feedback DIRECT we only show the UNIQUE_ANSWER type that is currently available
  962. $question_type_custom_list = array ( UNIQUE_ANSWER => self::$questionTypes[UNIQUE_ANSWER]);
  963. }
  964. //blocking edition
  965. $show_quiz_edition = true;
  966. if (isset($exerciseId) && !empty($exerciseId)) {
  967. $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
  968. $sql="SELECT max_score FROM $TBL_LP_ITEM
  969. WHERE item_type = '".TOOL_QUIZ."' AND path ='".Database::escape_string($exerciseId)."'";
  970. $result = Database::query($sql);
  971. if (Database::num_rows($result) > 0) {
  972. $show_quiz_edition = false;
  973. }
  974. }
  975. echo '<ul class="question_menu">';
  976. foreach ($question_type_custom_list as $i=>$a_type) {
  977. // include the class of the type
  978. require_once($a_type[0]);
  979. // get the picture of the type and the langvar which describes it
  980. eval('$img = '.$a_type[1].'::$typePicture;');
  981. eval('$explanation = get_lang('.$a_type[1].'::$explanationLangVar);');
  982. echo '<li>';
  983. echo '<div class="icon_image_content">';
  984. if ($show_quiz_edition) {
  985. echo '<a href="admin.php?'.api_get_cidreq().'&newQuestion=yes&answerType='.$i.'">'.Display::return_icon($img, $explanation).'</a>';
  986. echo '<br>';
  987. echo '<a href="admin.php?'.api_get_cidreq().'&newQuestion=yes&answerType='.$i.'">'.$explanation.'</a>';
  988. } else {
  989. $img = pathinfo($img);
  990. $img = $img['filename'];
  991. echo ''.Display::return_icon($img.'_na.gif',$explanation).'';
  992. echo '<br>';
  993. echo ''.$explanation.'';
  994. }
  995. echo '</div>';
  996. echo '</li>';
  997. }
  998. echo '<li>';
  999. echo '<div class="icon_image_content">';
  1000. if ($show_quiz_edition) {
  1001. if ($feedbacktype==1) {
  1002. echo $url = '<a href="question_pool.php?'.api_get_cidreq().'&type=1&fromExercise='.$exerciseId.'">';
  1003. } else {
  1004. echo $url = '<a href="question_pool.php?'.api_get_cidreq().'&fromExercise='.$exerciseId.'">';
  1005. }
  1006. echo Display::return_icon('database.png', get_lang('GetExistingQuestion'), '');
  1007. } else {
  1008. echo Display::return_icon('database_na.png', get_lang('GetExistingQuestion'), '');
  1009. }
  1010. echo '<br>';
  1011. echo $url;
  1012. echo get_lang('GetExistingQuestion');
  1013. echo '</a>';
  1014. echo '</div></li>';
  1015. echo '</ul>';
  1016. }
  1017. static function get_types_information()
  1018. {
  1019. return self::$questionTypes;
  1020. }
  1021. static function updateId()
  1022. {
  1023. return self::$questionTypes;
  1024. }
  1025. }
  1026. endif;
  1027. ?>