1<?php
2/* Copyright (c) 1998-2013 ILIAS open source, Extended GPL, see docs/LICENSE */
3
4require_once './Modules/TestQuestionPool/classes/class.assQuestionGUI.php';
5require_once './Modules/TestQuestionPool/interfaces/interface.ilGuiQuestionScoringAdjustable.php';
6require_once './Modules/TestQuestionPool/interfaces/interface.ilGuiAnswerScoringAdjustable.php';
7require_once './Modules/Test/classes/inc.AssessmentConstants.php';
8
9/**
10 * Numeric question GUI representation
11 *
12 * The assNumericGUI class encapsulates the GUI representation
13 * for numeric questions.
14 *
15 * @author		Helmut Schottmüller <helmut.schottmueller@mac.com>
16 * @author		Nina Gharib <nina@wgserve.de>
17 * @author		Björn Heyser <bheyser@databay.de>
18 * @author		Maximilian Becker <mbecker@databay.de>
19 *
20 * @version	$Id$
21 *
22 * @ingroup ModulesTestQuestionPool
23 * @ilCtrl_Calls assNumericGUI: ilFormPropertyDispatchGUI
24 */
25class assNumericGUI extends assQuestionGUI implements ilGuiQuestionScoringAdjustable, ilGuiAnswerScoringAdjustable
26{
27    /**
28     * assNumericGUI constructor
29     *
30     * The constructor takes possible arguments an creates an instance of the assNumericGUI object.
31     *
32     * @param integer $id The database id of a Numeric question object
33     *
34     * @return assNumericGUI
35     */
36    public function __construct($id = -1)
37    {
38        parent::__construct();
39        require_once './Modules/TestQuestionPool/classes/class.assNumeric.php';
40        $this->object = new assNumeric();
41        if ($id >= 0) {
42            $this->object->loadFromDb($id);
43        }
44    }
45
46    public function getCommand($cmd)
47    {
48        if (substr($cmd, 0, 6) == "delete") {
49            $cmd = "delete";
50        }
51        return $cmd;
52    }
53
54    /**
55     * {@inheritdoc}
56     */
57    protected function writePostData($always = false)
58    {
59        $hasErrors = (!$always) ? $this->editQuestion(true) : false;
60        if (!$hasErrors) {
61            require_once 'Services/Form/classes/class.ilPropertyFormGUI.php';
62            $this->writeQuestionGenericPostData();
63            $this->writeQuestionSpecificPostData(new ilPropertyFormGUI());
64            $this->writeAnswerSpecificPostData(new ilPropertyFormGUI());
65            $this->saveTaxonomyAssignments();
66            return 0;
67        }
68        return 1;
69    }
70
71    /**
72     * Creates an output of the edit form for the question
73     *
74     * @param bool $checkonly
75     *
76     * @return bool
77     */
78    public function editQuestion($checkonly = false)
79    {
80        $save = $this->isSaveCommand();
81        $this->getQuestionTemplate();
82
83        include_once("./Services/Form/classes/class.ilPropertyFormGUI.php");
84        $form = new ilPropertyFormGUI();
85        $this->editForm = $form;
86
87        $form->setFormAction($this->ctrl->getFormAction($this));
88        $form->setTitle($this->outQuestionType());
89        $form->setMultipart(true);
90        $form->setTableWidth("100%");
91        $form->setId("assnumeric");
92
93        $this->addBasicQuestionFormProperties($form);
94        $this->populateQuestionSpecificFormPart($form);
95        $this->populateAnswerSpecificFormPart($form);
96        $this->populateTaxonomyFormSection($form);
97        $this->addQuestionFormCommandButtons($form);
98
99        $errors = false;
100
101        if ($save) {
102            $form->setValuesByPost();
103            $errors = !$form->checkInput();
104            $form->setValuesByPost(); // again, because checkInput now performs the whole stripSlashes handling and we need this if we don't want to have duplication of backslashes
105
106            $lower = $form->getItemByPostVar('lowerlimit');
107            $upper = $form->getItemByPostVar('upperlimit');
108
109            if (!$this->checkRange($lower->getValue(), $upper->getValue())) {
110                global $DIC;
111                $lower->setAlert($DIC->language()->txt('qpl_numeric_lower_needs_valid_lower_alert'));
112                $upper->setAlert($DIC->language()->txt('qpl_numeric_upper_needs_valid_upper_alert'));
113                ilUtil::sendFailure($DIC->language()->txt("form_input_not_valid"));
114                $errors = true;
115            }
116
117            if ($errors) {
118                $checkonly = false;
119            }
120        }
121
122        if (!$checkonly) {
123            $this->tpl->setVariable("QUESTION_DATA", $form->getHTML());
124        }
125        return $errors;
126    }
127
128    /**
129    * Checks the range limits
130    *
131    * Checks the Range limits Upper and Lower for their correctness
132    *
133    * @return boolean
134    */
135    public function checkRange($lower, $upper)
136    {
137        include_once "./Services/Math/classes/class.EvalMath.php";
138        $eval = new EvalMath();
139        $eval->suppress_errors = true;
140        if (($eval->e($lower) !== false) and ($eval->e($upper) !== false)) {
141            if ($eval->e($lower) <= $eval->e($upper)) {
142                return true;
143            } else {
144                return false;
145            }
146        }
147        return false;
148    }
149
150    /**
151     * Question type specific support of intermediate solution output
152     * The function getSolutionOutput respects getUseIntermediateSolution()
153     * @return bool
154     */
155    public function supportsIntermediateSolutionOutput()
156    {
157        return true;
158    }
159
160    /**
161     * Get the question solution output
162     *
163     * @param integer $active_id             The active user id
164     * @param integer $pass                  The test pass
165     * @param boolean $graphicalOutput       Show visual feedback for right/wrong answers
166     * @param boolean $result_output         Show the reached points for parts of the question
167     * @param boolean $show_question_only    Show the question without the ILIAS content around
168     * @param boolean $show_feedback         Show the question feedback
169     * @param boolean $show_correct_solution Show the correct solution instead of the user solution
170     * @param boolean $show_manual_scoring   Show specific information for the manual scoring output
171     * @param bool    $show_question_text
172     *
173     * @return string The solution output of the question as HTML code
174     */
175    public function getSolutionOutput(
176        $active_id,
177        $pass = null,
178        $graphicalOutput = false,
179        $result_output = false,
180        $show_question_only = true,
181        $show_feedback = false,
182        $show_correct_solution = false,
183        $show_manual_scoring = false,
184        $show_question_text = true
185    ) {
186        // get the solution of the user for the active pass or from the last pass if allowed
187        $solutions = array();
188        if (($active_id > 0) && (!$show_correct_solution)) {
189            $solutions = $this->object->getSolutionValues($active_id, $pass, !$this->getUseIntermediateSolution());
190        } else {
191            array_push($solutions, array("value1" => sprintf($this->lng->txt("value_between_x_and_y"), $this->object->getLowerLimit(), $this->object->getUpperLimit())));
192        }
193
194        // generate the question output
195        require_once './Services/UICore/classes/class.ilTemplate.php';
196        $template = new ilTemplate("tpl.il_as_qpl_numeric_output_solution.html", true, true, "Modules/TestQuestionPool");
197        $solutiontemplate = new ilTemplate("tpl.il_as_tst_solution_output.html", true, true, "Modules/TestQuestionPool");
198        if (is_array($solutions)) {
199            if (($active_id > 0) && (!$show_correct_solution)) {
200                if ($graphicalOutput) {
201                    if ($this->object->getStep() === null) {
202                        $reached_points = $this->object->getReachedPoints($active_id, $pass);
203                    } else {
204                        $reached_points = $this->object->calculateReachedPoints($active_id, $pass);
205                    }
206                    // output of ok/not ok icons for user entered solutions
207                    if ($reached_points == $this->object->getMaximumPoints()) {
208                        $template->setCurrentBlock("icon_ok");
209                        $template->setVariable("ICON_OK", ilUtil::getImagePath("icon_ok.svg"));
210                        $template->setVariable("TEXT_OK", $this->lng->txt("answer_is_right"));
211                        $template->parseCurrentBlock();
212                    } else {
213                        $template->setCurrentBlock("icon_ok");
214                        $template->setVariable("ICON_NOT_OK", ilUtil::getImagePath("icon_not_ok.svg"));
215                        $template->setVariable("TEXT_NOT_OK", $this->lng->txt("answer_is_wrong"));
216                        $template->parseCurrentBlock();
217                    }
218                }
219            }
220            foreach ($solutions as $solution) {
221                $template->setVariable("NUMERIC_VALUE", $solution["value1"]);
222            }
223            if (count($solutions) == 0) {
224                $template->setVariable("NUMERIC_VALUE", "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;");
225            }
226        }
227        $template->setVariable("NUMERIC_SIZE", $this->object->getMaxChars());
228        $questiontext = $this->object->getQuestion();
229        if ($show_question_text == true) {
230            $template->setVariable("QUESTIONTEXT", $this->object->prepareTextareaOutput($questiontext, true));
231        }
232        $questionoutput = $template->get();
233        //$feedback = ($show_feedback) ? $this->getAnswerFeedbackOutput($active_id, $pass) : ""; // Moving new method
234        // due to deprecation.
235        $feedback = ($show_feedback && !$this->isTestPresentationContext()) ? $this->getGenericFeedbackOutput($active_id, $pass) : "";
236        if (strlen($feedback)) {
237            $cssClass = (
238                $this->hasCorrectSolution($active_id, $pass) ?
239                ilAssQuestionFeedback::CSS_CLASS_FEEDBACK_CORRECT : ilAssQuestionFeedback::CSS_CLASS_FEEDBACK_WRONG
240            );
241
242            $solutiontemplate->setVariable("ILC_FB_CSS_CLASS", $cssClass);
243            $solutiontemplate->setVariable("FEEDBACK", $this->object->prepareTextareaOutput($feedback, true));
244        }
245        $solutiontemplate->setVariable("SOLUTION_OUTPUT", $questionoutput);
246
247        $solutionoutput = $solutiontemplate->get();
248        if (!$show_question_only) {
249            // get page object output
250            $solutionoutput = $this->getILIASPage($solutionoutput);
251        }
252        return $solutionoutput;
253    }
254
255    /**
256     * @param bool $show_question_only
257     *
258     * @return string
259     */
260    public function getPreview($show_question_only = false, $showInlineFeedback = false)
261    {
262        // generate the question output
263        require_once './Services/UICore/classes/class.ilTemplate.php';
264        $template = new ilTemplate("tpl.il_as_qpl_numeric_output.html", true, true, "Modules/TestQuestionPool");
265        if (is_object($this->getPreviewSession())) {
266            $template->setVariable("NUMERIC_VALUE", " value=\"" . $this->getPreviewSession()->getParticipantsSolution() . "\"");
267        }
268        $template->setVariable("NUMERIC_SIZE", $this->object->getMaxChars());
269        $questiontext = $this->object->getQuestion();
270        $template->setVariable("QUESTIONTEXT", $this->object->prepareTextareaOutput($questiontext, true));
271        $questionoutput = $template->get();
272        if (!$show_question_only) {
273            // get page object output
274            $questionoutput = $this->getILIASPage($questionoutput);
275        }
276        return $questionoutput;
277    }
278
279    /**
280     * @param integer		$active_id
281     * @param integer|null	$pass
282     * @param bool			$is_postponed
283     * @param bool			$use_post_solutions
284     *
285     * @return string
286     */
287    // hey: prevPassSolutions - pass will be always available from now on
288    public function getTestOutput($active_id, $pass, $is_postponed = false, $use_post_solutions = false, $inlineFeedback)
289    // hey.
290    {
291        $solutions = null;
292        // get the solution of the user for the active pass or from the last pass if allowed
293        if ($use_post_solutions !== false) {
294            $solutions = array(
295                array('value1' => $use_post_solutions['numeric_result'])
296            );
297        } elseif ($active_id) {
298
299            // hey: prevPassSolutions - obsolete due to central check
300            #include_once "./Modules/Test/classes/class.ilObjTest.php";
301            #if (!ilObjTest::_getUsePreviousAnswers($active_id, true))
302            #{
303            #	if (is_null($pass)) $pass = ilObjTest::_getPass($active_id);
304            #}
305            $solutions = $this->object->getTestOutputSolutions($active_id, $pass);
306            // hey.
307        }
308
309        // generate the question output
310        require_once './Services/UICore/classes/class.ilTemplate.php';
311        $template = new ilTemplate("tpl.il_as_qpl_numeric_output.html", true, true, "Modules/TestQuestionPool");
312        if (is_array($solutions)) {
313            foreach ($solutions as $solution) {
314                $template->setVariable("NUMERIC_VALUE", " value=\"" . $solution["value1"] . "\"");
315            }
316        }
317        $template->setVariable("NUMERIC_SIZE", $this->object->getMaxChars());
318        $questiontext = $this->object->getQuestion();
319        $template->setVariable("QUESTIONTEXT", $this->object->prepareTextareaOutput($questiontext, true));
320        $questionoutput = $template->get();
321        $pageoutput = $this->outQuestionPage("", $is_postponed, $active_id, $questionoutput);
322        return $pageoutput;
323    }
324
325    /**
326     * Sets the ILIAS tabs for this question type
327     *
328     * @todo:	MOVE THIS STEPS TO COMMON QUESTION CLASS assQuestionGUI
329     */
330    public function setQuestionTabs()
331    {
332        /** @var $rbacsystem ilRbacSystem */
333        /** @var $ilTabs ilTabsGUI */
334        global $DIC;
335        $rbacsystem = $DIC['rbacsystem'];
336        $ilTabs = $DIC['ilTabs'];
337
338        $ilTabs->clearTargets();
339
340        $this->ctrl->setParameterByClass("ilAssQuestionPageGUI", "q_id", $_GET["q_id"]);
341        include_once "./Modules/TestQuestionPool/classes/class.assQuestion.php";
342        $q_type = $this->object->getQuestionType();
343
344        if (strlen($q_type)) {
345            $classname = $q_type . "GUI";
346            $this->ctrl->setParameterByClass(strtolower($classname), "sel_question_types", $q_type);
347            $this->ctrl->setParameterByClass(strtolower($classname), "q_id", $_GET["q_id"]);
348        }
349
350        if ($_GET["q_id"]) {
351            if ($rbacsystem->checkAccess('write', $_GET["ref_id"])) {
352                // edit page
353                $ilTabs->addTarget(
354                    "edit_page",
355                    $this->ctrl->getLinkTargetByClass("ilAssQuestionPageGUI", "edit"),
356                    array("edit", "insert", "exec_pg"),
357                    "",
358                    "",
359                    $force_active
360                );
361            }
362
363            $this->addTab_QuestionPreview($ilTabs);
364        }
365
366        $force_active = false;
367        if ($rbacsystem->checkAccess('write', $_GET["ref_id"])) {
368            $url = "";
369            if ($classname) {
370                $url = $this->ctrl->getLinkTargetByClass($classname, "editQuestion");
371            }
372            // edit question properties
373            $ilTabs->addTarget(
374                "edit_question",
375                $url,
376                array("editQuestion", "save", "cancel", "saveEdit", "originalSyncForm"),
377                $classname,
378                "",
379                $force_active
380            );
381        }
382
383        // add tab for question feedback within common class assQuestionGUI
384        $this->addTab_QuestionFeedback($ilTabs);
385
386        // add tab for question hint within common class assQuestionGUI
387        $this->addTab_QuestionHints($ilTabs);
388
389        // add tab for question's suggested solution within common class assQuestionGUI
390        $this->addTab_SuggestedSolution($ilTabs, $classname);
391
392        // Assessment of questions sub menu entry
393        if ($_GET["q_id"]) {
394            $ilTabs->addTarget(
395                "statistics",
396                $this->ctrl->getLinkTargetByClass($classname, "assessment"),
397                array("assessment"),
398                $classname,
399                ""
400            );
401        }
402
403        $this->addBackTab($ilTabs);
404    }
405
406    /**
407     * @param int $active_id
408     * @param int $pass
409     *
410     * @return mixed|string
411     */
412    public function getSpecificFeedbackOutput($userSolution)
413    {
414        $output = "";
415        return $this->object->prepareTextareaOutput($output, true);
416    }
417
418    public function writeQuestionSpecificPostData(ilPropertyFormGUI $form)
419    {
420        $this->object->setMaxChars($_POST["maxchars"]);
421    }
422
423    public function writeAnswerSpecificPostData(ilPropertyFormGUI $form)
424    {
425        $this->object->setLowerLimit($_POST['lowerlimit']);
426        $this->object->setUpperLimit($_POST['upperlimit']);
427        $this->object->setPoints($_POST['points']);
428    }
429
430    public function populateQuestionSpecificFormPart(\ilPropertyFormGUI $form)
431    {
432        // maxchars
433        $maxchars = new ilNumberInputGUI($this->lng->txt("maxchars"), "maxchars");
434        $maxchars->setInfo($this->lng->txt('qpl_maxchars_info_numeric_question'));
435        $maxchars->setSize(10);
436        $maxchars->setDecimals(0);
437        $maxchars->setMinValue(1);
438        $maxchars->setRequired(true);
439        if ($this->object->getMaxChars() > 0) {
440            $maxchars->setValue($this->object->getMaxChars());
441        }
442        $form->addItem($maxchars);
443    }
444
445    public function populateAnswerSpecificFormPart(\ilPropertyFormGUI $form)
446    {
447        // points
448        $points = new ilNumberInputGUI($this->lng->txt("points"), "points");
449        $points->allowDecimals(true);
450        $points->setValue($this->object->getPoints() > 0 ? $this->object->getPoints() : '');
451        $points->setRequired(true);
452        $points->setSize(3);
453        $points->setMinValue(0.0);
454        $points->setMinvalueShouldBeGreater(true);
455        $form->addItem($points);
456
457        $header = new ilFormSectionHeaderGUI();
458        $header->setTitle($this->lng->txt("range"));
459        $form->addItem($header);
460
461        // lower bound
462        $lower_limit = new ilFormulaInputGUI($this->lng->txt("range_lower_limit"), "lowerlimit");
463        $lower_limit->setSize(25);
464        $lower_limit->setMaxLength(20);
465        $lower_limit->setRequired(true);
466        $lower_limit->setValue($this->object->getLowerLimit());
467        $form->addItem($lower_limit);
468
469        // upper bound
470        $upper_limit = new ilFormulaInputGUI($this->lng->txt("range_upper_limit"), "upperlimit");
471        $upper_limit->setSize(25);
472        $upper_limit->setMaxLength(20);
473        $upper_limit->setRequired(true);
474        $upper_limit->setValue($this->object->getUpperLimit());
475        $form->addItem($upper_limit);
476
477        // reset input length, if max chars are set
478        if ($this->object->getMaxChars() > 0) {
479            $lower_limit->setSize($this->object->getMaxChars());
480            $lower_limit->setMaxLength($this->object->getMaxChars());
481            $upper_limit->setSize($this->object->getMaxChars());
482            $upper_limit->setMaxLength($this->object->getMaxChars());
483        }
484    }
485
486    /**
487     * Returns a list of postvars which will be suppressed in the form output when used in scoring adjustment.
488     * The form elements will be shown disabled, so the users see the usual form but can only edit the settings, which
489     * make sense in the given context.
490     *
491     * E.g. array('cloze_type', 'image_filename')
492     *
493     * @return string[]
494     */
495    public function getAfterParticipationSuppressionAnswerPostVars()
496    {
497        return array();
498    }
499
500    /**
501     * Returns a list of postvars which will be suppressed in the form output when used in scoring adjustment.
502     * The form elements will be shown disabled, so the users see the usual form but can only edit the settings, which
503     * make sense in the given context.
504     *
505     * E.g. array('cloze_type', 'image_filename')
506     *
507     * @return string[]
508     */
509    public function getAfterParticipationSuppressionQuestionPostVars()
510    {
511        return array();
512    }
513
514    /**
515     * Returns an html string containing a question specific representation of the answers so far
516     * given in the test for use in the right column in the scoring adjustment user interface.
517     *
518     * @param array $relevant_answers
519     *
520     * @return string
521     */
522    public function getAggregatedAnswersView($relevant_answers)
523    {
524        return  $this->renderAggregateView(
525            $this->aggregateAnswers($relevant_answers)
526        )->get();
527    }
528
529    public function aggregateAnswers($relevant_answers_chosen)
530    {
531        $aggregate = array();
532
533        foreach ($relevant_answers_chosen as $relevant_answer) {
534            if (array_key_exists($relevant_answer['value1'], $aggregate)) {
535                $aggregate[$relevant_answer['value1']]++;
536            } else {
537                $aggregate[$relevant_answer['value1']] = 1;
538            }
539        }
540        return $aggregate;
541    }
542
543    /**
544     * @param $aggregate
545     *
546     * @return ilTemplate
547     */
548    public function renderAggregateView($aggregate)
549    {
550        $tpl = new ilTemplate('tpl.il_as_aggregated_answers_table.html', true, true, "Modules/TestQuestionPool");
551
552        $tpl->setCurrentBlock('headercell');
553        $tpl->setVariable('HEADER', $this->lng->txt('tst_answer_aggr_answer_header'));
554        $tpl->parseCurrentBlock();
555
556        $tpl->setCurrentBlock('headercell');
557        $tpl->setVariable('HEADER', $this->lng->txt('tst_answer_aggr_frequency_header'));
558        $tpl->parseCurrentBlock();
559
560        foreach ($aggregate as $key => $value) {
561            $tpl->setCurrentBlock('aggregaterow');
562            $tpl->setVariable('OPTION', $key);
563            $tpl->setVariable('COUNT', $value);
564            $tpl->parseCurrentBlock();
565        }
566        return $tpl;
567    }
568
569    public function getAnswersFrequency($relevantAnswers, $questionIndex)
570    {
571        $answers = array();
572
573        foreach ($relevantAnswers as $ans) {
574            if (!isset($answers[$ans['value1']])) {
575                $answers[$ans['value1']] = array(
576                    'answer' => $ans['value1'], 'frequency' => 0
577                );
578            }
579
580            $answers[$ans['value1']]['frequency']++;
581        }
582
583        return $answers;
584    }
585
586    public function populateCorrectionsFormProperties(ilPropertyFormGUI $form)
587    {
588        // points
589        $points = new ilNumberInputGUI($this->lng->txt("points"), "points");
590        $points->allowDecimals(true);
591        $points->setValue($this->object->getPoints() > 0 ? $this->object->getPoints() : '');
592        $points->setRequired(true);
593        $points->setSize(3);
594        $points->setMinValue(0.0);
595        $points->setMinvalueShouldBeGreater(true);
596        $form->addItem($points);
597
598        $header = new ilFormSectionHeaderGUI();
599        $header->setTitle($this->lng->txt("range"));
600        $form->addItem($header);
601
602        // lower bound
603        $lower_limit = new ilFormulaInputGUI($this->lng->txt("range_lower_limit"), "lowerlimit");
604        $lower_limit->setSize(25);
605        $lower_limit->setMaxLength(20);
606        $lower_limit->setRequired(true);
607        $lower_limit->setValue($this->object->getLowerLimit());
608        $form->addItem($lower_limit);
609
610        // upper bound
611        $upper_limit = new ilFormulaInputGUI($this->lng->txt("range_upper_limit"), "upperlimit");
612        $upper_limit->setSize(25);
613        $upper_limit->setMaxLength(20);
614        $upper_limit->setRequired(true);
615        $upper_limit->setValue($this->object->getUpperLimit());
616        $form->addItem($upper_limit);
617
618        // reset input length, if max chars are set
619        if ($this->object->getMaxChars() > 0) {
620            $lower_limit->setSize($this->object->getMaxChars());
621            $lower_limit->setMaxLength($this->object->getMaxChars());
622            $upper_limit->setSize($this->object->getMaxChars());
623            $upper_limit->setMaxLength($this->object->getMaxChars());
624        }
625    }
626
627    /**
628     * @param ilPropertyFormGUI $form
629     */
630    public function saveCorrectionsFormProperties(ilPropertyFormGUI $form)
631    {
632        $this->object->setPoints((float) $form->getInput('points'));
633        $this->object->setLowerLimit((float) $form->getInput('lowerlimit'));
634        $this->object->setUpperLimit((float) $form->getInput('upperlimit'));
635    }
636}
637