Completed
Push — 1.10.x ( 4eb5d1...71e5e5 )
by Yannick
46:56
created

ImsAssessmentItem::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 6
rs 9.4285
1
<?php
2
/* For licensing terms, see /license.txt */
3
/**
4
 * @author Claro Team <[email protected]>
5
 * @author Yannick Warnier <[email protected]>
6
 * @package chamilo.exercise
7
 */
8
/**
9
 * Code
10
 */
11
require dirname(__FILE__) . '/qti2_classes.php';
12
/**
13
 * Classes
14
*/
15
16
/**
17
 * An IMS/QTI item. It corresponds to a single question.
18
 * This class allows export from Claroline to IMS/QTI2.0 XML format of a single question.
19
 * It is not usable as-is, but must be subclassed, to support different kinds of questions.
20
 *
21
 * Every start_*() and corresponding end_*(), as well as export_*() methods return a string.
22
 *
23
 * note: Attached files are NOT exported.
24
 * @package chamilo.exercise
25
 */
26
class ImsAssessmentItem
27
{
28
    public $question;
29
    public $question_ident;
30
    public $answer;
31
32
    /**
33
     * Constructor.
34
     *
35
     * @param Ims2Question $question Ims2Question object we want to export.
36
     */
37
     function __construct($question)
38
     {
39
        $this->question = $question;
40
        $this->answer = $this->question->setAnswer();
41
        $this->questionIdent = "QST_" . $question->id ;
42
     }
43
44
     /**
45
      * Start the XML flow.
46
      *
47
      * This opens the <item> block, with correct attributes.
48
      *
49
      */
50
      function start_item()
51
      {
52
        $string = '<assessmentItem xmlns="http://www.imsglobal.org/xsd/imsqti_v2p1"
53
                    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
54
                    xsi:schemaLocation="http://www.imsglobal.org/xsd/imsqti_v2p1 imsqti_v2p1.xsd"
55
                    identifier="'.$this->questionIdent.'"
56
                    title="'.htmlspecialchars(formatExerciseQtiTitle($this->question->selectTitle())).'">'."\n";
57
58
        return $string;
59
      }
60
61
      /**
62
       * End the XML flow, closing the </item> tag.
63
       *
64
       */
65
      function end_item()
66
      {
67
        return "</assessmentItem>\n";
68
      }
69
70
     /**
71
      * Start the itemBody
72
      *
73
      */
74
     function start_item_body()
75
     {
76
        return '  <itemBody>' . "\n";
77
     }
78
79
     /**
80
      * End the itemBody part.
81
      *
82
      */
83
     function end_item_body()
84
     {
85
        return "  </itemBody>\n";
86
     }
87
88
     /**
89
      * add the response processing template used.
90
      *
91
      */
92
93
      function add_response_processing()
94
      {
95
          return '  <responseProcessing template="http://www.imsglobal.org/question/qti_v2p1/rptemplates/map_correct"/>' . "\n";
96
      }
97
98
     /**
99
      * Export the question as an IMS/QTI Item.
100
      *
101
      * This is a default behaviour, some classes may want to override this.
102
      *
103
      * @param $standalone: Boolean stating if it should be exported as a stand-alone question
104
      * @return string string, the XML flow for an Item.
105
      */
106
     function export($standalone = false)
107
     {
108
        $head = $foot = "";
109
110
        if ($standalone) {
111
            $head = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' . "\n";
112
        }
113
        //TODO understand why answer might be a non-object sometimes
114
        if (!is_object($this->answer)) {
115
            return $head;
116
        }
117
118
        $res = $head
119
            .$this->start_item()
120
            .$this->answer->imsExportResponsesDeclaration($this->questionIdent)
121
            .$this->start_item_body()
122
            .$this->answer->imsExportResponses(
123
                $this->questionIdent,
124
                $this->question->question,
125
                $this->question->description,
126
                $this->question->picture
127
            )
128
            .$this->end_item_body()
129
            .$this->add_response_processing()
130
            .$this->end_item()
131
            .$foot;
132
133
        return $res;
134
     }
135
}
136
137
/**
138
 * This class represents an entire exercise to be exported in IMS/QTI.
139
 * It will be represented by a single <section> containing several <item>.
140
 *
141
 * Some properties cannot be exported, as IMS does not support them :
142
 *   - type (one page or multiple pages)
143
 *   - start_date and end_date
144
 *   - max_attempts
145
 *   - show_answer
146
 *   - anonymous_attempts
147
 *
148
 * @author Amand Tihon <[email protected]>
149
 * @package chamilo.exercise
150
 */
