1<?php
2/* Copyright (c) 1998-2013 ILIAS open source, Extended GPL, see docs/LICENSE */
3
4require_once 'Modules/TestQuestionPool/classes/class.ilQuestionPoolTaxonomiesDuplicator.php';
5require_once 'Modules/TestQuestionPool/classes/class.assQuestion.php';
6require_once 'Services/Taxonomy/classes/class.ilObjTaxonomy.php';
7
8/**
9 * @author		Björn Heyser <bheyser@databay.de>
10 * @version		$Id$
11 *
12 * @package		Modules/Test
13 */
14class ilTestRandomQuestionSetStagingPoolBuilder
15{
16    /**
17     * @var ilDBInterface
18     */
19    public $db = null;
20
21    /**
22     * @var ilObjTest
23     */
24    public $testOBJ = null;
25
26    public function __construct(ilDBInterface $db, ilObjTest $testOBJ)
27    {
28        $this->db = $db;
29        $this->testOBJ = $testOBJ;
30    }
31
32    // =================================================================================================================
33
34    public function rebuild(ilTestRandomQuestionSetSourcePoolDefinitionList $sourcePoolDefinitionList)
35    {
36        $this->reset();
37
38        // fau: taxFilter/typeFilter - copy only the needed questions, and copy every question only once
39        // TODO-RND2017: remove non cheap methods and rename cheap ones
40        #$this->build($sourcePoolDefinitionList);
41        $this->buildCheap($sourcePoolDefinitionList);
42        // fau.
43    }
44
45    public function reset()
46    {
47        $this->removeMirroredTaxonomies();
48
49        $this->removeStagedQuestions();
50
51        $this->cleanupTestSettings();
52    }
53
54    private function removeMirroredTaxonomies()
55    {
56        $taxonomyIds = ilObjTaxonomy::getUsageOfObject($this->testOBJ->getId());
57
58        foreach ($taxonomyIds as $taxId) {
59            $taxonomy = new ilObjTaxonomy($taxId);
60            $taxonomy->delete();
61        }
62    }
63
64    private function removeStagedQuestions()
65    {
66        $query = 'SELECT * FROM tst_rnd_cpy WHERE tst_fi = %s';
67        $res = $this->db->queryF(
68            $query,
69            array('integer'),
70            array($this->testOBJ->getTestId())
71        );
72
73        while ($row = $this->db->fetchAssoc($res)) {
74            $question = assQuestion::_instanciateQuestion($row['qst_fi']);
75
76            if ($question instanceof assQuestion) {
77                $question->delete($row['qst_fi']);
78            } else {
79                global $DIC; /* @var ILIAS\DI\Container $DIC */
80                $DIC['ilLog']->warning(
81                    "could not delete staged random question (ref={$this->testOBJ->getRefId()} / qst={$row['qst_fi']})"
82                );
83            }
84        }
85
86        $query = "DELETE FROM tst_rnd_cpy WHERE tst_fi = %s";
87        $this->db->manipulateF($query, array('integer'), array($this->testOBJ->getTestId()));
88    }
89
90    private function build(ilTestRandomQuestionSetSourcePoolDefinitionList $sourcePoolDefinitionList)
91    {
92        $involvedSourcePoolIds = $sourcePoolDefinitionList->getInvolvedSourcePoolIds();
93
94        foreach ($involvedSourcePoolIds as $sourcePoolId) {
95            $questionIdMapping = $this->stageQuestionsFromSourcePool($sourcePoolId);
96
97            $taxonomiesKeysMap = $this->mirrorSourcePoolTaxonomies($sourcePoolId, $questionIdMapping);
98
99            $this->applyMappedTaxonomiesKeys($sourcePoolDefinitionList, $taxonomiesKeysMap, $sourcePoolId);
100        }
101    }
102
103    private function stageQuestionsFromSourcePool($sourcePoolId)
104    {
105        $questionIdMapping = array();
106
107        $query = 'SELECT question_id FROM qpl_questions WHERE obj_fi = %s AND complete = %s AND original_id IS NULL';
108        $res = $this->db->queryF($query, array('integer', 'text'), array($sourcePoolId, 1));
109
110        while ($row = $this->db->fetchAssoc($res)) {
111            $question = assQuestion::_instanciateQuestion($row['question_id']);
112            $duplicateId = $question->duplicate(true, null, null, null, $this->testOBJ->getId());
113
114            $nextId = $this->db->nextId('tst_rnd_cpy');
115            $this->db->insert('tst_rnd_cpy', array(
116                'copy_id' => array('integer', $nextId),
117                'tst_fi' => array('integer', $this->testOBJ->getTestId()),
118                'qst_fi' => array('integer', $duplicateId),
119                'qpl_fi' => array('integer', $sourcePoolId)
120            ));
121
122            $questionIdMapping[ $row['question_id'] ] = $duplicateId;
123        }
124
125        return $questionIdMapping;
126    }
127
128    // fau: taxFilter/typeFilter - select only the needed questions, and copy every question only once
129    private function buildCheap(ilTestRandomQuestionSetSourcePoolDefinitionList $sourcePoolDefinitionList)
130    {
131        // TODO-RND2017: refactor using assQuestionList and wrap with assQuestionListCollection for unioning
132
133        $questionIdMappingPerPool = array();
134
135        // select questions to be copied by the definitions
136        // note: a question pool may appear many times in this list
137
138        /* @var ilTestRandomQuestionSetSourcePoolDefinition $definition */
139        foreach ($sourcePoolDefinitionList as $definition) {
140            $taxFilter = $definition->getOriginalTaxonomyFilter();
141            $typeFilter = $definition->getTypeFilter();
142            $lifecycleFilter = $definition->getLifecycleFilter();
143
144            if (!empty($taxFilter)) {
145                require_once 'Services/Taxonomy/classes/class.ilObjTaxonomy.php';
146
147                $filterItems = null;
148                foreach ($taxFilter as $taxId => $nodeIds) {
149                    $taxItems = array();
150                    foreach ($nodeIds as $nodeId) {
151                        $nodeItems = ilObjTaxonomy::getSubTreeItems(
152                            'qpl',
153                            $definition->getPoolId(),
154                            'quest',
155                            $taxId,
156                            $nodeId
157                        );
158
159                        foreach ($nodeItems as $nodeItem) {
160                            $taxItems[] = $nodeItem['item_id'];
161                        }
162                    }
163
164                    $filterItems = isset($filterItems) ? array_intersect($filterItems, array_unique($taxItems)) : array_unique($taxItems);
165                }
166
167                // stage only the questions applying to the tax/type filter
168                // and save the duplication map for later use
169
170                $questionIdMappingPerPool = $this->stageQuestionsFromSourcePoolCheap(
171                    $definition->getPoolId(),
172                    $questionIdMappingPerPool,
173                    array_values($filterItems),
174                    $typeFilter,
175                    $lifecycleFilter
176                );
177            } else {
178                // stage only the questions applying to the tax/type filter
179                // and save the duplication map for later use
180
181                $questionIdMappingPerPool = $this->stageQuestionsFromSourcePoolCheap(
182                    $definition->getPoolId(),
183                    $questionIdMappingPerPool,
184                    null,
185                    $typeFilter,
186                    $lifecycleFilter
187                );
188            }
189        }
190
191        // copy the taxonomies to the test and map them
192        foreach ($questionIdMappingPerPool as $sourcePoolId => $questionIdMapping) {
193            $taxonomiesKeysMap = $this->mirrorSourcePoolTaxonomies($sourcePoolId, $questionIdMapping);
194            $this->applyMappedTaxonomiesKeys($sourcePoolDefinitionList, $taxonomiesKeysMap, $sourcePoolId);
195        }
196    }
197
198    private function stageQuestionsFromSourcePoolCheap($sourcePoolId, $questionIdMappingPerPool, $filterIds = null, $typeFilter = null, $lifecycleFilter = null)
199    {
200        $query = 'SELECT question_id FROM qpl_questions WHERE obj_fi = %s AND complete = %s AND original_id IS NULL';
201        if (!empty($filterIds)) {
202            $query .= ' AND ' . $this->db->in('question_id', $filterIds, false, 'integer');
203        }
204        if (!empty($typeFilter)) {
205            $query .= ' AND ' . $this->db->in('question_type_fi', $typeFilter, false, 'integer');
206        }
207        if (!empty($lifecycleFilter)) {
208            $query .= ' AND ' . $this->db->in('lifecycle', $lifecycleFilter, false, 'text');
209        }
210        $res = $this->db->queryF($query, array('integer', 'text'), array($sourcePoolId, 1));
211
212        while ($row = $this->db->fetchAssoc($res)) {
213            if (!isset($questionIdMappingPerPool[$sourcePoolId])) {
214                $questionIdMappingPerPool[$sourcePoolId] = array();
215            }
216            if (!isset($questionIdMappingPerPool[$sourcePoolId][ $row['question_id'] ])) {
217                $question = assQuestion::_instantiateQuestion($row['question_id']);
218                $duplicateId = $question->duplicate(true, null, null, null, $this->testOBJ->getId());
219
220                $nextId = $this->db->nextId('tst_rnd_cpy');
221                $this->db->insert('tst_rnd_cpy', array(
222                    'copy_id' => array('integer', $nextId),
223                    'tst_fi' => array('integer', $this->testOBJ->getTestId()),
224                    'qst_fi' => array('integer', $duplicateId),
225                    'qpl_fi' => array('integer', $sourcePoolId)
226                ));
227
228                $questionIdMappingPerPool[$sourcePoolId][ $row['question_id'] ] = $duplicateId;
229            }
230        }
231
232        return $questionIdMappingPerPool;
233    }
234    // fau.
235
236    private function mirrorSourcePoolTaxonomies($sourcePoolId, $questionIdMapping)
237    {
238        $duplicator = new ilQuestionPoolTaxonomiesDuplicator();
239
240        $duplicator->setSourceObjId($sourcePoolId);
241        $duplicator->setSourceObjType('qpl');
242        $duplicator->setTargetObjId($this->testOBJ->getId());
243        $duplicator->setTargetObjType($this->testOBJ->getType());
244        $duplicator->setQuestionIdMapping($questionIdMapping);
245
246        $duplicator->duplicate($duplicator->getAllTaxonomiesForSourceObject());
247
248        return $duplicator->getDuplicatedTaxonomiesKeysMap();
249    }
250
251    /**
252     * @param ilTestRandomQuestionSetSourcePoolDefinitionList $sourcePoolDefinitionList
253     * @param ilQuestionPoolDuplicatedTaxonomiesKeysMap $taxonomiesKeysMap
254     * @param integer $sourcePoolId
255     */
256    private function applyMappedTaxonomiesKeys(ilTestRandomQuestionSetSourcePoolDefinitionList $sourcePoolDefinitionList, ilQuestionPoolDuplicatedTaxonomiesKeysMap $taxonomiesKeysMap, $sourcePoolId)
257    {
258        foreach ($sourcePoolDefinitionList as $definition) {
259            /** @var ilTestRandomQuestionSetSourcePoolDefinition $definition */
260
261            if ($definition->getPoolId() == $sourcePoolId) {
262                // fau: taxFilter/typeFilter - map the enhanced taxonomy filter
263                #$definition->setMappedFilterTaxId(
264                #	$taxonomiesKeysMap->getMappedTaxonomyId($definition->getOriginalFilterTaxId())
265                #);
266
267                #$definition->setMappedFilterTaxNodeId(
268                #	$taxonomiesKeysMap->getMappedTaxNodeId($definition->getOriginalFilterTaxNodeId())
269                #);
270
271                $definition->mapTaxonomyFilter($taxonomiesKeysMap);
272                // fau.
273            }
274        }
275    }
276
277    private function cleanupTestSettings()
278    {
279        $this->testOBJ->setResultFilterTaxIds(array());
280        $this->testOBJ->saveToDb(true);
281    }
282
283    // =================================================================================================================
284}
285