1<?php
2 /*
3   +----------------------------------------------------------------------------+
4   | ILIAS open source                                                          |
5   +----------------------------------------------------------------------------+
6   | Copyright (c) 1998-2001 ILIAS open source, University of Cologne           |
7   |                                                                            |
8   | This program is free software; you can redistribute it and/or              |
9   | modify it under the terms of the GNU General Public License                |
10   | as published by the Free Software Foundation; either version 2             |
11   | of the License, or (at your option) any later version.                     |
12   |                                                                            |
13   | This program is distributed in the hope that it will be useful,            |
14   | but WITHOUT ANY WARRANTY; without even the implied warranty of             |
15   | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              |
16   | GNU General Public License for more details.                               |
17   |                                                                            |
18   | You should have received a copy of the GNU General Public License          |
19   | along with this program; if not, write to the Free Software                |
20   | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
21   +----------------------------------------------------------------------------+
22*/
23
24include_once "./Modules/SurveyQuestionPool/classes/class.SurveyQuestion.php";
25
26/**
27* SingleChoice survey question
28*
29* The SurveySingleChoiceQuestion class defines and encapsulates basic methods and attributes
30* for single choice survey question types.
31*
32* @author		Helmut Schottmüller <helmut.schottmueller@mac.com>
33* @version	$Id$
34* @extends SurveyQuestion
35* @ingroup ModulesSurveyQuestionPool
36*/
37class SurveySingleChoiceQuestion extends SurveyQuestion
38{
39    /**
40    * Categories contained in this question
41    *
42    * @var array
43    */
44    public $categories;
45
46    /**
47    * SurveySingleChoiceQuestion constructor
48    *
49    * The constructor takes possible arguments an creates an instance of the SurveySingleChoiceQuestion object.
50    *
51    * @param string $title A title string to describe the question
52    * @param string $description A description string to describe the question
53    * @param string $author A string containing the name of the questions author
54    * @param integer $owner A numerical ID to identify the owner/creator
55    * @access public
56    */
57    public function __construct($title = "", $description = "", $author = "", $questiontext = "", $owner = -1, $orientation = 1)
58    {
59        global $DIC;
60
61        $this->db = $DIC->database();
62        $this->user = $DIC->user();
63        $this->lng = $DIC->language();
64        parent::__construct($title, $description, $author, $questiontext, $owner);
65
66        include_once "./Modules/SurveyQuestionPool/classes/class.SurveyCategories.php";
67        $this->orientation = $orientation;
68        $this->categories = new SurveyCategories();
69    }
70
71    /**
72    * Gets the available categories for a given phrase
73    *
74    * @param integer $phrase_id The database id of the given phrase
75    * @result array All available categories
76    * @access public
77    */
78    public function &getCategoriesForPhrase($phrase_id)
79    {
80        $ilDB = $this->db;
81        $categories = array();
82        $result = $ilDB->queryF(
83            "SELECT svy_category.* FROM svy_category, svy_phrase_cat WHERE svy_phrase_cat.category_fi = svy_category.category_id AND svy_phrase_cat.phrase_fi = %s ORDER BY svy_phrase_cat.sequence",
84            array('integer'),
85            array($phrase_id)
86        );
87        while ($row = $ilDB->fetchAssoc($result)) {
88            if (($row["defaultvalue"] == 1) and ($row["owner_fi"] == 0)) {
89                $categories[$row["category_id"]] = $this->lng->txt($row["title"]);
90            } else {
91                $categories[$row["category_id"]] = $row["title"];
92            }
93        }
94        return $categories;
95    }
96
97    /**
98    * Adds a phrase to the question
99    *
100    * @param integer $phrase_id The database id of the given phrase
101    * @access public
102    */
103    public function addPhrase($phrase_id)
104    {
105        $ilUser = $this->user;
106        $ilDB = $this->db;
107
108        $result = $ilDB->queryF(
109            "SELECT svy_category.* FROM svy_category, svy_phrase_cat WHERE svy_phrase_cat.category_fi = svy_category.category_id AND svy_phrase_cat.phrase_fi = %s AND (svy_category.owner_fi = 0 OR svy_category.owner_fi = %s) ORDER BY svy_phrase_cat.sequence",
110            array('integer', 'integer'),
111            array($phrase_id, $ilUser->getId())
112        );
113        while ($row = $ilDB->fetchAssoc($result)) {
114            $neutral = $row["neutral"];
115            if (($row["defaultvalue"] == 1) and ($row["owner_fi"] == 0)) {
116                $this->categories->addCategory($this->lng->txt($row["title"]), 0, $neutral);
117            } else {
118                $this->categories->addCategory($row["title"], 0, $neutral);
119            }
120        }
121    }
122
123    /**
124    * Returns the question data fields from the database
125    *
126    * @param integer $id The question ID from the database
127    * @return array Array containing the question fields and data from the database
128    * @access public
129    */
130    public function getQuestionDataArray($id)
131    {
132        $ilDB = $this->db;
133
134        $result = $ilDB->queryF(
135            "SELECT svy_question.*, " . $this->getAdditionalTableName() . ".* FROM svy_question, " . $this->getAdditionalTableName() . " WHERE svy_question.question_id = %s AND svy_question.question_id = " . $this->getAdditionalTableName() . ".question_fi",
136            array('integer'),
137            array($id)
138        );
139        if ($result->numRows() == 1) {
140            return $ilDB->fetchAssoc($result);
141        } else {
142            return array();
143        }
144    }
145
146    /**
147    * Loads a SurveySingleChoiceQuestion object from the database
148    *
149    * @param integer $id The database id of the single choice survey question
150    * @access public
151    */
152    public function loadFromDb($id)
153    {
154        $ilDB = $this->db;
155
156        $result = $ilDB->queryF(
157            "SELECT svy_question.*, " . $this->getAdditionalTableName() . ".* FROM svy_question LEFT JOIN " . $this->getAdditionalTableName() . " ON " . $this->getAdditionalTableName() . ".question_fi = svy_question.question_id WHERE svy_question.question_id = %s",
158            array('integer'),
159            array($id)
160        );
161        if ($result->numRows() == 1) {
162            $data = $ilDB->fetchAssoc($result);
163            $this->setId($data["question_id"]);
164            $this->setTitle($data["title"]);
165            $this->label = $data['label'];
166            $this->setDescription($data["description"]);
167            $this->setObjId($data["obj_fi"]);
168            $this->setAuthor($data["author"]);
169            $this->setOwner($data["owner_fi"]);
170            include_once("./Services/RTE/classes/class.ilRTE.php");
171            $this->setQuestiontext(ilRTE::_replaceMediaObjectImageSrc($data["questiontext"], 1));
172            $this->setObligatory($data["obligatory"]);
173            $this->setComplete($data["complete"]);
174            $this->setOriginalId($data["original_id"]);
175            $this->setOrientation($data["orientation"]);
176
177            $this->categories->flushCategories();
178            $result = $ilDB->queryF(
179                "SELECT svy_variable.*, svy_category.title, svy_category.neutral FROM svy_variable, svy_category WHERE svy_variable.question_fi = %s AND svy_variable.category_fi = svy_category.category_id ORDER BY sequence ASC",
180                array('integer'),
181                array($id)
182            );
183            if ($result->numRows() > 0) {
184                while ($data = $ilDB->fetchAssoc($result)) {
185                    $this->categories->addCategory($data["title"], $data["other"], $data["neutral"], null, ($data['scale']) ? $data['scale'] : ($data['sequence'] + 1));
186                }
187            }
188        }
189        parent::loadFromDb($id);
190    }
191
192    /**
193    * Returns true if the question is complete for use
194    *
195    * @result boolean True if the question is complete for use, otherwise false
196    * @access public
197    */
198    public function isComplete()
199    {
200        if (
201            strlen($this->getTitle()) &&
202            strlen($this->getAuthor()) &&
203            strlen($this->getQuestiontext()) &&
204            $this->categories->getCategoryCount()
205        ) {
206            return 1;
207        } else {
208            return 0;
209        }
210    }
211
212    /**
213    * Saves a SurveySingleChoiceQuestion object to a database
214    *
215    * @access public
216    */
217    public function saveToDb($original_id = "")
218    {
219        $ilDB = $this->db;
220
221        $affectedRows = parent::saveToDb($original_id);
222        if ($affectedRows == 1) {
223            $this->log->debug("Before save Category-> DELETE from svy_qst_sc WHERE question_fi = " . $this->getId() . " AND INSERT again the same id and orientation in svy_qst_sc");
224            $affectedRows = $ilDB->manipulateF(
225                "DELETE FROM " . $this->getAdditionalTableName() . " WHERE question_fi = %s",
226                array('integer'),
227                array($this->getId())
228            );
229            $affectedRows = $ilDB->manipulateF(
230                "INSERT INTO " . $this->getAdditionalTableName() . " (question_fi, orientation) VALUES (%s, %s)",
231                array('integer', 'text'),
232                array(
233                    $this->getId(),
234                    $this->getOrientation()
235                )
236            );
237
238            $this->saveMaterial();
239            $this->saveCategoriesToDb();
240        }
241    }
242
243    public function saveCategoriesToDb()
244    {
245        $ilDB = $this->db;
246
247        $this->log->debug("DELETE from svy_variable before the INSERT into svy_variable. if scale > 0  we get scale value else we get null");
248
249        $affectedRows = $ilDB->manipulateF(
250            "DELETE FROM svy_variable WHERE question_fi = %s",
251            array('integer'),
252            array($this->getId())
253        );
254
255        for ($i = 0; $i < $this->categories->getCategoryCount(); $i++) {
256            $cat = $this->categories->getCategory($i);
257            $category_id = $this->saveCategoryToDb($cat->title, $cat->neutral);
258            $next_id = $ilDB->nextId('svy_variable');
259            $affectedRows = $ilDB->manipulateF(
260                "INSERT INTO svy_variable (variable_id, category_fi, question_fi, value1, other, sequence, scale, tstamp) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)",
261                array('integer','integer','integer','float','integer','integer', 'integer','integer'),
262                array($next_id, $category_id, $this->getId(), ($i + 1), $cat->other, $i, ($cat->scale > 0) ? $cat->scale : null, time())
263            );
264
265            $debug_scale = ($cat->scale > 0) ? $cat->scale : null;
266            $this->log->debug("INSERT INTO svy_variable category_fi= " . $category_id . " question_fi= " . $this->getId() . " value1= " . ($i + 1) . " other= " . $cat->other . " sequence= " . $i . " scale =" . $debug_scale);
267        }
268        $this->saveCompletionStatus();
269    }
270
271    /**
272    * Returns an xml representation of the question
273    *
274    * @return string The xml representation of the question
275    * @access public
276    */
277    public function toXML($a_include_header = true, $obligatory_state = "")
278    {
279        include_once("./Services/Xml/classes/class.ilXmlWriter.php");
280        $a_xml_writer = new ilXmlWriter;
281        $a_xml_writer->xmlHeader();
282        $this->insertXML($a_xml_writer, $a_include_header, $obligatory_state);
283        $xml = $a_xml_writer->xmlDumpMem(false);
284        if (!$a_include_header) {
285            $pos = strpos($xml, "?>");
286            $xml = substr($xml, $pos + 2);
287        }
288        return $xml;
289    }
290
291    /**
292    * Adds the question XML to a given XMLWriter object
293    *
294    * @param object $a_xml_writer The XMLWriter object
295    * @param boolean $a_include_header Determines wheather or not the XML should be used
296    * @access public
297    */
298    public function insertXML(&$a_xml_writer, $a_include_header = true)
299    {
300        $attrs = array(
301            "id" => $this->getId(),
302            "title" => $this->getTitle(),
303            "type" => $this->getQuestiontype(),
304            "obligatory" => $this->getObligatory()
305        );
306        $a_xml_writer->xmlStartTag("question", $attrs);
307
308        $a_xml_writer->xmlElement("description", null, $this->getDescription());
309        $a_xml_writer->xmlElement("author", null, $this->getAuthor());
310        if (strlen($this->label)) {
311            $attrs = array(
312                "label" => $this->label,
313            );
314        } else {
315            $attrs = array();
316        }
317        $a_xml_writer->xmlStartTag("questiontext", $attrs);
318        $this->addMaterialTag($a_xml_writer, $this->getQuestiontext());
319        $a_xml_writer->xmlEndTag("questiontext");
320
321        $a_xml_writer->xmlStartTag("responses");
322
323        for ($i = 0; $i < $this->categories->getCategoryCount(); $i++) {
324            $attrs = array(
325                "id" => $i
326            );
327            if (strlen($this->categories->getCategory($i)->other)) {
328                $attrs['other'] = $this->categories->getCategory($i)->other;
329            }
330            if (strlen($this->categories->getCategory($i)->neutral)) {
331                $attrs['neutral'] = $this->categories->getCategory($i)->neutral;
332            }
333            if (strlen($this->categories->getCategory($i)->label)) {
334                $attrs['label'] = $this->categories->getCategory($i)->label;
335            }
336            if (strlen($this->categories->getCategory($i)->scale)) {
337                $attrs['scale'] = $this->categories->getCategory($i)->scale;
338            }
339            $a_xml_writer->xmlStartTag("response_single", $attrs);
340            $this->addMaterialTag($a_xml_writer, $this->categories->getCategory($i)->title);
341            $a_xml_writer->xmlEndTag("response_single");
342        }
343
344        $a_xml_writer->xmlEndTag("responses");
345
346        if (count($this->material)) {
347            if (preg_match("/il_(\d*?)_(\w+)_(\d+)/", $this->material["internal_link"], $matches)) {
348                $attrs = array(
349                    "label" => $this->material["title"]
350                );
351                $a_xml_writer->xmlStartTag("material", $attrs);
352                $intlink = "il_" . IL_INST_ID . "_" . $matches[2] . "_" . $matches[3];
353                if (strcmp($matches[1], "") != 0) {
354                    $intlink = $this->material["internal_link"];
355                }
356                $a_xml_writer->xmlElement("mattext", null, $intlink);
357                $a_xml_writer->xmlEndTag("material");
358            }
359        }
360
361        $a_xml_writer->xmlStartTag("metadata");
362        $a_xml_writer->xmlStartTag("metadatafield");
363        $a_xml_writer->xmlElement("fieldlabel", null, "orientation");
364        $a_xml_writer->xmlElement("fieldentry", null, $this->getOrientation());
365        $a_xml_writer->xmlEndTag("metadatafield");
366        $a_xml_writer->xmlEndTag("metadata");
367
368        $a_xml_writer->xmlEndTag("question");
369    }
370
371    /**
372    * Import additional meta data from the question import file. Usually
373    * the meta data section is used to store question elements which are not
374    * part of the standard XML schema.
375    *
376    * @return array $a_meta Array containing the additional meta data
377    * @access public
378    */
379    public function importAdditionalMetadata($a_meta)
380    {
381        foreach ($a_meta as $key => $value) {
382            switch ($value["label"]) {
383                case "orientation":
384                    $this->setOrientation($value["entry"]);
385                    break;
386            }
387        }
388    }
389
390    /**
391    * Adds standard numbers as categories
392    *
393    * @param integer $lower_limit The lower limit
394    * @param integer $upper_limit The upper limit
395    * @access public
396    */
397    public function addStandardNumbers($lower_limit, $upper_limit)
398    {
399        for ($i = $lower_limit; $i <= $upper_limit; $i++) {
400            $this->categories->addCategory($i);
401        }
402    }
403
404    /**
405    * Saves a set of categories to a default phrase
406    *
407    * @param array $phrases The database ids of the seleted phrases
408    * @param string $title The title of the default phrase
409    * @access public
410    */
411    public function savePhrase($title)
412    {
413        $ilUser = $this->user;
414        $ilDB = $this->db;
415
416        $next_id = $ilDB->nextId('svy_phrase');
417        $affectedRows = $ilDB->manipulateF(
418            "INSERT INTO svy_phrase (phrase_id, title, defaultvalue, owner_fi, tstamp) VALUES (%s, %s, %s, %s, %s)",
419            array('integer','text','text','integer','integer'),
420            array($next_id, $title, 1, $ilUser->getId(), time())
421        );
422        $phrase_id = $next_id;
423
424        $counter = 1;
425        foreach ($_SESSION['save_phrase_data'] as $data) {
426            $next_id = $ilDB->nextId('svy_category');
427            $affectedRows = $ilDB->manipulateF(
428                "INSERT INTO svy_category (category_id, title, defaultvalue, owner_fi, tstamp, neutral) VALUES (%s, %s, %s, %s, %s, %s)",
429                array('integer','text','text','integer','integer','text'),
430                array($next_id, $data['answer'], 1, $ilUser->getId(), time(), $data['neutral'])
431            );
432            $category_id = $next_id;
433            $next_id = $ilDB->nextId('svy_phrase_cat');
434            $affectedRows = $ilDB->manipulateF(
435                "INSERT INTO svy_phrase_cat (phrase_category_id, phrase_fi, category_fi, sequence, other, scale) VALUES (%s, %s, %s, %s, %s, %s)",
436                array('integer', 'integer', 'integer','integer', 'integer', 'integer'),
437                array($next_id, $phrase_id, $category_id, $counter, ($data['other']) ? 1 : 0, $data['scale'])
438            );
439            $counter++;
440        }
441    }
442
443    /**
444    * Returns the question type of the question
445    *
446    * @return integer The question type of the question
447    * @access public
448    */
449    public function getQuestionType()
450    {
451        return "SurveySingleChoiceQuestion";
452    }
453
454    /**
455    * Returns the name of the additional question data table in the database
456    *
457    * @return string The additional table name
458    * @access public
459    */
460    public function getAdditionalTableName()
461    {
462        return "svy_qst_sc";
463    }
464
465    /**
466    * Creates the user data of the svy_answer table from the POST data
467    *
468    * @return array User data according to the svy_answer table
469    * @access public
470    */
471    public function &getWorkingDataFromUserInput($post_data)
472    {
473        $entered_value = $post_data[$this->getId() . "_value"];
474        $data = array();
475        if (strlen($entered_value)) {
476            array_push($data, array("value" => $entered_value, "textanswer" => $post_data[$this->getId() . '_' . $entered_value . '_other']));
477        }
478        for ($i = 0; $i < $this->categories->getCategoryCount(); $i++) {
479            $cat = $this->categories->getCategory($i);
480            if ($cat->other) {
481                if ($i != $entered_value) {
482                    if (strlen($post_data[$this->getId() . "_" . $i . "_other"])) {
483                        array_push($data, array("value" => $i, "textanswer" => $post_data[$this->getId() . '_' . $i . '_other'], "uncheck" => true));
484                    }
485                }
486            }
487        }
488        return $data;
489    }
490
491    /**
492    * Checks the input of the active user for obligatory status
493    * and entered values
494    *
495    * @param array $post_data The contents of the $_POST array
496    * @param integer $survey_id The database ID of the active survey
497    * @return string Empty string if the input is ok, an error message otherwise
498    * @access public
499    */
500    public function checkUserInput($post_data, $survey_id)
501    {
502        $entered_value = $post_data[$this->getId() . "_value"];
503
504        $this->log->debug("Entered value = " . $entered_value);
505
506        if ((!$this->getObligatory($survey_id)) && (strlen($entered_value) == 0)) {
507            return "";
508        }
509
510        if (strlen($entered_value) == 0) {
511            return $this->lng->txt("question_not_checked");
512        }
513
514        for ($i = 0; $i < $this->categories->getCategoryCount(); $i++) {
515            $cat = $this->categories->getCategory($i);
516            if ($cat->other) {
517                if ($i == $entered_value) {
518                    if (array_key_exists($this->getId() . "_" . $entered_value . "_other", $post_data) && !strlen($post_data[$this->getId() . "_" . $entered_value . "_other"])) {
519                        return $this->lng->txt("question_mr_no_other_answer");
520                    }
521                } else {
522                    if (strlen($post_data[$this->getId() . "_" . $i . "_other"])) {
523                        return $this->lng->txt("question_sr_no_other_answer_checked");
524                    }
525                }
526            }
527        }
528
529        return "";
530    }
531
532    public function saveUserInput($post_data, $active_id, $a_return = false)
533    {
534        $ilDB = $this->db;
535
536        $entered_value = $post_data[$this->getId() . "_value"];
537
538        if ($a_return) {
539            return array(array("value" => $entered_value,
540                "textanswer" => $post_data[$this->getId() . "_" . $entered_value . "_other"]));
541        }
542        if (strlen($entered_value) == 0) {
543            return;
544        }
545
546        $next_id = $ilDB->nextId('svy_answer');
547        #20216
548        $fields = array();
549        $fields['answer_id'] = array("integer", $next_id);
550        $fields['question_fi'] = array("integer", $this->getId());
551        $fields['active_fi'] = array("integer", $active_id);
552        $fields['value'] = array("float", (strlen($entered_value)) ? $entered_value : null);
553        $fields['textanswer'] = array("clob", ($post_data[$this->getId() . "_" . $entered_value . "_other"]) ?
554            $this->stripSlashesAddSpaceFallback($post_data[$this->getId() . "_" . $entered_value . "_other"]) : null);
555        $fields['tstamp'] = array("integer", time());
556
557        $affectedRows = $ilDB->insert("svy_answer", $fields);
558
559        $debug_value = (strlen($entered_value)) ? $entered_value : "NULL";
560        $debug_answer = ($post_data[$this->getId() . "_" . $entered_value . "_other"]) ? $post_data[$this->getId() . "_" . $entered_value . "_other"] : "NULL";
561        $this->log->debug("INSERT svy_answer answer_id=" . $next_id . " question_fi=" . $this->getId() . " active_fi=" . $active_id . " value=" . $debug_value . " textanswer=" . $debug_answer);
562    }
563
564    /**
565    * Import response data from the question import file
566    *
567    * @return array $a_data Array containing the response data
568    * @access public
569    */
570    public function importResponses($a_data)
571    {
572        foreach ($a_data as $id => $data) {
573            $categorytext = "";
574            foreach ($data["material"] as $material) {
575                $categorytext .= $material["text"];
576            }
577            $this->categories->addCategory(
578                $categorytext,
579                strlen($data['other']) ? $data['other'] : 0,
580                strlen($data['neutral']) ? $data['neutral'] : 0,
581                strlen($data['label']) ? $data['label'] : null,
582                strlen($data['scale']) ? $data['scale'] : null
583            );
584        }
585    }
586
587    /**
588    * Returns if the question is usable for preconditions
589    *
590    * @return boolean TRUE if the question is usable for a precondition, FALSE otherwise
591    * @access public
592    */
593    public function usableForPrecondition()
594    {
595        return true;
596    }
597
598    /**
599    * Returns the available relations for the question
600    *
601    * @return array An array containing the available relations
602    * @access public
603    */
604    public function getAvailableRelations()
605    {
606        return array("<", "<=", "=", "<>", ">=", ">");
607    }
608
609    /**
610    * Returns the options for preconditions
611    *
612    * @return array
613    */
614    public function getPreconditionOptions()
615    {
616        $lng = $this->lng;
617
618        $options = array();
619        for ($i = 0; $i < $this->categories->getCategoryCount(); $i++) {
620            $category = $this->categories->getCategory($i);
621            $options[$category->scale - 1] = $category->scale . " - " . $category->title;
622        }
623        return $options;
624    }
625
626    /**
627    * Creates a form property for the precondition value
628    *
629    * @return The ILIAS form element
630    * @access public
631    */
632    public function getPreconditionSelectValue($default = "", $title, $variable)
633    {
634        include_once "./Services/Form/classes/class.ilSelectInputGUI.php";
635        $step3 = new ilSelectInputGUI($title, $variable);
636        $options = $this->getPreconditionOptions();
637        $step3->setOptions($options);
638        $step3->setValue($default);
639        return $step3;
640    }
641
642    /**
643    * Returns the output for a precondition value
644    *
645    * @param string $value The precondition value
646    * @return string The output of the precondition value
647    * @access public
648    */
649    public function getPreconditionValueOutput($value)
650    {
651        // #18136
652        $category = $this->categories->getCategoryForScale($value + 1);
653
654        // #17895 - see getPreconditionOptions()
655        return $category->scale .
656            " - " .
657            ((strlen($category->title)) ? $category->title : $this->lng->txt('other_answer'));
658    }
659
660    public function getCategories()
661    {
662        return $this->categories;
663    }
664}
665