151
class ImsSection
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
152
{
153
    public $exercise;
154
155
    /**
156
     * Constructor.
157
     * @param Exercise $exe The Exercise instance to export
158
     * @return ImsSection
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
159
     * @author Amand Tihon <[email protected]>
160
     */
161
    function __construct($exe)
162
    {
163
        $this->exercise = $exe;
164
    }
165
166
    function start_section()
167
    {
168
        $out = '<section ident="EXO_' . $this->exercise->selectId() . '" title="' .cleanAttribute(formatExerciseQtiDescription($this->exercise->selectTitle())) . '">' . "\n";
169
        return $out;
170
    }
171
172
    function end_section()
173
    {
174
        return "</section>\n";
175
    }
176
177
    function export_duration()
178
    {
179
        if ($max_time = $this->exercise->selectTimeLimit())
180
        {
181
            // return exercise duration in ISO8601 format.
182
            $minutes = floor($max_time / 60);
183
            $seconds = $max_time % 60;
184
            return '<duration>PT' . $minutes . 'M' . $seconds . "S</duration>\n";
185
        } else {
186
            return '';
187
        }
188
    }
189
190
    /**
191
     * Export the presentation (Exercise's description)
192
     * @author Amand Tihon <[email protected]>
193
     */
194
    function export_presentation()
195
    {
196
        $out = "<presentation_material><flow_mat><material>\n"
197
             . "  <mattext><![CDATA[" . formatExerciseQtiDescription($this->exercise->selectDescription()) . "]]></mattext>\n"
198
             . "</material></flow_mat></presentation_material>\n";
199
        return $out;
200
    }
201
202
    /**
203
     * Export the ordering information.
204
     * Either sequential, through all questions, or random, with a selected number of questions.
205
     * @author Amand Tihon <[email protected]>
206
     */
207
    function export_ordering()
208
    {
209
        $out = '';
210
        if ($n = $this->exercise->getShuffle()) {
211
            $out.= "<selection_ordering>"
212
                 . "  <selection>\n"
213
                 . "    <selection_number>" . $n . "</selection_number>\n"
214
                 . "  </selection>\n"
215
                 . '  <order order_type="Random" />'
216
                 . "\n</selection_ordering>\n";
217
        } else {
218
            $out.= '<selection_ordering sequence_type="Normal">' . "\n"
219
                 . "  <selection />\n"
220
                 . "</selection_ordering>\n";
221
        }
222
223
        return $out;
224
    }
225
226
    /**
227
     * Export the questions, as a succession of <items>
228
     * @author Amand Tihon <[email protected]>
229
     */
230
    function export_questions()
231
    {
232
        $out = "";
233
        foreach ($this->exercise->selectQuestionList() as $q) {
234
        	$out .= export_question_qti($q, false);
235
        }
236
        return $out;
237
    }
238
239
    /**
240
     * Export the exercise in IMS/QTI.
241
     *
242
     * @param bool $standalone Wether it should include XML tag and DTD line.
243
     * @return string string containing the XML flow
244
     * @author Amand Tihon <[email protected]>
245
     */
246
    function export($standalone)
247
    {
248
        $head = $foot = "";
249
        if ($standalone) {
250
            $head = '<?xml version = "1.0" encoding = "UTF-8" standalone = "no"?>' . "\n"
251
                  . '<!DOCTYPE questestinterop SYSTEM "ims_qtiasiv2p1.dtd">' . "\n"
252
                  . "<questestinterop>\n";
253
            $foot = "</questestinterop>\n";
254
        }
255
        $out = $head
256
             . $this->start_section()
257
             . $this->export_duration()
258
             . $this->export_presentation()
259
             . $this->export_ordering()
260
             . $this->export_questions()
261
             . $this->end_section()
262
             . $foot;
263
264
        return $out;
265
    }
266
}
267
268
/*
269
    Some quick notes on identifiers generation.
270
    The IMS format requires some blocks, like items, responses, feedbacks, to be uniquely
271
    identified.
272
    The unicity is mandatory in a single XML, of course, but it's prefered that the identifier stays
273
    coherent for an entire site.
274
275
    Here's the method used to generate those identifiers.
276
    Question identifier :: "QST_" + <Question Id from the DB> + "_" + <Question numeric type>
277
    Response identifier :: <Question identifier> + "_A_" + <Response Id from the DB>
278
    Condition identifier :: <Question identifier> + "_C_" + <Response Id from the DB>
279
    Feedback identifier :: <Question identifier> + "_F_" + <Response Id from the DB>
280
*/
281
/**
282
 * Class ImsItem
283
 *
284
 * An IMS/QTI item. It corresponds to a single question.
285
 * This class allows export from Claroline to IMS/QTI XML format.
286
 * It is not usable as-is, but must be subclassed, to support different kinds of questions.
287
 *
288
 * Every start_*() and corresponding end_*(), as well as export_*() methods return a string.
289
 *
290
 * warning: Attached files are NOT exported.
291
 * @author Amand Tihon <[email protected]>
292
 *
293
 * @package chamilo.exercise
294
 */
