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 manager class
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 mod_h5pactivity\local\report\participants;
29
use mod_h5pactivity\local\report\attempts;
30
use mod_h5pactivity\local\report\results;
31
use context_module;
32
use cm_info;
33
use moodle_recordset;
34
use core_user;
35
use stdClass;
36
use core\dml\sql_join;
37
use mod_h5pactivity\event\course_module_viewed;
38
 
39
/**
40
 * Class manager for H5P activity
41
 *
42
 * @package    mod_h5pactivity
43
 * @since      Moodle 3.9
44
 * @copyright  2020 Ferran Recio <ferran@moodle.com>
45
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
46
 */
47
class manager {
48
 
49
    /** No automathic grading using attempt results. */
50
    const GRADEMANUAL = 0;
51
 
52
    /** Use highest attempt results for grading. */
53
    const GRADEHIGHESTATTEMPT = 1;
54
 
55
    /** Use average attempt results for grading. */
56
    const GRADEAVERAGEATTEMPT = 2;
57
 
58
    /** Use last attempt results for grading. */
59
    const GRADELASTATTEMPT = 3;
60
 
61
    /** Use first attempt results for grading. */
62
    const GRADEFIRSTATTEMPT = 4;
63
 
64
    /** Participants cannot review their own attempts. */
65
    const REVIEWNONE = 0;
66
 
67
    /** Participants can review their own attempts when have one attempt completed. */
68
    const REVIEWCOMPLETION = 1;
69
 
70
    /** @var stdClass course_module record. */
71
    private $instance;
72
 
73
    /** @var context_module the current context. */
74
    private $context;
75
 
76
    /** @var cm_info course_modules record. */
77
    private $coursemodule;
78
 
79
    /**
80
     * Class contructor.
81
     *
82
     * @param cm_info $coursemodule course module info object
83
     * @param stdClass $instance H5Pactivity instance object.
84
     */
85
    public function __construct(cm_info $coursemodule, stdClass $instance) {
86
        $this->coursemodule = $coursemodule;
87
        $this->instance = $instance;
88
        $this->context = context_module::instance($coursemodule->id);
89
        $this->instance->cmidnumber = $coursemodule->idnumber;
90
    }
91
 
92
    /**
93
     * Create a manager instance from an instance record.
94
     *
95
     * @param stdClass $instance a h5pactivity record
96
     * @return manager
97
     */
98
    public static function create_from_instance(stdClass $instance): self {
99
        $coursemodule = get_coursemodule_from_instance('h5pactivity', $instance->id);
100
        // Ensure that $this->coursemodule is a cm_info object.
101
        $coursemodule = cm_info::create($coursemodule);
102
        return new self($coursemodule, $instance);
103
    }
104
 
105
    /**
106
     * Create a manager instance from an course_modules record.
107
     *
108
     * @param stdClass|cm_info $coursemodule a h5pactivity record
109
     * @return manager
110
     */
111
    public static function create_from_coursemodule($coursemodule): self {
112
        global $DB;
113
        // Ensure that $this->coursemodule is a cm_info object.
114
        $coursemodule = cm_info::create($coursemodule);
115
        $instance = $DB->get_record('h5pactivity', ['id' => $coursemodule->instance], '*', MUST_EXIST);
116
        return new self($coursemodule, $instance);
117
    }
118
 
119
    /**
120
     * Return the available grading methods.
121
     * @return string[] an array "option value" => "option description"
122
     */
123
    public static function get_grading_methods(): array {
124
        return [
125
            self::GRADEHIGHESTATTEMPT => get_string('grade_highest_attempt', 'mod_h5pactivity'),
126
            self::GRADEAVERAGEATTEMPT => get_string('grade_average_attempt', 'mod_h5pactivity'),
127
            self::GRADELASTATTEMPT => get_string('grade_last_attempt', 'mod_h5pactivity'),
128
            self::GRADEFIRSTATTEMPT => get_string('grade_first_attempt', 'mod_h5pactivity'),
129
            self::GRADEMANUAL => get_string('grade_manual', 'mod_h5pactivity'),
130
        ];
131
    }
132
 
133
    /**
134
     * Return the selected attempt criteria.
135
     * @return string[] an array "grademethod value", "attempt description"
136
     */
137
    public function get_selected_attempt(): array {
138
        $types = [
139
            self::GRADEHIGHESTATTEMPT => get_string('attempt_highest', 'mod_h5pactivity'),
140
            self::GRADEAVERAGEATTEMPT => get_string('attempt_average', 'mod_h5pactivity'),
141
            self::GRADELASTATTEMPT => get_string('attempt_last', 'mod_h5pactivity'),
142
            self::GRADEFIRSTATTEMPT => get_string('attempt_first', 'mod_h5pactivity'),
143
            self::GRADEMANUAL => get_string('attempt_none', 'mod_h5pactivity'),
144
        ];
145
        if ($this->instance->enabletracking) {
146
            $key = $this->instance->grademethod;
147
        } else {
148
            $key = self::GRADEMANUAL;
149
        }
150
        return [$key, $types[$key]];
151
    }
152
 
153
    /**
154
     * Return the available review modes.
155
     *
156
     * @return string[] an array "option value" => "option description"
157
     */
158
    public static function get_review_modes(): array {
159
        return [
160
            self::REVIEWCOMPLETION => get_string('review_on_completion', 'mod_h5pactivity'),
161
            self::REVIEWNONE => get_string('review_none', 'mod_h5pactivity'),
162
        ];
163
    }
164
 
165
    /**
166
     * Check if tracking is enabled in a particular h5pactivity for a specific user.
167
     *
168
     * @return bool if tracking is enabled in this activity
169
     */
170
    public function is_tracking_enabled(): bool {
171
        return $this->instance->enabletracking;
172
    }
173
 
174
    /**
175
     * Check if the user has permission to submit a particular h5pactivity for a specific user.
176
     *
177
     * @param stdClass|null $user user record (default $USER)
178
     * @return bool if the user has permission to submit in this activity
179
     */
180
    public function can_submit(stdClass $user = null): bool {
181
        global $USER;
182
 
183
        if (empty($user)) {
184
            $user = $USER;
185
        }
186
        return has_capability('mod/h5pactivity:submit', $this->context, $user, false);
187
    }
188
 
189
    /**
190
     * Check if a user can see the activity attempts list.
191
     *
192
     * @param stdClass|null $user user record (default $USER)
193
     * @return bool if the user can see the attempts link
194
     */
195
    public function can_view_all_attempts(stdClass $user = null): bool {
196
        global $USER;
197
        if (!$this->instance->enabletracking) {
198
            return false;
199
        }
200
        if (empty($user)) {
201
            $user = $USER;
202
        }
203
        return has_capability('mod/h5pactivity:reviewattempts', $this->context, $user);
204
    }
205
 
206
    /**
207
     * Check if a user can see own attempts.
208
     *
209
     * @param stdClass|null $user user record (default $USER)
210
     * @return bool if the user can see the own attempts link
211
     */
212
    public function can_view_own_attempts(stdClass $user = null): bool {
213
        global $USER;
214
        if (!$this->instance->enabletracking) {
215
            return false;
216
        }
217
        if (empty($user)) {
218
            $user = $USER;
219
        }
220
        if (has_capability('mod/h5pactivity:reviewattempts', $this->context, $user, false)) {
221
            return true;
222
        }
223
        if ($this->instance->reviewmode == self::REVIEWNONE) {
224
            return false;
225
        }
226
        if ($this->instance->reviewmode == self::REVIEWCOMPLETION) {
227
            return true;
228
        }
229
        return false;
230
 
231
    }
232
 
233
    /**
234
     * Return a relation of userid and the valid attempt's scaled score.
235
     *
236
     * The returned elements contain a record
237
     * of userid, scaled value, attemptid and timemodified. In case the grading method is "GRADEAVERAGEATTEMPT"
238
     * the attemptid will be zero. In case that tracking is disabled or grading method is "GRADEMANUAL"
239
     * the method will return null.
240
     *
241
     * @param int $userid a specific userid or 0 for all user attempts.
242
     * @return array|null of userid, scaled value and, if exists, the attempt id
243
     */
244
    public function get_users_scaled_score(int $userid = 0): ?array {
245
        global $DB;
246
 
247
        $scaled = [];
248
        if (!$this->instance->enabletracking) {
249
            return null;
250
        }
251
 
252
        if ($this->instance->grademethod == self::GRADEMANUAL) {
253
            return null;
254
        }
255
 
256
        $sql = '';
257
 
258
        // General filter.
259
        $where = 'a.h5pactivityid = :h5pactivityid';
260
        $params['h5pactivityid'] = $this->instance->id;
261
 
262
        if ($userid) {
263
            $where .= ' AND a.userid = :userid';
264
            $params['userid'] = $userid;
265
        }
266
 
267
        // Average grading needs aggregation query.
268
        if ($this->instance->grademethod == self::GRADEAVERAGEATTEMPT) {
269
            $sql = "SELECT a.userid, AVG(a.scaled) AS scaled, 0 AS attemptid, MAX(timemodified) AS timemodified
270
                      FROM {h5pactivity_attempts} a
271
                     WHERE $where AND a.completion = 1
272
                  GROUP BY a.userid";
273
        }
274
 
275
        if (empty($sql)) {
276
            // Decide which attempt is used for the calculation.
277
            $condition = [
278
                self::GRADEHIGHESTATTEMPT => "a.scaled < b.scaled",
279
                self::GRADELASTATTEMPT => "a.attempt < b.attempt",
280
                self::GRADEFIRSTATTEMPT => "a.attempt > b.attempt",
281
            ];
282
            $join = $condition[$this->instance->grademethod] ?? $condition[self::GRADEHIGHESTATTEMPT];
283
 
284
            $sql = "SELECT a.userid, a.scaled, MAX(a.id) AS attemptid, MAX(a.timemodified) AS timemodified
285
                      FROM {h5pactivity_attempts} a
286
                 LEFT JOIN {h5pactivity_attempts} b ON a.h5pactivityid = b.h5pactivityid
287
                           AND a.userid = b.userid AND b.completion = 1
288
                           AND $join
289
                     WHERE $where AND b.id IS NULL AND a.completion = 1
290
                  GROUP BY a.userid, a.scaled";
291
        }
292
 
293
        return $DB->get_records_sql($sql, $params);
294
    }
295
 
296
    /**
297
     * Count the activity completed attempts.
298
     *
299
     * If no user is provided the method will count all active users attempts.
300
     * Check get_active_users_join PHPdoc to a more detailed description of "active users".
301
     *
302
     * @param int|null $userid optional user id (default null)
303
     * @return int the total amount of attempts
304
     */
305
    public function count_attempts(int $userid = null): int {
306
        global $DB;
307
 
308
        // Counting records is enough for one user.
309
        if ($userid) {
310
            $params['userid'] = $userid;
311
            $params = [
312
                'h5pactivityid' => $this->instance->id,
313
                'userid' => $userid,
314
                'completion' => 1,
315
            ];
316
            return $DB->count_records('h5pactivity_attempts', $params);
317
        }
318
 
319
        $usersjoin = $this->get_active_users_join();
320
 
321
        // Final SQL.
322
        return $DB->count_records_sql(
323
            "SELECT COUNT(*)
324
               FROM {user} u $usersjoin->joins
325
              WHERE $usersjoin->wheres",
326
            array_merge($usersjoin->params)
327
        );
328
    }
329
 
330
    /**
331
     * Return the join to collect all activity active users.
332
     *
333
     * The concept of active user is relative to the activity permissions. All users with
334
     * "mod/h5pactivity:view" are potential users but those with "mod/h5pactivity:reviewattempts"
335
     * are evaluators and they don't count as valid submitters.
336
     *
337
     * Note that, in general, the active list has the same effect as checking for "mod/h5pactivity:submit"
338
     * but submit capability cannot be used because is a write capability and does not apply to frozen contexts.
339
     *
340
     * @since Moodle 3.11
341
     * @param bool $allpotentialusers if true, the join will return all active users, not only the ones with attempts.
342
     * @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
343
     * @return sql_join the active users attempts join
344
     */
345
    public function get_active_users_join(bool $allpotentialusers = false, $currentgroup = false): sql_join {
346
 
347
        // Only valid users counts. By default, all users with submit capability are considered potential ones.
348
        $context = $this->get_context();
349
        $coursemodule = $this->get_coursemodule();
350
 
351
        // Ensure user can view users from all groups.
352
        if ($currentgroup === 0 && $coursemodule->effectivegroupmode == SEPARATEGROUPS
353
                && !has_capability('moodle/site:accessallgroups', $context)) {
354
 
355
            return new sql_join('', '1=2', [], true);
356
        }
357
 
358
        // We want to present all potential users.
359
        $capjoin = get_enrolled_with_capabilities_join($context, '', 'mod/h5pactivity:view', $currentgroup);
360
 
361
        if ($capjoin->cannotmatchanyrows) {
362
            return $capjoin;
363
        }
364
 
365
        // But excluding all reviewattempts users converting a capabilities join into left join.
366
        $reviewersjoin = get_with_capability_join($context, 'mod/h5pactivity:reviewattempts', 'u.id');
367
        if ($reviewersjoin->cannotmatchanyrows) {
368
            return $capjoin;
369
        }
370
 
371
        $capjoin = new sql_join(
372
            $capjoin->joins . "\n LEFT " . str_replace('ra', 'reviewer', $reviewersjoin->joins),
373
            $capjoin->wheres . " AND reviewer.userid IS NULL",
374
            $capjoin->params
375
        );
376
 
377
        if ($allpotentialusers) {
378
            return $capjoin;
379
        }
380
 
381
        // Add attempts join.
382
        $where = "ha.h5pactivityid = :h5pactivityid AND ha.completion = :completion";
383
        $params = [
384
            'h5pactivityid' => $this->instance->id,
385
            'completion' => 1,
386
        ];
387
 
388
        return new sql_join(
389
            $capjoin->joins . "\n JOIN {h5pactivity_attempts} ha ON ha.userid = u.id",
390
            $capjoin->wheres . " AND $where",
391
            array_merge($capjoin->params, $params)
392
        );
393
    }
394
 
395
    /**
396
     * Return an array of all users and it's total attempts.
397
     *
398
     * Note: this funciton only returns the list of users with attempts,
399
     * it does not check all participants.
400
     *
401
     * @return array indexed count userid => total number of attempts
402
     */
403
    public function count_users_attempts(): array {
404
        global $DB;
405
        $params = [
406
            'h5pactivityid' => $this->instance->id,
407
        ];
408
        $sql = "SELECT userid, count(*)
409
                  FROM {h5pactivity_attempts}
410
                 WHERE h5pactivityid = :h5pactivityid
411
                 GROUP BY userid";
412
        return $DB->get_records_sql_menu($sql, $params);
413
    }
414
 
415
    /**
416
     * Return the current context.
417
     *
418
     * @return context_module
419
     */
420
    public function get_context(): context_module {
421
        return $this->context;
422
    }
423
 
424
    /**
425
     * Return the current instance.
426
     *
427
     * @return stdClass the instance record
428
     */
429
    public function get_instance(): stdClass {
430
        return $this->instance;
431
    }
432
 
433
    /**
434
     * Return the current cm_info.
435
     *
436
     * @return cm_info the course module
437
     */
438
    public function get_coursemodule(): cm_info {
439
        return $this->coursemodule;
440
    }
441
 
442
    /**
443
     * Return the specific grader object for this activity.
444
     *
445
     * @return grader
446
     */
447
    public function get_grader(): grader {
448
        $idnumber = $this->coursemodule->idnumber ?? '';
449
        return new grader($this->instance, $idnumber);
450
    }
451
 
452
    /**
453
     * Return the suitable report to show the attempts.
454
     *
455
     * This method controls the access to the different reports
456
     * the activity have.
457
     *
458
     * @param int $userid an opional userid to show
459
     * @param int $attemptid an optional $attemptid to show
460
     * @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
461
     * @return report|null available report (or null if no report available)
462
     */
463
    public function get_report(int $userid = null, int $attemptid = null, $currentgroup = false): ?report {
464
        global $USER, $CFG;
465
 
466
        require_once("{$CFG->dirroot}/user/lib.php");
467
 
468
        // If tracking is disabled, no reports are available.
469
        if (!$this->instance->enabletracking) {
470
            return null;
471
        }
472
 
473
        $attempt = null;
474
        if ($attemptid) {
475
            $attempt = $this->get_attempt($attemptid);
476
            if (!$attempt) {
477
                return null;
478
            }
479
            // If we have and attempt we can ignore the provided $userid.
480
            $userid = $attempt->get_userid();
481
        }
482
 
483
        if ($this->can_view_all_attempts()) {
484
            $user = core_user::get_user($userid);
485
 
486
            // Ensure user can view the attempt of specific userid, respecting access checks.
487
            if ($user && $user->id != $USER->id) {
488
                $course = get_course($this->coursemodule->course);
489
                if (!groups_user_groups_visible($course, $user->id, $this->coursemodule)) {
490
                    return null;
491
                }
492
            }
493
        } else if ($this->can_view_own_attempts()) {
494
            $user = core_user::get_user($USER->id);
495
            if ($userid && $user->id != $userid) {
496
                return null;
497
            }
498
        } else {
499
            return null;
500
        }
501
 
502
        // Only enrolled users has reports.
503
        if ($user && !is_enrolled($this->context, $user, 'mod/h5pactivity:view')) {
504
            return null;
505
        }
506
 
507
        // Create the proper report.
508
        if ($user && $attempt) {
509
            return new results($this, $user, $attempt);
510
        } else if ($user) {
511
            return new attempts($this, $user);
512
        }
513
        return new participants($this, $currentgroup);
514
    }
515
 
516
    /**
517
     * Return a single attempt.
518
     *
519
     * @param int $attemptid the attempt id
520
     * @return attempt
521
     */
522
    public function get_attempt(int $attemptid): ?attempt {
523
        global $DB;
524
        $record = $DB->get_record('h5pactivity_attempts', [
525
            'id' => $attemptid,
526
            'h5pactivityid' => $this->instance->id,
527
        ]);
528
        if (!$record) {
529
            return null;
530
        }
531
        return new attempt($record);
532
    }
533
 
534
    /**
535
     * Return an array of all user attempts (including incompleted)
536
     *
537
     * @param int $userid the user id
538
     * @return attempt[]
539
     */
540
    public function get_user_attempts(int $userid): array {
541
        global $DB;
542
        $records = $DB->get_records(
543
            'h5pactivity_attempts',
544
            ['userid' => $userid, 'h5pactivityid' => $this->instance->id],
545
            'id ASC'
546
        );
547
        if (!$records) {
548
            return [];
549
        }
550
        $result = [];
551
        foreach ($records as $record) {
552
            $result[] = new attempt($record);
553
        }
554
        return $result;
555
    }
556
 
557
    /**
558
     * Trigger module viewed event and set the module viewed for completion.
559
     *
560
     * @param stdClass $course course object
561
     * @return void
562
     */
563
    public function set_module_viewed(stdClass $course): void {
564
        global $CFG;
565
        require_once($CFG->libdir . '/completionlib.php');
566
 
567
        // Trigger module viewed event.
568
        $event = course_module_viewed::create([
569
            'objectid' => $this->instance->id,
570
            'context' => $this->context
571
        ]);
572
        $event->add_record_snapshot('course', $course);
573
        $event->add_record_snapshot('course_modules', $this->coursemodule);
574
        $event->add_record_snapshot('h5pactivity', $this->instance);
575
        $event->trigger();
576
 
577
        // Completion.
578
        $completion = new \completion_info($course);
579
        $completion->set_module_viewed($this->coursemodule);
580
    }
581
}