Feature: Prevent students from sharing device while self-marking.
[moodle-mod_attendance.git] / locallib.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 * local functions and constants for module attendance
19 *
20 * @package mod_attendance
21 * @copyright 2011 Artem Andreev <andreev.artem@gmail.com>
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25 defined('MOODLE_INTERNAL') || die();
26
27 require_once($CFG->libdir . '/gradelib.php');
28 require_once(dirname(__FILE__).'/renderhelpers.php');
29
30 define('ATT_VIEW_DAYS', 1);
31 define('ATT_VIEW_WEEKS', 2);
32 define('ATT_VIEW_MONTHS', 3);
33 define('ATT_VIEW_ALLPAST', 4);
34 define('ATT_VIEW_ALL', 5);
35 define('ATT_VIEW_NOTPRESENT', 6);
36 define('ATT_VIEW_SUMMARY', 7);
37
38 define('ATT_SORT_DEFAULT', 0);
39 define('ATT_SORT_LASTNAME', 1);
40 define('ATT_SORT_FIRSTNAME', 2);
41
42 define('ATTENDANCE_AUTOMARK_DISABLED', 0);
43 define('ATTENDANCE_AUTOMARK_ALL', 1);
44 define('ATTENDANCE_AUTOMARK_CLOSE', 2);
45
46 // Max number of sessions available in the warnings set form to trigger warnings.
47 define('ATTENDANCE_MAXWARNAFTER', 100);
48
49 /**
50 * Get statuses,
51 *
52 * @param int $attid
53 * @param bool $onlyvisible
54 * @param int $statusset
55 * @return array
56 */
57 function attendance_get_statuses($attid, $onlyvisible=true, $statusset = -1) {
58 global $DB;
59
60 // Set selector.
61 $params = array('aid' => $attid);
62 $setsql = '';
63 if ($statusset >= 0) {
64 $params['statusset'] = $statusset;
65 $setsql = ' AND setnumber = :statusset ';
66 }
67
68 if ($onlyvisible) {
69 $statuses = $DB->get_records_select('attendance_statuses', "attendanceid = :aid AND visible = 1 AND deleted = 0 $setsql",
70 $params, 'setnumber ASC, grade DESC');
71 } else {
72 $statuses = $DB->get_records_select('attendance_statuses', "attendanceid = :aid AND deleted = 0 $setsql",
73 $params, 'setnumber ASC, grade DESC');
74 }
75
76 return $statuses;
77 }
78
79 /**
80 * Get the name of the status set.
81 *
82 * @param int $attid
83 * @param int $statusset
84 * @param bool $includevalues
85 * @return string
86 */
87 function attendance_get_setname($attid, $statusset, $includevalues = true) {
88 $statusname = get_string('statusset', 'mod_attendance', $statusset + 1);
89 if ($includevalues) {
90 $statuses = attendance_get_statuses($attid, true, $statusset);
91 $statusesout = array();
92 foreach ($statuses as $status) {
93 $statusesout[] = $status->acronym;
94 }
95 if ($statusesout) {
96 if (count($statusesout) > 6) {
97 $statusesout = array_slice($statusesout, 0, 6);
98 $statusesout[] = '&helip;';
99 }
100 $statusesout = implode(' ', $statusesout);
101 $statusname .= ' ('.$statusesout.')';
102 }
103 }
104
105 return $statusname;
106 }
107
108 /**
109 * Get users courses and the relevant attendances.
110 *
111 * @param int $userid
112 * @return array
113 */
114 function attendance_get_user_courses_attendances($userid) {
115 global $DB;
116
117 $usercourses = enrol_get_users_courses($userid);
118
119 list($usql, $uparams) = $DB->get_in_or_equal(array_keys($usercourses), SQL_PARAMS_NAMED, 'cid0');
120
121 $sql = "SELECT att.id as attid, att.course as courseid, course.fullname as coursefullname,
122 course.startdate as coursestartdate, att.name as attname, att.grade as attgrade
123 FROM {attendance} att
124 JOIN {course} course
125 ON att.course = course.id
126 WHERE att.course $usql
127 ORDER BY coursefullname ASC, attname ASC";
128
129 $params = array_merge($uparams, array('uid' => $userid));
130
131 return $DB->get_records_sql($sql, $params);
132 }
133
134 /**
135 * Used to calculate a fraction based on the part and total values
136 *
137 * @param float $part - part of the total value
138 * @param float $total - total value.
139 * @return float the calculated fraction.
140 */
141 function attendance_calc_fraction($part, $total) {
142 if ($total == 0) {
143 return 0;
144 } else {
145 return $part / $total;
146 }
147 }
148
149 /**
150 * Check to see if statusid in use to help prevent deletion etc.
151 *
152 * @param integer $statusid
153 */
154 function attendance_has_logs_for_status($statusid) {
155 global $DB;
156 return $DB->record_exists('attendance_log', array('statusid' => $statusid));
157 }
158
159 /**
160 * Helper function to add sessiondate_selector to add/update forms.
161 *
162 * @param MoodleQuickForm $mform
163 */
164 function attendance_form_sessiondate_selector (MoodleQuickForm $mform) {
165
166 $mform->addElement('date_selector', 'sessiondate', get_string('sessiondate', 'attendance'));
167
168 for ($i = 0; $i <= 23; $i++) {
169 $hours[$i] = sprintf("%02d", $i);
170 }
171 for ($i = 0; $i < 60; $i += 5) {
172 $minutes[$i] = sprintf("%02d", $i);
173 }
174
175 $sesendtime = array();
176 $sesendtime[] =& $mform->createElement('static', 'from', '', get_string('from', 'attendance'));
177 $sesendtime[] =& $mform->createElement('select', 'starthour', get_string('hour', 'form'), $hours, false, true);
178 $sesendtime[] =& $mform->createElement('select', 'startminute', get_string('minute', 'form'), $minutes, false, true);
179 $sesendtime[] =& $mform->createElement('static', 'to', '', get_string('to', 'attendance'));
180 $sesendtime[] =& $mform->createElement('select', 'endhour', get_string('hour', 'form'), $hours, false, true);
181 $sesendtime[] =& $mform->createElement('select', 'endminute', get_string('minute', 'form'), $minutes, false, true);
182 $mform->addGroup($sesendtime, 'sestime', get_string('time', 'attendance'), array(' '), true);
183 }
184
185 /**
186 * Count the number of status sets that exist for this instance.
187 *
188 * @param int $attendanceid
189 * @return int
190 */
191 function attendance_get_max_statusset($attendanceid) {
192 global $DB;
193
194 $max = $DB->get_field_sql('SELECT MAX(setnumber) FROM {attendance_statuses} WHERE attendanceid = ? AND deleted = 0',
195 array($attendanceid));
196 if ($max) {
197 return $max;
198 }
199 return 0;
200 }
201
202 /**
203 * Returns the maxpoints for each statusset
204 *
205 * @param array $statuses
206 * @return array
207 */
208 function attendance_get_statusset_maxpoints($statuses) {
209 $statussetmaxpoints = array();
210 foreach ($statuses as $st) {
211 if (!isset($statussetmaxpoints[$st->setnumber])) {
212 $statussetmaxpoints[$st->setnumber] = $st->grade;
213 }
214 }
215 return $statussetmaxpoints;
216 }
217
218 /**
219 * Update user grades
220 *
221 * @param mod_attendance_structure|stdClass $attendance
222 * @param array $userids
223 */
224 function attendance_update_users_grade($attendance, $userids=array()) {
225 global $DB;
226
227 if (empty($attendance->grade)) {
228 return false;
229 }
230
231 list($course, $cm) = get_course_and_cm_from_instance($attendance->id, 'attendance');
232
233 $summary = new mod_attendance_summary($attendance->id, $userids);
234
235 if (empty($userids)) {
236 $context = context_module::instance($cm->id);
237 $userids = array_keys(get_enrolled_users($context, 'mod/attendance:canbelisted', 0, 'u.id'));
238 }
239
240 if ($attendance->grade < 0) {
241 $dbparams = array('id' => -($attendance->grade));
242 $scale = $DB->get_record('scale', $dbparams);
243 $scalearray = explode(',', $scale->scale);
244 $attendancegrade = count($scalearray);
245 } else {
246 $attendancegrade = $attendance->grade;
247 }
248
249 $grades = array();
250 foreach ($userids as $userid) {
251 $grades[$userid] = new stdClass();
252 $grades[$userid]->userid = $userid;
253
254 if ($summary->has_taken_sessions($userid)) {
255 $usersummary = $summary->get_taken_sessions_summary_for($userid);
256 $grades[$userid]->rawgrade = $usersummary->takensessionspercentage * $attendancegrade;
257 } else {
258 $grades[$userid]->rawgrade = null;
259 }
260 }
261
262 return grade_update('mod/attendance', $course->id, 'mod', 'attendance', $attendance->id, 0, $grades);
263 }
264
265 /**
266 * Add an attendance status variable
267 *
268 * @param stdClass $status
269 * @return bool
270 */
271 function attendance_add_status($status) {
272 global $DB;
273 if (empty($status->context)) {
274 $status->context = context_system::instance();
275 }
276
277 if (!empty($status->acronym) && !empty($status->description)) {
278 $status->deleted = 0;
279 $status->visible = 1;
280 $status->setunmarked = 0;
281
282 $id = $DB->insert_record('attendance_statuses', $status);
283 $status->id = $id;
284
285 $event = \mod_attendance\event\status_added::create(array(
286 'objectid' => $status->attendanceid,
287 'context' => $status->context,
288 'other' => array('acronym' => $status->acronym,
289 'description' => $status->description,
290 'grade' => $status->grade)));
291 if (!empty($status->cm)) {
292 $event->add_record_snapshot('course_modules', $status->cm);
293 }
294 $event->add_record_snapshot('attendance_statuses', $status);
295 $event->trigger();
296 return true;
297 } else {
298 return false;
299 }
300 }
301
302 /**
303 * Remove a status variable from an attendance instance
304 *
305 * @param stdClass $status
306 * @param stdClass $context
307 * @param stdClass $cm
308 */
309 function attendance_remove_status($status, $context = null, $cm = null) {
310 global $DB;
311 if (empty($context)) {
312 $context = context_system::instance();
313 }
314 $DB->set_field('attendance_statuses', 'deleted', 1, array('id' => $status->id));
315 $event = \mod_attendance\event\status_removed::create(array(
316 'objectid' => $status->id,
317 'context' => $context,
318 'other' => array(
319 'acronym' => $status->acronym,
320 'description' => $status->description
321 )));
322 if (!empty($cm)) {
323 $event->add_record_snapshot('course_modules', $cm);
324 }
325 $event->add_record_snapshot('attendance_statuses', $status);
326 $event->trigger();
327 }
328
329 /**
330 * Update status variable for a particular Attendance module instance
331 *
332 * @param stdClass $status
333 * @param string $acronym
334 * @param string $description
335 * @param int $grade
336 * @param bool $visible
337 * @param stdClass $context
338 * @param stdClass $cm
339 * @param int $studentavailability
340 * @param bool $setunmarked
341 * @return array
342 */
343 function attendance_update_status($status, $acronym, $description, $grade, $visible,
344 $context = null, $cm = null, $studentavailability = null, $setunmarked = false) {
345 global $DB;
346
347 if (empty($context)) {
348 $context = context_system::instance();
349 }
350
351 if (isset($visible)) {
352 $status->visible = $visible;
353 $updated[] = $visible ? get_string('show') : get_string('hide');
354 } else if (empty($acronym) || empty($description)) {
355 return array('acronym' => $acronym, 'description' => $description);
356 }
357
358 $updated = array();
359
360 if ($acronym) {
361 $status->acronym = $acronym;
362 $updated[] = $acronym;
363 }
364 if ($description) {
365 $status->description = $description;
366 $updated[] = $description;
367 }
368 if (isset($grade)) {
369 $status->grade = $grade;
370 $updated[] = $grade;
371 }
372 if (isset($studentavailability)) {
373 if (empty($studentavailability)) {
374 if ($studentavailability !== '0') {
375 $studentavailability = null;
376 }
377 }
378
379 $status->studentavailability = $studentavailability;
380 $updated[] = $studentavailability;
381 }
382 if ($setunmarked) {
383 $status->setunmarked = 1;
384 } else {
385 $status->setunmarked = 0;
386 }
387 $DB->update_record('attendance_statuses', $status);
388
389 $event = \mod_attendance\event\status_updated::create(array(
390 'objectid' => $status->attendanceid,
391 'context' => $context,
392 'other' => array('acronym' => $acronym, 'description' => $description, 'grade' => $grade,
393 'updated' => implode(' ', $updated))));
394 if (!empty($cm)) {
395 $event->add_record_snapshot('course_modules', $cm);
396 }
397 $event->add_record_snapshot('attendance_statuses', $status);
398 $event->trigger();
399 }
400
401 /**
402 * Similar to core random_string function but only lowercase letters.
403 * designed to make it relatively easy to provide a simple password in class.
404 *
405 * @param int $length The length of the string to be created.
406 * @return string
407 */
408 function attendance_random_string($length=6) {
409 $randombytes = random_bytes_emulate($length);
410 $pool = 'abcdefghijklmnopqrstuvwxyz';
411 $pool .= '0123456789';
412 $poollen = strlen($pool);
413 $string = '';
414 for ($i = 0; $i < $length; $i++) {
415 $rand = ord($randombytes[$i]);
416 $string .= substr($pool, ($rand % ($poollen)), 1);
417 }
418 return $string;
419 }
420
421 /**
422 * Check to see if this session is open for student marking.
423 *
424 * @param stdclass $sess the session record from attendance_sessions.
425 * @return boolean
426 */
427 function attendance_can_student_mark($sess) {
428 global $DB, $USER, $OUTPUT;
429 $canmark = false;
430 $attconfig = get_config('attendance');
431 if (!empty($attconfig->studentscanmark) && !empty($sess->studentscanmark)) {
432 if (empty($attconfig->studentscanmarksessiontime)) {
433 $canmark = true;
434 } else {
435 $duration = $sess->duration;
436 if (empty($duration)) {
437 $duration = $attconfig->studentscanmarksessiontimeend * 60;
438 }
439 if ($sess->sessdate < time() && time() < ($sess->sessdate + $duration)) {
440 $canmark = true;
441 }
442 }
443 }
444 // Check if another student has marked attendance from this IP address recently.
445 if ($canmark && !empty($sess->preventsharedip)) {
446 $time = time() - ($sess->preventsharediptime * 60);
447 $sql = 'sessionid = ? AND studentid <> ? AND timetaken > ? AND ipaddress = ?';
448 $params = array($sess->id, $USER->id, $time, getremoteaddr());
449 $record = $DB->get_record_select('attendance_log', $sql, $params);
450 if (!empty($record)) {
451 // Trigger an ip_shared event.
452 $attendanceid = $DB->get_field('attendance_sessions', 'attendanceid', array('id' => $record->sessionid));
453 $cm = get_coursemodule_from_instance('attendance', $attendanceid);
454 $event = \mod_attendance\event\session_ip_shared::create(array(
455 'objectid' => 0,
456 'context' => \context_module::instance($cm->id),
457 'other' => array(
458 'sessionid' => $record->sessionid,
459 'otheruser' => $record->studentid
460 )
461 ));
462
463 $event->trigger();
464
465 echo $OUTPUT->notification(get_string('preventsharederror', 'attendance'));
466 return false;
467 }
468 }
469 return $canmark;
470 }
471
472 /**
473 * Generate worksheet for Attendance export
474 *
475 * @param stdclass $data The data for the report
476 * @param string $filename The name of the file
477 * @param string $format excel|ods
478 *
479 */
480 function attendance_exporttotableed($data, $filename, $format) {
481 global $CFG;
482
483 if ($format === 'excel') {
484 require_once("$CFG->libdir/excellib.class.php");
485 $filename .= ".xls";
486 $workbook = new MoodleExcelWorkbook("-");
487 } else {
488 require_once("$CFG->libdir/odslib.class.php");
489 $filename .= ".ods";
490 $workbook = new MoodleODSWorkbook("-");
491 }
492 // Sending HTTP headers.
493 $workbook->send($filename);
494 // Creating the first worksheet.
495 $myxls = $workbook->add_worksheet('Attendances');
496 // Format types.
497 $formatbc = $workbook->add_format();
498 $formatbc->set_bold(1);
499
500 $myxls->write(0, 0, get_string('course'), $formatbc);
501 $myxls->write(0, 1, $data->course);
502 $myxls->write(1, 0, get_string('group'), $formatbc);
503 $myxls->write(1, 1, $data->group);
504
505 $i = 3;
506 $j = 0;
507 foreach ($data->tabhead as $cell) {
508 // Merge cells if the heading would be empty (remarks column).
509 if (empty($cell)) {
510 $myxls->merge_cells($i, $j - 1, $i, $j);
511 } else {
512 $myxls->write($i, $j, $cell, $formatbc);
513 }
514 $j++;
515 }
516 $i++;
517 $j = 0;
518 foreach ($data->table as $row) {
519 foreach ($row as $cell) {
520 $myxls->write($i, $j++, $cell);
521 }
522 $i++;
523 $j = 0;
524 }
525 $workbook->close();
526 }
527
528 /**
529 * Generate csv for Attendance export
530 *
531 * @param stdclass $data The data for the report
532 * @param string $filename The name of the file
533 *
534 */
535 function attendance_exporttocsv($data, $filename) {
536 $filename .= ".txt";
537
538 header("Content-Type: application/download\n");
539 header("Content-Disposition: attachment; filename=\"$filename\"");
540 header("Expires: 0");
541 header("Cache-Control: must-revalidate,post-check=0,pre-check=0");
542 header("Pragma: public");
543
544 echo get_string('course')."\t".$data->course."\n";
545 echo get_string('group')."\t".$data->group."\n\n";
546
547 echo implode("\t", $data->tabhead)."\n";
548 foreach ($data->table as $row) {
549 echo implode("\t", $row)."\n";
550 }
551 }
552
553 /**
554 * Get session data for form.
555 * @param stdClass $formdata moodleform - attendance form.
556 * @param mod_attendance_structure $att - used to get attendance level subnet.
557 * @return array.
558 */
559 function attendance_construct_sessions_data_for_add($formdata, mod_attendance_structure $att) {
560 global $CFG;
561
562 $sesstarttime = $formdata->sestime['starthour'] * HOURSECS + $formdata->sestime['startminute'] * MINSECS;
563 $sesendtime = $formdata->sestime['endhour'] * HOURSECS + $formdata->sestime['endminute'] * MINSECS;
564 $sessiondate = $formdata->sessiondate + $sesstarttime;
565 $duration = $sesendtime - $sesstarttime;
566 if (empty(get_config('attendance', 'enablewarnings'))) {
567 $absenteereport = get_config('attendance', 'absenteereport_default');
568 } else {
569 $absenteereport = empty($formdata->absenteereport) ? 0 : 1;
570 }
571
572 $now = time();
573
574 if (empty(get_config('attendance', 'studentscanmark'))) {
575 $formdata->studentscanmark = 0;
576 }
577
578 $sessions = array();
579 if (isset($formdata->addmultiply)) {
580 $startdate = $sessiondate;
581 $enddate = $formdata->sessionenddate + DAYSECS; // Because enddate in 0:0am.
582
583 if ($enddate < $startdate) {
584 return null;
585 }
586
587 // Getting first day of week.
588 $sdate = $startdate;
589 $dinfo = usergetdate($sdate);
590 if ($CFG->calendar_startwday === '0') { // Week start from sunday.
591 $startweek = $startdate - $dinfo['wday'] * DAYSECS; // Call new variable.
592 } else {
593 $wday = $dinfo['wday'] === 0 ? 7 : $dinfo['wday'];
594 $startweek = $startdate - ($wday - 1) * DAYSECS;
595 }
596
597 $wdaydesc = array(0 => 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat');
598
599 while ($sdate < $enddate) {
600 if ($sdate < $startweek + WEEKSECS) {
601 $dinfo = usergetdate($sdate);
602 if (isset($formdata->sdays) && array_key_exists($wdaydesc[$dinfo['wday']], $formdata->sdays)) {
603 $sess = new stdClass();
604 $sess->sessdate = make_timestamp($dinfo['year'], $dinfo['mon'], $dinfo['mday'],
605 $formdata->sestime['starthour'], $formdata->sestime['startminute']);
606 $sess->duration = $duration;
607 $sess->descriptionitemid = $formdata->sdescription['itemid'];
608 $sess->description = $formdata->sdescription['text'];
609 $sess->descriptionformat = $formdata->sdescription['format'];
610 $sess->timemodified = $now;
611 $sess->absenteereport = $absenteereport;
612 $sess->studentpassword = '';
613 if (isset($formdata->studentscanmark)) { // Students will be able to mark their own attendance.
614 $sess->studentscanmark = 1;
615 if (!empty($formdata->usedefaultsubnet)) {
616 $sess->subnet = $att->subnet;
617 } else {
618 $sess->subnet = $formdata->subnet;
619 }
620 $sess->automark = $formdata->automark;
621 if (isset($formdata->autoassignstatus)) {
622 $sess->autoassignstatus = 1;
623 }
624 $sess->automarkcompleted = 0;
625 if (!empty($formdata->randompassword)) {
626 $sess->studentpassword = attendance_random_string();
627 } else if (!empty($formdata->studentpassword)) {
628 $sess->studentpassword = $formdata->studentpassword;
629 }
630 if (!empty($formdata->preventsharedip)) {
631 $sess->preventsharedip = $formdata->preventsharedip;
632 }
633 if (!empty($formdata->preventsharediptime)) {
634 $sess->preventsharediptime = $formdata->preventsharediptime;
635 }
636 } else {
637 $sess->subnet = '';
638 $sess->automark = 0;
639 $sess->automarkcompleted = 0;
640 $sess->preventsharedip = 0;
641 $sess->preventsharediptime = '';
642 }
643 $sess->statusset = $formdata->statusset;
644
645 attendance_fill_groupid($formdata, $sessions, $sess);
646 }
647 $sdate += DAYSECS;
648 } else {
649 $startweek += WEEKSECS * $formdata->period;
650 $sdate = $startweek;
651 }
652 }
653 } else {
654 $sess = new stdClass();
655 $sess->sessdate = $sessiondate;
656 $sess->duration = $duration;
657 $sess->descriptionitemid = $formdata->sdescription['itemid'];
658 $sess->description = $formdata->sdescription['text'];
659 $sess->descriptionformat = $formdata->sdescription['format'];
660 $sess->timemodified = $now;
661 $sess->studentscanmark = 0;
662 $sess->autoassignstatus = 0;
663 $sess->subnet = '';
664 $sess->studentpassword = '';
665 $sess->automark = 0;
666 $sess->automarkcompleted = 0;
667 $sess->absenteereport = $absenteereport;
668
669 if (isset($formdata->studentscanmark) && !empty($formdata->studentscanmark)) {
670 // Students will be able to mark their own attendance.
671 $sess->studentscanmark = 1;
672 if (isset($formdata->autoassignstatus) && !empty($formdata->autoassignstatus)) {
673 $sess->autoassignstatus = 1;
674 }
675 if (!empty($formdata->randompassword)) {
676 $sess->studentpassword = attendance_random_string();
677 } else if (!empty($formdata->studentpassword)) {
678 $sess->studentpassword = $formdata->studentpassword;
679 }
680 if (!empty($formdata->usedefaultsubnet)) {
681 $sess->subnet = $att->subnet;
682 } else {
683 $sess->subnet = $formdata->subnet;
684 }
685
686 if (!empty($formdata->automark)) {
687 $sess->automark = $formdata->automark;
688 }
689 if (!empty($formdata->preventsharedip)) {
690 $sess->preventsharedip = $formdata->preventsharedip;
691 }
692 if (!empty($formdata->preventsharediptime)) {
693 $sess->preventsharediptime = $formdata->preventsharediptime;
694 }
695 }
696 $sess->statusset = $formdata->statusset;
697
698 attendance_fill_groupid($formdata, $sessions, $sess);
699 }
700
701 return $sessions;
702 }
703
704 /**
705 * Helper function for attendance_construct_sessions_data_for_add().
706 *
707 * @param stdClass $formdata
708 * @param stdClass $sessions
709 * @param stdClass $sess
710 */
711 function attendance_fill_groupid($formdata, &$sessions, $sess) {
712 if ($formdata->sessiontype == mod_attendance_structure::SESSION_COMMON) {
713 $sess = clone $sess;
714 $sess->groupid = 0;
715 $sessions[] = $sess;
716 } else {
717 foreach ($formdata->groups as $groupid) {
718 $sess = clone $sess;
719 $sess->groupid = $groupid;
720 $sessions[] = $sess;
721 }
722 }
723 }
724
725 /**
726 * Generates a summary of points for the courses selected.
727 *
728 * @param array $courseids optional list of courses to return
729 * @param string $orderby - optional order by param
730 * @return stdClass
731 */
732 function attendance_course_users_points($courseids = array(), $orderby = '') {
733 global $DB;
734
735 $where = '';
736 $params = array();
737 $where .= ' AND ats.sessdate < :enddate ';
738 $params['enddate'] = time();
739
740 $joingroup = 'LEFT JOIN {groups_members} gm ON (gm.userid = atl.studentid AND gm.groupid = ats.groupid)';
741 $where .= ' AND (ats.groupid = 0 or gm.id is NOT NULL)';
742
743 if (!empty($courseids)) {
744 list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
745 $where .= ' AND c.id ' . $insql;
746 $params = array_merge($params, $inparams);
747 }
748
749 $sql = "SELECT courseid, coursename, sum(points) / sum(maxpoints) as percentage FROM (
750 SELECT a.id, a.course as courseid, c.fullname as coursename, atl.studentid AS userid, COUNT(DISTINCT ats.id) AS numtakensessions,
751 SUM(stg.grade) AS points, SUM(stm.maxgrade) AS maxpoints
752 FROM {attendance_sessions} ats
753 JOIN {attendance} a ON a.id = ats.attendanceid
754 JOIN {course} c ON c.id = a.course
755 JOIN {attendance_log} atl ON (atl.sessionid = ats.id)
756 JOIN {attendance_statuses} stg ON (stg.id = atl.statusid AND stg.deleted = 0 AND stg.visible = 1)
757 JOIN (SELECT attendanceid, setnumber, MAX(grade) AS maxgrade
758 FROM {attendance_statuses}
759 WHERE deleted = 0
760 AND visible = 1
761 GROUP BY attendanceid, setnumber) stm
762 ON (stm.setnumber = ats.statusset AND stm.attendanceid = ats.attendanceid)
763 {$joingroup}
764 WHERE ats.sessdate >= c.startdate
765 AND ats.lasttaken != 0
766 {$where}
767 GROUP BY a.id, a.course, c.fullname, atl.studentid
768 ) p GROUP by courseid, coursename {$orderby}";
769
770 return $DB->get_records_sql($sql, $params);
771 }
772
773 /**
774 * Generates a list of users flagged absent.
775 *
776 * @param array $courseids optional list of courses to return
777 * @param string $orderby how to order results.
778 * @param bool $allfornotify get notification list for scheduled task.
779 * @return stdClass
780 */
781 function attendance_get_users_to_notify($courseids = array(), $orderby = '', $allfornotify = false) {
782 global $DB;
783
784 $joingroup = 'LEFT JOIN {groups_members} gm ON (gm.userid = atl.studentid AND gm.groupid = ats.groupid)';
785 $where = ' AND (ats.groupid = 0 or gm.id is NOT NULL)';
786 $having = '';
787 $params = array();
788
789 if (!empty($courseids)) {
790 list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
791 $where .= ' AND c.id ' . $insql;
792 $params = array_merge($params, $inparams);
793 }
794 if ($allfornotify) {
795 // Exclude warnings that have already sent the max num.
796 $having .= ' AND n.maxwarn > COUNT(DISTINCT ns.id) ';
797 }
798
799 $unames = get_all_user_name_fields(true);
800 $unames2 = get_all_user_name_fields(true, 'u');
801
802 $idfield = $DB->sql_concat('cm.id', 'atl.studentid', 'n.id');
803 $sql = "SELECT {$idfield} as uniqueid, a.id as aid, {$unames2}, a.name as aname, cm.id as cmid, c.id as courseid,
804 c.fullname as coursename, atl.studentid AS userid, n.id as notifyid, n.warningpercent, n.emailsubject,
805 n.emailcontent, n.emailcontentformat, n.emailuser, n.thirdpartyemails, n.warnafter, n.maxwarn,
806 COUNT(DISTINCT ats.id) AS numtakensessions, SUM(stg.grade) AS points, SUM(stm.maxgrade) AS maxpoints,
807 COUNT(DISTINCT ns.id) as nscount, MAX(ns.timesent) as timesent,
808 SUM(stg.grade) / SUM(stm.maxgrade) AS percent
809 FROM {attendance_sessions} ats
810 JOIN {attendance} a ON a.id = ats.attendanceid
811 JOIN {course_modules} cm ON cm.instance = a.id
812 JOIN {course} c on c.id = cm.course
813 JOIN {modules} md ON md.id = cm.module AND md.name = 'attendance'
814 JOIN {attendance_log} atl ON (atl.sessionid = ats.id)
815 JOIN {user} u ON (u.id = atl.studentid)
816 JOIN {attendance_statuses} stg ON (stg.id = atl.statusid AND stg.deleted = 0 AND stg.visible = 1)
817 JOIN {attendance_warning} n ON n.idnumber = a.id
818 LEFT JOIN {attendance_warning_done} ns ON ns.notifyid = n.id AND ns.userid = atl.studentid
819 JOIN (SELECT attendanceid, setnumber, MAX(grade) AS maxgrade
820 FROM {attendance_statuses}
821 WHERE deleted = 0
822 AND visible = 1
823 GROUP BY attendanceid, setnumber) stm
824 ON (stm.setnumber = ats.statusset AND stm.attendanceid = ats.attendanceid)
825 {$joingroup}
826 WHERE ats.absenteereport = 1 {$where}
827 GROUP BY uniqueid, a.id, a.name, a.course, c.fullname, atl.studentid, n.id, n.warningpercent,
828 n.emailsubject, n.emailcontent, n.emailcontentformat, n.warnafter, n.maxwarn,
829 n.emailuser, n.thirdpartyemails, cm.id, c.id, {$unames2}, ns.userid
830 HAVING n.warnafter <= COUNT(DISTINCT ats.id) AND n.warningpercent > ((SUM(stg.grade) / SUM(stm.maxgrade)) * 100)
831 {$having}
832 {$orderby}";
833
834 if (!$allfornotify) {
835 $idfield = $DB->sql_concat('cmid', 'userid');
836 // Only show one record per attendance for teacher reports.
837 $sql = "SELECT DISTINCT {$idfield} as id, {$unames}, aid, cmid, courseid, aname, coursename, userid,
838 numtakensessions, percent, MAX(timesent) as timesent
839 FROM ({$sql}) as m
840 GROUP BY id, aid, cmid, courseid, aname, userid, numtakensessions,
841 percent, coursename, {$unames} {$orderby}";
842 }
843
844 return $DB->get_records_sql($sql, $params);
845
846 }
847
848 /**
849 * Template variables into place in supplied email content.
850 *
851 * @param object $record db record of details
852 * @return array - the content of the fields after templating.
853 */
854 function attendance_template_variables($record) {
855 $templatevars = array(
856 '/%coursename%/' => $record->coursename,
857 '/%courseid%/' => $record->courseid,
858 '/%userfirstname%/' => $record->firstname,
859 '/%userlastname%/' => $record->lastname,
860 '/%userid%/' => $record->userid,
861 '/%warningpercent%/' => $record->warningpercent,
862 '/%attendancename%/' => $record->aname,
863 '/%cmid%/' => $record->cmid,
864 '/%numtakensessions%/' => $record->numtakensessions,
865 '/%points%/' => $record->points,
866 '/%maxpoints%/' => $record->maxpoints,
867 '/%percent%/' => $record->percent,
868 );
869 $extrauserfields = get_all_user_name_fields();
870 foreach ($extrauserfields as $extra) {
871 $templatevars['/%'.$extra.'%/'] = $record->$extra;
872 }
873 $patterns = array_keys($templatevars); // The placeholders which are to be replaced.
874 $replacements = array_values($templatevars); // The values which are to be templated in for the placeholders.
875 // Array to describe which fields in reengagement object should have a template replacement.
876 $replacementfields = array('emailsubject', 'emailcontent');
877
878 // Replace %variable% with relevant value everywhere it occurs in reengagement->field.
879 foreach ($replacementfields as $field) {
880 $record->$field = preg_replace($patterns, $replacements, $record->$field);
881 }
882 return $record;
883 }
884
885 /**
886 * Find highest available status for a user.
887 *
888 * @param mod_attendance_structure $att attendance structure
889 * @param stdclass $attforsession attendance_session record.
890 * @return bool/int
891 */
892 function attendance_session_get_highest_status(mod_attendance_structure $att, $attforsession) {
893 // Find the status to set here.
894 $statuses = $att->get_statuses();
895 $highestavailablegrade = 0;
896 $highestavailablestatus = new stdClass();
897 foreach ($statuses as $status) {
898 if ($status->studentavailability === '0') {
899 // This status is never available to students.
900 continue;
901 }
902 if (!empty($status->studentavailability)) {
903 $toolateforstatus = (($attforsession->sessdate + ($status->studentavailability * 60)) < time());
904 if ($toolateforstatus) {
905 continue;
906 }
907 }
908 // This status is available to the student.
909 if ($status->grade > $highestavailablegrade) {
910 // This is the most favourable grade so far; save it.
911 $highestavailablegrade = $status->grade;
912 $highestavailablestatus = $status;
913 }
914 }
915 if (empty($highestavailablestatus)) {
916 return false;
917 }
918 return $highestavailablestatus->id;
919 }
920
921 /**
922 * Get available automark options.
923 *
924 * @return array
925 */
926 function attendance_get_automarkoptions() {
927 $options = array();
928 $options[ATTENDANCE_AUTOMARK_DISABLED] = get_string('noautomark', 'attendance');
929 if (strpos(get_config('tool_log', 'enabled_stores'), 'logstore_standard') !== false) {
930 $options[ATTENDANCE_AUTOMARK_ALL] = get_string('automarkall', 'attendance');
931 }
932 $options[ATTENDANCE_AUTOMARK_CLOSE] = get_string('automarkclose', 'attendance');
933 return $options;
934 }