295
296
class ImsItem
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
297
{
298
    public $question;
299
    public $question_ident;
300
    public $answer;
301
302
    /**
303
     * Constructor.
304
     *
305
     * @param $question The Question object we want to export.
306
     * @return ImsItem
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
307
     * @author Anamd Tihon
308
     */
309
     function __construct($question)
310
     {
311
        $this->question = $question;
312
        $this->answer = $question->answer;
313
        $this->questionIdent = "QST_" . $question->selectId() ;
314
     }
315
316
     /**
317
      * Start the XML flow.
318
      *
319
      * This opens the <item> block, with correct attributes.
320
      *
321
      * @author Amand Tihon <[email protected]>
322
      */
323
      function start_item()
324
      {
325
        return '<item title="' . cleanAttribute(formatExerciseQtiDescription($this->question->selectTitle())) . '" ident="' . $this->questionIdent . '">' . "\n";
326
      }
327
328
      /**
329
       * End the XML flow, closing the </item> tag.
330
       *
331
       * @author Amand Tihon <[email protected]>
332
       */
333
      function end_item()
334
      {
335
        return "</item>\n";
336
      }
337
338
     /**
339
      * Create the opening, with the question itself.
340
      *
341
      * This means it opens the <presentation> but doesn't close it, as this is the role of end_presentation().
342
      * In between, the export_responses from the subclass should have been called.
343
      *
344
      * @author Amand Tihon <[email protected]>
345
      */
346
     function start_presentation()
347
     {
348
        return '<presentation label="' . $this->questionIdent . '"><flow>' . "\n"
349
             . '<material><mattext>' . formatExerciseQtiDescription($this->question->selectDescription()) . "</mattext></material>\n";
350
     }
351
352
     /**
353
      * End the </presentation> part, opened by export_header.
354
      *
355
      * @author Amand Tihon <[email protected]>
356
      */
357
     function end_presentation()
358
     {
359
        return "</flow></presentation>\n";
360
     }
361
362
     /**
363
      * Start the response processing, and declare the default variable, SCORE, at 0 in the outcomes.
364
      *
365
      * @author Amand Tihon <[email protected]>
366
      */
367
     function start_processing()
368
     {
369
        return '<resprocessing><outcomes><decvar vartype="Integer" defaultval="0" /></outcomes>' . "\n";
370
     }
371
372
     /**
373
      * End the response processing part.
374
      *
375
      * @author Amand Tihon <[email protected]>
376
      */
377
     function end_processing()
378
     {
379
        return "</resprocessing>\n";
380
     }
381
382
     /**
383
      * Export the question as an IMS/QTI Item.
384
      *
385
      * This is a default behaviour, some classes may want to override this.
386
      *
387
      * @param $standalone: Boolean stating if it should be exported as a stand-alone question
388
      * @return string string, the XML flow for an Item.
389
      * @author Amand Tihon <[email protected]>
390
      */
391
     function export($standalone = False)
392
     {
393
        global $charset;
394
        $head = $foot = "";
395
396
        if ($standalone) {
397
            $head = '<?xml version = "1.0" encoding = "'.$charset.'" standalone = "no"?>' . "\n"
398
                  . '<!DOCTYPE questestinterop SYSTEM "ims_qtiasiv2p1.dtd">' . "\n"
399
                  . "<questestinterop>\n";
400
            $foot = "</questestinterop>\n";
401
        }
402
403
        return $head
404
            . $this->start_item()
405
            . $this->start_presentation()
406
            . $this->answer->imsExportResponses($this->questionIdent)
407
            . $this->end_presentation()
408
            . $this->start_processing()
409
            . $this->answer->imsExportProcessing($this->questionIdent)
410
            . $this->end_processing()
411
            . $this->answer->imsExportFeedback($this->questionIdent)
412
            . $this->end_item()
413
            . $foot;
414
     }
415
}
416
417
/**
418
 * Send a complete exercise in IMS/QTI format, from its ID
419
 *
420
 * @param int $exerciseId The exercise to export
421
 * @param boolean $standalone Wether it should include XML tag and DTD line.
422
 * @return string XML as a string, or an empty string if there's no exercise with given ID.
423
 */
