Compare commits

...

85 Commits

Author SHA1 Message Date
Dan Marsden f485ce7d89 Fix #466 - sanity check sort var before using. 5 years ago
Dan Marsden a37d399b03 Fix #455 - Fix some hard-coded language strings. 5 years ago
Dan Marsden 0d3a162057 Fix #445 - use attendance specific strings. 5 years ago
Dan Marsden e078bb9d87
Backport fixes for issue#395 and Issue #398 related to db schema issues. (#437) 5 years ago
Nadav Kavalerchik 119946f24a Swap hour:min input fields in a new session form, when in RTL (#434) 5 years ago
Dan Marsden 9f35bbe24f Fix #422 - Only ask for fields once. 5 years ago
Dan Marsden 29e822459d Fix #427 - correct some bad sql. 5 years ago
Dan Marsden 5af8de8c36 Fix Travis config. 5 years ago
Dan Marsden 9836d3cfb2 Fix #421 - include filelib.php in structure class. 5 years ago
Dan Marsden baf90b0304 Make it clear the import requires a courseshortname 6 years ago
maksudr 53f075ab6c Fix #357 - Added QR code icon (#376) 6 years ago
Dan Marsden 9bc4d9e8dd Prevent Calendarevent value being set when disabled at site level. 6 years ago
maksudr 2a91d6d7f5 Fixes #349 - Fixed calendarevent for imports 6 years ago
Morgan Harris 71bc7f15aa Webservice: allow multiple instances in one course (#372) 6 years ago
Dan Marsden f9a0425f04 Fix #371 set grade to 0 if null. 6 years ago
SSH230 0d932f23a3 Fix 319 add support to show description on course page. 6 years ago
Morgan Harris 6d4cf2d81d Allow group sessions in webservices (#364) 6 years ago
Dan Marsden 5082677a2d Fix #363 typo in key defintion - causes failure in Oracle upgrades. 6 years ago
Dan Marsden fdbabea033 Fix #350 - update description in calendar even on session update. 6 years ago
Dan Marsden c338beb31a Fix #332 tidy up delete message in attendance plugin. 6 years ago
Dan Marsden cf09ca702e Fix #340 Check duration of session when checking if a student enrolled before a session. 6 years ago
Dan Marsden 650611ee4e
coding guideline - remove extra carriage return 6 years ago
Dan Marsden 24b54e1062
remove incorrect space. 6 years ago
Dan Marsden 2540512f1e Fix #359 - increase number of allowed status items in the logs. 6 years ago
Dan Marsden ac06e6cf76 Fix #358 check index exists before dropping. 6 years ago
Dan Marsden 0478b99c45 Fix stupid way of checking if user already has a recorded attendance. 6 years ago
Dan Marsden 596c63ecba PHPDoc guidline fix. 6 years ago
Dan Marsden 126dc33dc8 Bump version for plugins db release. 6 years ago
Dan Marsden 253fd5f504 Hack a way to provide a selectall function on teacher marking. 6 years ago
Tõnis Tartes a412914418 ISSUE-336 - Viewing single session table total counts in also users w… (#337) 6 years ago
Dan Marsden b69eb13de4 Fix #346 correct redirect after saving attendance. 6 years ago
Dan Marsden d17a0d1ad3 bump version. 6 years ago
Dan Marsden 04042370dd Coding guideline fix for css. 6 years ago
Dan Marsden d6aedc26b0 Move group onto new line in Mobile app. 6 years ago
Dan Marsden 31a29ff647 Fix #338 improve css for more/clean themes with attendance report. 6 years ago
Dan Marsden a93f6d202a Fix #344 show full session date in mobile app. 6 years ago
Dan Marsden e98011347d Fix #342 prevent sharedip warning from showing more than once. 6 years ago
Dan Marsden 899abd4911 Don't show sharedip warning when teacher logged in. 6 years ago
Dan Marsden 310522bb93 Add user profile image to teacher marking page. 6 years ago
Dan Marsden c394dccf87 Show message to user if preventsharederror is hit. 6 years ago
Dan Marsden 78f5b91185 Improve css tags, and make sure messages wrap correctly. 6 years ago
Dan Marsden 4576ce28ca ignore moodle mobile mustache template 6 years ago
Dan Marsden a84562228c Implement group session support. 6 years ago
Dan Marsden 22c45da5e4 Prevent teacher marking page from being cached in device. 6 years ago
Dan Marsden 1906820255 Fix example template. 6 years ago
Dan Marsden 1be1851416 WIP show currently selected status in form. 6 years ago
Dan Marsden 1d57b0ebfa Tidy up radio buttons used in teacher view. 6 years ago
Dan Marsden c5d9b46791 WIP Take teacher attendance. 6 years ago
Dan Marsden edcc1a9c97 Adjust take_from_form_data function to allow better re-use. 6 years ago
Dan Marsden d632ac4eae WIP Teacher submission form. 6 years ago
Dan Marsden 818ae8e559 Add support for session passwords when student marking. 6 years ago
Dan Marsden 38d5e8cee2 Re-organise form handling so to avoid cache issues and improve UX 6 years ago
Dan Marsden f0bbb6795d Hide session marking links from teachers for now - not implemented yet. 6 years ago
Dan Marsden dcac278642 Mobile Support - add ability for students to self-mark open sessions. 6 years ago
Dan Marsden f4a7d9fbb2 Don't save values if not numeric. 6 years ago
Dan Marsden 6324708c86 bump versoin for plugins db. 6 years ago
Dan Marsden 3a0759067b Coding guidelines. 6 years ago
Dan Marsden 7ac9b2eb7d Use correct travis branch. 6 years ago
Dan Marsden 8e513a7b6b Convert to using tcpdf lib to generate qrcodes. 6 years ago
Dan Marsden 67c63bfb1b Improve QRcode support. 6 years ago
Eoin Campbell 37a5549c6a Support QR codes to speed up attendance recording, using local_qrlinks plugin or external web service 6 years ago
Dan Marsden dd705d75df Add missing icon - PNG used by mobile app. 6 years ago
Dan Marsden f6f408e1b5 Fix #328 correct validation check for statusset and not marked status. 6 years ago
Dan Marsden 89b974f184 Add Mustache example content. 6 years ago
Dan Marsden 6542eee57e Display user summary in mobile app. 6 years ago
Dan Marsden fabd021710 Fix some coding guideline things. 7 years ago
Dan Marsden ec74ba2dbc Fix some coding guideline issues. 7 years ago
Dan Marsden 4855bfcc29 Fixes #307 add list of modules in course on index.php page. 7 years ago
Dan Marsden 37c42b4bc2 Fixes #316 provide option to prevent sharing ip for this session without a timeframe. 7 years ago
Dan Marsden a6576acd19 Create new setting to control default, and set correct value in db for existing records. 7 years ago
Nick Phillips 965e7f5e02 Make events optional per-session. 7 years ago
Dan Marsden 4e11ea78b2 make table use full available space. 7 years ago
Dan Marsden 8d3ca3d143 Use new string in settings page. 7 years ago
Dan Marsden 6a4a0572aa Fixes #299 (maybe?) 7 years ago
Dan Marsden 2522b8b619 Fix for session add/update when studentscanmark is empty but automark set to close 7 years ago
Dan Marsden 6826bb831e Fix #196 Thanks to @antonio-c-mariani for this code. 7 years ago
Dan Marsden c94fc8679a Move lowgrade report link to after time based reports. 7 years ago
Dan Marsden a7f83c2e8d Fix #197 move checkboxes to beside names to improve UI. 7 years ago
Dan Marsden 0400a932dd Fixes #213 float student names in report. 7 years ago
Dan Marsden df9edb01a7 Fix #317 typo in html entity, and entities not rendering inside select options. 7 years ago
Dan Marsden a971a53fcd bump version for release. 7 years ago
Neill Magill 35c88f84ea Cog menu on teacher page under Boost based themes 7 years ago
Cameron Ball b8a6731dbe Implement data privacy provider. 7 years ago
Dan Marsden 3872569a88 Prevent text_to_html from adding divs to email subject. 7 years ago
Dan Marsden dec2abf3fc Create 3.5 stable branch. 7 years ago
  1. 15
      .travis.yml
  2. 6
      absentee.php
  3. 28
      add_form.php
  4. 60
      attendance.php
  5. 2
      backup/moodle2/backup_attendance_stepslib.php
  6. 5
      classes/attendance_webservices_handler.php
  7. 36
      classes/calendar_helpers.php
  8. 50
      classes/event/course_module_instance_list_viewed.php
  9. 3
      classes/event/session_ip_shared.php
  10. 24
      classes/import/sessions.php
  11. 451
      classes/output/mobile.php
  12. 1
      classes/page_with_filter_controls.php
  13. 495
      classes/privacy/provider.php
  14. 80
      classes/structure.php
  15. 21
      classes/summary.php
  16. 2
      classes/task/notify.php
  17. 8
      coursesummary.php
  18. 10
      db/install.xml
  19. 70
      db/mobile.php
  20. 74
      db/upgrade.php
  21. 4
      defaultstatus.php
  22. 10
      export.php
  23. 3
      externallib.php
  24. 63
      index.php
  25. 40
      lang/en/attendance.php
  26. 5
      lib.php
  27. 138
      locallib.php
  28. 1
      manage.php
  29. 30
      mobilestyles.css
  30. 12
      password.php
  31. 11
      password_ajax.php
  32. BIN
      pix/icon.png
  33. 1
      pix/qrcode.svg
  34. 5
      preferences.php
  35. 23
      renderables.php
  36. 157
      renderer.php
  37. 83
      renderhelpers.php
  38. 1
      report.php
  39. 6
      resetcalendar.php
  40. 5
      sessions.php
  41. 17
      settings.php
  42. 2
      student_attendance_form.php
  43. 44
      styles.css
  44. 25
      take.php
  45. 105
      templates/mobile_teacher_form.mustache
  46. 82
      templates/mobile_user_form.mustache
  47. 144
      templates/mobile_view_page.mustache
  48. 1
      tempusers.php
  49. 81
      tests/attendance_webservices_test.php
  50. 4
      tests/behat/attendance_mod.feature
  51. 14
      tests/behat/report.feature
  52. 36
      update_form.php
  53. 6
      version.php

15
.travis.yml

@ -4,11 +4,13 @@ sudo: true
addons:
firefox: "47.0.1"
postgresql: "9.3"
postgresql: "9.4"
apt:
packages:
- oracle-java8-installer
- oracle-java8-set-default
- openjdk-8-jre-headless
services:
- mysql
cache:
directories:
@ -18,9 +20,11 @@ cache:
php:
- 7.1
- 7.2
env:
global:
- MOODLE_BRANCH=master
- MOODLE_BRANCH=MOODLE_35_STABLE
- MUSTACHE_IGNORE_NAMES=mobile_teacher_form.mustache
matrix:
- DB=pgsql
- DB=mysqli
@ -30,8 +34,9 @@ before_install:
- nvm install 8.9
- nvm use 8.9
- cd ../..
- composer create-project -n --no-dev --prefer-dist moodlerooms/moodle-plugin-ci ci ^2
- composer create-project -n --no-dev --prefer-dist blackboard-open-source/moodle-plugin-ci ci ^2
- export PATH="$(cd ci/bin; pwd):$(cd ci/vendor/bin; pwd):$PATH"
- PATH=$(echo "$PATH" | sed -e 's/:\/usr\/local\/lib\/jvm\/openjdk11\/bin//') JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-amd64
install:
- moodle-plugin-ci install

6
absentee.php

@ -102,6 +102,12 @@ $table->setup();
$sortcolumns = $table->get_sort_columns();
// Now do sorting if specified.
// Sanity check $sort var before including in sql. Make sure it matches a known column.
$allowedsort = array_diff(array_keys($table->columns), $table->column_nosort);
if (!in_array($sort, $allowedsort)) {
$sort = '';
}
$orderby = ' ORDER BY percent ASC';
if (!empty($sort)) {
$direction = ' DESC';

28
add_form.php

@ -125,6 +125,17 @@ class mod_attendance_add_form extends moodleform {
array('maxfiles' => EDITOR_UNLIMITED_FILES, 'noclean' => true, 'context' => $modcontext));
$mform->setType('sdescription', PARAM_RAW);
if (!empty($pluginconfig->enablecalendar)) {
$mform->addElement('checkbox', 'calendarevent', '', get_string('calendarevent', 'attendance'));
$mform->addHelpButton('calendarevent', 'calendarevent', 'attendance');
if (isset($pluginconfig->calendarevent_default)) {
$mform->setDefault('calendarevent', $pluginconfig->calendarevent_default);
}
} else {
$mform->addElement('hidden', 'calendarevent', 0);
$mform->setType('calendarevent', PARAM_INT);
}
// If warnings allow selector for reporting.
if (!empty(get_config('attendance', 'enablewarnings'))) {
$mform->addElement('checkbox', 'absenteereport', '', get_string('includeabsentee', 'attendance'));
@ -198,6 +209,7 @@ class mod_attendance_add_form extends moodleform {
$mgroup[] = & $mform->createElement('text', 'studentpassword', get_string('studentpassword', 'attendance'));
$mgroup[] = & $mform->createElement('checkbox', 'randompassword', '', get_string('randompassword', 'attendance'));
$mgroup[] = & $mform->createElement('checkbox', 'includeqrcode', '', get_string('includeqrcode', 'attendance'));
$mform->addGroup($mgroup, 'passwordgrp', get_string('passwordgrp', 'attendance'), array(' '), false);
$mform->setType('studentpassword', PARAM_TEXT);
@ -210,7 +222,6 @@ class mod_attendance_add_form extends moodleform {
$mform->addElement('checkbox', 'autoassignstatus', '', get_string('autoassignstatus', 'attendance'));
$mform->addHelpButton('autoassignstatus', 'autoassignstatus', 'attendance');
$mform->hideif('autoassignstatus', 'studentscanmark', 'notchecked');
if (isset($pluginconfig->autoassignstatus)) {
$mform->setDefault('autoassignstatus', $pluginconfig->autoassignstatus);
}
@ -220,6 +231,9 @@ class mod_attendance_add_form extends moodleform {
if (isset($pluginconfig->randompassword_default)) {
$mform->setDefault('randompassword', $pluginconfig->randompassword_default);
}
if (isset($pluginconfig->includeqrcode_default)) {
$mform->setDefault('includeqrcode', $pluginconfig->includeqrcode_default);
}
if (isset($pluginconfig->automark_default)) {
$mform->setDefault('automark', $pluginconfig->automark_default);
}
@ -243,17 +257,19 @@ class mod_attendance_add_form extends moodleform {
$mform->hideif('subnet', 'usedefaultsubnet', 'checked');
$mgroup3 = array();
$mgroup3[] = & $mform->createElement('checkbox', 'preventsharedip', '');
$options = attendance_get_sharedipoptions();
$mgroup3[] = & $mform->createElement('select', 'preventsharedip',
get_string('preventsharedip', 'attendance'), $options);
$mgroup3[] = & $mform->createElement('text', 'preventsharediptime',
get_string('preventsharediptime', 'attendance'), '', 'test');
$mgroup3[] = & $mform->createElement('static', 'preventsharediptimedesc', '',
get_string('preventsharedipminutes', 'attendance'));
$mform->addGroup($mgroup3, 'preventsharedgroup', get_string('preventsharedip', 'attendance'), array(' '), false);
$mform->addHelpButton('preventsharedgroup', 'preventsharedip', 'attendance');
$mform->setAdvanced('preventsharedgroup');
$mform->setType('preventsharedip', PARAM_INT);
$mform->setType('preventsharediptime', PARAM_INT);
$mform->hideif('preventsharedgroup', 'studentscanmark', 'notchecked');
$mform->disabledIf('preventsharediptime', 'preventsharedip', 'notchecked');
$mform->hideIf('preventsharediptime', 'preventsharedip', 'noteq', ATTENDANCE_SHAREDIP_MINUTES);
if (isset($pluginconfig->preventsharedip)) {
$mform->setDefault('preventsharedip', $pluginconfig->preventsharedip);
}
@ -327,7 +343,7 @@ class mod_attendance_add_form extends moodleform {
$this->_form->setConstant('previoussessiondate', $data['sessiondate']);
}
if ($data['automark'] == ATTENDANCE_AUTOMARK_CLOSE) {
if (!empty($data['studentscanmark']) && $data['automark'] == ATTENDANCE_AUTOMARK_CLOSE) {
$cm = $this->_customdata['cm'];
// Check that the selected statusset has a status to use when unmarked.
$sql = 'SELECT id

60
attendance.php

@ -30,6 +30,7 @@ $pageparams = new mod_attendance_sessions_page_params();
// Check that the required parameters are present.
$id = required_param('sessid', PARAM_INT);
$qrpass = optional_param('qrpass', '', PARAM_TEXT);
$attforsession = $DB->get_record('attendance_sessions', array('id' => $id), '*', MUST_EXIST);
$attendance = $DB->get_record('attendance', array('id' => $attforsession->attendanceid), '*', MUST_EXIST);
@ -39,9 +40,9 @@ $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST)
// Require the user is logged in.
require_login($course, true, $cm);
if (!attendance_can_student_mark($attforsession)) {
// TODO: should we add a log message here? - student has got to submit page but cannot save attendance (time ran out?)
redirect(new moodle_url('/mod/attendance/view.php', array('id' => $cm->id)));
list($canmark, $reason) = attendance_can_student_mark($attforsession);
if (!$canmark) {
redirect(new moodle_url('/mod/attendance/view.php', array('id' => $cm->id)), get_string($reason, 'attendance'));
exit;
}
@ -55,8 +56,14 @@ if (!empty($attforsession->subnet) && !address_in_subnet(getremoteaddr(), $attfo
$pageparams->sessionid = $id;
$att = new mod_attendance_structure($attendance, $cm, $course, $PAGE->context, $pageparams);
// Require that a session key is passed to this page.
require_sesskey();
if (empty($attforsession->includeqrcode)) {
$qrpass = ''; // Override qrpass if set, as it is not allowed.
}
if (empty($qrpass)) {
// Sesskey is required on this page when QR code not in use.
require_sesskey();
}
// Check to see if autoassignstatus is in use and no password required.
if ($attforsession->autoassignstatus && empty($attforsession->studentpassword)) {
@ -78,12 +85,48 @@ if ($attforsession->autoassignstatus && empty($attforsession->studentpassword))
}
}
// Create the form.
$mform = new mod_attendance_student_attendance_form(null,
array('course' => $course, 'cm' => $cm, 'modcontext' => $PAGE->context, 'session' => $attforsession, 'attendance' => $att));
// Check to see if autoassignstatus is in use and if qrcode is being used.
if (!empty($qrpass) && !empty($attforsession->autoassignstatus)) {
$fromform = new stdClass();
// Check if password required and if set correctly.
if (!empty($attforsession->studentpassword) &&
$attforsession->studentpassword !== $qrpass) {
$url = new moodle_url('/mod/attendance/attendance.php', array('sessid' => $id, 'sesskey' => sesskey()));
redirect($url, get_string('incorrectpassword', 'mod_attendance'), null, \core\output\notification::NOTIFY_ERROR);
}
// Set the password and session id in the form, because they are saved in the attendance log.
$fromform->studentpassword = $qrpass;
$fromform->sessid = $attforsession->id;
$fromform->status = attendance_session_get_highest_status($att, $attforsession);
if (empty($fromform->status)) {
$url = new moodle_url('/mod/attendance/view.php', array('id' => $cm->id));
print_error('attendance_no_status', 'mod_attendance', $url);
}
if (!empty($fromform->status)) {
$success = $att->take_from_student($fromform);
$url = new moodle_url('/mod/attendance/view.php', array('id' => $cm->id));
if ($success) {
// Redirect back to the view page.
redirect($url, get_string('studentmarked', 'attendance'));
} else {
print_error('attendance_already_submitted', 'mod_attendance', $url);
}
}
}
$PAGE->set_url($att->url_sessions());
// Create the form.
$mform = new mod_attendance_student_attendance_form(null,
array('course' => $course, 'cm' => $cm, 'modcontext' => $PAGE->context, 'session' => $attforsession,
'attendance' => $att, 'password' => $qrpass));
if ($mform->is_cancelled()) {
// The user cancelled the form, so redirect them to the view page.
$url = new moodle_url('/mod/attendance/view.php', array('id' => $cm->id));
@ -129,4 +172,3 @@ $output = $PAGE->get_renderer('mod_attendance');
echo $output->header();
$mform->display();
echo $output->footer();

2
backup/moodle2/backup_attendance_stepslib.php

@ -59,7 +59,7 @@ class backup_attendance_activity_structure_step extends backup_activity_structur
'groupid', 'sessdate', 'duration', 'lasttaken', 'lasttakenby', 'timemodified',
'description', 'descriptionformat', 'studentscanmark', 'studentpassword', 'autoassignstatus',
'subnet', 'automark', 'automarkcompleted', 'statusset', 'absenteereport', 'preventsharedip',
'preventsharediptime', 'caleventid'));
'preventsharediptime', 'caleventid', 'calendarevent', 'includeqrcode'));
// XML nodes declaration - user data.
$logs = new backup_nested_element('logs');

5
classes/attendance_webservices_handler.php

@ -51,7 +51,9 @@ class attendance_handler {
$context = context_course::instance($attendance->course);
if (has_capability('mod/attendance:takeattendances', $context, $userid)) {
$course = $usercourses[$attendance->course];
if (!isset($course->attendance_instance)) {
$course->attendance_instance = array();
}
$att = new stdClass();
$att->id = $attendance->id;
@ -109,7 +111,8 @@ class attendance_handler {
$session->courseid = $DB->get_field('attendance', 'course', array('id' => $session->attendanceid));
$session->statuses = attendance_get_statuses($session->attendanceid, true, $session->statusset);
$coursecontext = context_course::instance($session->courseid);
$session->users = get_enrolled_users($coursecontext, 'mod/attendance:canbelisted', 0, 'u.id, u.firstname, u.lastname');
$session->users = get_enrolled_users($coursecontext, 'mod/attendance:canbelisted',
$session->groupid, 'u.id, u.firstname, u.lastname');
$session->attendance_log = array();
if ($attendancelog = $DB->get_records('attendance_log', array('sessionid' => $sessionid),

36
classes/calendar_helpers.php

@ -38,8 +38,8 @@ function attendance_create_calendar_event(&$session) {
if ($session->caleventid) {
return $session->caleventid;
}
if (empty(get_config('attendance', 'enablecalendar'))) {
// Calendar events are not used.
if (empty(get_config('attendance', 'enablecalendar')) || $session->calendarevent === 0) {
// Calendar events are not used, or event not required for this session.
return true;
}
@ -98,22 +98,46 @@ function attendance_create_calendar_events($sessionsids) {
/**
* Update calendar event duration and date
*
* @param int $caleventid calendar event id
* @param int $timeduration duration of the event
* @param int $timestart start time of the event
* @param stdClass $session Session data
* @return bool result of updating
*/
function attendance_update_calendar_event($caleventid, $timeduration, $timestart) {
function attendance_update_calendar_event($session) {
global $DB;
$caleventid = $session->caleventid;
$timeduration = $session->duration;
$timestart = $session->sessdate;
if (empty(get_config('attendance', 'enablecalendar'))) {
// Calendar events are not used.
return true;
}
// Should there even be an event?
if ($session->calendarevent == 0) {
if ($session->caleventid != 0) {
// There is an existing event we should delete, calendarevent just got turned off.
$DB->delete_records_list('event', 'id', array($caleventid));
$session->caleventid = 0;
$DB->update_record('attendance_sessions', $session);
return true;
} else {
// This should be the common case when session does not want event.
return true;
}
}
// Do we need new event (calendarevent option has just been turned on)?
if ($session->caleventid == 0) {
return attendance_create_calendar_event($session);
}
// Boring update.
$caleventdata = new stdClass();
$caleventdata->timeduration = $timeduration;
$caleventdata->timestart = $timestart;
$caleventdata->timemodified = time();
$caleventdata->description = $session->description;
$calendarevent = calendar_event::load($caleventid);
if ($calendarevent) {

50
classes/event/course_module_instance_list_viewed.php

@ -0,0 +1,50 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_attendance instance list viewed event.
*
* @package mod_attendance
* @copyright 2018 Dan Marsden
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_attendance\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_attendance instance list viewed event class.
*
* @package mod_attendance
* @copyright 2018 Dan Marsden
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed {
/**
* Create the event from course record.
*
* @param \stdClass $course
* @return course_module_instance_list_viewed
*/
public static function create_from_course(\stdClass $course) {
$params = array(
'context' => \context_course::instance($course->id)
);
$event = self::create($params);
$event->add_record_snapshot('course', $course);
return $event;
}
}

3
classes/event/session_ip_shared.php

@ -15,8 +15,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file contains an event for when self-marking is blocked because
* another student used the same IP address to self-mark.
* Event for when self-marking is blocked because another student used the same IP address to self-mark.
*
* @package mod_attendance
* @author Dan Marsden <dan@danmarsden.com>

24
classes/import/sessions.php

@ -92,7 +92,7 @@ class sessions {
*/
public static function list_required_headers() {
return array(
get_string('course', 'attendance'),
get_string('courseshortname', 'attendance'),
get_string('groups', 'attendance'),
get_string('sessiondate', 'attendance'),
get_string('from', 'attendance'),
@ -109,7 +109,9 @@ class sessions {
get_string('autoassignstatus', 'attendance'),
get_string('absenteereport', 'attendance'),
get_string('preventsharedip', 'attendance'),
get_string('preventsharediptime', 'attendance')
get_string('preventsharediptime', 'attendance'),
get_string('calendarevent', 'attendance'),
get_string('includeqrcode', 'attendance'),
);
}
@ -148,6 +150,8 @@ class sessions {
'absenteereport' => $data->header15,
'preventsharedip' => $data->header16,
'preventsharediptime' => $data->header17,
'calendarevent' => $data->header18,
'includeqrcode' => $data->header19
);
} else {
return array(
@ -168,7 +172,9 @@ class sessions {
'autoassignstatus' => 14,
'absenteereport' => 15,
'preventsharedip' => 16,
'preventsharediptime' => 17
'preventsharediptime' => 17,
'calendarevent' => 18,
'includeqrcode' => 19
);
}
}
@ -334,6 +340,18 @@ class sessions {
$session->preventsharediptime = $this->get_column_data($row, $mapping['preventsharediptime']);
}
if ($mapping['calendarevent'] == -1) {
$session->calendarevent = $pluginconfig->calendarevent_default;
} else {
$session->calendarevent = $this->get_column_data($row, $mapping['calendarevent']);
}
if ($mapping['includeqrcode'] == -1) {
$session->includeqrcode = $pluginconfig->includeqrcode_default;
} else {
$session->includeqrcode = $this->get_column_data($row, $mapping['includeqrcode']);
}
$session->statusset = 0;
$sessions[] = $session;

451
classes/output/mobile.php

@ -0,0 +1,451 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains the mobile output class for the attendance
*
* @package mod_attendance
* @copyright 2018 Dan Marsden
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_attendance\output;
defined('MOODLE_INTERNAL') || die();
/**
* Mobile output class for the attendance.
*
* @copyright 2018 Dan Marsden
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mobile {
/*
* Subnet warning. - constants used to prevent warnings from showing multiple times.
*/
const MESSAGE_SUBNET = 10;
/*
* Prevent shared warning. used to prevent warnings from showing multiple times.
*/
const MESSAGE_PREVENTSHARED = 30;
/**
* Returns the initial page when viewing the activity for the mobile app.
*
* @param array $args Arguments from tool_mobile_get_content WS
* @return array HTML, javascript and other data
*/
public static function mobile_view_activity($args) {
global $OUTPUT, $DB, $USER, $USER, $CFG;
require_once($CFG->dirroot.'/mod/attendance/locallib.php');
$cmid = $args['cmid'];
$courseid = $args['courseid'];
$takenstatus = empty($args['status']) ? '' : $args['status'];
$sessid = empty($args['sessid']) ? '' : $args['sessid'];
$password = empty($args['studentpass']) ? '' : $args['studentpass'];
// Capabilities check.
$cm = get_coursemodule_from_id('attendance', $cmid);
require_login($courseid, false , $cm, true, true);
$context = \context_module::instance($cm->id);
require_capability('mod/attendance:view', $context);
$attendance = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST);
$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
$data = array(); // Data to pass to renderer.
$data['cmid'] = $cmid;
$data['courseid'] = $courseid;
$data['attendance'] = $attendance;
$data['timestamp'] = time(); // Used to prevent attendance session marking page to be cached.
$data['attendancefunction'] = 'mobile_user_form';
$isteacher = false;
if (has_capability('mod/attendance:takeattendances', $context)) {
$isteacher = true;
$data['attendancefunction'] = 'mobile_teacher_form';
}
// Add stats for this use to output.
$pageparams = new \mod_attendance_view_page_params();
$pageparams->studentid = $USER->id;
$pageparams->group = groups_get_activity_group($cm, true);
$canseegroupsession = true;
if (!empty($sessid) && (!empty($takenstatus) || $isteacher)) {
$session = $DB->get_record('attendance_sessions', array('id' => $sessid));
$pageparams->grouptype = $session->groupid;
$pageparams->sessionid = $sessid;
if ($isteacher && !empty($session->groupid)) {
$allowedgroups = groups_get_activity_allowed_groups($cm);
if (!array_key_exists($session->groupid, $allowedgroups)) {
$canseegroupsession = false;
}
}
}
$pageparams->mode = \mod_attendance_view_page_params::MODE_THIS_COURSE;
$pageparams->view = 5; // Show all sessions for this course?
$att = new \mod_attendance_structure($attendance, $cm, $course, $context, $pageparams);
// Check if this teacher is allowed to view/mark this group session.
if ($isteacher && $canseegroupsession) {
$keys = array_keys($args);
$userkeys = preg_grep("/status\d+/", $keys);
if (!empty($userkeys)) { // If this is a post from the teacher form.
// Build data to pass to take_from_form_data.
$formdata = new \stdClass();
foreach ($userkeys as $uk) {
$userid = str_replace('status', '', $uk);
$status = $args[$uk];
$formdata->{'remarks'.$userid} = '';
$formdata->{'user'.$userid} = $status;
}
$att->take_from_form_data($formdata);
$data['showmessage'] = true;
$data['messages'][]['string'] = 'attendancesuccess';
}
}
// Get list of sessions within the next 24hrs and in last 6hrs.
// TODO: provide way of adjusting which sessions to show in app.
$time = time() - (6 * 60 * 60);
$data['sessions'] = array();
$sessions = $DB->get_records_select('attendance_sessions',
'attendanceid = ? AND sessdate > ? ORDER BY sessdate', array($attendance->id, $time));
if (!empty($sessions)) {
$userdata = new \attendance_user_data($att, $USER->id, true);
foreach ($sessions as $sess) {
if (!$isteacher && empty($userdata->sessionslog['c'.$sess->id])) {
// This session isn't viewable to this student - probably a group session.
continue;
}
// Check if this teacher is allowed to view this group session.
if ($isteacher && !empty($sess->groupid)) {
$allowedgroups = groups_get_activity_allowed_groups($cm);
if (!array_key_exists($sess->groupid, $allowedgroups)) {
continue;
}
}
list($canmark, $reason) = attendance_can_student_mark($sess);
if (!$isteacher && $reason == 'preventsharederror') {
$data['showmessage'] = true;
$data['messages'][self::MESSAGE_PREVENTSHARED]['string'] = 'preventsharederror'; // Lang string to show as a message.
}
if ($isteacher || $canmark) {
$html = array('time' => strip_tags(construct_session_full_date_time($sess->sessdate, $sess->duration)),
'groupname' => '');
if (!empty($sess->groupid)) {
// TODO In-efficient way to get group name - we should get all groups in one query.
$html['groupname'] = $DB->get_field('groups', 'name', array('id' => $sess->groupid));
}
// Check if Status already recorded.
if (!$isteacher && !empty($userdata->sessionslog['c'.$sess->id]->statusid)) {
$html['currentstatus'] = $userdata->statuses[$userdata->sessionslog['c'.$sess->id]->statusid]->description;
} else {
// Status has not been recorded - If student, check auto-assign and form data.
$html['sessid'] = $sess->id;
if (!$isteacher) {
if (!empty($sess->subnet) && !address_in_subnet(getremoteaddr(), $sess->subnet)) {
$data['showmessage'] = true;
$data['messages'][self::MESSAGE_SUBNET]['string'] = 'subnetwrong'; // Lang string to show as a message.
$html['sessid'] = null; // Unset sessid as we cannot record session on this ip.
} else if ($sess->autoassignstatus && empty($sess->studentpassword)) {
$statusid = attendance_session_get_highest_status($att, $sess);
if (empty($statusid)) {
$data['showmessage'] = true;
$data['messages'][]['string'] = 'attendance_no_status';
}
$take = new \stdClass();
$take->status = $statusid;
$take->sessid = $sess->id;
$success = $att->take_from_student($take);
if ($success) {
$html['currentstatus'] = $userdata->statuses[$statusid]->description;
$html['sessid'] = null; // Unset sessid as we have recorded session.
}
} else if ($sess->id == $sessid) {
if (!empty($sess->studentpassword) && $password != $sess->studentpassword) {
// Password incorrect.
$data['showmessage'] = true;
$data['messages'][]['string'] = 'incorrectpasswordshort';
} else {
$statuses = $att->get_statuses();
// Check if user has access to all statuses.
foreach ($statuses as $status) {
if ($status->studentavailability === '0') {
unset($statuses[$status->id]);
continue;
}
if (!empty($status->studentavailability) &&
time() > $sess->sessdate + ($status->studentavailability * 60)) {
unset($statuses[$status->id]);
continue;
}
}
if ($sess->autoassignstatus) {
// If this is an auto-assign, get the highest status available.
$takenstatus = attendance_session_get_highest_status($att, $sess);
}
if (empty($statuses[$takenstatus])) {
// This status has probably expired and is not available - they need to choose a new one.
$data['showmessage'] = true;
$data['messages'][]['string'] = 'invalidstatus';
} else {
$take = new \stdClass();
$take->status = $takenstatus;
$take->sessid = $sess->id;
$success = $att->take_from_student($take);
if ($success) {
$html['currentstatus'] = $userdata->statuses[$takenstatus]->description;
$html['sessid'] = null; // Unset sessid as we have recorded session.
}
}
}
}
}
}
$data['sessions'][] = $html;
}
}
}
$summary = new \mod_attendance_summary($att->id, array($USER->id), $att->pageparams->startdate,
$att->pageparams->enddate);
$data['summary'] = $summary->get_all_sessions_summary_for($USER->id);
return [
'templates' => [
[
'id' => 'main',
'html' => $OUTPUT->render_from_template('mod_attendance/mobile_view_page', $data),
],
],
'javascript' => '',
'otherdata' => ''
];
}
/**
* Returns the form to take attendance for the mobile app.
*
* @param array $args Arguments from tool_mobile_get_content WS
* @return array HTML, javascript and other data
*/
public static function mobile_user_form($args) {
global $OUTPUT, $DB, $CFG;
require_once($CFG->dirroot.'/mod/attendance/locallib.php');
$args = (object) $args;
$cmid = $args->cmid;
$courseid = $args->courseid;
$sessid = $args->sessid;
// Capabilities check.
$cm = get_coursemodule_from_id('attendance', $cmid);
require_login($courseid, false , $cm, true, true);
$context = \context_module::instance($cm->id);
require_capability('mod/attendance:view', $context);
$attendance = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST);
$attforsession = $DB->get_record('attendance_sessions', array('id' => $sessid), '*', MUST_EXIST);
$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
$pageparams = new \mod_attendance_sessions_page_params();
$pageparams->sessionid = $sessid;
$att = new \mod_attendance_structure($attendance, $cm, $course, $context, $pageparams);
$data = array(); // Data to pass to renderer.
$data['attendance'] = $attendance;
$data['cmid'] = $cmid;
$data['courseid'] = $courseid;
$data['sessid'] = $sessid;
$data['messages'] = array();
$data['showmessage'] = false;
$data['showstatuses'] = true;
$data['showpassword'] = false;
$data['statuses'] = array();
$data['disabledduetotime'] = false;
list($canmark, $reason) = attendance_can_student_mark($attforsession, false);
// Check if subnet is set and if the user is in the allowed range.
if (!$canmark) {
$data['messages'][]['string'] = $reason; // Lang string to show as a message.
$data['showstatuses'] = false; // Hide all statuses.
} else if (!empty($attforsession->subnet) && !address_in_subnet(getremoteaddr(), $attforsession->subnet)) {
$data['messages'][self::MESSAGE_SUBNET]['string'] = 'subnetwrong'; // Lang string to show as a message.
$data['showstatuses'] = false; // Hide all statuses.
} else if ($attforsession->autoassignstatus && empty($attforsession->studentpassword)) {
// This shouldn't happen as the main function should handle this scenario.
// Hide all status just in case the user manages to hit this page accidentally.
$data['showstatuses'] = false; // Hide all statuses.
} else {
// Show user form for submitting a status.
$statuses = $att->get_statuses();
// Check if user has access to all statuses.
foreach ($statuses as $status) {
if ($status->studentavailability === '0') {
unset($statuses[$status->id]);
continue;
}
if (!empty($status->studentavailability) &&
time() > $attforsession->sessdate + ($status->studentavailability * 60)) {
unset($statuses[$status->id]);
continue;
$data['disabledduetotime'] = true;
}
$data['statuses'][] = array('stid' => $status->id, 'description' => $status->description);
}
if (empty($data['statuses'])) {
$data['messages'][]['string'] = 'attendance_no_status';
$data['showstatuses'] = false; // Hide all statuses.
} else if (!empty($attforsession->studentpassword)) {
$data['showpassword'] = true;
if ($attforsession->autoassignstatus) {
// If this is an auto status - don't show the statuses, but show the form.
$data['statuses'] = array();
}
}
}
if (!empty($data['messages'])) {
$data['showmessage'] = true;
}
return [
'templates' => [
[
'id' => 'main',
'html' => $OUTPUT->render_from_template('mod_attendance/mobile_user_form', $data),
'cache-view' => false
],
],
'javascript' => '',
'otherdata' => ''
];
}
/**
* Returns the form to take attendance for the mobile app.
*
* @param array $args Arguments from tool_mobile_get_content WS
* @return array HTML, javascript and other data
*/
public static function mobile_teacher_form($args) {
global $OUTPUT, $DB, $CFG, $PAGE;
require_once($CFG->dirroot.'/mod/attendance/locallib.php');
$args = (object) $args;
$cmid = $args->cmid;
$courseid = $args->courseid;
$sessid = $args->sessid;
// Capabilities check.
$cm = get_coursemodule_from_id('attendance', $cmid);
require_login($courseid, false , $cm, true, true);
$context = \context_module::instance($cm->id);
require_capability('mod/attendance:takeattendances', $context);
$attendance = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST);
$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
$pageparams = new \mod_attendance_sessions_page_params();
$pageparams->sessionid = $sessid;
$att = new \mod_attendance_structure($attendance, $cm, $course, $context, $pageparams);
$data = array(); // Data to pass to renderer.
$data['attendance'] = $attendance;
$data['cmid'] = $cmid;
$data['courseid'] = $courseid;
$data['sessid'] = $sessid;
$data['messages'] = array();
$data['showmessage'] = false;
$data['statuses'] = array();
$data['btnargs'] = ''; // Stores list of userid status args that should be added to form post.
$statuses = $att->get_statuses();
$otherdata = array();
$existinglog = $DB->get_records('attendance_log',
array('sessionid' => $sessid), '', 'studentid,statusid');
foreach ($existinglog as $log) {
if (!empty($log->statusid)) {
$otherdata['status'.$log->studentid] = $log->statusid;
}
}
foreach ($statuses as $status) {
$data['statuses'][] = array('stid' => $status->id, 'acronym' => $status->acronym,
'description' => $status->description, 'selectall' => '');
}
$data['users'] = array();
$users = $att->get_users($att->get_session_info($sessid)->groupid, 0);
foreach ($users as $user) {
$userpicture = new \user_picture($user);
$userpicture->size = 1; // Size f1.
$profileimageurl = $userpicture->get_url($PAGE)->out(false);
$data['users'][] = array('userid' => $user->id, 'fullname' => $user->fullname, 'profileimageurl' => $profileimageurl);
// Generate args to use in submission button here.
$data['btnargs'] .= ', status'. $user->id. ': CONTENT_OTHERDATA.status'. $user->id;
// Really Hacky way to do a select-all. This really needs to be moved into a JS function within the app.
foreach ($statuses as $status) {
foreach ($data['statuses'] as $id => $st) { // Statuses not ordered by statusid.
if ($st['stid'] == $status->id) { // Find the item that we need to add to.
$data['statuses'][$id]['selectall'] .= "CONTENT_OTHERDATA.status".$user->id."=".$status->id.";";
}
}
}
}
if (!empty($data['messages'])) {
$data['showmessage'] = true;
}
return [
'templates' => [
[
'id' => 'main',
'html' => $OUTPUT->render_from_template('mod_attendance/mobile_teacher_form', $data),
'cache-view' => false
],
],
'javascript' => '',
'otherdata' => $otherdata
];
}
}

1
classes/page_with_filter_controls.php

@ -154,6 +154,7 @@ class mod_attendance_page_with_filter_controls {
$this->enddate = time();
break;
case ATT_VIEW_ALL:
case ATT_VIEW_NOTPRESENT:
$this->startdate = 0;
$this->enddate = 0;
break;

495
classes/privacy/provider.php

@ -0,0 +1,495 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* mod_attendance Data provider.
*
* @package mod_attendance
* @copyright 2018 Cameron Ball <cameron@cameron1729.xyz>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_attendance\privacy;
defined('MOODLE_INTERNAL') || die();
use context;
use context_module;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\{writer, transform, helper, contextlist, approved_contextlist};
use stdClass;
/**
* Data provider for mod_attendance.
*
* @copyright 2018 Cameron Ball <cameron@cameron1729.xyz>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class provider implements
\core_privacy\local\request\plugin\provider,
\core_privacy\local\metadata\provider
{
/**
* Returns meta data about this system.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection) : collection {
$collection->add_database_table(
'attendance_log',
[
'sessionid' => 'privacy:metadata:sessionid',
'studentid' => 'privacy:metadata:studentid',
'statusid' => 'privacy:metadata:statusid',
'statusset' => 'privacy:metadata:statusset',
'timetaken' => 'privacy:metadata:timetaken',
'takenby' => 'privacy:metadata:takenby',
'remarks' => 'privacy:metadata:remarks',
'ipaddress' => 'privacy:metadata:ipaddress'
],
'privacy:metadata:attendancelog'
);
$collection->add_database_table(
'attendance_sessions',
[
'groupid' => 'privacy:metadata:groupid',
'sessdate' => 'privacy:metadata:sessdate',
'duration' => 'privacy:metadata:duration',
'lasttaken' => 'privacy:metadata:lasttaken',
'lasttakenby' => 'privacy:metadata:lasttakenby',
'timemodified' => 'privacy:metadata:timemodified'
],
'privacy:metadata:attendancesessions'
);
$collection->add_database_table(
'attendance_warning_done',
[
'notifyid' => 'privacy:metadata:notifyid',
'userid' => 'privacy:metadata:userid',
'timesent' => 'privacy:metadata:timesent'
],
'privacy:metadata:attendancewarningdone'
);
return $collection;
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* In the case of attendance, that is any attendance where a student has had their
* attendance taken or has taken attendance for someone else.
*
* @param int $userid The user to search.
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid) : contextlist {
return (new contextlist)->add_from_sql(
"SELECT ctx.id
FROM {course_modules} cm
JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
JOIN {attendance} a ON cm.instance = a.id
JOIN {attendance_sessions} asess ON asess.attendanceid = a.id
JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel
JOIN {attendance_log} al ON asess.id = al.sessionid AND (al.studentid = :userid OR al.takenby = :takenbyid)",
[
'modulename' => 'attendance',
'contextlevel' => CONTEXT_MODULE,
'userid' => $userid,
'takenbyid' => $userid
]
);
}
/**
* Delete all data for all users in the specified context.
*
* @param context $context The specific context to delete data for.
*/
public static function delete_data_for_all_users_in_context(context $context) {
global $DB;
if (!$context instanceof context_module) {
return;
}
if (!$cm = get_coursemodule_from_id('attendance', $context->instanceid)) {
return;
}
// Delete all information recorded against sessions associated with this module.
$DB->delete_records_select(
'attendance_log',
"sessionid IN (SELECT id FROM {attendance_sessions} WHERE attendanceid = :attendanceid",
[
'attendanceid' => $cm->instance
]
);
// Delete all completed warnings associated with a warning associated with this module.
$DB->delete_records_select(
'attendance_warning_done',
"notifyid IN (SELECT id from {attendance_warning} WHERE idnumber = :attendanceid)",
['attendanceid' => $cm->instance]
);
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
global $DB;
$userid = (int)$contextlist->get_user()->id;
foreach ($contextlist as $context) {
if (!$context instanceof context_module) {
continue;
}
if (!$cm = get_coursemodule_from_id('attendance', $context->instanceid)) {
continue;
}
$attendanceid = (int)$DB->get_record('attendance', ['id' => $cm->instance])->id;
$sessionids = array_keys(
$DB->get_records('attendance_sessions', ['attendanceid' => $attendanceid])
);
self::delete_user_from_session_attendance_log($userid, $sessionids);
self::delete_user_from_sessions($userid, $sessionids);
self::delete_user_from_attendance_warnings_log($userid, $attendanceid);
}
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
global $DB;
$params = [
'modulename' => 'attendance',
'contextlevel' => CONTEXT_MODULE,
'studentid' => $contextlist->get_user()->id,
'takenby' => $contextlist->get_user()->id
];
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
$sql = "SELECT
al.*,
asess.id as session,
asess.description,
ctx.id as contextid,
a.name as attendancename,
a.id as attendanceid,
statuses.description as statusdesc, statuses.grade as statusgrade
FROM {course_modules} cm
JOIN {attendance} a ON cm.instance = a.id
JOIN {attendance_sessions} asess ON asess.attendanceid = a.id
JOIN {attendance_log} al on (al.sessionid = asess.id AND (studentid = :studentid OR al.takenby = :takenby))
JOIN {context} ctx ON cm.id = ctx.instanceid
JOIN {attendance_statuses} statuses ON statuses.id = al.statusid
WHERE (ctx.id {$contextsql})";
$attendances = $DB->get_records_sql($sql, $params + $contextparams);
self::export_attendance_logs(
get_string('attendancestaken', 'mod_attendance'),
array_filter(
$attendances,
function(stdClass $attendance) use ($contextlist) : bool {
return $attendance->takenby == $contextlist->get_user()->id;
}
)
);
self::export_attendance_logs(
get_string('attendanceslogged', 'mod_attendance'),
array_filter(
$attendances,
function(stdClass $attendance) use ($contextlist) : bool {
return $attendance->studentid == $contextlist->get_user()->id;
}
)
);
self::export_attendances(
$contextlist->get_user(),
$attendances,
self::group_by_property(
$DB->get_records_sql(
"SELECT
*,
a.id as attendanceid
FROM {attendance_warning_done} awd
JOIN {attendance_warning} aw ON awd.notifyid = aw.id
JOIN {attendance} a on aw.idnumber = a.id
WHERE userid = :userid",
['userid' => $contextlist->get_user()->id]
),
'notifyid'
)
);
}
/**
* Delete a user from session logs.
*
* @param int $userid The id of the user to remove.
* @param array $sessionids Array of session ids from which to remove the student from the relevant logs.
*/
private static function delete_user_from_session_attendance_log(int $userid, array $sessionids) {
global $DB;
// Delete records where user was marked as attending.
list($sessionsql, $sessionparams) = $DB->get_in_or_equal($sessionids, SQL_PARAMS_NAMED);
$DB->delete_records_select(
'attendance_log',
"(studentid = :studentid) AND sessionid $sessionsql",
['studentid' => $userid] + $sessionparams
);
// Get every log record where user took the attendance.
$attendancetakenids = array_keys(
$DB->get_records_sql(
"SELECT * from {attendance_log}
WHERE takenby = :takenbyid AND sessionid $sessionsql",
['takenbyid' => $userid] + $sessionparams
)
);
if (!$attendancetakenids) {
return;
}
// Don't delete the record from the log, but update to site admin taking attendance.
list($attendancetakensql, $attendancetakenparams) = $DB->get_in_or_equal($attendancetakenids, SQL_PARAMS_NAMED);
$DB->set_field_select(
'attendance_log',
'takenby',
2,
"id $attendancetakensql",
$attendancetakenparams
);
}
/**
* Delete a user from sessions.
*
* Not much user data is stored in a session, but it's possible that a user id is saved
* in the "lasttakenby" field.
*
* @param int $userid The id of the user to remove.
* @param array $sessionids Array of session ids from which to remove the student.
*/
private static function delete_user_from_sessions(int $userid, array $sessionids) {
global $DB;
// Get all sessions where user was last to mark attendance.
list($sessionsql, $sessionparams) = $DB->get_in_or_equal($sessionids, SQL_PARAMS_NAMED);
$sessionstaken = $DB->get_records_sql(
"SELECT * from {attendance_sessions}
WHERE lasttakenby = :lasttakenbyid AND id $sessionsql",
['lasttakenbyid' => $userid] + $sessionparams
);
if (!$sessionstaken) {
return;
}
// Don't delete the session, but update last taken by to the site admin.
list($sessionstakensql, $sessionstakenparams) = $DB->get_in_or_equal(array_keys($sessionstaken), SQL_PARAMS_NAMED);
$DB->set_field_select(
'attendance_sessions',
'lasttakenby',
2,
"id $sessionstakensql",
$sessionstakenparams
);
}
/**
* Delete a user from the attendance waring log.
*
* @param int $userid The id of the user to remove.
* @param int $attendanceid The id of the attendance instance to remove the relevant warnings from.
*/
private static function delete_user_from_attendance_warnings_log(int $userid, int $attendanceid) {
global $DB;
// Get all warnings because the user could have their ID listed in the thirdpartyemails column as a comma delimited string.
$warnings = $DB->get_records(
'attendance_warning',
['idnumber' => $attendanceid]
);
if (!$warnings) {
return;
}
// Update the third party emails list for all the relevant warnings.
$updatedwarnings = array_map(
function(stdClass $warning) use ($userid) : stdClass {
$warning->thirdpartyemails = implode(',', array_diff(explode(',', $warning->thirdpartyemails), [$userid]));
return $warning;
},
array_filter(
$warnings,
function (stdClass $warning) use ($userid) : bool {
return in_array($userid, explode(',', $warning->thirdpartyemails));
}
)
);
// Sadly need to update each individually, no way to bulk update as all the thirdpartyemails field can be different.
foreach ($updatedwarnings as $updatedwarning) {
$DB->update_record('attendance_warning', $updatedwarning);
}
// Delete any record of the user being notified.
list($warningssql, $warningsparams) = $DB->get_in_or_equal(array_keys($warnings), SQL_PARAMS_NAMED);
$DB->delete_records_select(
'attendance_warning_done',
"userid = :userid AND notifyid $warningssql",
['userid' => $userid] + $warningsparams
);
}
/**
* Helper function to group an array of stdClasses by a common property.
*
* @param array $classes An array of classes to group.
* @param string $property A common property to group the classes by.
*/
private static function group_by_property(array $classes, string $property) : array {
return array_reduce(
$classes,
function (array $classes, stdClass $class) use ($property) : array {
$classes[$class->{$property}][] = $class;
return $classes;
},
[]
);
}
/**
* Helper function to transform a row from the database in to session data to export.
*
* The properties of the "dbrow" are very specific to the result of the SQL from
* the export_user_data function.
*
* @param stdClass $dbrow A row from the database containing session information.
* @return stdClass The transformed row.
*/
private static function transform_db_row_to_session_data(stdClass $dbrow) : stdClass {
return (object) [
'name' => $dbrow->attendancename,
'session' => $dbrow->session,
'takenbyid' => $dbrow->takenby,
'studentid' => $dbrow->studentid,
'status' => $dbrow->statusdesc,
'grade' => $dbrow->statusgrade,
'sessiondescription' => $dbrow->description,
'timetaken' => transform::datetime($dbrow->timetaken),
'remarks' => $dbrow->remarks,
'ipaddress' => $dbrow->ipaddress
];
}
/**
* Helper function to transform a row from the database in to warning data to export.
*
* The properties of the "dbrow" are very specific to the result of the SQL from
* the export_user_data function.
*
* @param stdClass $warning A row from the database containing warning information.
* @return stdClass The transformed row.
*/
private static function transform_warning_data(stdClass $warning) : stdClass {
return (object) [
'timesent' => transform::datetime($warning->timesent),
'thirdpartyemails' => $warning->thirdpartyemails,
'subject' => $warning->emailsubject,
'body' => $warning->emailcontent
];
}
/**
* Helper function to export attendance logs.
*
* The array of "attendances" is actually the result returned by the SQL in export_user_data.
* It is more of a list of sessions. Which is why it needs to be grouped by context id.
*
* @param string $path The path in the export (relative to the current context).
* @param array $attendances Array of attendances to export the logs for.
*/
private static function export_attendance_logs(string $path, array $attendances) {
$attendancesbycontextid = self::group_by_property($attendances, 'contextid');
foreach ($attendancesbycontextid as $contextid => $sessions) {
$context = context::instance_by_id($contextid);
$sessionsbyid = self::group_by_property($sessions, 'sessionid');
foreach ($sessionsbyid as $sessionid => $sessions) {
writer::with_context($context)->export_data(
[get_string('session', 'attendance') . ' ' . $sessionid, $path],
(object)[array_map([self::class, 'transform_db_row_to_session_data'], $sessions)]
);
};
}
}
/**
* Helper function to export attendances (and associated warnings for the user).
*
* The array of "attendances" is actually the result returned by the SQL in export_user_data.
* It is more of a list of sessions. Which is why it needs to be grouped by context id.
*
* @param stdClass $user The user to export attendances for. This is needed to retrieve context data.
* @param array $attendances Array of attendances to export.
* @param array $warningsmap Mapping between an attendance id and warnings.
*/
private static function export_attendances(stdClass $user, array $attendances, array $warningsmap) {
$attendancesbycontextid = self::group_by_property($attendances, 'contextid');
foreach ($attendancesbycontextid as $contextid => $attendance) {
$context = context::instance_by_id($contextid);
// It's "safe" to get the attendanceid from the first element in the array - since they're grouped by context.
// i.e., module context.
// The reason there can be more than one "attendance" is that the attendances array will contain multiple records
// for the same attendance instance if there are multiple sessions. It is not the same as a raw record from the
// attendances table. See the SQL in export_user_data.
$warnings = array_map([self::class, 'transform_warning_data'], $warningsmap[$attendance[0]->attendanceid] ?? []);
writer::with_context($context)->export_data(
[],
(object)array_merge(
(array) helper::get_context_data($context, $user),
['warnings' => $warnings]
)
);
}
}
}

80
classes/structure.php

@ -23,7 +23,9 @@
*/
defined('MOODLE_INTERNAL') || die();
global $CFG; // This class is included inside existing functions.
require_once(dirname(__FILE__) . '/calendar_helpers.php');
require_once($CFG->libdir .'/filelib.php');
/**
* Main class with all Attendance related info.
@ -96,6 +98,10 @@ class mod_attendance_structure {
/** @var array of sessionid. */
private $sessioninfo = array();
/** @var float number [0..1], the threshold for student to be shown at low grade report */
private $lowgradethreshold;
/**
* Initializes the attendance API instance using the data from DB
*
@ -450,13 +456,18 @@ class mod_attendance_structure {
public function add_sessions($sessions) {
global $DB;
$config = get_config('attendance');
foreach ($sessions as $sess) {
$sess->attendanceid = $this->id;
$sess->automarkcompleted = 0;
if (!isset($sess->automark)) {
$sess->automark = 0;
}
if (empty($config->enablecalendar)) {
// If calendard disabled at site level, don't use it.
$sess->calendarevent = 0;
}
$sess->id = $DB->insert_record('attendance_sessions', $sess);
$description = file_save_draft_area_files($sess->descriptionitemid,
$this->context->id, 'mod_attendance', 'session', $sess->id,
@ -500,7 +511,9 @@ class mod_attendance_structure {
if (!isset($sess->preventsharediptime)) {
$sess->preventsharediptime = '';
}
if (!isset($sess->includeqrcode)) {
$sess->includeqrcode = 0;
}
$event->add_record_snapshot('attendance_sessions', $sess);
$event->trigger();
}
@ -530,6 +543,7 @@ class mod_attendance_structure {
array('subdirs' => false, 'maxfiles' => -1, 'maxbytes' => 0), $formdata->sdescription['text']);
$sess->description = $description;
$sess->descriptionformat = $formdata->sdescription['format'];
$sess->calendarevent = empty($formdata->calendarevent) ? 0 : $formdata->calendarevent;
$sess->studentscanmark = 0;
$sess->autoassignstatus = 0;
@ -539,6 +553,7 @@ class mod_attendance_structure {
$sess->automarkcompleted = 0;
$sess->preventsharedip = 0;
$sess->preventsharediptime = '';
$sess->includeqrcode = 0;
if (!empty(get_config('attendance', 'enablewarnings'))) {
$sess->absenteereport = empty($formdata->absenteereport) ? 0 : 1;
}
@ -565,6 +580,9 @@ class mod_attendance_structure {
if (!empty($formdata->preventsharediptime)) {
$sess->preventsharediptime = $formdata->preventsharediptime;
}
if (!empty($formdata->includeqrcode)) {
$sess->includeqrcode = $formdata->includeqrcode;
}
}
@ -575,7 +593,7 @@ class mod_attendance_structure {
// This shouldn't really happen, but just in case to prevent fatal error.
attendance_create_calendar_event($sess);
} else {
attendance_update_calendar_event($sess->caleventid, $sess->duration, $sess->sessdate);
attendance_update_calendar_event($sess);
}
$info = construct_session_full_date_time($sess->sessdate, $sess->duration);
@ -611,8 +629,10 @@ class mod_attendance_structure {
$record->takenby = $USER->id;
$record->ipaddress = getremoteaddr(null);
$dbsesslog = $this->get_session_log($mformdata->sessid);
if (array_key_exists($record->studentid, $dbsesslog)) {
$existingattendance = $DB->record_exists('attendance_log',
array('sessionid' => $mformdata->sessid, 'studentid' => $USER->id));
if ($existingattendance) {
// Already recorded do not save.
return false;
}
@ -724,28 +744,6 @@ class mod_attendance_structure {
$event->add_record_snapshot('course_modules', $this->cm);
$event->add_record_snapshot('attendance_sessions', $session);
$event->trigger();
$group = 0;
if ($this->pageparams->grouptype != self::SESSION_COMMON) {
$group = $this->pageparams->grouptype;
} else {
if ($this->pageparams->group) {
$group = $this->pageparams->group;
}
}
$totalusers = count_enrolled_users(context_module::instance($this->cm->id), 'mod/attendance:canbelisted', $group);
$usersperpage = $this->pageparams->perpage;
if (!empty($this->pageparams->page) && $this->pageparams->page && $totalusers && $usersperpage) {
$numberofpages = ceil($totalusers / $usersperpage);
if ($this->pageparams->page < $numberofpages) {
$params['page'] = $this->pageparams->page + 1;
redirect($this->url_take($params), get_string('moreattendance', 'attendance'));
}
}
redirect($this->url_manage(), get_string('attendancesuccess', 'attendance'));
}
/**
@ -786,7 +784,7 @@ class mod_attendance_structure {
$groups = $groupid;
}
$users = get_users_by_capability($this->context, 'mod/attendance:canbelisted',
$userfields.',u.id, u.firstname, u.lastname, u.email',
$userfields,
$orderby, $startusers, $usersperpage, $groups,
'', false, true);
} else {
@ -802,7 +800,7 @@ class mod_attendance_structure {
$groups = $groupid;
}
$users = get_users_by_capability($this->context, 'mod/attendance:canbelisted',
$userfields.',u.id, u.firstname, u.lastname, u.email',
$userfields,
$orderby, '', '', $groups,
'', false, true);
} else {
@ -832,6 +830,7 @@ class mod_attendance_structure {
$enrolments = $DB->get_records_sql($sql, $params);
foreach ($users as $user) {
$users[$user->id]->fullname = fullname($user);
$users[$user->id]->enrolmentstatus = $enrolments[$user->id]->status;
$users[$user->id]->enrolmentstart = $enrolments[$user->id]->mintime;
$users[$user->id]->enrolmentend = $enrolments[$user->id]->maxtime;
@ -1196,7 +1195,7 @@ class mod_attendance_structure {
$sess->timemodified = $now;
$DB->update_record('attendance_sessions', $sess);
if ($sess->caleventid) {
attendance_update_calendar_event($sess->caleventid, $duration, $sess->sessdate);
attendance_update_calendar_event($sess);
}
$event = \mod_attendance\event\session_duration_updated::create(array(
'objectid' => $this->id,
@ -1271,4 +1270,25 @@ class mod_attendance_structure {
}
return;
}
/**
* Gets the lowgrade threshold to use.
*
*/
public function get_lowgrade_threshold() {
if (!isset($this->lowgradethreshold)) {
$this->lowgradethreshold = 1;
if ($this->grade > 0) {
$gradeitem = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod',
'itemmodule' => 'attendance', 'iteminstance' => $this->id));
if ($gradeitem->gradepass > 0 && $gradeitem->grademax != $gradeitem->grademin) {
$this->lowgradethreshold = ($gradeitem->gradepass - $gradeitem->grademin) /
($gradeitem->grademax - $gradeitem->grademin);
}
}
}
return $this->lowgradethreshold;
}
}

21
classes/summary.php

@ -137,6 +137,11 @@ class mod_attendance_summary {
$usersummary->userstakensessionsbyacronym = array();
}
$usersummary->pointssessionscompleted = format_float($usersummary->takensessionspoints, 1, true, true) . ' / ' .
format_float($usersummary->takensessionsmaxpoints, 1, true, true);
$usersummary->percentagesessionscompleted = format_float($usersummary->takensessionspercentage * 100) . '%';
return $usersummary;
}
@ -168,11 +173,25 @@ class mod_attendance_summary {
}
$usersummary->allsessionspercentage = attendance_calc_fraction($usersummary->takensessionspoints,
$usersummary->allsessionsmaxpoints);
$usersummary->allsessionspercentage = format_float($usersummary->allsessionspercentage * 100) . '%';
$deltapoints = $usersummary->allsessionsmaxpoints - $usersummary->takensessionsmaxpoints;
$usersummary->maxpossiblepoints = $usersummary->takensessionspoints + $deltapoints;
$usersummary->maxpossiblepercentage = attendance_calc_fraction($usersummary->maxpossiblepoints,
$usersummary->maxpossiblepoints = format_float($usersummary->maxpossiblepoints, 1, true, true) . ' / ' .
format_float($usersummary->allsessionsmaxpoints, 1, true, true);
$usersummary->maxpossiblepercentage = attendance_calc_fraction(($usersummary->takensessionspoints + $deltapoints),
$usersummary->allsessionsmaxpoints);
$usersummary->maxpossiblepercentage = format_float($usersummary->maxpossiblepercentage * 100) . '%';
$usersummary->pointssessionscompleted = format_float($usersummary->takensessionspoints, 1, true, true) . ' / ' .
format_float($usersummary->takensessionsmaxpoints, 1, true, true);
$usersummary->percentagesessionscompleted = format_float($usersummary->takensessionspercentage * 100) . '%';
$usersummary->pointsallsessions = format_float($usersummary->takensessionspoints, 1, true, true) . ' / ' .
format_float($usersummary->allsessionsmaxpoints, 1, true, true);
return $usersummary;
}

2
classes/task/notify.php

@ -94,7 +94,7 @@ class notify extends \core\task\scheduled_task {
$oldforcelang = force_current_language($user->lang);
$emailcontent = format_text($record->emailcontent, $record->emailcontentformat);
$emailsubject = format_text($record->emailsubject);
$emailsubject = format_text($record->emailsubject, FORMAT_HTML);
email_to_user($user, $from, $emailsubject, $emailcontent, $emailcontent);
force_current_language($oldforcelang);

8
coursesummary.php

@ -95,8 +95,14 @@ $table->setup();
// Work out direction of sort required.
$sortcolumns = $table->get_sort_columns();
// Now do sorting if specified.
// Sanity check $sort var before including in sql. Make sure it matches a known column.
$allowedsort = array_diff(array_keys($table->columns), $table->column_nosort);
if (!in_array($sort, $allowedsort)) {
$sort = '';
}
// Now do sorting if specified.
$orderby = ' ORDER BY percentage ASC';
if (!empty($sort)) {
$direction = ' DESC';

10
db/install.xml

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mod/attendance/db" VERSION="20170620" COMMENT="XMLDB file for Moodle mod/attendance"
<XMLDB PATH="mod/attendance/db" VERSION="20190125" COMMENT="XMLDB file for Moodle mod/attendance"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
@ -45,9 +45,11 @@
<FIELD NAME="automarkcompleted" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="statusset" TYPE="int" LENGTH="5" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Which set of statuses to use"/>
<FIELD NAME="absenteereport" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
<FIELD NAME="preventsharedip" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
<FIELD NAME="preventsharedip" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="preventsharediptime" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="caleventid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="calendarevent" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
<FIELD NAME="includeqrcode" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Include a QR code image when displaying the password"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="Primary key for attendance_sessions"/>
@ -65,7 +67,7 @@
<FIELD NAME="sessionid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="studentid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="statusid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="link with attendance_status table"/>
<FIELD NAME="statusset" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="statusset" TYPE="char" LENGTH="1333" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="timetaken" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="When attendance of this student was taken"/>
<FIELD NAME="takenby" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="remarks" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
@ -125,7 +127,7 @@
<FIELD NAME="idnumber" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="attendance id or other id relating to this warning."/>
<FIELD NAME="warningpercent" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Percentage that triggers this warning."/>
<FIELD NAME="warnafter" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Start warning after this number of taken sessions."/>
<FIELD NAME="maxwarn" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Maximum number of warnings to send."/>
<FIELD NAME="maxwarn" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Maximum number of warnings to send."/>
<FIELD NAME="emailuser" TYPE="int" LENGTH="4" NOTNULL="true" SEQUENCE="false" COMMENT="Should the user be notified at this level."/>
<FIELD NAME="emailsubject" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="Email subject line for emails going to user"/>
<FIELD NAME="emailcontent" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="The html-formatted text that should be sent to the user"/>

70
db/mobile.php

@ -0,0 +1,70 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Defines mobile handlers.
*
* @package mod_attendance
* @copyright 2018 Dan Marsdenb
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$addons = [
'mod_attendance' => [
'handlers' => [
'view' => [
'displaydata' => [
'icon' => $CFG->wwwroot . '/mod/attendance/pix/icon.png',
'class' => '',
],
'delegate' => 'CoreCourseModuleDelegate',
'method' => 'mobile_view_activity',
'styles' => [
'url' => '/mod/attendance/mobilestyles.css',
'version' => 22
]
]
],
'lang' => [ // Language strings that are used in all the handlers.
['pluginname', 'attendance'],
['sessionscompleted', 'attendance'],
['pointssessionscompleted', 'attendance'],
['percentagesessionscompleted', 'attendance'],
['sessionstotal', 'attendance'],
['pointsallsessions', 'attendance'],
['percentageallsessions', 'attendance'],
['maxpossiblepoints', 'attendance'],
['maxpossiblepercentage', 'attendance'],
['submitattendance', 'attendance'],
['strftimeh', 'attendance'],
['strftimehm', 'attendance'],
['attendancesuccess', 'attendance'],
['attendance_no_status', 'attendance'],
['attendance_already_submitted', 'attendance'],
['somedisabledstatus', 'attendance'],
['invalidstatus', 'attendance'],
['preventsharederror', 'attendance'],
['closed', 'attendance'],
['subnetwrong', 'attendance'],
['enterpassword', 'attendance'],
['incorrectpasswordshort', 'attendance'],
['attendancesuccess', 'attendance'],
['setallstatuses', 'attendance']
],
]
];

74
db/upgrade.php

@ -386,7 +386,7 @@ function xmldb_attendance_upgrade($oldversion=0) {
// Adding keys to table attendance_warning.
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$table->add_key('level_id', XMLDB_KEY_UNIQUE, array('idnumber, warningpercent, warnafter'));
$table->add_key('level_id', XMLDB_KEY_UNIQUE, array('idnumber', 'warningpercent', 'warnafter'));
// Conditionally launch create table for attendance_warning.
$dbman->create_table($table);
@ -401,7 +401,7 @@ function xmldb_attendance_upgrade($oldversion=0) {
$DB->execute("DROP INDEX ". $name);
}
}
$index = new xmldb_key('level_id', XMLDB_KEY_UNIQUE, array('idnumber, warningpercent', 'warnafter'));
$index = new xmldb_key('level_id', XMLDB_KEY_UNIQUE, array('idnumber', 'warningpercent', 'warnafter'));
$dbman->add_key($table, $index);
}
// Attendance savepoint reached.
@ -426,7 +426,9 @@ function xmldb_attendance_upgrade($oldversion=0) {
$table = new xmldb_table('attendance_warning_done');
$index = new xmldb_index('notifyid_userid', XMLDB_INDEX_UNIQUE, array('notifyid', 'userid'));
if ($dbman->index_exists($table, $index)) {
$dbman->drop_index($table, $index);
}
$index = new xmldb_index('notifyid', XMLDB_INDEX_NOTUNIQUE, array('notifyid', 'userid'));
$dbman->add_index($table, $index);
@ -511,5 +513,73 @@ function xmldb_attendance_upgrade($oldversion=0) {
upgrade_mod_savepoint(true, 2018050100, 'attendance');
}
if ($oldversion < 2018051402) {
$table = new xmldb_table('attendance_sessions');
$field = new xmldb_field('calendarevent', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED,
XMLDB_NOTNULL, null, '1', 'caleventid');
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
if (empty(get_config('attendance', 'enablecalendar'))) {
// Calendar disabled on this site, set calendarevent for existing records to 0.
$DB->execute("UPDATE {attendance_sessions} set calendarevent = 0");
}
}
upgrade_mod_savepoint(true, 2018051402, 'attendance');
}
if ($oldversion < 2018051404) {
$table = new xmldb_table('attendance_sessions');
$field = new xmldb_field('includeqrcode', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED,
XMLDB_NOTNULL, null, '0', 'calendarevent');
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
upgrade_mod_savepoint(true, 2018051404, 'attendance');
}
if ($oldversion < 2018051408) {
// Changing precision of field statusset on table attendance_log to (1333).
$table = new xmldb_table('attendance_log');
$field = new xmldb_field('statusset', XMLDB_TYPE_CHAR, '1333', null, null, null, null, 'statusid');
// Launch change of precision for field statusset.
$dbman->change_field_precision($table, $field);
// Attendance savepoint reached.
upgrade_mod_savepoint(true, 2018051408, 'attendance');
}
if ($oldversion < 2018051409) {
// Make sure default value to '0'.
$table = new xmldb_table('attendance_sessions');
$field = new xmldb_field('preventsharedip', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED,
XMLDB_NOTNULL, null, '0', 'absenteereport');
if ($dbman->field_exists($table, $field)) {
$dbman->change_field_default($table, $field);
}
// Make sure sessiondetailspos is not null.
$table = new xmldb_table('attendance');
$field = new xmldb_field('sessiondetailspos', XMLDB_TYPE_CHAR, '5', null, XMLDB_NOTNULL, null, 'left', 'subnet');
if ($dbman->field_exists($table, $field)) {
$dbman->change_field_notnull($table, $field);
}
// Make sure maxwarn has default value of '1'.
$table = new xmldb_table('attendance_warning');
$field = new xmldb_field('maxwarn', XMLDB_TYPE_INTEGER, '10', null, true, null, '1', 'warnafter');
if ($dbman->field_exists($table, $field)) {
$dbman->change_field_default($table, $field);
}
// Attendance savepoint reached.
upgrade_mod_savepoint(true, 2018051409, 'attendance');
}
return $result;
}

4
defaultstatus.php

@ -76,7 +76,7 @@ switch ($action) {
break;
}
$message = get_string('deletecheckfull', '', get_string('variable', 'attendance'));
$message = get_string('deletecheckfull', 'attendance', get_string('variable', 'attendance'));
$message .= str_repeat(html_writer::empty_tag('br'), 2);
$message .= $status->acronym.': '.
($status->description ? $status->description : get_string('nodescription', 'attendance'));
@ -113,7 +113,7 @@ switch ($action) {
if ($unmarkedstatus == $id) {
$setunmarked = true;
}
if (!isset($studentavailability[$id])) {
if (!isset($studentavailability[$id]) || !is_numeric($studentavailability[$id])) {
$studentavailability[$id] = 0;
}
$errors[$id] = attendance_update_status($status, $acronym[$id], $description[$id], $grade[$id],

10
export.php

@ -22,6 +22,8 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('NO_OUTPUT_BUFFERING', true);
require_once(dirname(__FILE__).'/../../config.php');
require_once(dirname(__FILE__).'/locallib.php');
require_once(dirname(__FILE__).'/export_form.php');
@ -44,6 +46,7 @@ $att = new mod_attendance_structure($att, $cm, $course, $context);
$PAGE->set_url($att->url_export());
$PAGE->set_title($course->shortname. ": ".$att->name);
$PAGE->set_heading($course->fullname);
$PAGE->force_settings_menu(true);
$PAGE->set_cacheable(true);
$PAGE->navbar->add(get_string('export', 'attendance'));
@ -76,7 +79,9 @@ if ($formdata = $mform->get_data()) {
$reportdata = new attendance_report_data($att);
if ($reportdata->users) {
$filename = clean_filename($course->shortname.'_Attendances_'.userdate(time(), '%Y%m%d-%H%M'));
$filename = clean_filename($course->shortname.'_'.
get_string('modulenameplural', 'attendance').
'_'.userdate(time(), '%Y%m%d-%H%M'));
$group = $formdata->group ? $reportdata->groups[$formdata->group] : 0;
$data = new stdClass;
@ -178,8 +183,7 @@ if ($formdata = $mform->get_data()) {
}
$data->table[$i][] = $usersummary->numtakensessions;
$data->table[$i][] = format_float($usersummary->takensessionspoints, 1, true, true) . ' / ' .
format_float($usersummary->takensessionsmaxpoints, 1, true, true);
$data->table[$i][] = $usersummary->pointssessionscompleted;
$data->table[$i][] = format_float($usersummary->takensessionspercentage * 100);
$i++;

3
externallib.php

@ -72,7 +72,8 @@ class mod_wsattendance_external extends external_api {
'autoassignstatus' => new external_value(PARAM_INT, 'Automatically assign a status to students.'),
'preventsharedip' => new external_value(PARAM_INT, 'Prevent students from sharing IP addresses.'),
'preventsharediptime' => new external_value(PARAM_INT, 'Time delay before IP address is allowed again.'),
'statusset' => new external_value(PARAM_INT, 'Session statusset.'));
'statusset' => new external_value(PARAM_INT, 'Session statusset.'),
'includeqrcode' => new external_value(PARAM_INT, 'Include QR code when displaying password'));
return $session;
}

63
index.php

@ -30,11 +30,64 @@ $course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST);
require_login($course);
$PAGE->set_url('/mod/attendance/index.php', array('id' => $id));
$PAGE->set_pagelayout('incourse');
// TODO: check if this is correct behaviour - other modules list all the instances of the module in the course.
if ($att = get_all_instances_in_course('attendance', $course, null, true)) {
$att = array_pop($att);
redirect("view.php?id=$att->coursemodule");
\mod_attendance\event\course_module_instance_list_viewed::create_from_course($course)->trigger();
// Print the header.
$strplural = get_string("modulename", "attendance");
$PAGE->navbar->add($strplural);
$PAGE->set_title($strplural);
$PAGE->set_heading($course->fullname);
echo $OUTPUT->header();
echo $OUTPUT->heading(format_string($strplural));
$context = context_course::instance($course->id);
require_capability('mod/attendance:view', $context);
if (! $atts = get_all_instances_in_course("attendance", $course)) {
$url = new moodle_url('/course/view.php', array('id' => $course->id));
notice(get_string('thereareno', 'moodle', $strplural), $url);
die;
}
$usesections = course_format_uses_sections($course->format);
// Print the list of instances.
$timenow = time();
$strname = get_string("name");
$table = new html_table();
if ($usesections) {
$strsectionname = get_string('sectionname', 'format_'.$course->format);
$table->head = array ($strsectionname, $strname);
$table->align = array ("center", "left");
} else {
print_error('notfound', 'attendance');
$table->head = array ($strname);
$table->align = array ("left");
}
foreach ($atts as $att) {
// Get the responses of each attendance.
$viewurl = new moodle_url('/mod/attendance/view.php', array('id' => $att->coursemodule));
$dimmedclass = $att->visible ? '' : 'class="dimmed"';
$link = '<a '.$dimmedclass.' href="'.$viewurl->out().'">'.$att->name.'</a>';
if ($usesections) {
$tabledata = array (get_section_name($course, $att->section), $link);
} else {
$tabledata = array ($link);
}
$table->data[] = $tabledata;
}
echo "<br />";
echo html_writer::table($table);
echo $OUTPUT->footer();

40
lang/en/attendance.php

@ -53,7 +53,7 @@ $string['attendance:view'] = 'Viewing Attendances';
$string['attendance:viewreports'] = 'Viewing Reports';
$string['attendance:viewsummaryreports'] = 'View course summary reports';
$string['attendance:warningemails'] = 'Can be subscribed to emails with absentee users';
$string['attendance_already_submitted'] = 'You may not self register attendance that has already been set.';
$string['attendance_already_submitted'] = 'Your attendance has already been set.';
$string['attendancedata'] = 'Attendance data';
$string['attendanceforthecourse'] = 'Attendance for the course';
$string['attendancegrade'] = 'Attendance grade';
@ -61,6 +61,8 @@ $string['attendancenotset'] = 'You must set your attendance';
$string['attendancenotstarted'] = 'Attendance has not started yet for this course';
$string['attendancepercent'] = 'Attendance percent';
$string['attendancereport'] = 'Attendance report';
$string['attendanceslogged'] = 'Attendances logged';
$string['attendancestaken'] = 'Attendances taken';
$string['attendancesuccess'] = 'Attendance has been successfully taken';
$string['attendanceupdated'] = 'Attendance successfully updated';
$string['attforblockdirstillexists'] = 'old mod/attforblock directory still exists - you must delete this directory on your server before running this upgrade.';
@ -76,6 +78,9 @@ $string['autorecorded'] = 'system auto recorded';
$string['averageattendance'] = 'Average attendance';
$string['averageattendancegraded'] = 'Average attendance';
$string['calclose'] = 'Close';
$string['calendarevent'] = 'Create calendar event for session';
$string['calendarevent_help'] = 'If enabled, a calendar event will be created for this session.
If disabled, any existing calendar event for this session will be deleted.';
$string['caleventcreated'] = 'Calendar event for session successfully created';
$string['caleventdeleted'] = 'Calendar event for session successfully deleted';
$string['calmonths'] = 'January,February,March,April,May,June,July,August,September,October,November,December';
@ -89,6 +94,7 @@ $string['changeattendance'] = 'Change attendance';
$string['changeduration'] = 'Change duration';
$string['changesession'] = 'Change session';
$string['checkweekdays'] = 'Select weekdays that fall within your selected session date range.';
$string['closed'] = 'This session is not currently available for self-marking';
$string['column'] = 'column';
$string['columns'] = 'columns';
$string['commonsession'] = 'All students';
@ -100,6 +106,7 @@ $string['confirmdeleteuser'] = 'Are you sure you want to delete user \'{$a->full
$string['copyfrom'] = 'Copy attendance data from';
$string['countofselected'] = 'Count of selected';
$string['course'] = 'Course';
$string['courseshortname'] = 'Course shortname';
$string['coursesummary'] = 'Course summary report';
$string['createmultiplesessions'] = 'Create multiple sessions';
$string['createmultiplesessions_help'] = 'This function allows you to create multiple sessions in one simple step.
@ -132,6 +139,7 @@ $string['defaultview_desc'] = 'This is the default view shown to teachers on fir
$string['delete'] = 'Delete';
$string['deletewarningconfirm'] = 'Are you sure you want to delete this warning?';
$string['deletedgroup'] = 'The group associated with this session has been deleted';
$string['deletecheckfull'] = 'Are you absolutely sure you want to completely delete the {$a}, including all user data?';
$string['deletehiddensessions'] = 'Delete all hidden sessions';
$string['deletelogs'] = 'Delete attendance data';
$string['deleteselected'] = 'Delete selected';
@ -234,8 +242,10 @@ $string['importsessions'] = 'Import Sessions';
$string['identifyby'] = 'Identify student by';
$string['includeall'] = 'Select all sessions';
$string['includenottaken'] = 'Include not taken sessions';
$string['includeqrcode'] = 'Include QR code';
$string['includeremarks'] = 'Include remarks';
$string['incorrectpassword'] = 'You have entered an incorrect password and your attendance has not been recorded, please enter the correct password.';
$string['incorrectpasswordshort'] = 'Incorrect password, attendance not recorded.';
$string['indetail'] = 'In detail...';
$string['invalidaction'] = 'You must select an action';
$string['invalidemails'] = 'You must specify addresses of existing user accounts, could not find: {$a}';
@ -245,7 +255,7 @@ $string['invalidsessionendtime'] = 'The end time must be greater than start time
$string['invalidstatus'] = 'You have selected an invalid status, please try again';
$string['iptimemissing'] = 'Invalid minutes to release';
$string['jumpto'] = 'Jump to';
$string['lowgrade'] = 'Low grade';
$string['below'] = 'Below {$a}%';
$string['maxpossible'] = 'Maximum possible';
$string['maxpossible_help'] = 'Shows the score each user can reach if they receive the maximum points in each session not yet taken (past and future):
<ul>
@ -315,8 +325,10 @@ $string['oversessionstaken_help'] = 'Shows statistics for sessions where attenda
<li><strong>Points</strong>: points awarded based on the taken sessions.</li>
<li><strong>Percentage</strong>: percentage of points awarded over the maxium possible points of the taken sessions.</li>
</ul>';
$string['pageof'] = 'Page {$a->page} of {$a->numpages}';
$string['participant'] = 'Participant';
$string['password'] = 'Password';
$string['enterpassword'] = 'Enter password';
$string['passwordgrp'] = 'Student password';
$string['passwordgrp_help'] = 'If set students will be required to enter this password before they can set their own attendance status for the session. If empty, no password is required.';
$string['passwordrequired'] = 'You must enter the session password before you can submit your attendance';
@ -333,10 +345,10 @@ $string['preventsharedip'] = 'Prevent students sharing IP address';
$string['preventsharedip_help'] = 'Prevent students from using the same device (identified using IP address) to take attendance for other students.';
$string['preventsharediptime'] = 'Time to allow re-use of IP address (minutes)';
$string['preventsharediptime_help'] = 'Allow an IP address to be re-used for taking attendance in this session after this time has elapsed.';
$string['preventsharedipminutes'] = '(minutes to release IP)';
$string['preventsharederror'] = 'Self-marking has been disabled for a session because this device appears to have been used to record attendance for another student.';
$string['priorto'] = 'The session date is prior to the course start date ({$a}) so that the new sessions scheduled before this date will be hidden (not accessible). You can change the course start date at any time (see course settings) in order to have access to earlier sessions.<br><br>Please change the session date or just click the "Add session" button again to confirm?';
$string['processingfile'] = 'Processing file';
$string['qrcode'] = 'QR Code';
$string['randompassword'] = 'Random password';
$string['remark'] = 'Remark for: {$a}';
$string['remarks'] = 'Remarks';
@ -423,12 +435,14 @@ $string['sessionupdated'] = 'Session successfully updated';
$string['set_by_student'] = 'Self-recorded';
$string['setallstatuses'] = 'Set status for all users';
$string['setallstatusesto'] = 'Set status for all users to «{$a}»';
$string['setperiod'] = 'Specified time in minutes to release IP';
$string['settings'] = 'Settings';
$string['setunmarked'] = 'Automatically set when not marked';
$string['setunmarked_help'] = 'If enabled in the session, set this status if a student has not marked their own attendance.';
$string['showdefaults'] = 'Show defaults';
$string['showduration'] = 'Show duration';
$string['showextrauserdetails'] = 'Show extra user details';
$string['showqrcode'] = 'Show QR Code';
$string['showsessiondetails'] = 'Show session details';
$string['showsessiondescriptiononreport'] = 'Show session description in report';
$string['showsessiondescriptiononreport_desc'] = 'Show the session description in the attendance report listing.';
@ -520,3 +534,23 @@ $string['includeabsentee'] = 'Include session when calculating absentee report';
$string['includeabsentee_help'] = 'If checked this session will be included in the absentee report calculations.';
$string['attendance_no_status'] = 'No valid status was available - you may be too late to record attendance.';
$string['studentmarked'] = 'Your attendance in this session has been recorded.';
$string['privacy:metadata:sessionid'] = 'Attendance session ID.';
$string['privacy:metadata:studentid'] = 'ID of student having attendance recorded.';
$string['privacy:metadata:statusid'] = 'ID of student\'s attendance status.';
$string['privacy:metadata:statusset'] = 'Status set to which status ID belongs.';
$string['privacy:metadata:timetaken'] = 'Timestamp of when attendance was taken for the student.';
$string['privacy:metadata:takenby'] = 'User ID of the user who took attendance for the student.';
$string['privacy:metadata:remarks'] = 'Comments about the user\'s attendance.';
$string['privacy:metadata:ipaddress'] = 'IP address attendance was marked from.';
$string['privacy:metadata:groupid'] = 'Group ID associated with session.';
$string['privacy:metadata:sessdate'] = 'Timestamp of when session starts.';
$string['privacy:metadata:duration'] = 'Session duration in seconds';
$string['privacy:metadata:lasttaken'] = 'Timestamp of when session attendance was last taken.';
$string['privacy:metadata:lasttakenby'] = 'User ID of the last user to take attendance in this session';
$string['privacy:metadata:timemodified'] = 'Timestamp of when session was last modified';
$string['privacy:metadata:notifyid'] = 'ID of attendance session warning is associated with.';
$string['privacy:metadata:userid'] = 'ID of user to send warning to.';
$string['privacy:metadata:timesent'] = 'Timestamp when warning was sent.';
$string['privacy:metadata:attendancelog'] = 'Log of user attendances recorded.';
$string['privacy:metadata:attendancesessions'] = 'Sessions to which attendance will be recorded.';
$string['privacy:metadata:attendancewarningdone'] = 'Log of warnings sent to users over their attendance record.';

5
lib.php

@ -40,6 +40,8 @@ function attendance_supports($feature) {
return true;
case FEATURE_GROUPINGS:
return true;
case FEATURE_SHOW_DESCRIPTION:
return true;
case FEATURE_MOD_INTRO:
return true;
case FEATURE_BACKUP_MOODLE2:
@ -285,8 +287,7 @@ function attendance_user_outline($course, $user, $mod, $attendance) {
$summary = new mod_attendance_summary($attendance->id, $user->id);
$usersummary = $summary->get_all_sessions_summary_for($user->id);
$result->info = format_float($usersummary->takensessionspoints, 1, true, true) . ' / ' .
format_float($usersummary->allsessionsmaxpoints, 1, true, true);
$result->info = $usersummary->pointsallsessions;
}
return $result;

138
locallib.php

@ -43,6 +43,10 @@ define('ATTENDANCE_AUTOMARK_DISABLED', 0);
define('ATTENDANCE_AUTOMARK_ALL', 1);
define('ATTENDANCE_AUTOMARK_CLOSE', 2);
define('ATTENDANCE_SHAREDIP_DISABLED', 0);
define('ATTENDANCE_SHAREDIP_MINUTES', 1);
define('ATTENDANCE_SHAREDIP_FORCE', 2);
// Max number of sessions available in the warnings set form to trigger warnings.
define('ATTENDANCE_MAXWARNAFTER', 100);
@ -95,7 +99,7 @@ function attendance_get_setname($attid, $statusset, $includevalues = true) {
if ($statusesout) {
if (count($statusesout) > 6) {
$statusesout = array_slice($statusesout, 0, 6);
$statusesout[] = '&helip;';
$statusesout[] = '...';
}
$statusesout = implode(' ', $statusesout);
$statusname .= ' ('.$statusesout.')';
@ -173,12 +177,21 @@ function attendance_form_sessiondate_selector (MoodleQuickForm $mform) {
}
$sesendtime = array();
if (!right_to_left()) {
$sesendtime[] =& $mform->createElement('static', 'from', '', get_string('from', 'attendance'));
$sesendtime[] =& $mform->createElement('select', 'starthour', get_string('hour', 'form'), $hours, false, true);
$sesendtime[] =& $mform->createElement('select', 'startminute', get_string('minute', 'form'), $minutes, false, true);
$sesendtime[] =& $mform->createElement('static', 'to', '', get_string('to', 'attendance'));
$sesendtime[] =& $mform->createElement('select', 'endhour', get_string('hour', 'form'), $hours, false, true);
$sesendtime[] =& $mform->createElement('select', 'endminute', get_string('minute', 'form'), $minutes, false, true);
} else {
$sesendtime[] =& $mform->createElement('static', 'from', '', get_string('from', 'attendance'));
$sesendtime[] =& $mform->createElement('select', 'startminute', get_string('minute', 'form'), $minutes, false, true);
$sesendtime[] =& $mform->createElement('select', 'starthour', get_string('hour', 'form'), $hours, false, true);
$sesendtime[] =& $mform->createElement('static', 'to', '', get_string('to', 'attendance'));
$sesendtime[] =& $mform->createElement('select', 'endminute', get_string('minute', 'form'), $minutes, false, true);
$sesendtime[] =& $mform->createElement('select', 'endhour', get_string('hour', 'form'), $hours, false, true);
}
$mform->addGroup($sesendtime, 'sestime', get_string('time', 'attendance'), array(' '), true);
}
@ -422,15 +435,18 @@ function attendance_random_string($length=6) {
* Check to see if this session is open for student marking.
*
* @param stdclass $sess the session record from attendance_sessions.
* @return boolean
* @param boolean $log - if student cannot mark, generate log event.
* @return array (boolean, string reason for failure)
*/
function attendance_can_student_mark($sess) {
function attendance_can_student_mark($sess, $log = true) {
global $DB, $USER, $OUTPUT;
$canmark = false;
$reason = 'closed';
$attconfig = get_config('attendance');
if (!empty($attconfig->studentscanmark) && !empty($sess->studentscanmark)) {
if (empty($attconfig->studentscanmarksessiontime)) {
$canmark = true;
$reason = '';
} else {
$duration = $sess->duration;
if (empty($duration)) {
@ -438,16 +454,28 @@ function attendance_can_student_mark($sess) {
}
if ($sess->sessdate < time() && time() < ($sess->sessdate + $duration)) {
$canmark = true;
$reason = '';
}
}
}
// Check if another student has marked attendance from this IP address recently.
if ($canmark && !empty($sess->preventsharedip)) {
if ($sess->preventsharedip == ATTENDANCE_SHAREDIP_MINUTES) {
$time = time() - ($sess->preventsharediptime * 60);
$sql = 'sessionid = ? AND studentid <> ? AND timetaken > ? AND ipaddress = ?';
$params = array($sess->id, $USER->id, $time, getremoteaddr());
$record = $DB->get_record_select('attendance_log', $sql, $params);
} else {
// Assume ATTENDANCE_SHAREDIP_FORCED.
$sql = 'sessionid = ? AND studentid <> ? AND ipaddress = ?';
$params = array($sess->id, $USER->id, getremoteaddr());
$record = $DB->get_record_select('attendance_log', $sql, $params);
}
if (!empty($record)) {
$canmark = false;
$reason = 'preventsharederror';
if ($log) {
// Trigger an ip_shared event.
$attendanceid = $DB->get_field('attendance_sessions', 'attendanceid', array('id' => $record->sessionid));
$cm = get_coursemodule_from_instance('attendance', $attendanceid);
@ -461,12 +489,10 @@ function attendance_can_student_mark($sess) {
));
$event->trigger();
echo $OUTPUT->notification(get_string('preventsharederror', 'attendance'));
return false;
}
}
return $canmark;
}
return array($canmark, $reason);
}
/**
@ -492,7 +518,7 @@ function attendance_exporttotableed($data, $filename, $format) {
// Sending HTTP headers.
$workbook->send($filename);
// Creating the first worksheet.
$myxls = $workbook->add_worksheet('Attendances');
$myxls = $workbook->add_worksheet(get_string('modulenameplural', 'attendance'));
// Format types.
$formatbc = $workbook->add_format();
$formatbc->set_bold(1);
@ -575,6 +601,11 @@ function attendance_construct_sessions_data_for_add($formdata, mod_attendance_st
$formdata->studentscanmark = 0;
}
$calendarevent = 0;
if (isset($formdata->calendarevent)) { // Calendar event should be created.
$calendarevent = 1;
}
$sessions = array();
if (isset($formdata->addmultiply)) {
$startdate = $sessiondate;
@ -607,9 +638,11 @@ function attendance_construct_sessions_data_for_add($formdata, mod_attendance_st
$sess->descriptionitemid = $formdata->sdescription['itemid'];
$sess->description = $formdata->sdescription['text'];
$sess->descriptionformat = $formdata->sdescription['format'];
$sess->calendarevent = $calendarevent;
$sess->timemodified = $now;
$sess->absenteereport = $absenteereport;
$sess->studentpassword = '';
$sess->includeqrcode = 0;
if (isset($formdata->studentscanmark)) { // Students will be able to mark their own attendance.
$sess->studentscanmark = 1;
if (!empty($formdata->usedefaultsubnet)) {
@ -627,6 +660,9 @@ function attendance_construct_sessions_data_for_add($formdata, mod_attendance_st
} else if (!empty($formdata->studentpassword)) {
$sess->studentpassword = $formdata->studentpassword;
}
if (!empty($formdata->includeqrcode)) {
$sess->includeqrcode = $formdata->includeqrcode;
}
if (!empty($formdata->preventsharedip)) {
$sess->preventsharedip = $formdata->preventsharedip;
}
@ -657,6 +693,7 @@ function attendance_construct_sessions_data_for_add($formdata, mod_attendance_st
$sess->descriptionitemid = $formdata->sdescription['itemid'];
$sess->description = $formdata->sdescription['text'];
$sess->descriptionformat = $formdata->sdescription['format'];
$sess->calendarevent = $calendarevent;
$sess->timemodified = $now;
$sess->studentscanmark = 0;
$sess->autoassignstatus = 0;
@ -665,6 +702,7 @@ function attendance_construct_sessions_data_for_add($formdata, mod_attendance_st
$sess->automark = 0;
$sess->automarkcompleted = 0;
$sess->absenteereport = $absenteereport;
$sess->includeqrcode = 0;
if (isset($formdata->studentscanmark) && !empty($formdata->studentscanmark)) {
// Students will be able to mark their own attendance.
@ -677,6 +715,9 @@ function attendance_construct_sessions_data_for_add($formdata, mod_attendance_st
} else if (!empty($formdata->studentpassword)) {
$sess->studentpassword = $formdata->studentpassword;
}
if (!empty($formdata->includeqrcode)) {
$sess->includeqrcode = $formdata->includeqrcode;
}
if (!empty($formdata->usedefaultsubnet)) {
$sess->subnet = $att->subnet;
} else {
@ -932,3 +973,84 @@ function attendance_get_automarkoptions() {
$options[ATTENDANCE_AUTOMARK_CLOSE] = get_string('automarkclose', 'attendance');
return $options;
}
/**
* Get available sharedip options.
*
* @return array
*/
function attendance_get_sharedipoptions() {
$options = array();
$options[ATTENDANCE_SHAREDIP_DISABLED] = get_string('no');
$options[ATTENDANCE_SHAREDIP_FORCE] = get_string('yes');
$options[ATTENDANCE_SHAREDIP_MINUTES] = get_string('setperiod', 'attendance');
return $options;
}
/**
* Used to print simple time - 1am instead of 1:00am.
*
* @param int $time - unix timestamp.
*/
function attendance_strftimehm($time) {
$mins = userdate($time, '%M');
if ($mins == '00') {
$format = get_string('strftimeh', 'attendance');
} else {
$format = get_string('strftimehm', 'attendance');
}
$userdate = userdate($time, $format);
// Some Lang packs use %p to suffix with AM/PM but not all strftime support this.
// Check if %p is in use and make sure it's being respected.
if (stripos($format, '%p')) {
// Check if $userdate did something with %p by checking userdate against the same format without %p.
$formatwithoutp = str_ireplace('%p', '', $format);
if (userdate($time, $formatwithoutp) == $userdate) {
// The date is the same with and without %p - we have a problem.
if (userdate($time, '%H') > 11) {
$userdate .= 'pm';
} else {
$userdate .= 'am';
}
}
// Some locales and O/S don't respect correct intended case of %p vs %P
// This can cause problems with behat which expects AM vs am.
if (strpos($format, '%p')) { // Should be upper case according to PHP spec.
$userdate = str_replace('am', 'AM', $userdate);
$userdate = str_replace('pm', 'PM', $userdate);
}
}
return $userdate;
}
/**
* Used to print simple time - 1am instead of 1:00am.
*
* @param int $datetime - unix timestamp.
* @param int $duration - number of seconds.
*/
function attendance_construct_session_time($datetime, $duration) {
$starttime = attendance_strftimehm($datetime);
$endtime = attendance_strftimehm($datetime + $duration);
return $starttime . ($duration > 0 ? ' - ' . $endtime : '');
}
/**
* Used to print session time.
*
* @param int $datetime - unix timestamp.
* @param int $duration - number of seconds duration.
* @return string.
*/
function construct_session_full_date_time($datetime, $duration) {
$sessinfo = userdate($datetime, get_string('strftimedmyw', 'attendance'));
$sessinfo .= ' '.attendance_construct_session_time($datetime, $duration);
return $sessinfo;
}

1
manage.php

@ -75,6 +75,7 @@ $PAGE->set_url($att->url_manage());
$PAGE->set_title($course->shortname. ": ".$att->name);
$PAGE->set_heading($course->fullname);
$PAGE->set_cacheable(true);
$PAGE->force_settings_menu(true);
$PAGE->navbar->add($att->name);
$output = $PAGE->get_renderer('mod_attendance');

30
mobilestyles.css

@ -0,0 +1,30 @@
.attendance_mobile_teacher_form .item-radio {
display: inline-block;
margin-top: 10px;
margin-left: 5px;
padding: 0;
width: 70px;
}
.attendance_mobile_teacher_form .item-inner {
padding: 0;
}
.attendance_mobile_teacher_form .radiolabel .item-inner {
text-align: center;
}
.attendance_mobile_teacher_form .item-inner .input-wrapper label,
.attendance_mobile_teacher_form .radio {
margin: 0;
}
.attendance_mobile_teacher_form .radio .radio-icon {
display: none;
}
.attendance_mobile_teacher_form .messages .label,
.attendance_mobile_user_form .messages .label,
.attendance_mobile_view_page .messages .label {
white-space: normal;
}
.attendance_mobile_teacher_form .attendance_user_row {
padding-bottom: 5px;
}

12
password.php

@ -26,6 +26,8 @@
*/
require_once(dirname(__FILE__).'/../../config.php');
require_once(dirname(__FILE__).'/locallib.php');
require_once($CFG->libdir.'/tcpdf/tcpdf_barcodes_2d.php'); // Used for generating qrcode.
$session = required_param('session', PARAM_INT);
$session = $DB->get_record('attendance_sessions', array('id' => $session), '*', MUST_EXIST);
@ -47,5 +49,15 @@ $PAGE->set_context(context_system::instance());
$PAGE->set_title(get_string('password', 'attendance'));
echo $OUTPUT->header();
echo html_writer::tag('h2', get_string('passwordgrp', 'attendance'));
echo html_writer::span($session->studentpassword, 'student-password');
if (isset($session->includeqrcode) && $session->includeqrcode == 1) {
$qrcodeurl = $CFG->wwwroot . '/mod/attendance/attendance.php?qrpass=' . $session->studentpassword . '&sessid=' . $session->id;
echo html_writer::tag('h3', get_string('qrcode', 'attendance'));
$barcode = new TCPDF2DBarcode($qrcodeurl, 'QRCODE');
$image = $barcode->getBarcodePngData(15, 15);
echo html_writer::img('data:image/png;base64,' . base64_encode($image), get_string('qrcode', 'attendance'));
}
echo $OUTPUT->footer();

11
password_ajax.php

@ -44,7 +44,14 @@ $PAGE->set_pagelayout('popup');
$PAGE->set_context(context_system::instance());
$data->heading = '';
$data->text = html_writer::span($session->studentpassword, 'student-password');
$data->heading = get_string('passwordgrp', 'attendance');
if (isset($session->includeqrcode) && $session->includeqrcode == 1) {
$studentattendancepage = '/mod/attendance/password.php?session=' . $session->id;
$data->text = html_writer::tag('p', html_writer::span($session->studentpassword, 'student-password') .
html_writer::empty_tag('br') .
html_writer::link($CFG->wwwroot . $studentattendancepage, get_string('showqrcode', 'attendance')));
} else {
$data->text = html_writer::span($session->studentpassword, 'student-password');
}
echo json_encode($data);

BIN
pix/icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

1
pix/qrcode.svg

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="qrcode" class="svg-inline--fa fa-qrcode fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M0 224h192V32H0v192zM64 96h64v64H64V96zm192-64v192h192V32H256zm128 128h-64V96h64v64zM0 480h192V288H0v192zm64-128h64v64H64v-64zm352-64h32v128h-96v-32h-32v96h-64V288h96v32h64v-32zm0 160h32v32h-32v-32zm-64 0h32v32h-32v-32z"></path></svg>

Before

Width:  |  Height:  |  Size: 451 B

After

Width:  |  Height:  |  Size: 451 B

5
preferences.php

@ -52,6 +52,7 @@ $att = new mod_attendance_structure($att, $cm, $course, $context, $pageparams);
$PAGE->set_url($att->url_preferences());
$PAGE->set_title($course->shortname. ": ".$att->name.' - '.get_string('settings', 'attendance'));
$PAGE->set_heading($course->fullname);
$PAGE->force_settings_menu(true);
$PAGE->set_cacheable(true);
$PAGE->navbar->add(get_string('settings', 'attendance'));
@ -69,7 +70,7 @@ switch ($att->pageparams->action) {
$newdescription = optional_param('newdescription', null, PARAM_TEXT);
$newgrade = optional_param('newgrade', 0, PARAM_RAW);
$newstudentavailability = optional_param('newstudentavailability', null, PARAM_INT);
$newgrade = unformat_float($newgrade);
$newgrade = empty($newgrade) ? 0 : unformat_float($newgrade);
$newstatus = new stdClass();
$newstatus->attendanceid = $att->id;
@ -104,7 +105,7 @@ switch ($att->pageparams->action) {
redirect($att->url_preferences(), get_string('statusdeleted', 'attendance'));
}
$message = get_string('deletecheckfull', '', get_string('variable', 'attendance'));
$message = get_string('deletecheckfull', 'attendance', get_string('variable', 'attendance'));
$message .= str_repeat(html_writer::empty_tag('br'), 2);
$message .= $status->acronym.': '.
($status->description ? $status->description : get_string('nodescription', 'attendance'));

23
renderables.php

@ -466,8 +466,9 @@ class attendance_user_data implements renderable {
* attendance_user_data constructor.
* @param mod_attendance_structure $att
* @param int $userid
* @param boolean $mobile - this is called by the mobile code, don't generate everything.
*/
public function __construct(mod_attendance_structure $att, $userid) {
public function __construct(mod_attendance_structure $att, $userid, $mobile = false) {
$this->user = $att->get_user($userid);
$this->pageparams = $att->pageparams;
@ -475,10 +476,12 @@ class attendance_user_data implements renderable {
if ($this->pageparams->mode == mod_attendance_view_page_params::MODE_THIS_COURSE) {
$this->statuses = $att->get_statuses(true, true);
if (!$mobile) {
$this->summary = new mod_attendance_summary($att->id, array($userid), $att->pageparams->startdate,
$att->pageparams->enddate);
$this->filtercontrols = new attendance_filter_controls($att);
}
$this->sessionslog = $att->get_user_filtered_sessions_log_extended($userid);
@ -548,11 +551,6 @@ class attendance_report_data implements renderable {
* @param mod_attendance_structure $att
*/
public function __construct(mod_attendance_structure $att) {
$currenttime = time();
if ($att->pageparams->view == ATT_VIEW_NOTPRESENT) {
$att->pageparams->enddate = $currenttime;
}
$this->pageparams = $att->pageparams;
$this->users = $att->get_users($att->pageparams->group, $att->pageparams->page);
@ -582,8 +580,9 @@ class attendance_report_data implements renderable {
foreach ($this->users as $key => $user) {
$usersummary = $this->summary->get_taken_sessions_summary_for($user->id);
if ($att->pageparams->view != ATT_VIEW_NOTPRESENT ||
$usersummary->takensessionspoints < $usersummary->takensessionsmaxpoints ||
$usersummary->takensessionsmaxpoints == 0) {
attendance_calc_fraction($usersummary->takensessionspoints, $usersummary->takensessionsmaxpoints) <
$att->get_lowgrade_threshold()) {
$this->usersgroups[$user->id] = groups_get_all_groups($att->course->id, $user->id);
$this->sessionslog[$user->id] = $att->get_user_filtered_sessions_log($user->id);
@ -855,8 +854,14 @@ class attendance_password_icon implements renderable, templatable {
$data->heading = '';
$data->text = $this->text;
if ($this->includeqrcode == 1) {
$pix = 'qrcode';
} else {
$pix = 'key';
}
$data->alt = $title;
$data->icon = (new pix_icon('key', '', 'attendance'))->export_for_template($output);
$data->icon = (new pix_icon($pix, '', 'attendance'))->export_for_template($output);
$data->linktext = '';
$data->title = $title;
$data->url = (new moodle_url('/mod/attendance/password.php', [

157
renderer.php

@ -129,7 +129,11 @@ class mod_attendance_renderer extends plugin_renderer_base {
'page' => $fcontrols->pageparams->page - 1)),
$this->output->larrow());
}
$pagingcontrols .= html_writer::tag('span', "Page {$fcontrols->pageparams->page} of $numberofpages",
$a = new stdClass();
$a->page = $fcontrols->pageparams->page;
$a->numpages = $numberofpages;
$text = get_string('pageof', 'attendance', $a);
$pagingcontrols .= html_writer::tag('span', $text,
array('class' => 'attbtn'));
if ($fcontrols->pageparams->page < $numberofpages) {
$pagingcontrols .= html_writer::link($fcontrols->url(array('curdate' => $fcontrols->curdate,
@ -202,12 +206,13 @@ class mod_attendance_renderer extends plugin_renderer_base {
protected function render_view_controls(attendance_filter_controls $fcontrols) {
$views[ATT_VIEW_ALL] = get_string('all', 'attendance');
$views[ATT_VIEW_ALLPAST] = get_string('allpast', 'attendance');
if ($fcontrols->reportcontrol && $fcontrols->att->grade > 0) {
$views[ATT_VIEW_NOTPRESENT] = get_string('lowgrade', 'attendance');
}
$views[ATT_VIEW_MONTHS] = get_string('months', 'attendance');
$views[ATT_VIEW_WEEKS] = get_string('weeks', 'attendance');
$views[ATT_VIEW_DAYS] = get_string('days', 'attendance');
if ($fcontrols->reportcontrol && $fcontrols->att->grade > 0) {
$a = $fcontrols->att->get_lowgrade_threshold() * 100;
$views[ATT_VIEW_NOTPRESENT] = get_string('below', 'attendance', $a);
}
if ($fcontrols->reportcontrol) {
$views[ATT_VIEW_SUMMARY] = get_string('summary', 'attendance');
}
@ -252,8 +257,8 @@ class mod_attendance_renderer extends plugin_renderer_base {
$table->width = '100%';
$table->head = array(
'#',
get_string('date'),
get_string('time'),
get_string('date', 'attendance'),
get_string('time', 'attendance'),
get_string('sessiontypeshort', 'attendance'),
get_string('description', 'attendance'),
get_string('actions'),
@ -317,6 +322,13 @@ class mod_attendance_renderer extends plugin_renderer_base {
has_capability('mod/attendance:changeattendances', $sessdata->att->context))) {
$icon = new attendance_password_icon($sess->studentpassword, $sess->id);
if ($sess->includeqrcode == 1) {
$icon->includeqrcode = 1;
} else {
$icon->includeqrcode = 0;
}
$actions .= $this->render($icon);
}
@ -414,7 +426,8 @@ class mod_attendance_renderer extends plugin_renderer_base {
$table .= $this->render_attendance_take_grid($takedata);
}
$table .= html_writer::input_hidden_params($takedata->url(array('sesskey' => sesskey(),
'page' => $takedata->pageparams->page)));
'page' => $takedata->pageparams->page,
'perpage' => $takedata->pageparams->perpage)));
$table .= html_writer::end_div();
$params = array(
'type' => 'submit',
@ -431,7 +444,7 @@ class mod_attendance_renderer extends plugin_renderer_base {
$sessionstats[] = array();
foreach ($takedata->sessionlog as $userlog) {
foreach ($takedata->statuses as $status) {
if ($userlog->statusid == $status->id) {
if ($userlog->statusid == $status->id && in_array($userlog->studentid, array_keys($takedata->users))) {
$sessionstats[$status->id]++;
}
}
@ -523,7 +536,11 @@ class mod_attendance_renderer extends plugin_renderer_base {
$controls .= html_writer::link($takedata->url(array('page' => $takedata->pageparams->page - 1)),
$this->output->larrow());
}
$controls .= html_writer::tag('span', "Page {$takedata->pageparams->page} of $numberofpages",
$a = new stdClass();
$a->page = $takedata->pageparams->page;
$a->numpages = $numberofpages;
$text = get_string('pageof', 'attendance', $a);
$controls .= html_writer::tag('span', $text,
array('class' => 'attbtn'));
if ($takedata->pageparams->page < $numberofpages) {
$controls .= html_writer::link($takedata->url(array('page' => $takedata->pageparams->page + 1,
@ -567,7 +584,7 @@ class mod_attendance_renderer extends plugin_renderer_base {
$controls .= $this->output->render($select);
}
if (count($takedata->sessions4copy) > 0) {
if (isset($takedata->sessions4copy) && count($takedata->sessions4copy) > 0) {
$controls .= html_writer::empty_tag('br');
$controls .= html_writer::empty_tag('br');
@ -977,12 +994,11 @@ class mod_attendance_renderer extends plugin_renderer_base {
$usersummary = $userdata->summary[$ca->attid]->get_all_sessions_summary_for($userdata->user->id);
$row->cells[] = $usersummary->numtakensessions;
$row->cells[] = format_float($usersummary->takensessionspoints, 1, true, true) . ' / ' .
format_float($usersummary->takensessionsmaxpoints, 1, true, true);
$row->cells[] = $usersummary->pointssessionscompleted;
if (empty($usersummary->numtakensessions)) {
$row->cells[] = '-';
} else {
$row->cells[] = format_float($usersummary->takensessionspercentage * 100) . '%';
$row->cells[] = $usersummary->percentagesessionscompleted;
}
}
@ -1100,7 +1116,7 @@ class mod_attendance_renderer extends plugin_renderer_base {
$row->cells[] = format_float($status->grade, 1, true, true) . ' / ' .
format_float($statussetmaxpoints[$status->setnumber], 1, true, true);
$row->cells[] = $sess->remarks;
} else if ($sess->sessdate < $userdata->user->enrolmentstart) {
} else if (($sess->sessdate + $sess->duration) < $userdata->user->enrolmentstart) {
$cell = new html_table_cell(get_string('enrolmentstart', 'attendance',
userdate($userdata->user->enrolmentstart, '%d.%m.%Y')));
$cell->colspan = 3;
@ -1111,7 +1127,8 @@ class mod_attendance_renderer extends plugin_renderer_base {
$cell->colspan = 3;
$row->cells[] = $cell;
} else {
if (attendance_can_student_mark($sess)) {
list($canmark, $reason) = attendance_can_student_mark($sess, false);
if ($canmark) {
// Student can mark their own attendance.
// URL to the page that lets the student modify their attendance.
@ -1150,7 +1167,7 @@ class mod_attendance_renderer extends plugin_renderer_base {
* @return string
*/
private function construct_time($datetime, $duration) {
$time = html_writer::tag('nobr', construct_session_time($datetime, $duration));
$time = html_writer::tag('nobr', attendance_construct_session_time($datetime, $duration));
return $time;
}
@ -1193,20 +1210,16 @@ class mod_attendance_renderer extends plugin_renderer_base {
// Check if the user should be able to bulk send messages to other users on the course.
$bulkmessagecapability = has_capability('moodle/course:bulkmessaging', $PAGE->context);
if ($bulkmessagecapability) {
$bulkmessagingrows = $this->get_bulkmessage_rows($reportdata);
}
// Extract rows from each part and collate them into one row each.
$sessiondetailsleft = $reportdata->pageparams->sessiondetailspos == 'left';
foreach ($userrows as $index => $row) {
$summaryrow = isset($summaryrows[$index]->cells) ? $summaryrows[$index]->cells : array();
$bulkmessagingrow = isset($bulkmessagingrows[$index]->cells) ? $bulkmessagingrows[$index]->cells : array();
$sessionrow = isset($sessionrows[$index]->cells) ? $sessionrows[$index]->cells : array();
if ($sessiondetailsleft) {
$row->cells = array_merge($row->cells, $sessionrow, $acronymrows[$index]->cells, $summaryrow, $bulkmessagingrow);
$row->cells = array_merge($row->cells, $sessionrow, $acronymrows[$index]->cells, $summaryrow);
} else {
$row->cells = array_merge($row->cells, $acronymrows[$index]->cells, $summaryrow, $sessionrow, $bulkmessagingrow);
$row->cells = array_merge($row->cells, $acronymrows[$index]->cells, $summaryrow, $sessionrow);
}
$table->data[] = $row;
}
@ -1216,7 +1229,9 @@ class mod_attendance_renderer extends plugin_renderer_base {
$output = html_writer::empty_tag('input', array('name' => 'sesskey', 'type' => 'hidden', 'value' => sesskey()));
$output .= html_writer::empty_tag('input', array('name' => 'id', 'type' => 'hidden', 'value' => $COURSE->id));
$output .= html_writer::empty_tag('input', array('name' => 'returnto', 'type' => 'hidden', 'value' => s(me())));
$output .= html_writer::table($table).html_writer::tag('div', get_string('users').': '.count($reportdata->users));;
$output .= html_writer::start_div('attendancereporttable');
$output .= html_writer::table($table).html_writer::tag('div', get_string('users').': '.count($reportdata->users));
$output .= html_writer::end_div();
$output .= html_writer::tag('div',
html_writer::empty_tag('input', array('type' => 'submit',
'value' => get_string('messageselectadd'),
@ -1231,14 +1246,16 @@ class mod_attendance_renderer extends plugin_renderer_base {
/**
* Build and return the rows that will make up the left part of the attendance report.
* This consists of student names and icons, as well as header cells for these columns.
* This consists of student names, as well as header cells for these columns.
*
* @param attendance_report_data $reportdata the report data
* @return array Array of html_table_row objects
*/
protected function get_user_rows(attendance_report_data $reportdata) {
global $OUTPUT;
global $OUTPUT, $PAGE;
$rows = array();
$bulkmessagecapability = has_capability('moodle/course:bulkmessaging', $PAGE->context);
$extrafields = get_extra_user_fields($reportdata->att->context);
$showextrauserdetails = $reportdata->pageparams->showextrauserdetails;
$params = $reportdata->pageparams->get_significant_params();
@ -1257,16 +1274,26 @@ class mod_attendance_renderer extends plugin_renderer_base {
$extrafields = array();
}
}
$usercolspan = 1 + count($extrafields);
$usercolspan = count($extrafields);
$row = new html_table_row();
$row->cells[] = $this->build_header_cell('');
$row->cells[] = $this->build_header_cell($text, false, false, $usercolspan);
$cell = $this->build_header_cell($text, false, false);
$cell->attributes['class'] = $cell->attributes['class'] . ' headcol';
$row->cells[] = $cell;
if (!empty($usercolspan)) {
$row->cells[] = $this->build_header_cell('', false, false, $usercolspan);
}
$rows[] = $row;
$row = new html_table_row();
$row->cells[] = $this->build_header_cell('');
$row->cells[] = $this->build_header_cell($this->construct_fullname_head($reportdata), false, false);
$text = '';
if ($bulkmessagecapability) {
$text .= html_writer::checkbox('cb_selector', 0, false, '', array('id' => 'cb_selector'));
}
$text .= $this->construct_fullname_head($reportdata);
$cell = $this->build_header_cell($text, false, false);
$cell->attributes['class'] = $cell->attributes['class'] . ' headcol';
$row->cells[] = $cell;
foreach ($extrafields as $field) {
$row->cells[] = $this->build_header_cell(get_string($field), false, false);
@ -1276,9 +1303,15 @@ class mod_attendance_renderer extends plugin_renderer_base {
foreach ($reportdata->users as $user) {
$row = new html_table_row();
$row->cells[] = $this->build_data_cell($this->user_picture($user));
$text = html_writer::link($reportdata->url_view(array('studentid' => $user->id)), fullname($user));
$row->cells[] = $this->build_data_cell($text, false, false, null, null, false);
$text = '';
if ($bulkmessagecapability) {
$text .= html_writer::checkbox('user'.$user->id, 'on', false, '', array('class' => 'attendancesesscheckbox'));
}
$text .= html_writer::link($reportdata->url_view(array('studentid' => $user->id)), fullname($user));
$cell = $this->build_data_cell($text, false, false, null, null, false);
$cell->attributes['class'] = $cell->attributes['class'] . ' headcol';
$row->cells[] = $cell;
foreach ($extrafields as $field) {
$row->cells[] = $this->build_data_cell($user->$field, false, false);
}
@ -1286,9 +1319,13 @@ class mod_attendance_renderer extends plugin_renderer_base {
}
$row = new html_table_row();
$row->cells[] = $this->build_data_cell('');
$text = ($reportdata->pageparams->view == ATT_VIEW_SUMMARY) ? '' : get_string('summary');
$row->cells[] = $this->build_data_cell($text, false, true, $usercolspan);
$cell = $this->build_data_cell($text, false, true, $usercolspan);
$cell->attributes['class'] = $cell->attributes['class'] . ' headcol';
$row->cells[] = $cell;
if (!empty($usercolspan)) {
$row->cells[] = $this->build_header_cell('', false, false, $usercolspan);
}
$rows[] = $row;
return $rows;
@ -1417,24 +1454,20 @@ class mod_attendance_renderer extends plugin_renderer_base {
$contrast = $startwithcontrast;
$row = new html_table_row();
$row->cells[] = $this->build_data_cell($usersummary->numtakensessions, $contrast);
$text = format_float($usersummary->takensessionspoints, 1, true, true) . ' / ' .
format_float($usersummary->takensessionsmaxpoints, 1, true, true);
$row->cells[] = $this->build_data_cell($text, $contrast);
$row->cells[] = $this->build_data_cell($usersummary->pointssessionscompleted, $contrast);
$row->cells[] = $this->build_data_cell(format_float($usersummary->takensessionspercentage * 100) . '%', $contrast);
if ($reportdata->pageparams->view == ATT_VIEW_SUMMARY) {
$contrast = !$contrast;
$row->cells[] = $this->build_data_cell($usersummary->numallsessions, $contrast);
$text = format_float($usersummary->takensessionspoints, 1, true, true) . ' / ' .
format_float($usersummary->allsessionsmaxpoints, 1, true, true);
$text = $usersummary->pointsallsessions;
$row->cells[] = $this->build_data_cell($text, $contrast);
$row->cells[] = $this->build_data_cell(format_float($usersummary->allsessionspercentage * 100) . '%', $contrast);
$row->cells[] = $this->build_data_cell($usersummary->allsessionspercentage, $contrast);
$contrast = !$contrast;
$text = format_float($usersummary->maxpossiblepoints, 1, true, true) . ' / ' .
format_float($usersummary->allsessionsmaxpoints, 1, true, true);
$text = $usersummary->maxpossiblepoints;
$row->cells[] = $this->build_data_cell($text, $contrast);
$row->cells[] = $this->build_data_cell(format_float($usersummary->maxpossiblepercentage * 100) . '%', $contrast);
$row->cells[] = $this->build_data_cell($usersummary->maxpossiblepercentage, $contrast);
}
$rows[] = $row;
@ -1588,42 +1621,6 @@ class mod_attendance_renderer extends plugin_renderer_base {
return $rows;
}
/**
* Build and return the rows that will make up the right part of the attendance report.
* This consists of checkbox column for bulk message.
*
* @param attendance_report_data $reportdata the report data
* @return array Array of html_table_row objects
*/
protected function get_bulkmessage_rows(attendance_report_data $reportdata) {
$rows = array();
$row = new html_table_row();
$row->cells[] = $this->build_header_cell('');
$rows[] = $row;
// Display the table header for bulk messaging.
// The checkbox must have an id of cb_selector so that the JavaScript will pick it up.
$row = new html_table_row();
$text = html_writer::checkbox('cb_selector', 0, false, '', array('id' => 'cb_selector'));
$row->cells[] = $this->build_header_cell($text);
$rows[] = $row;
foreach ($reportdata->users as $user) {
// Create the checkbox for bulk messaging.
$row = new html_table_row();
$text = html_writer::checkbox('user'.$user->id, 'on', false, '', array('class' => 'attendancesesscheckbox'));
$row->cells[] = $this->build_data_cell($text);
$rows[] = $row;
}
$row = new html_table_row();
$row->cells[] = $this->build_data_cell('');
$rows[] = $row;
return $rows;
}
/**
* Build and return a html_table_cell for header rows
*

83
renderhelpers.php

@ -74,7 +74,7 @@ class user_sessions_cells_generator {
$this->construct_remarks_cell($this->reportdata->sessionslog[$this->user->id][$sess->id]->remarks);
}
} else {
if ($this->user->enrolmentstart > $sess->sessdate) {
if ($this->user->enrolmentstart > ($sess->sessdate + $sess->duration)) {
$starttext = get_string('enrolmentstart', 'attendance', userdate($this->user->enrolmentstart, '%d.%m.%Y'));
$this->construct_enrolments_info_cell($starttext);
} else if ($this->user->enrolmentend and $this->user->enrolmentend < $sess->sessdate) {
@ -309,72 +309,6 @@ class user_sessions_cells_text_generator extends user_sessions_cells_generator {
}
}
/**
* Used to print simple time - 1am instead of 1:00am.
*
* @param int $time - unix timestamp.
*/
function attendance_strftimehm($time) {
$mins = userdate($time, '%M');
if ($mins == '00') {
$format = get_string('strftimeh', 'attendance');
} else {
$format = get_string('strftimehm', 'attendance');
}
$userdate = userdate($time, $format);
// Some Lang packs use %p to suffix with AM/PM but not all strftime support this.
// Check if %p is in use and make sure it's being respected.
if (stripos($format, '%p')) {
// Check if $userdate did something with %p by checking userdate against the same format without %p.
$formatwithoutp = str_ireplace('%p', '', $format);
if (userdate($time, $formatwithoutp) == $userdate) {
// The date is the same with and without %p - we have a problem.
if (userdate($time, '%H') > 11) {
$userdate .= 'pm';
} else {
$userdate .= 'am';
}
}
// Some locales and O/S don't respect correct intended case of %p vs %P
// This can cause problems with behat which expects AM vs am.
if (strpos($format, '%p')) { // Should be upper case according to PHP spec.
$userdate = str_replace('am', 'AM', $userdate);
$userdate = str_replace('pm', 'PM', $userdate);
}
}
return $userdate;
}
/**
* Used to print simple time - 1am instead of 1:00am.
*
* @param int $datetime - unix timestamp.
* @param int $duration - number of seconds.
*/
function construct_session_time($datetime, $duration) {
$starttime = attendance_strftimehm($datetime);
$endtime = attendance_strftimehm($datetime + $duration);
return $starttime . ($duration > 0 ? ' - ' . $endtime : '');
}
/**
* Used to print session time.
*
* @param int $datetime - unix timestamp.
* @param int $duration - number of seconds duration.
* @return string.
*/
function construct_session_full_date_time($datetime, $duration) {
$sessinfo = userdate($datetime, get_string('strftimedmyw', 'attendance'));
$sessinfo .= ' '.construct_session_time($datetime, $duration);
return $sessinfo;
}
/**
* Used to construct user summary.
*
@ -394,14 +328,13 @@ function construct_user_data_stat($usersummary, $view) {
$row = new html_table_row();
$row->attributes['class'] = 'normal';
$row->cells[] = get_string('pointssessionscompleted', 'attendance') . ':';
$row->cells[] = format_float($usersummary->takensessionspoints, 1, true, true) . ' / ' .
format_float($usersummary->takensessionsmaxpoints, 1, true, true);
$row->cells[] = $usersummary->pointssessionscompleted;
$stattable->data[] = $row;
$row = new html_table_row();
$row->attributes['class'] = 'normal';
$row->cells[] = get_string('percentagesessionscompleted', 'attendance') . ':';
$row->cells[] = format_float($usersummary->takensessionspercentage * 100) . '%';
$row->cells[] = $usersummary->percentagesessionscompleted;
$stattable->data[] = $row;
if ($view == ATT_VIEW_ALL) {
@ -414,27 +347,25 @@ function construct_user_data_stat($usersummary, $view) {
$row = new html_table_row();
$row->attributes['class'] = 'highlight';
$row->cells[] = get_string('pointsallsessions', 'attendance') . ':';
$row->cells[] = format_float($usersummary->takensessionspoints, 1, true, true) . ' / ' .
format_float($usersummary->allsessionsmaxpoints, 1, true, true);
$row->cells[] = $usersummary->pointsallsessions;
$stattable->data[] = $row;
$row = new html_table_row();
$row->attributes['class'] = 'highlight';
$row->cells[] = get_string('percentageallsessions', 'attendance') . ':';
$row->cells[] = format_float($usersummary->allsessionspercentage * 100) . '%';
$row->cells[] = $usersummary->allsessionspercentage;
$stattable->data[] = $row;
$row = new html_table_row();
$row->attributes['class'] = 'normal';
$row->cells[] = get_string('maxpossiblepoints', 'attendance') . ':';
$row->cells[] = format_float($usersummary->maxpossiblepoints, 1, true, true) . ' / ' .
format_float($usersummary->allsessionsmaxpoints, 1, true, true);
$row->cells[] = $usersummary->maxpossiblepoints;
$stattable->data[] = $row;
$row = new html_table_row();
$row->attributes['class'] = 'normal';
$row->cells[] = get_string('maxpossiblepercentage', 'attendance') . ':';
$row->cells[] = format_float($usersummary->maxpossiblepercentage * 100) . '%';
$row->cells[] = $usersummary->maxpossiblepercentage;
$stattable->data[] = $row;
}

1
report.php

@ -56,6 +56,7 @@ $PAGE->set_url($att->url_report());
$PAGE->set_pagelayout('report');
$PAGE->set_title($course->shortname. ": ".$att->name.' - '.get_string('report', 'attendance'));
$PAGE->set_heading($course->fullname);
$PAGE->force_settings_menu(true);
$PAGE->set_cacheable(true);
$PAGE->navbar->add(get_string('report', 'attendance'));

6
resetcalendar.php

@ -47,9 +47,9 @@ $tabmenu = attendance_print_settings_tabs('resetcalendar');
echo $tabmenu;
if (get_config('attendance', 'enablecalendar')) {
// Check to see if all sessions have calendar events.
// Check to see if all sessions that need them have calendar events.
if ($action == 'create' && confirm_sesskey()) {
$sessions = $DB->get_recordset('attendance_sessions', array('caleventid' => 0));
$sessions = $DB->get_recordset('attendance_sessions', array('caleventid' => 0, 'calendarevent' => 1));
foreach ($sessions as $session) {
attendance_create_calendar_event($session);
if ($session->caleventid) {
@ -59,7 +59,7 @@ if (get_config('attendance', 'enablecalendar')) {
$sessions->close();
echo $OUTPUT->notification(get_string('eventscreated', 'mod_attendance'), 'notifysuccess');
} else {
if ($DB->record_exists('attendance_sessions', array('caleventid' => 0))) {
if ($DB->record_exists('attendance_sessions', array('caleventid' => 0, 'calendarevent' => 1))) {
$createurl = new moodle_url('/mod/attendance/resetcalendar.php', array('action' => 'create'));
$returnurl = new moodle_url('/admin/settings.php', array('section' => 'modsettingattendance'));

5
sessions.php

@ -58,6 +58,7 @@ $att = new mod_attendance_structure($att, $cm, $course, $context, $pageparams);
$PAGE->set_url($att->url_sessions(array('action' => $pageparams->action)));
$PAGE->set_title($course->shortname. ": ".$att->name);
$PAGE->set_heading($course->fullname);
$PAGE->force_settings_menu(true);
$PAGE->set_cacheable(true);
$PAGE->navbar->add($att->name);
@ -121,7 +122,7 @@ switch ($att->pageparams->action) {
$sessinfo = $att->get_session_info($sessionid);
$message = get_string('deletecheckfull', '', get_string('session', 'attendance'));
$message = get_string('deletecheckfull', 'attendance', get_string('session', 'attendance'));
$message .= str_repeat(html_writer::empty_tag('br'), 2);
$message .= userdate($sessinfo->sessdate, get_string('strftimedmyhm', 'attendance'));
$message .= html_writer::empty_tag('br');
@ -136,7 +137,7 @@ switch ($att->pageparams->action) {
exit;
case mod_attendance_sessions_page_params::ACTION_DELETE_SELECTED:
$confirm = optional_param('confirm', null, PARAM_INT);
$message = get_string('deletecheckfull', '', get_string('session', 'attendance'));
$message = get_string('deletecheckfull', 'attendance', get_string('sessions', 'attendance'));
if (isset($confirm) && confirm_sesskey()) {
$sessionsids = required_param('sessionsids', PARAM_ALPHANUMEXT);

17
settings.php

@ -29,8 +29,11 @@ if ($ADMIN->fulltree) {
require_once(dirname(__FILE__).'/locallib.php');
$tabmenu = attendance_print_settings_tabs();
$settings->add(new admin_setting_heading('attendance_header', '', $tabmenu));
$plugininfos = core_plugin_manager::instance()->get_plugins_of_type('local');
// Paging options.
$options = array(
0 => get_string('donotusepaging', 'attendance'),
@ -64,7 +67,7 @@ if ($ADMIN->fulltree) {
$options = array(
ATT_VIEW_ALL => get_string('all', 'attendance'),
ATT_VIEW_ALLPAST => get_string('allpast', 'attendance'),
ATT_VIEW_NOTPRESENT => get_string('lowgrade', 'attendance'),
ATT_VIEW_NOTPRESENT => get_string('below', 'attendance', 'X'),
ATT_VIEW_MONTHS => get_string('months', 'attendance'),
ATT_VIEW_WEEKS => get_string('weeks', 'attendance'),
ATT_VIEW_DAYS => get_string('days', 'attendance')
@ -105,6 +108,9 @@ if ($ADMIN->fulltree) {
$description = new lang_string('defaultsessionsettings_help', 'mod_attendance');
$settings->add(new admin_setting_heading('defaultsessionsettings', $name, $description));
$settings->add(new admin_setting_configcheckbox('attendance/calendarevent_default',
get_string('calendarevent', 'attendance'), '', 1));
$settings->add(new admin_setting_configcheckbox('attendance/absenteereport_default',
get_string('includeabsentee', 'attendance'), '', 1));
@ -119,11 +125,16 @@ if ($ADMIN->fulltree) {
$settings->add(new admin_setting_configcheckbox('attendance/randompassword_default',
get_string('randompassword', 'attendance'), '', 0));
$settings->add(new admin_setting_configcheckbox('attendance/includeqrcode_default',
get_string('includeqrcode', 'attendance'), '', 0));
$settings->add(new admin_setting_configcheckbox('attendance/autoassignstatus',
get_string('autoassignstatus', 'attendance'), '', 0));
$settings->add(new admin_setting_configcheckbox('attendance/preventsharedip',
get_string('preventsharedip', 'attendance'), '', 0));
$options = attendance_get_sharedipoptions();
$settings->add(new admin_setting_configselect('attendance/preventsharedip',
get_string('preventsharedip', 'attendance'),
'', ATTENDANCE_SHAREDIP_DISABLED, $options));
$settings->add(new admin_setting_configtext('attendance/preventsharediptime',
get_string('preventsharediptime', 'attendance'), get_string('preventsharediptime_help', 'attendance'), '', PARAM_RAW));

2
student_attendance_form.php

@ -44,6 +44,7 @@ class mod_attendance_student_attendance_form extends moodleform {
$attforsession = $this->_customdata['session'];
$attblock = $this->_customdata['attendance'];
$password = $this->_customdata['password'];
$statuses = $attblock->get_statuses();
// Check if user has access to all statuses.
@ -81,6 +82,7 @@ class mod_attendance_student_attendance_form extends moodleform {
$mform->addElement('text', 'studentpassword', get_string('password', 'attendance'));
$mform->setType('studentpassword', PARAM_TEXT);
$mform->addRule('studentpassword', get_string('passwordrequired', 'attendance'), 'required');
$mform->setDefault('studentpassword', $password);
}
if (!$attforsession->autoassignstatus) {

44
styles.css

@ -25,7 +25,6 @@
.path-mod-attendance .attwidth {
margin: auto;
width: 90%;
}
.path-mod-attendance .userwithoutenrol,
@ -251,3 +250,46 @@
display: none;
}
}
#page-mod-attendance-report .attendancereporttable {
overflow-x: scroll;
overflow-y: visible;
padding: 0;
margin-left: 180px;
}
#page-mod-attendance-report .attendancereporttable .headcol {
position: absolute;
width: 200px;
left: 10px;
top: auto;
border-top-width: 1px;
}
#page-mod-attendance-report .attendancereporttable .headcol input[type='checkbox'] {
margin-right: 4px;
}
.attendancereporttable img.icon {
padding-left: 5px;
}
/* CSS for old clean based themes with docks etc. */
#page-mod-attendance-report.has-region-side-pre .attendancereporttable .headcol {
left: 370px;
}
#page-mod-attendance-report.has-region-side-pre .attendancereporttable {
margin-left: 230px;
}
#page-mod-attendance-report.has-region-side-pre.content-only .attendancereporttable .headcol {
left: 80px;
}
@media (max-width: 767px) {
#page-mod-attendance-report.has-region-side-pre .attendancereporttable .headcol {
left: 80px;
}
}

25
take.php

@ -61,6 +61,31 @@ if (!empty($pageparams->grouptype) && !array_key_exists($pageparams->grouptype,
if (($formdata = data_submitted()) && confirm_sesskey()) {
$att->take_from_form_data($formdata);
$group = 0;
if ($att->pageparams->grouptype != mod_attendance_structure::SESSION_COMMON) {
$group = $att->pageparams->grouptype;
} else {
if ($att->pageparams->group) {
$group = $att->pageparams->group;
}
}
$totalusers = count_enrolled_users(context_module::instance($cm->id), 'mod/attendance:canbelisted', $group);
$usersperpage = $att->pageparams->perpage;
if (!empty($att->pageparams->page) && $att->pageparams->page && $totalusers && $usersperpage) {
$numberofpages = ceil($totalusers / $usersperpage);
if ($att->pageparams->page < $numberofpages) {
$params = array(
'sessionid' => $att->pageparams->sessionid,
'grouptype' => $att->pageparams->grouptype);
$params['page'] = $att->pageparams->page + 1;
redirect($att->url_take($params), get_string('moreattendance', 'attendance'));
}
}
redirect($att->url_manage(), get_string('attendancesuccess', 'attendance'));
}
$PAGE->set_url($att->url_take());

105
templates/mobile_teacher_form.mustache

@ -0,0 +1,105 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template mod_attendance/mobile_user_form
The page to take attendance
Classes required for JS:
* None
Data attibutes required for JS:
* All data attributes are required
Context variables required for this template:
* attendance
* summary
* cmid
Example context (json):
{
"attendance": {
"id": "1",
"course": "2",
"name": "Class Attendance",
"intro": "Intro"
},
"cmid": "25",
"courseid": "4",
"sessid": "43",
"btnargs" : ""
}
}}
{{=<% %>=}}
<div class="attendance_mobile_teacher_form">
<span class="description">
<core-course-module-description description="<% attendance.intro %>" component="mod_attendance" componentId="<% cmid %>"></core-course-module-description>
</span>
<%#showmessage%>
<%#messages%>
<span class="messages">
<ion-item>
{{ 'plugin.mod_attendance.<% string %>' | translate }}
</ion-item>
</span>
<%/messages%>
<%/showmessage%>
<span class="attendance_selectall">
<ion-item>
{{ 'plugin.mod_attendance.setallstatuses' | translate }}
</ion-item>
<ion-list radio-group>
<%#statuses%>
<span class="radiolabel">
<ion-item>
<ion-label><% acronym %></ion-label>
<ion-radio (ionSelect)="<% selectall %>" value="<% stid %>"></ion-radio>
</ion-item>
</span>
<%/statuses%>
</ion-list>
</span>
<%#users%>
<span class="attendance_user_row">
<!-- User and status of the submission. -->
<span ion-item text-wrap title="<% fullname %>">
<ion-avatar item-start>
<img src="<% profileimageurl %>" core-external-content role="presentation" onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
<h2><% fullname %></h2>
<ng-container *ngTemplateOutlet="submissionStatus"></ng-container>
</span>
<ion-list radio-group [(ngModel)]="CONTENT_OTHERDATA.status<% userid %>">
<%#statuses%>
<span class="radiolabel">
<ion-item>
<ion-label><% acronym %></ion-label>
<ion-radio value="<% stid %>"></ion-radio>
</ion-item>
</span>
<%/statuses%>
</ion-list>
</span>
<%/users%>
<ion-item>
<button ion-button core-site-plugins-new-content component="mod_attendance" method="mobile_view_activity" [args]="{cmid: <% cmid %>, courseid: <% courseid %>, sessid: <% sessid %><% btnargs %>}">
{{ 'plugin.mod_attendance.submitattendance' | translate }}
</button>
</ion-item>
</div>

82
templates/mobile_user_form.mustache

@ -0,0 +1,82 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template mod_attendance/mobile_user_form
The page to take attendance
Classes required for JS:
* None
Data attibutes required for JS:
* All data attributes are required
Context variables required for this template:
* attendance
* summary
* cmid
Example context (json):
{
"attendance": {
"id": "1",
"course": "2",
"name": "Class Attendance",
"intro": "Intro"
},
"cmid": "25",
"courseid": "4",
"sessid": "43"
}
}}
{{=<% %>=}}
<div class="attendance_mobile_user_form">
<core-course-module-description description="<% attendance.intro %>" component="mod_attendance" componentId="<% cmid %>"></core-course-module-description>
<%#showmessage%>
<%#messages%>
<span class="messages">
<ion-item>
{{ 'plugin.mod_attendance.<% string %>' | translate }}
</ion-item>
</span>
<%/messages%>
<%/showmessage%>
<%#showpassword%>
<ion-list [(ngModel)]="studentpass">
<ion-item>
<ion-label>{{ 'plugin.mod_attendance.enterpassword' | translate }}:</ion-label>
<ion-input type="text" name="studentpass"></ion-input>
</ion-item>
</ion-list>
<%/showpassword%>
<%#showstatuses%>
<ion-list radio-group [(ngModel)]="status">
<%#statuses%>
<ion-item>
<ion-label><% description %></ion-label>
<ion-radio value="<% stid %>"></ion-radio>
</ion-item>
<%/statuses%>
</ion-list>
<button ion-button core-site-plugins-new-content component="mod_attendance" method="mobile_view_activity" [args]="{cmid: <% cmid %>, courseid: <% courseid %>, sessid: <% sessid %>, status: status, studentpass: studentpass}">
{{ 'plugin.mod_attendance.submitattendance' | translate }}
</button>
<%/showstatuses%>
<%#disabledduetotime%>
{{ 'plugin.mod_attendance.somedisabledstatus' | translate }}
<%/disabledduetotime%>
</div>

144
templates/mobile_view_page.mustache

@ -0,0 +1,144 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template mod_attendance/mobile_view_page
The main page to view the attendance activity
Classes required for JS:
* None
Data attibutes required for JS:
* All data attributes are required
Context variables required for this template:
* attendance
* summary
* cmid
Example context (json):
{
"attendance": {
"id": "1",
"course": "2",
"name": "Class Attendance",
"intro": "Intro"
},
"summary": {
"numtakensessions": "1",
"pointssessionscompleted": "2",
"percentagesessionscompleted": "2"
},
"cmid": "25",
"timestamp": "1234"
}
}}
{{=<% %>=}}
<div class="attendance_mobile_view_page">
<core-course-module-description description="<% attendance.intro %>" component="mod_attendance" componentId="<% cmid %>"></core-course-module-description>
<%#showmessage%>
<%#messages%>
<span class="messages">
<ion-item>
{{ 'plugin.mod_attendance.<% string %>' | translate }}
</ion-item>
</span>
<%/messages%>
<%/showmessage%>
<%#sessions%>
<ion-item>
<h2><% time %></h2>
<h3><% groupname %></h3>
<h3><% currentstatus %></h3>
<%#sessid%>
<button ion-button core-site-plugins-new-content component="mod_attendance" method="<% attendancefunction %>" [args]="{cmid: <% cmid %>, courseid: <% courseid %>, sessid: <% sessid %>, timestamp: <% timestamp %>}">
{{ 'plugin.mod_attendance.submitattendance' | translate }}
</button>
<%/sessid%>
</ion-item>
<%/sessions%>
<ion-item>
<ion-grid>
<ion-row>
<ion-col col-9 class="text-left">
{{ 'plugin.mod_attendance.sessionscompleted' | translate }}
</ion-col>
<ion-col col-2 class="text-left">
<% summary.numtakensessions %>
</ion-col>
</ion-row>
<ion-row>
<ion-col col-9 class="text-left">
{{ 'plugin.mod_attendance.pointssessionscompleted' | translate }}
</ion-col>
<ion-col col-2 class="text-left">
<% summary.pointssessionscompleted %>
</ion-col>
</ion-row>
<ion-row>
<ion-col col-9 class="text-left">
{{ 'plugin.mod_attendance.percentagesessionscompleted' | translate }}
</ion-col>
<ion-col col-2 class="text-left">
<% summary.percentagesessionscompleted %>
</ion-col>
</ion-row>
<ion-row>
<ion-col col-9 class="text-left">
{{ 'plugin.mod_attendance.sessionstotal' | translate }}
</ion-col>
<ion-col col-2 class="text-left">
<% summary.numallsessions %>
</ion-col>
</ion-row>
<ion-row>
<ion-col col-9 class="text-left">
{{ 'plugin.mod_attendance.pointsallsessions' | translate }}
</ion-col>
<ion-col col-2 class="text-left">
<% summary.percentagesessionscompleted %>
</ion-col>
</ion-row>
<ion-row>
<ion-col col-9 class="text-left">
{{ 'plugin.mod_attendance.percentageallsessions' | translate }}
</ion-col>
<ion-col col-2 class="text-left">
<% summary.allsessionspercentage %>
</ion-col>
</ion-row>
<ion-row>
<ion-col col-9 class="text-left">
{{ 'plugin.mod_attendance.maxpossiblepoints' | translate }}
</ion-col>
<ion-col col-2 class="text-left">
<% summary.maxpossiblepoints %>
</ion-col>
</ion-row>
<ion-row>
<ion-col col-9 class="text-left">
{{ 'plugin.mod_attendance.maxpossiblepercentage' | translate }}
</ion-col>
<ion-col col-2 class="text-left">
<% summary.maxpossiblepercentage %>
</ion-col>
</ion-row>
</ion-grid>
</ion-item>
</div>

1
tempusers.php

@ -42,6 +42,7 @@ require_capability('mod/attendance:managetemporaryusers', $context);
$PAGE->set_title($course->shortname.": ".$att->name.' - '.get_string('tempusers', 'attendance'));
$PAGE->set_heading($course->fullname);
$PAGE->force_settings_menu(true);
$PAGE->set_cacheable(true);
$PAGE->navbar->add(get_string('tempusers', 'attendance'));

81
tests/attendance_webservices_test.php

@ -46,6 +46,8 @@ class attendance_webservices_tests extends advanced_testcase {
/** @var stdClass */
protected $teacher;
/** @var array */
protected $students;
/** @var array */
protected $sessions;
/**
@ -57,20 +59,7 @@ class attendance_webservices_tests extends advanced_testcase {
$this->category = $this->getDataGenerator()->create_category();
$this->course = $this->getDataGenerator()->create_course(array('category' => $this->category->id));
$record = new stdClass();
$record->course = $this->course->id;
$record->name = "Attendance";
$record->grade = 100;
$DB->insert_record('attendance', $record);
$this->getDataGenerator()->create_module('attendance', array('course' => $this->course->id));
$moduleid = $DB->get_field('modules', 'id', array('name' => 'attendance'));
$cm = $DB->get_record('course_modules', array('course' => $this->course->id, 'module' => $moduleid));
$context = context_course::instance($this->course->id);
$att = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST);
$this->attendance = new mod_attendance_structure($att, $cm, $this->course, $context);
$this->attendance = $this->create_attendance();
$this->create_and_enrol_users();
@ -86,6 +75,7 @@ class attendance_webservices_tests extends advanced_testcase {
$session->statusset = 0;
$session->groupid = 0;
$session->absenteereport = 1;
$session->calendarevent = 0;
// Creating two sessions.
$this->sessions[] = $session;
@ -93,11 +83,21 @@ class attendance_webservices_tests extends advanced_testcase {
$this->attendance->add_sessions($this->sessions);
}
private function create_attendance() {
global $DB;
$att = $this->getDataGenerator()->create_module('attendance', array('course' => $this->course->id));
$cm = $DB->get_record('course_modules', array('id' => $att->cmid));
unset($att->cmid);
return new mod_attendance_structure($att, $cm, $this->course);
}
/** Creating 10 students and 1 teacher. */
protected function create_and_enrol_users() {
$this->students = array();
for ($i = 0; $i < 10; $i++) {
$student = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($student->id, $this->course->id, 5); // Enrol as student.
$this->students[] = $student;
}
$this->teacher = $this->getDataGenerator()->create_user();
@ -120,6 +120,25 @@ class attendance_webservices_tests extends advanced_testcase {
$this->assertEquals(count($attendanceinstance['today_sessions']), 2);
}
public function test_get_courses_with_today_sessions_multiple_instances() {
$this->resetAfterTest(true);
// Make another attendance.
$second = $this->create_attendance();
// Just add the same session.
$secondsession = clone $this->sessions[0];
$secondsession->sessdate += 3600;
$second->add_sessions([$secondsession]);
$courseswithsessions = attendance_handler::get_courses_with_today_sessions($this->teacher->id);
$this->assertTrue(is_array($courseswithsessions));
$this->assertEquals(count($courseswithsessions), 1);
$course = array_pop($courseswithsessions);
$this->assertEquals(count($course->attendance_instances), 2);
}
public function test_get_session() {
$this->resetAfterTest(true);
@ -136,6 +155,40 @@ class attendance_webservices_tests extends advanced_testcase {
$this->assertEquals(count($sessioninfo->users), 10);
}
public function test_get_session_with_group() {
$this->resetAfterTest(true);
// Create a group in our course, and add some students to it.
$group = new stdClass();
$group->courseid = $this->course->id;
$group = $this->getDataGenerator()->create_group($group);
for ($i = 0; $i < 5; $i++) {
$member = new stdClass;
$member->groupid = $group->id;
$member->userid = $this->students[$i]->id;
$this->getDataGenerator()->create_group_member($member);
}
// Add a session that's identical to the first, but with a group.
$session = clone $this->sessions[0];
$session->groupid = $group->id;
$session->sessdate += 3600; // Make sure it appears second in the list.
$this->attendance->add_sessions([$session]);
$courseswithsessions = attendance_handler::get_courses_with_today_sessions($this->teacher->id);
$course = array_pop($courseswithsessions);
$attendanceinstance = array_pop($course->attendance_instances);
$session = array_pop($attendanceinstance['today_sessions']);
$sessioninfo = attendance_handler::get_session($session->id);
$this->assertEquals($session->id, $sessioninfo->id);
$this->assertEquals($group->id, $sessioninfo->groupid);
$this->assertEquals(count($sessioninfo->users), 5);
}
public function test_update_user_status() {
$this->resetAfterTest(true);

4
tests/behat/attendance_mod.feature

@ -78,7 +78,7 @@ Feature: Teachers and Students can record session attendance
And I click on "Get these logs" "button"
Then "Attendance taken by student" "link" should exist
Scenario: Teachers can view low grade report and send a message
Scenario: Teachers can view below % report and send a message
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I follow "Attendance"
@ -88,7 +88,7 @@ Feature: Teachers and Students can record session attendance
| id_sestime_endhour | 02 |
And I click on "id_submitbutton" "button"
And I follow "Report"
And I follow "Low grade"
And I follow "Below"
And I set the field "cb_selector" to "1"
And I click on "Send a message" "button"
And I should see "Message body"

14
tests/behat/report.feature

@ -22,8 +22,6 @@ Feature: Visiting reports
And I add a "Attendance" to section "1" and I fill the form with:
| Name | Attendance |
And I follow "Attendance"
And I follow "Add a block"
And I follow "Administration"
And I follow "Add session"
And I set the following fields to these values:
| id_sestime_starthour | 01 |
@ -41,7 +39,7 @@ Feature: Visiting reports
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I follow "Attendance"
And I follow "Edit settings"
And I navigate to "Edit settings" in current page administration
Then I set the following fields to these values:
| id_grade_modgrade_type | Point |
| id_grade_modgrade_point | 50 |
@ -75,7 +73,7 @@ Feature: Visiting reports
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I follow "Attendance"
And I follow "Edit settings"
And I navigate to "Edit settings" in current page administration
Then I set the following fields to these values:
| id_grade_modgrade_type | Point |
| id_grade_modgrade_point | 50 |
@ -88,7 +86,7 @@ Feature: Visiting reports
And I press "Save attendance"
When I follow "Attendance"
And I follow "Edit settings"
And I navigate to "Edit settings" in current page administration
Then I set the following fields to these values:
| id_grade_modgrade_type | Point |
| id_grade_modgrade_point | 70 |
@ -114,7 +112,7 @@ Feature: Visiting reports
When I log in as "teacher1"
And I am on "Course 1" course homepage
And I follow "Attendance"
And I follow "Edit settings"
And I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| id_grade_modgrade_type | Point |
| id_grade_modgrade_point | 50 |
@ -156,7 +154,7 @@ Feature: Visiting reports
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I follow "Attendance"
And I follow "Edit settings"
And I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| id_grade_modgrade_type | Point |
| id_grade_modgrade_point | 50 |
@ -203,7 +201,7 @@ Feature: Visiting reports
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I follow "Attendance"
And I follow "Edit settings"
And I navigate to "Edit settings" in current page administration
Then I set the following fields to these values:
| id_grade_modgrade_type | Point |
| id_grade_modgrade_point | 50 |

36
update_form.php

@ -62,10 +62,12 @@ class mod_attendance_update_form extends moodleform {
$endhour = floor($endtime / HOURSECS);
$endminute = floor(($endtime - $endhour * HOURSECS) / MINSECS);
$data = array('sessiondate' => $sess->sessdate,
$data = array(
'sessiondate' => $sess->sessdate,
'sestime' => array('starthour' => $starthour, 'startminute' => $startminute,
'endhour' => $endhour, 'endminute' => $endminute),
'sdescription' => $sess->description_editor,
'calendarevent' => $sess->calendarevent,
'studentscanmark' => $sess->studentscanmark,
'studentpassword' => $sess->studentpassword,
'autoassignstatus' => $sess->autoassignstatus,
@ -74,7 +76,9 @@ class mod_attendance_update_form extends moodleform {
'absenteereport' => $sess->absenteereport,
'automarkcompleted' => 0,
'preventsharedip' => $sess->preventsharedip,
'preventsharediptime' => $sess->preventsharediptime);
'preventsharediptime' => $sess->preventsharediptime,
'includeqrcode' => $sess->includeqrcode
);
if ($sess->subnet == $attendancesubnet) {
$data['usedefaultsubnet'] = 1;
} else {
@ -99,17 +103,24 @@ class mod_attendance_update_form extends moodleform {
// Show which status set is in use.
$maxstatusset = attendance_get_max_statusset($this->_customdata['att']->id);
if ($maxstatusset > 0) {
$mform->addElement('static', 'statusset', get_string('usestatusset', 'mod_attendance'),
$mform->addElement('static', 'statussetstring', get_string('usestatusset', 'mod_attendance'),
attendance_get_setname($this->_customdata['att']->id, $sess->statusset));
} else {
$mform->addElement('hidden', 'statusset', $maxstatusset);
$mform->setType('statusset', PARAM_INT);
}
$mform->addElement('hidden', 'statusset', $sess->statusset);
$mform->setType('statusset', PARAM_INT);
$mform->addElement('editor', 'sdescription', get_string('description', 'attendance'),
array('rows' => 1, 'columns' => 80), $defopts);
$mform->setType('sdescription', PARAM_RAW);
if (!empty(get_config('attendance', 'enablecalendar'))) {
$mform->addElement('checkbox', 'calendarevent', '', get_string('calendarevent', 'attendance'));
$mform->addHelpButton('calendarevent', 'calendarevent', 'attendance');
} else {
$mform->addElement('hidden', 'calendarevent', 0);
$mform->setType('calendarevent', PARAM_INT);
}
// If warnings allow selector for reporting.
if (!empty(get_config('attendance', 'enablewarnings'))) {
$mform->addElement('checkbox', 'absenteereport', '', get_string('includeabsentee', 'attendance'));
@ -137,6 +148,8 @@ class mod_attendance_update_form extends moodleform {
$mform->hideif('studentpassword', 'studentscanmark', 'notchecked');
$mform->hideif('studentpassword', 'automark', 'eq', ATTENDANCE_AUTOMARK_ALL);
$mform->hideif('randompassword', 'automark', 'eq', ATTENDANCE_AUTOMARK_ALL);
$mform->addElement('checkbox', 'includeqrcode', '', get_string('includeqrcode', 'attendance'));
$mform->hideif('includeqrcode', 'studentscanmark', 'notchecked');
$mform->addElement('checkbox', 'autoassignstatus', '', get_string('autoassignstatus', 'attendance'));
$mform->addHelpButton('autoassignstatus', 'autoassignstatus', 'attendance');
$mform->hideif('autoassignstatus', 'studentscanmark', 'notchecked');
@ -159,18 +172,18 @@ class mod_attendance_update_form extends moodleform {
$mform->settype('automarkcompleted', PARAM_INT);
$mgroup3 = array();
$mgroup3[] = & $mform->createElement('checkbox', 'preventsharedip', '');
$options = attendance_get_sharedipoptions();
$mgroup3[] = & $mform->createElement('select', 'preventsharedip',
get_string('preventsharedip', 'attendance'), $options);
$mgroup3[] = & $mform->createElement('text', 'preventsharediptime',
get_string('preventsharediptime', 'attendance'), '', 'test');
$mgroup3[] = & $mform->createElement('static', 'preventsharediptimedesc', '',
get_string('preventsharedipminutes', 'attendance'));
$mform->addGroup($mgroup3, 'preventsharedgroup',
get_string('preventsharedip', 'attendance'), array(' '), false);
$mform->addHelpButton('preventsharedgroup', 'preventsharedip', 'attendance');
$mform->setAdvanced('preventsharedgroup');
$mform->setType('preventsharediptime', PARAM_INT);
$mform->hideif('preventsharedgroup', 'studentscanmark', 'notchecked');
$mform->disabledIf('preventsharediptime', 'preventsharedip', 'notchecked');
$mform->hideIf('preventsharediptime', 'preventsharedip', 'noteq', ATTENDANCE_SHAREDIP_MINUTES);
} else {
$mform->addElement('hidden', 'studentscanmark', '0');
$mform->settype('studentscanmark', PARAM_INT);
@ -185,7 +198,6 @@ class mod_attendance_update_form extends moodleform {
}
$mform->setDefaults($data);
$this->add_action_buttons(true);
}
@ -204,7 +216,7 @@ class mod_attendance_update_form extends moodleform {
$errors['sestime'] = get_string('invalidsessionendtime', 'attendance');
}
if ($data['automark'] == ATTENDANCE_AUTOMARK_CLOSE) {
if (!empty($data['studentscanmark']) && $data['automark'] == ATTENDANCE_AUTOMARK_CLOSE) {
$cm = $this->_customdata['cm'];
// Check that the selected statusset has a status to use when unmarked.
$sql = 'SELECT id

6
version.php

@ -23,9 +23,9 @@
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2018051400;
$plugin->version = 2018051409;
$plugin->requires = 2018050800; // Requires 3.5.
$plugin->release = '3.5.0';
$plugin->maturity = MATURITY_ALPHA;
$plugin->release = '3.5.6';
$plugin->maturity = MATURITY_STABLE;
$plugin->cron = 0;
$plugin->component = 'mod_attendance';

Loading…
Cancel
Save