Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 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
namespace mod_quiz;
18
 
19
use stdClass;
20
 
21
/**
22
 * Helper for sending quiz related notifications.
23
 *
24
 * @package    mod_quiz
25
 * @copyright  2024 David Woloszyn <david.woloszyn@moodle.com>
26
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27
 */
28
class notification_helper {
29
    /**
30
     * @var int Default date range of 48 hours.
31
     */
32
    private const DEFAULT_DATE_RANGE = (DAYSECS * 2);
33
 
34
    /**
35
     * Get all quizzes that have an approaching open date (includes users and groups with open date overrides).
36
     *
37
     * @return \moodle_recordset Returns the matching quiz records.
38
     */
39
    public static function get_quizzes_within_date_range(): \moodle_recordset {
40
        global $DB;
41
 
42
        $timenow = self::get_time_now();
43
        $futuretime = self::get_future_time();
44
 
45
        $sql = "SELECT DISTINCT q.id
46
                  FROM {quiz} q
47
                  JOIN {course} c ON q.course = c.id
48
                  JOIN {course_modules} cm ON q.id = cm.instance
49
                  JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
50
             LEFT JOIN {quiz_overrides} qo ON q.id = qo.quiz
51
                 WHERE (q.timeopen < :futuretime OR qo.timeopen < :qo_futuretime)
52
                   AND (q.timeopen > :timenow OR qo.timeopen > :qo_timenow)
53
                   AND cm.visible = 1
54
                   AND c.visible = 1";
55
 
56
        $params = [
57
            'timenow' => $timenow,
58
            'futuretime' => $futuretime,
59
            'qo_timenow' => $timenow,
60
            'qo_futuretime' => $futuretime,
61
            'modulename' => 'quiz',
62
        ];
63
 
64
        return $DB->get_recordset_sql($sql, $params);
65
    }
66
 
67
    /**
68
     * Get all users that have an approaching open date within a quiz.
69
     *
70
     * @param int $quizid The quiz id.
71
     * @return array The users after all filtering has been applied.
72
     */
73
    public static function get_users_within_quiz(int $quizid): array {
74
        // Get quiz data.
75
        $quizobj = quiz_settings::create($quizid);
76
        $quiz = $quizobj->get_quiz();
77
 
78
        // Get our users.
79
        $users = get_enrolled_users(
80
            context: \context_module::instance($quizobj->get_cm()->id),
81
            withcapability: 'mod/quiz:attempt',
82
            userfields: 'u.id, u.firstname, u.suspended, u.auth',
83
        );
84
 
85
        // Filter a list of users who meet the availability conditions.
86
        $info = new \core_availability\info_module($quizobj->get_cm());
87
        $users = $info->filter_user_list($users);
88
 
89
        // Check for any override dates.
90
        $overrides = $quizobj->get_override_manager()->get_all_overrides();
91
 
92
        foreach ($users as $key => $user) {
93
            if ($user->suspended || ($user->auth == 'nologin')) {
94
                unset($users[$key]);
95
                continue;
96
            }
97
            // Time open and time close dates can be user specific with an override.
98
            // We begin by assuming it is the same as recorded in the quiz.
99
            $user->timeopen = $quiz->timeopen;
100
            $user->timeclose = $quiz->timeclose;
101
 
102
            // Set the override type to 'none' to begin with.
103
            $user->overridetype = 'none';
104
 
105
            // Update this user with any applicable override dates.
106
            if (!empty($overrides)) {
107
                self::update_user_with_date_overrides($overrides, $user);
108
            }
109
 
110
            // If the 'timeopen' date has no value, even after overriding, unset this user.
111
            if (empty($quiz->timeopen) && empty($user->timeopen)) {
112
                unset($users[$key]);
113
                continue;
114
            }
115
 
116
            // Check the date is within our range.
117
            // We have to check here because we don't know if this quiz was selected because it only had users with overrides.
118
            if (!self::is_time_within_range($user->timeopen)) {
119
                unset($users[$key]);
120
                continue;
121
            }
122
 
123
            // Check if the user has already received this notification.
124
            $match = [
125
                'quizid' => strval($quizid),
126
                'timeopen' => $user->timeopen,
127
                'overridetype' => $user->overridetype,
128
            ];
129
 
130
            if (self::has_user_been_sent_a_notification_already($user->id, json_encode($match))) {
131
                unset($users[$key]);
132
            }
133
        }
134
 
135
        return $users;
136
    }
137
 
138
    /**
139
     * Send the notification to the user.
140
     *
141
     * @param stdClass $user The user's custom data.
142
     */
143
    public static function send_notification_to_user(stdClass $user): void {
144
        // Check if the user has submitted already.
145
        if (self::has_user_attempted($user)) {
146
            return;
147
        }
148
 
149
        // Get quiz data.
150
        $quizobj = quiz_settings::create($user->quizid);
151
        $quiz = $quizobj->get_quiz();
152
        $url = $quizobj->view_url();
153
 
154
        $stringparams = [
155
            'firstname' => $user->firstname,
156
            'quizname' => format_string($quiz->name,
157
                options: ['context' => $quizobj->get_context(), 'escape' => false]),
158
            'coursename' => format_string($quizobj->get_course()->fullname,
159
                options: ['context' => \context_course::instance($quizobj->get_course()->id), 'escape' => false]),
160
            'timeopen' => userdate($user->timeopen),
161
            'timeclose' => !empty($user->timeclose) ? userdate($user->timeclose) : get_string('statusna'),
162
            'url' => $url,
163
        ];
164
 
165
        $messagedata = [
166
            'user' => \core_user::get_user($user->id),
167
            'url' => $url->out(false),
168
            'subject' => get_string('quizopendatesoonsubject', 'mod_quiz', $stringparams),
169
            'quizname' => $stringparams['quizname'],
170
            'html' => get_string('quizopendatesoonhtml', 'mod_quiz', $stringparams),
171
        ];
172
 
173
        // Prepare message object.
174
        $message = new \core\message\message();
175
        $message->component = 'mod_quiz';
176
        $message->name = 'quiz_open_soon';
177
        $message->userfrom = \core_user::get_noreply_user();
178
        $message->userto = $messagedata['user'];
179
        $message->subject = $messagedata['subject'];
180
        $message->fullmessageformat = FORMAT_HTML;
181
        $message->fullmessage = html_to_text($messagedata['html']);
182
        $message->fullmessagehtml = $messagedata['html'];
183
        $message->smallmessage = $messagedata['subject'];
184
        $message->notification = 1;
185
        $message->contexturl = $messagedata['url'];
186
        $message->contexturlname = $messagedata['quizname'];
187
        // Use custom data to avoid future notifications being sent again.
188
        $message->customdata = [
189
            'quizid' => $user->quizid,
190
            'timeopen' => $user->timeopen,
191
            'overridetype' => $user->overridetype,
192
        ];
193
 
194
        message_send($message);
195
    }
196
 
197
    /**
198
     * Get the time now.
199
     *
200
     * @return int The time now as a timestamp.
201
     */
202
    protected static function get_time_now(): int {
203
        return \core\di::get(\core\clock::class)->time();
204
    }
205
 
206
    /**
207
     * Get a future time that serves as the cut-off for this notification.
208
     *
209
     * @param int|null $range Amount of seconds added to the now time (optional).
210
     * @return int The time now value plus the range.
211
     */
212
    protected static function get_future_time(?int $range = null): int {
213
        $range = $range ?? self::DEFAULT_DATE_RANGE;
214
        return self::get_time_now() + $range;
215
    }
216
 
217
    /**
218
     * Check if a time is within the current time now and the future time values.
219
     *
220
     * @param int $time The timestamp to check.
221
     * @return boolean
222
     */
223
    protected static function is_time_within_range(int $time): bool {
224
        return ($time > self::get_time_now() && $time < self::get_future_time());
225
    }
226
 
227
    /**
228
     * Update user's recorded date based on the overrides.
229
     *
230
     * @param array $overrides The overrides to check.
231
     * @param stdClass $user The user records we will be updating.
232
     */
233
    protected static function update_user_with_date_overrides(array $overrides, stdClass $user): void {
234
 
235
        foreach ($overrides as $override) {
236
            // User override.
237
            if ($override->userid === $user->id) {
238
                $user->timeopen = !empty($override->timeopen) ? $override->timeopen : $user->timeopen;
239
                $user->timeclose = !empty($override->timeclose) ? $override->timeclose : $user->timeclose;
240
                $user->overridetype = 'user';
241
                // User override has precedence over group. Return here.
242
                return;
243
            }
244
            // Group override.
245
            if (!empty($override->groupid) && groups_is_member($override->groupid, $user->id)) {
246
                // If user is a member of multiple groups, and we have set this already, use the earliest date.
247
                if ($user->overridetype === 'group' && $user->timeopen < $override->timeopen) {
248
                    continue;
249
                }
250
                $user->timeopen = !empty($override->timeopen) ? $override->timeopen : $user->timeopen;
251
                $user->timeclose = !empty($override->timeclose) ? $override->timeclose : $user->timeclose;
252
                $user->overridetype = 'group';
253
            }
254
        }
255
    }
256
 
257
    /**
258
     * Check if a user has attempted this quiz already.
259
     *
260
     * @param stdClass $user The user record we will be checking.
261
     * @return bool Return true if attempt found.
262
     */
263
    protected static function has_user_attempted(stdClass $user): bool {
264
        global $DB;
265
 
266
        return $DB->record_exists('quiz_attempts', [
267
            'quiz' => $user->quizid,
268
            'userid' => $user->id,
269
        ]);
270
    }
271
 
272
    /**
273
     * Check if a user has been sent a notification already.
274
     *
275
     * @param int $userid The user id.
276
     * @param string $match The custom data string to match on.
277
     * @return bool Returns true if already sent.
278
     */
279
    protected static function has_user_been_sent_a_notification_already(int $userid, string $match): bool {
280
        global $DB;
281
 
282
        $sql = "SELECT COUNT(n.id)
283
                  FROM {notifications} n
284
                 WHERE " . $DB->sql_compare_text('n.customdata', 255) . " = " . $DB->sql_compare_text(':match', 255) . "
285
                   AND n.useridto = :userid";
286
 
287
        $result = $DB->count_records_sql($sql, [
288
            'userid' => $userid,
289
            'match' => $match,
290
        ]);
291
 
292
        return ($result > 0);
293
    }
294
}