424 View Code Duplication
function export_exercise_to_qti($exerciseId, $standalone = true)
425
{
426
    $exercise = new Exercise();
427
    if (!$exercise->read($exerciseId)) {
428
        return '';
429
    }
430
    $ims = new ImsSection($exercise);
431
    $xml = $ims->export($standalone);
432
    return $xml;
433
}
434
435
/**
436
 * Returns the XML flow corresponding to one question
437
 *
438
 * @param int $questionId
439
 * @param bool $standalone (ie including XML tag, DTD declaration, etc)
440
 * @return string
441
 */
442 View Code Duplication
function export_question_qti($questionId, $standalone = true)
443
{
444
    $question = new Ims2Question();
445
    $qst = $question->read($questionId);
446
    if (!$qst or $qst->type == FREE_ANSWER) {
447
        return '';
448
    }
449
    $question->id = $qst->id;
450
    $question->type = $qst->type;
451
    $question->question = $qst->question;
452
    $question->description = $qst->description;
453
	$question->weighting=$qst->weighting;
454
	$question->position=$qst->position;
455
	$question->picture=$qst->picture;
456
    $ims = new ImsAssessmentItem($question);
457
458
    return $ims->export($standalone);
459
}
460
461
/**
462
 * Clean text like a description
463
 **/
464
function formatExerciseQtiDescription($text)
465
{
466
    $entities = api_html_entity_decode($text);
467
    return htmlspecialchars($entities);
468
}
469
470
/**
471
 * Clean titles
472
 * @param $text
473
 * @return string
474
 */
475
function formatExerciseQtiTitle($text)
476
{
477
    return htmlspecialchars($text);
478
}
479
480
/**
481
 * @param string $text
482
 */
483
function cleanAttribute($text)
484
{
485
    return $text;
486
}
487