Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * H5P activity attempt object
19
 *
20
 * @package    mod_h5pactivity
21
 * @since      Moodle 3.9
22
 * @copyright  2020 Ferran Recio <ferran@moodle.com>
23
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25
 
26
namespace mod_h5pactivity\local;
27
 
28
use core_xapi\handler;
29
use stdClass;
30
use core_xapi\local\statement;
31
 
32
/**
33
 * Class attempt for H5P activity
34
 *
35
 * @package    mod_h5pactivity
36
 * @since      Moodle 3.9
37
 * @copyright  2020 Ferran Recio <ferran@moodle.com>
38
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39
 */
40
class attempt {
41
 
42
    /** @var stdClass the h5pactivity_attempts record. */
43
    private $record;
44
 
45
    /** @var boolean if the DB statement has been updated. */
46
    private $scoreupdated = false;
47
 
48
    /**
49
     * Create a new attempt object.
50
     *
51
     * @param stdClass $record the h5pactivity_attempts record
52
     */
53
    public function __construct(stdClass $record) {
54
        $this->record = $record;
55
    }
56
 
57
    /**
58
     * Create a new user attempt in a specific H5P activity.
59
     *
60
     * @param stdClass $user a user record
61
     * @param stdClass $cm a course_module record
62
     * @return attempt|null a new attempt object or null if fail
63
     */
64
    public static function new_attempt(stdClass $user, stdClass $cm): ?attempt {
65
        global $DB;
66
        $record = new stdClass();
67
        $record->h5pactivityid = $cm->instance;
68
        $record->userid = $user->id;
69
        $record->timecreated = time();
70
        $record->timemodified = $record->timecreated;
71
        $record->rawscore = 0;
72
        $record->maxscore = 0;
73
        $record->duration = 0;
74
        $record->completion = null;
75
        $record->success = null;
76
 
77
        // Get last attempt number.
78
        $conditions = ['h5pactivityid' => $cm->instance, 'userid' => $user->id];
79
        $countattempts = $DB->count_records('h5pactivity_attempts', $conditions);
80
        $record->attempt = $countattempts + 1;
81
 
82
        $record->id = $DB->insert_record('h5pactivity_attempts', $record);
83
        if (!$record->id) {
84
            return null;
85
        }
86
        // Remove any xAPI State associated to this attempt.
87
        $context = \context_module::instance($cm->id);
88
        $xapihandler = handler::create('mod_h5pactivity');
89
        $xapihandler->wipe_states($context->id, $user->id);
90
 
91
        return new attempt($record);
92
    }
93
 
94
    /**
95
     * Get the last user attempt in a specific H5P activity.
96
     *
97
     * If no previous attempt exists, it generates a new one.
98
     *
99
     * @param stdClass $user a user record
100
     * @param stdClass $cm a course_module record
101
     * @return attempt|null a new attempt object or null if some problem accured
102
     */
103
    public static function last_attempt(stdClass $user, stdClass $cm): ?attempt {
104
        global $DB;
105
        $conditions = ['h5pactivityid' => $cm->instance, 'userid' => $user->id];
106
        $records = $DB->get_records('h5pactivity_attempts', $conditions, 'attempt DESC', '*', 0, 1);
107
        if (empty($records)) {
108
            return self::new_attempt($user, $cm);
109
        }
110
        return new attempt(array_shift($records));
111
    }
112
 
113
    /**
114
     * Wipe all attempt data for specific course_module and an optional user.
115
     *
116
     * @param stdClass $cm a course_module record
117
     * @param stdClass $user a user record
118
     */
119
    public static function delete_all_attempts(stdClass $cm, stdClass $user = null): void {
120
        global $DB;
121
 
122
        $where = 'a.h5pactivityid = :h5pactivityid';
123
        $conditions = ['h5pactivityid' => $cm->instance];
124
        if (!empty($user)) {
125
            $where .= ' AND a.userid = :userid';
126
            $conditions['userid'] = $user->id;
127
        }
128
 
129
        $DB->delete_records_select('h5pactivity_attempts_results', "attemptid IN (
130
                SELECT a.id
131
                FROM {h5pactivity_attempts} a
132
                WHERE $where)", $conditions);
133
 
134
        $DB->delete_records('h5pactivity_attempts', $conditions);
135
    }
136
 
137
    /**
138
     * Delete a specific attempt.
139
     *
140
     * @param attempt $attempt the attempt object to delete
141
     */
142
    public static function delete_attempt(attempt $attempt): void {
143
        global $DB;
144
        $attempt->delete_results();
145
        $DB->delete_records('h5pactivity_attempts', ['id' => $attempt->get_id()]);
146
    }
147
 
148
    /**
149
     * Save a new result statement into the attempt.
150
     *
151
     * It also updates the rawscore and maxscore if necessary.
152
     *
153
     * @param statement $statement the xAPI statement object
154
     * @param string $subcontent = '' optional subcontent identifier
155
     * @return bool if it can save the statement into db
156
     */
157
    public function save_statement(statement $statement, string $subcontent = ''): bool {
158
        global $DB;
159
 
160
        // Check statement data.
161
        $xapiobject = $statement->get_object();
162
        if (empty($xapiobject)) {
163
            return false;
164
        }
165
        $xapiresult = $statement->get_result();
166
        $xapidefinition = $xapiobject->get_definition();
167
        if (empty($xapidefinition) || empty($xapiresult)) {
168
            return false;
169
        }
170
 
171
        $xapicontext = $statement->get_context();
172
        if ($xapicontext) {
173
            $context = $xapicontext->get_data();
174
        } else {
175
            $context = new stdClass();
176
        }
177
        $definition = $xapidefinition->get_data();
178
        $result = $xapiresult->get_data();
179
        $duration = $xapiresult->get_duration();
180
 
181
        // Insert attempt_results record.
182
        $record = new stdClass();
183
        $record->attemptid = $this->record->id;
184
        $record->subcontent = $subcontent;
185
        $record->timecreated = time();
186
        $record->interactiontype = $definition->interactionType ?? 'other';
187
        $record->description = $this->get_description_from_definition($definition);
188
        $record->correctpattern = $this->get_correctpattern_from_definition($definition);
189
        $record->response = $result->response ?? '';
190
        $record->additionals = $this->get_additionals($definition, $context);
191
        $record->rawscore = 0;
192
        $record->maxscore = 0;
193
        if (isset($result->score)) {
194
            $record->rawscore = $result->score->raw ?? 0;
195
            $record->maxscore = $result->score->max ?? 0;
196
        }
197
        $record->duration = $duration;
198
        if (isset($result->completion)) {
199
            $record->completion = ($result->completion) ? 1 : 0;
200
        }
201
        if (isset($result->success)) {
202
            $record->success = ($result->success) ? 1 : 0;
203
        }
204
        if (!$DB->insert_record('h5pactivity_attempts_results', $record)) {
205
            return false;
206
        }
207
 
208
        // If no subcontent provided, results are propagated to the attempt itself.
209
        if (empty($subcontent)) {
210
            $this->set_duration($record->duration);
211
            $this->set_completion($record->completion ?? null);
212
            $this->set_success($record->success ?? null);
213
            // If Maxscore is not empty means that the rawscore is valid (even if it's 0)
214
            // and scaled score can be calculated.
215
            if ($record->maxscore) {
216
                $this->set_score($record->rawscore, $record->maxscore);
217
            }
218
        }
219
        // Refresh current attempt.
220
        return $this->save();
221
    }
222
 
223
    /**
224
     * Update the current attempt record into DB.
225
     *
226
     * @return bool true if update is succesful
227
     */
228
    public function save(): bool {
229
        global $DB;
230
        $this->record->timemodified = time();
231
        // Calculate scaled score.
232
        if ($this->scoreupdated) {
233
            if (empty($this->record->maxscore)) {
234
                $this->record->scaled = 0;
235
            } else {
236
                $this->record->scaled = $this->record->rawscore / $this->record->maxscore;
237
            }
238
        }
239
        return $DB->update_record('h5pactivity_attempts', $this->record);
240
    }
241
 
242
    /**
243
     * Set the attempt score.
244
     *
245
     * @param int|null $rawscore the attempt rawscore
246
     * @param int|null $maxscore the attempt maxscore
247
     */
248
    public function set_score(?int $rawscore, ?int $maxscore): void {
249
        $this->record->rawscore = $rawscore;
250
        $this->record->maxscore = $maxscore;
251
        $this->scoreupdated = true;
252
    }
253
 
254
    /**
255
     * Set the attempt duration.
256
     *
257
     * @param int|null $duration the attempt duration
258
     */
259
    public function set_duration(?int $duration): void {
260
        $this->record->duration = $duration;
261
    }
262
 
263
    /**
264
     * Set the attempt completion.
265
     *
266
     * @param int|null $completion the attempt completion
267
     */
268
    public function set_completion(?int $completion): void {
269
        $this->record->completion = $completion;
270
    }
271
 
272
    /**
273
     * Set the attempt success.
274
     *
275
     * @param int|null $success the attempt success
276
     */
277
    public function set_success(?int $success): void {
278
        $this->record->success = $success;
279
    }
280
 
281
    /**
282
     * Delete the current attempt results from the DB.
283
     */
284
    public function delete_results(): void {
285
        global $DB;
286
        $conditions = ['attemptid' => $this->record->id];
287
        $DB->delete_records('h5pactivity_attempts_results', $conditions);
288
    }
289
 
290
    /**
291
     * Return de number of results stored in this attempt.
292
     *
293
     * @return int the number of results stored in this attempt.
294
     */
295
    public function count_results(): int {
296
        global $DB;
297
        $conditions = ['attemptid' => $this->record->id];
298
        return $DB->count_records('h5pactivity_attempts_results', $conditions);
299
    }
300
 
301
    /**
302
     * Return all results stored in this attempt.
303
     *
304
     * @return stdClass[] results records.
305
     */
306
    public function get_results(): array {
307
        global $DB;
308
        $conditions = ['attemptid' => $this->record->id];
309
        return $DB->get_records('h5pactivity_attempts_results', $conditions, 'id ASC');
310
    }
311
 
312
    /**
313
     * Get additional data for some interaction types.
314
     *
315
     * @param stdClass $definition the statement object definition data
316
     * @param stdClass $context the statement optional context
317
     * @return string JSON encoded additional information
318
     */
319
    private function get_additionals(stdClass $definition, stdClass $context): string {
320
        $additionals = [];
321
        $interactiontype = $definition->interactionType ?? 'other';
322
        switch ($interactiontype) {
323
            case 'choice':
324
            case 'sequencing':
325
                $additionals['choices'] = $definition->choices ?? [];
326
            break;
327
 
328
            case 'matching':
329
                $additionals['source'] = $definition->source ?? [];
330
                $additionals['target'] = $definition->target ?? [];
331
            break;
332
 
333
            case 'likert':
334
                $additionals['scale'] = $definition->scale ?? [];
335
            break;
336
 
337
            case 'performance':
338
                $additionals['steps'] = $definition->steps ?? [];
339
            break;
340
        }
341
 
342
        $additionals['extensions'] = $definition->extensions ?? new stdClass();
343
 
344
        // Add context extensions.
345
        $additionals['contextExtensions'] = $context->extensions ?? new stdClass();
346
 
347
        if (empty($additionals)) {
348
            return '';
349
        }
350
        return json_encode($additionals);
351
    }
352
 
353
    /**
354
     * Extract the result description from statement object definition.
355
     *
356
     * In principle, H5P package can send a multilang description but the reality
357
     * is that most activities only send the "en_US" description if any and the
358
     * activity does not have any control over it.
359
     *
360
     * @param stdClass $definition the statement object definition
361
     * @return string The available description if any
362
     */
363
    private function get_description_from_definition(stdClass $definition): string {
364
        if (!isset($definition->description)) {
365
            return '';
366
        }
367
        $translations = (array) $definition->description;
368
        if (empty($translations)) {
369
            return '';
370
        }
371
        // By default, H5P packages only send "en-US" descriptions.
372
        return $translations['en-US'] ?? array_shift($translations);
373
    }
374
 
375
    /**
376
     * Extract the correct pattern from statement object definition.
377
     *
378
     * The correct pattern depends on the type of content and the plugin
379
     * has no control over it so we just store it in case that the statement
380
     * data have it.
381
     *
382
     * @param stdClass $definition the statement object definition
383
     * @return string The correct pattern if any
384
     */
385
    private function get_correctpattern_from_definition(stdClass $definition): string {
386
        if (!isset($definition->correctResponsesPattern)) {
387
            return '';
388
        }
389
        // Only arrays are allowed.
390
        if (is_array($definition->correctResponsesPattern)) {
391
            return json_encode($definition->correctResponsesPattern);
392
        }
393
        return '';
394
    }
395
 
396
    /**
397
     * Return the attempt number.
398
     *
399
     * @return int the attempt number
400
     */
401
    public function get_attempt(): int {
402
        return $this->record->attempt;
403
    }
404
 
405
    /**
406
     * Return the attempt ID.
407
     *
408
     * @return int the attempt id
409
     */
410
    public function get_id(): int {
411
        return $this->record->id;
412
    }
413
 
414
    /**
415
     * Return the attempt user ID.
416
     *
417
     * @return int the attempt userid
418
     */
419
    public function get_userid(): int {
420
        return $this->record->userid;
421
    }
422
 
423
    /**
424
     * Return the attempt H5P timecreated.
425
     *
426
     * @return int the attempt timecreated
427
     */
428
    public function get_timecreated(): int {
429
        return $this->record->timecreated;
430
    }
431
 
432
    /**
433
     * Return the attempt H5P timemodified.
434
     *
435
     * @return int the attempt timemodified
436
     */
437
    public function get_timemodified(): int {
438
        return $this->record->timemodified;
439
    }
440
 
441
    /**
442
     * Return the attempt H5P activity ID.
443
     *
444
     * @return int the attempt userid
445
     */
446
    public function get_h5pactivityid(): int {
447
        return $this->record->h5pactivityid;
448
    }
449
 
450
    /**
451
     * Return the attempt maxscore.
452
     *
453
     * @return int the maxscore value
454
     */
455
    public function get_maxscore(): int {
456
        return $this->record->maxscore;
457
    }
458
 
459
    /**
460
     * Return the attempt rawscore.
461
     *
462
     * @return int the rawscore value
463
     */
464
    public function get_rawscore(): int {
465
        return $this->record->rawscore;
466
    }
467
 
468
    /**
469
     * Return the attempt duration.
470
     *
471
     * @return int|null the duration value
472
     */
473
    public function get_duration(): ?int {
474
        return $this->record->duration;
475
    }
476
 
477
    /**
478
     * Return the attempt completion.
479
     *
480
     * @return int|null the completion value
481
     */
482
    public function get_completion(): ?int {
483
        return $this->record->completion;
484
    }
485
 
486
    /**
487
     * Return the attempt success.
488
     *
489
     * @return int|null the success value
490
     */
491
    public function get_success(): ?int {
492
        return $this->record->success;
493
    }
494
 
495
    /**
496
     * Return the attempt scaled.
497
     *
498
     * @return int|null the scaled value
499
     */
500
    public function get_scaled(): ?int {
501
        return is_null($this->record->scaled) ? $this->record->scaled : (int)$this->record->scaled;
502
    }
503
 
504
    /**
505
     * Return if the attempt has been modified.
506
     *
507
     * Note: adding a result only add track information unless the statement does
508
     * not specify subcontent. In this case this will update also the statement.
509
     *
510
     * @return bool if the attempt score have been modified
511
     */
512
    public function get_scoreupdated(): bool {
513
        return $this->scoreupdated;
514
    }
515
}