Feature: Prevent students from sharing device while self-marking.
[moodle-mod_attendance.git] / classes / import / sessions.php
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 * Import attendance sessions class.
19 *
20 * @package mod_attendance
21 * @author Chris Wharton <chriswharton@catalyst.net.nz>
22 * @copyright 2017 Catalyst IT
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26 namespace mod_attendance\import;
27
28 defined('MOODLE_INTERNAL') || die();
29
30 use csv_import_reader;
31 use mod_attendance_notifyqueue;
32 use mod_attendance_structure;
33 use stdClass;
34
35 /**
36 * Import attendance sessions.
37 *
38 * @package mod_attendance
39 * @author Chris Wharton <chriswharton@catalyst.net.nz>
40 * @copyright 2017 Catalyst IT
41 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42 */
43 class sessions {
44
45 /** @var string $error The errors message from reading the xml */
46 protected $error = '';
47
48 /** @var array $sessions The sessions info */
49 protected $sessions = array();
50
51 /** @var array $mappings The mappings info */
52 protected $mappings = array();
53
54 /** @var int The id of the csv import */
55 protected $importid = 0;
56
57 /** @var csv_import_reader|null $importer */
58 protected $importer = null;
59
60 /** @var array $foundheaders */
61 protected $foundheaders = array();
62
63 /** @var bool $useprogressbar Control whether importing should use progress bars or not. */
64 protected $useprogressbar = false;
65
66 /** @var \core\progress\display_if_slow|null $progress The progress bar instance. */
67 protected $progress = null;
68
69 /**
70 * Store an error message for display later
71 *
72 * @param string $msg
73 */
74 public function fail($msg) {
75 $this->error = $msg;
76 return false;
77 }
78
79 /**
80 * Get the CSV import id
81 *
82 * @return string The import id.
83 */
84 public function get_importid() {
85 return $this->importid;
86 }
87
88 /**
89 * Get the list of headers required for import.
90 *
91 * @return array The headers (lang strings)
92 */
93 public static function list_required_headers() {
94 return array(
95 get_string('course', 'attendance'),
96 get_string('groups', 'attendance'),
97 get_string('sessiondate', 'attendance'),
98 get_string('from', 'attendance'),
99 get_string('to', 'attendance'),
100 get_string('description', 'attendance'),
101 get_string('repeaton', 'attendance'),
102 get_string('repeatevery', 'attendance'),
103 get_string('repeatuntil', 'attendance'),
104 get_string('studentscanmark', 'attendance'),
105 get_string('passwordgrp', 'attendance'),
106 get_string('randompassword', 'attendance'),
107 get_string('subnet', 'attendance'),
108 get_string('automark', 'attendance'),
109 get_string('autoassignstatus', 'attendance'),
110 get_string('absenteereport', 'attendance'),
111 get_string('preventsharedip', 'attendance'),
112 get_string('preventsharediptime', 'attendance')
113 );
114 }
115
116 /**
117 * Get the list of headers found in the import.
118 *
119 * @return array The found headers (names from import)
120 */
121 public function list_found_headers() {
122 return $this->foundheaders;
123 }
124
125 /**
126 * Read the data from the mapping form.
127 *
128 * @param array $data The mapping data.
129 */
130 protected function read_mapping_data($data) {
131 if ($data) {
132 return array(
133 'course' => $data->header0,
134 'groups' => $data->header1,
135 'sessiondate' => $data->header2,
136 'from' => $data->header3,
137 'to' => $data->header4,
138 'description' => $data->header5,
139 'repeaton' => $data->header6,
140 'repeatevery' => $data->header7,
141 'repeatuntil' => $data->header8,
142 'studentscanmark' => $data->header9,
143 'passwordgrp' => $data->header10,
144 'randompassword' => $data->header11,
145 'subnet' => $data->header12,
146 'automark' => $data->header13,
147 'autoassignstatus' => $data->header14,
148 'absenteereport' => $data->header15,
149 'preventsharedip' => $data->header16,
150 'preventsharediptime' => $data->header17,
151 );
152 } else {
153 return array(
154 'course' => 0,
155 'groups' => 1,
156 'sessiondate' => 2,
157 'from' => 3,
158 'to' => 4,
159 'description' => 5,
160 'repeaton' => 6,
161 'repeatevery' => 7,
162 'repeatuntil' => 8,
163 'studentscanmark' => 9,
164 'passwordgrp' => 10,
165 'randompassword' => 11,
166 'subnet' => 12,
167 'automark' => 13,
168 'autoassignstatus' => 14,
169 'absenteereport' => 15,
170 'preventsharedip' => 16,
171 'preventsharediptime' => 17
172 );
173 }
174 }
175
176 /**
177 * Get the a column from the imported data.
178 *
179 * @param array $row The imported raw row
180 * @param int $index The column index we want
181 * @return string The column data.
182 */
183 protected function get_column_data($row, $index) {
184 if ($index < 0) {
185 return '';
186 }
187 return isset($row[$index]) ? $row[$index] : '';
188 }
189
190 /**
191 * Constructor - parses the raw text for sanity.
192 *
193 * @param string $text The raw csv text.
194 * @param string $encoding The encoding of the csv file.
195 * @param string $delimiter The specified delimiter for the file.
196 * @param string $importid The id of the csv import.
197 * @param array $mappingdata The mapping data from the import form.
198 * @param bool $useprogressbar Whether progress bar should be displayed, to avoid html output on CLI.
199 */
200 public function __construct($text = null, $encoding = null, $delimiter = null, $importid = 0,
201 $mappingdata = null, $useprogressbar = false) {
202 global $CFG;
203
204 require_once($CFG->libdir . '/csvlib.class.php');
205
206 $pluginconfig = get_config('attendance');
207
208 $type = 'sessions';
209
210 if (! $importid) {
211 if ($text === null) {
212 return;
213 }
214 $this->importid = csv_import_reader::get_new_iid($type);
215
216 $this->importer = new csv_import_reader($this->importid, $type);
217
218 if (! $this->importer->load_csv_content($text, $encoding, $delimiter)) {
219 $this->fail(get_string('invalidimportfile', 'attendance'));
220 $this->importer->cleanup();
221 return;
222 }
223 } else {
224 $this->importid = $importid;
225
226 $this->importer = new csv_import_reader($this->importid, $type);
227 }
228
229 if (! $this->importer->init()) {
230 $this->fail(get_string('invalidimportfile', 'attendance'));
231 $this->importer->cleanup();
232 return;
233 }
234
235 $this->foundheaders = $this->importer->get_columns();
236 $this->useprogressbar = $useprogressbar;
237 $domainid = 1;
238
239 $sessions = array();
240
241 while ($row = $this->importer->next()) {
242 // This structure mimics what the UI form returns.
243 $mapping = $this->read_mapping_data($mappingdata);
244
245 $session = new stdClass();
246 $session->course = $this->get_column_data($row, $mapping['course']);
247 if (empty($session->course)) {
248 \mod_attendance_notifyqueue::notify_problem(get_string('error:sessioncourseinvalid', 'attendance'));
249 continue;
250 }
251
252 // Handle multiple group assignments per session. Expect semicolon separated group names.
253 $groups = $this->get_column_data($row, $mapping['groups']);
254 if (! empty($groups)) {
255 $session->groups = explode(';', $groups);
256 $session->sessiontype = \mod_attendance_structure::SESSION_GROUP;
257 } else {
258 $session->sessiontype = \mod_attendance_structure::SESSION_COMMON;
259 }
260
261 // Expect standardised date format, eg YYYY-MM-DD.
262 $sessiondate = strtotime($this->get_column_data($row, $mapping['sessiondate']));
263 if ($sessiondate === false) {
264 \mod_attendance_notifyqueue::notify_problem(get_string('error:sessiondateinvalid', 'attendance'));
265 continue;
266 }
267 $session->sessiondate = $sessiondate;
268
269 // Expect standardised time format, eg HH:MM.
270 $from = $this->get_column_data($row, $mapping['from']);
271 if (empty($from)) {
272 \mod_attendance_notifyqueue::notify_problem(get_string('error:sessionstartinvalid', 'attendance'));
273 continue;
274 }
275 $from = explode(':', $from);
276 $session->sestime['starthour'] = $from[0];
277 $session->sestime['startminute'] = $from[1];
278
279 $to = $this->get_column_data($row, $mapping['to']);
280 if (empty($to)) {
281 \mod_attendance_notifyqueue::notify_problem(get_string('error:sessionendinvalid', 'attendance'));
282 continue;
283 }
284 $to = explode(':', $to);
285 $session->sestime['endhour'] = $to[0];
286 $session->sestime['endminute'] = $to[1];
287
288 // Wrap the plain text description in html tags.
289 $session->sdescription['text'] = '<p>' . $this->get_column_data($row, $mapping['description']) . '</p>';
290 $session->sdescription['format'] = FORMAT_HTML;
291 $session->sdescription['itemid'] = 0;
292 $session->passwordgrp = $this->get_column_data($row, $mapping['passwordgrp']);
293 $session->subnet = $this->get_column_data($row, $mapping['subnet']);
294 // Set session subnet restriction. Use the default activity level subnet if there isn't one set for this session.
295 if (empty($session->subnet)) {
296 $session->usedefaultsubnet = '1';
297 } else {
298 $session->usedefaultsubnet = '';
299 }
300
301 if ($mapping['studentscanmark'] == -1) {
302 $session->studentscanmark = $pluginconfig->studentscanmark_default;
303 } else {
304 $session->studentscanmark = $this->get_column_data($row, $mapping['studentscanmark']);
305 }
306 if ($mapping['randompassword'] == -1) {
307 $session->randompassword = $pluginconfig->randompassword_default;
308 } else {
309 $session->randompassword = $this->get_column_data($row, $mapping['randompassword']);
310 }
311 if ($mapping['automark'] == -1) {
312 $session->automark = $pluginconfig->automark_default;
313 } else {
314 $session->automark = $this->get_column_data($row, $mapping['automark']);
315 }
316 if ($mapping['autoassignstatus'] == -1) {
317 $session->autoassignstatus = $pluginconfig->autoassignstatus;
318 } else {
319 $session->autoassignstatus = $this->get_column_data($row, $mapping['autoassignstatus']);
320 }
321 if ($mapping['absenteereport'] == -1) {
322 $session->absenteereport = $pluginconfig->absenteereport_default;
323 } else {
324 $session->absenteereport = $this->get_column_data($row, $mapping['absenteereport']);
325 }
326 if ($mapping['preventsharedip'] == -1) {
327 $session->preventsharedip = $pluginconfig->preventsharedip;
328 } else {
329 $session->preventsharedip = $this->get_column_data($row, $mapping['preventsharedip']);
330 }
331 if ($mapping['preventsharediptime'] == -1) {
332 $session->preventsharediptime = $pluginconfig->preventsharediptime;
333 } else {
334 $session->preventsharediptime = $this->get_column_data($row, $mapping['preventsharediptime']);
335 }
336
337 $session->statusset = 0;
338
339 $sessions[] = $session;
340 }
341 $this->sessions = $sessions;
342
343 $this->importer->close();
344 if ($this->sessions == null) {
345 $this->fail(get_string('invalidimportfile', 'attendance'));
346 return;
347 } else {
348 // We are calling from browser, display progress bar.
349 if ($this->useprogressbar === true) {
350 $this->progress = new \core\progress\display_if_slow(get_string('processingfile', 'attendance'));
351 $this->progress->start_html();
352 } else {
353 // Avoid html output on CLI scripts.
354 $this->progress = new \core\progress\none();
355 }
356 $this->progress->start_progress('', count($this->sessions));
357 raise_memory_limit(MEMORY_EXTRA);
358 $this->progress->end_progress();
359 }
360 }
361
362 /**
363 * Get parse errors.
364 *
365 * @return array of errors from parsing the xml.
366 */
367 public function get_error() {
368 return $this->error;
369 }
370
371 /**
372 * Create sessions using the CSV data.
373 *
374 * @return void
375 */
376 public function import() {
377 global $DB;
378
379 // Count of sessions added.
380 $okcount = 0;
381
382 foreach ($this->sessions as $session) {
383 $groupids = array();
384 // Check course shortname matches.
385 if ($DB->record_exists('course', array(
386 'shortname' => $session->course
387 ))) {
388 // Get course.
389 $course = $DB->get_record('course', array(
390 'shortname' => $session->course
391 ), '*', MUST_EXIST);
392
393 // Check course has activities.
394 if ($DB->record_exists('attendance', array(
395 'course' => $course->id
396 ))) {
397 // Translate group names to group IDs. They are unique per course.
398 if ($session->sessiontype === \mod_attendance_structure::SESSION_GROUP) {
399 foreach ($session->groups as $groupname) {
400 $gid = groups_get_group_by_name($course->id, $groupname);
401 if ($gid === false) {
402 \mod_attendance_notifyqueue::notify_problem(get_string('sessionunknowngroup',
403 'attendance', $groupname));
404 } else {
405 $groupids[] = $gid;
406 }
407 }
408 $session->groups = $groupids;
409 }
410
411 // Get activities in course.
412 $activities = $DB->get_recordset('attendance', array(
413 'course' => $course->id
414 ), 'id', 'id');
415
416 foreach ($activities as $activity) {
417 // Build the session data.
418 $cm = get_coursemodule_from_instance('attendance', $activity->id, $course->id);
419 if (!empty($cm->deletioninprogress)) {
420 // Don't do anything if this attendance is in recycle bin.
421 continue;
422 }
423 $att = new mod_attendance_structure($activity, $cm, $course);
424 $sessions = attendance_construct_sessions_data_for_add($session, $att);
425
426 foreach ($sessions as $index => $sess) {
427 // Check for duplicate sessions.
428 if ($this->session_exists($sess)) {
429 mod_attendance_notifyqueue::notify_message(get_string('sessionduplicate', 'attendance', (array(
430 'course' => $session->course,
431 'activity' => $cm->name
432 ))));
433 unset($sessions[$index]);
434 } else {
435 $okcount ++;
436 }
437 }
438 if (! empty($sessions)) {
439 $att->add_sessions($sessions);
440 }
441 }
442 $activities->close();
443 } else {
444 mod_attendance_notifyqueue::notify_problem(get_string('error:coursehasnoattendance',
445 'attendance', $session->course));
446 }
447 } else {
448 mod_attendance_notifyqueue::notify_problem(get_string('error:coursenotfound', 'attendance', $session->course));
449 }
450 }
451
452 $message = get_string('sessionsgenerated', 'attendance', $okcount);
453 if ($okcount < 1) {
454 mod_attendance_notifyqueue::notify_message($message);
455 } else {
456 mod_attendance_notifyqueue::notify_success($message);
457 }
458
459 // Trigger a sessions imported event.
460 $event = \mod_attendance\event\sessions_imported::create(array(
461 'objectid' => 0,
462 'context' => \context_system::instance(),
463 'other' => array(
464 'count' => $okcount
465 )
466 ));
467
468 $event->trigger();
469 }
470
471 /**
472 * Check if an identical session exists.
473 *
474 * @param stdClass $session
475 * @return boolean
476 */
477 private function session_exists(stdClass $session) {
478 global $DB;
479
480 $check = clone $session;
481
482 // Remove the properties that aren't useful to check.
483 unset($check->description);
484 unset($check->descriptionitemid);
485 unset($check->timemodified);
486 $check = (array) $check;
487
488 if ($DB->record_exists('attendance_sessions', $check)) {
489 return true;
490 }
491 return false;
492 }
493 }