diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..168aff3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,36 @@ +### [Unreleased] +- New Feature: Allow automatic marking using site logs. +- Improvement: Allow default view for teachers to be set at admin level. + +### Date: 2017-May-23 +### Release: 2017052301 + +- New Feature: New site Level/course category report with average course attendance. +- New Feature: Allow unmarked students to be automatically marked when session closes. + +--- + +### Date: 2017-May-11 +### Release: 2017051104 + +- New Feature: Allow subnet mask to be set at the attendance session level. +- New Feature: Allow certain statuses to be hidden from students when self-marking attendance. +- New Feature: Allow student password to be viewed on session list page. +- Improvement: Improve usablity by grouping settings on session add form. +- Bug fix - fix issue with displaying dates when site hosted on Windows server. +- Bug fix - improve compliance with Moodle coding guidelines. + +--- + +### Date: 2017-Apr-21 +### Release: 2017042100 + +- Feature: Allow a random self-marking password to be used when creating session. +- Improvement: #63 use core useridentity setting when showing list of users. +- Improvement: #258 Add link to attendance on student overview report. +- Improvement: allow student self-marking to be restricted to the session time. +- Improvement: allow admin to set default values when teachers creating new sessions. +- Bug fix - improve compliance with Moodle coding guidelines - phpdocs etc. + +--- + diff --git a/add_form.php b/add_form.php index 338b06d..13819b2 100644 --- a/add_form.php +++ b/add_form.php @@ -132,7 +132,12 @@ class mod_attendance_add_form extends moodleform { $mform->addElement('checkbox', 'studentscanmark', '', get_string('studentscanmark', 'attendance')); $mform->addHelpButton('studentscanmark', 'studentscanmark', 'attendance'); - $mform->addElement('checkbox', 'automark', get_string('automark', 'attendance')); + $options = array( + ATTENDANCE_AUTOMARK_DISABLED => get_string('noautomark', 'attendance'), + ATTENDANCE_AUTOMARK_ALL => get_string('automarkall', 'attendance'), + ATTENDANCE_AUTOMARK_CLOSE => get_string('automarkclose', 'attendance')); + + $mform->addElement('select', 'automark', get_string('automark', 'attendance'), $options); $mform->setType('automark', PARAM_INT); $mform->addHelpButton('automark', 'automark', 'attendance'); $mform->disabledif('automark', 'studentscanmark', 'notchecked'); @@ -150,6 +155,8 @@ class mod_attendance_add_form extends moodleform { $mform->addHelpButton('passwordgrp', 'passwordgrp', 'attendance'); $mform->disabledif('randompassword', 'studentscanmark', 'notchecked'); $mform->disabledif('studentpassword', 'randompassword', 'checked'); + $mform->disabledif('studentpassword', 'automark', 'eq', ATTENDANCE_AUTOMARK_ALL); + $mform->disabledif('randompassword', 'automark', 'eq', ATTENDANCE_AUTOMARK_ALL); if (isset($pluginconfig->studentscanmark_default)) { $mform->setDefault('studentscanmark', $pluginconfig->studentscanmark_default); } diff --git a/classes/structure.php b/classes/structure.php index ceee72d..c271091 100644 --- a/classes/structure.php +++ b/classes/structure.php @@ -525,7 +525,7 @@ class mod_attendance_structure { 'objectid' => $this->id, 'context' => $this->context, 'other' => array('info' => $info, 'sessionid' => $sessionid, - 'action' => mod_attendance_sessions_page_params::ACTION_UPDATE))); + 'action' => mod_attendance_sessions_page_params::ACTION_UPDATE))); $event->add_record_snapshot('course_modules', $this->cm); $event->add_record_snapshot('attendance_sessions', $sess); $event->trigger(); @@ -1163,4 +1163,41 @@ class mod_attendance_structure { return null; } + + /** + * Gets the status to use when auto-marking. + * + * @param int $time the time the user first accessed the course. + * @param int $sessionid the related sessionid to check. + * @return int the statusid to assign to this user. + */ + public function get_automark_status($time, $sessionid) { + $statuses = $this->get_statuses(); + // Statuses are returned highest grade first, find the first high grade we can assign to this user. + + // Get status to use when unmarked. + $session = $this->sessioninfo[$sessionid]; + $duration = $session->duration; + if (empty($duration)) { + $duration = get_config('attendance', 'studentscanmarksessiontimeend') * 60; + } + if ($time > $session->sessdate + $duration) { + // This session closed after the users access - use the unmarked state. + foreach ($statuses as $status) { + if (!empty($status->setunmarked)) { + return $status->id; + } + } + } else { + foreach ($statuses as $status) { + if ($status->studentavailability !== '0' && + $this->sessioninfo[$sessionid]->sessdate + ($status->studentavailability * 60) > $time) { + + // Found first status we could set. + return $status->id; + } + } + } + return; + } } diff --git a/classes/task/auto_mark.php b/classes/task/auto_mark.php index 9a86c47..54a6f7b 100644 --- a/classes/task/auto_mark.php +++ b/classes/task/auto_mark.php @@ -44,13 +44,22 @@ class auto_mark extends \core\task\scheduled_task { $cachecm = array(); $cacheatt = array(); $cachecourse = array(); + $now = time(); // Store current time to use in queries so they all match nicely. + $sessions = $DB->get_recordset_select('attendance_sessions', - 'automark = 1 AND automarkcompleted = 0 AND sessdate < ? ', array(time())); + 'automark > 0 AND automarkcompleted < 2 AND sessdate < ? ', array($now)); foreach ($sessions as $session) { - // Would be nice to change duration field to a timestamp so we don't need this step. - if ($session->sessdate + $session->duration < time()) { + if ($session->sessdate + $session->duration < $now || // If session is over. + // OR if session is currently open and automark is set to do all. + ($session->sessdate < $now && $session->automark == 1)) { + + $userfirstaccess = array(); $donesomething = false; // Only trigger grades/events when an update actually occurs. + $sessionover = false; // Is this session over? + if ($session->sessdate + $session->duration < $now) { + $sessionover = true; + } // Store cm/att/course in cachefields so we don't make unnecessary db calls. // Would probably be nice to grab this stuff outside of the loop. @@ -84,6 +93,42 @@ class auto_mark extends \core\task\scheduled_task { } $pageparams->sessionid = $session->id; + if ($session->automark == 1) { + $userfirstacess = array(); + // If set to do full automarking, get all users that have accessed course during session open. + $id = $DB->sql_concat('userid', 'ip'); // Users may access from multiple ip, make the first field unique. + $sql = "SELECT $id, userid, ip, min(timecreated) as timecreated + FROM {logstore_standard_log} + WHERE courseid = ? AND timecreated > ? AND timecreated < ? + GROUP BY userid, ip"; + + $timestart = $session->sessdate; + if (empty($session->lasttakenby) && $session->lasttaken > $timestart) { + // If the last time session was taken it was done automatically, use the last time taken + // as the start time for the logs we are interested in to help with performance. + $timestart = $session->lasttaken; + } + $duration = $session->duration; + if (empty($duration)) { + $duration = get_config('attendance', 'studentscanmarksessiontimeend') * 60; + } + $timeend = $timestart + $duration; + $logusers = $DB->get_recordset_sql($sql, array($courseid, $timestart, $timeend)); + // Check if user access is in allowed subnet. + foreach ($logusers as $loguser) { + if (!empty($session->subnet) && !address_in_subnet($loguser->ip, $session->subnet)) { + // This record isn't in the right subnet. + continue; + } + if (empty($userfirstaccess[$loguser->userid]) || + $userfirstaccess[$loguser->userid] > $loguser->timecreated) { + // Users may have accessed from mulitple ip addresses, find the earliest access. + $userfirstaccess[$loguser->userid] = $loguser->timecreated; + } + } + $logusers->close(); + } + // Get all unmarked students. $att = new \mod_attendance_structure($cacheatt[$session->attendanceid], $cachecm[$session->attendanceid], $cachecourse[$courseid], $context, $pageparams); @@ -95,15 +140,23 @@ class auto_mark extends \core\task\scheduled_task { foreach ($existinglog as $log) { if (empty($log->statusid)) { - // Status needs updating. - $existinglog->statusid = $setunmarked; - $existinglog->timetaken = time(); - $existinglog->takenby = 0; - $existinglog->remarks = get_string('autorecorded', 'attendance'); - - $DB->update_record('attendance_log', $existinglog); - $updated++; - $donesomething = true; + if ($sessionover || !empty($userfirstaccess[$log->studentid])) { + // Status needs updating. + if ($sessionover) { + $log->statusid = $setunmarked; + } else if (!empty($userfirstaccess[$log->studentid])) { + $log->statusid = $att->get_automark_status($userfirstaccess[$log->studentid], $session->id); + } + if (!empty($log->statusid)) { + $log->timetaken = $now; + $log->takenby = 0; + $log->remarks = get_string('autorecorded', 'attendance'); + + $DB->update_record('attendance_log', $log); + $updated++; + $donesomething = true; + } + } } unset($users[$log->studentid]); } @@ -111,8 +164,7 @@ class auto_mark extends \core\task\scheduled_task { mtrace($updated . " session status updated"); $newlog = new \stdClass(); - $newlog->statusid = $setunmarked; - $newlog->timetaken = time(); + $newlog->timetaken = $now; $newlog->takenby = 0; $newlog->sessionid = $session->id; $newlog->remarks = get_string('autorecorded', 'attendance'); @@ -120,17 +172,31 @@ class auto_mark extends \core\task\scheduled_task { $added = 0; foreach ($users as $user) { - $newlog->studentid = $user->id; - $DB->insert_record('attendance_log', $newlog); - $added++; - $donesomething = true; + if ($sessionover || !empty($userfirstaccess[$user->id])) { + if ($sessionover) { + $newlog->statusid = $setunmarked; + } else if (!empty($userfirstaccess[$user->id])) { + $newlog->statusid = $att->get_automark_status($userfirstaccess[$user->id], $session->id); + } + if (!empty($newlog->statusid)) { + $newlog->studentid = $user->id; + $DB->insert_record('attendance_log', $newlog); + $added++; + $donesomething = true; + } + } } mtrace($added . " session status inserted"); // Update lasttaken time and automarkcompleted for this session. - $session->lasttaken = $newlog->timetaken; + $session->lasttaken = $now; $session->lasttakenby = 0; - $session->automarkcompleted = 1; + if ($sessionover) { + $session->automarkcompleted = 2; + } else { + $session->automarkcompleted = 1; + } + $DB->update_record('attendance_sessions', $session); if ($donesomething) { diff --git a/db/upgrade.php b/db/upgrade.php index 574516a..fb1bb3a 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -315,5 +315,21 @@ function xmldb_attendance_upgrade($oldversion=0) { upgrade_mod_savepoint(true, 2016121311, 'attendance'); } + if ($oldversion < 2016121314) { + // Automark values changed. + $default = get_config('attendance', 'automark_default'); + if (!empty($default)) { // Change default if set. + set_config('automark_default', 2, 'attendance'); + } + // Update any sessions set to use automark = 1. + $sql = "UPDATE {attendance_sessions} SET automark = 2 WHERE automark = 1"; + $DB->execute($sql); + + // Update automarkcompleted to 2 if already complete. + $sql = "UPDATE {attendance_sessions} SET automarkcompleted = 2 WHERE automarkcompleted = 1"; + $DB->execute($sql); + + upgrade_mod_savepoint(true, 2016121314, 'attendance'); + } return $result; } diff --git a/lang/en/attendance.php b/lang/en/attendance.php index aa778ce..c69ae83 100644 --- a/lang/en/attendance.php +++ b/lang/en/attendance.php @@ -62,8 +62,12 @@ $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.'; $string['attrecords'] = 'Attendances records'; -$string['automark'] = 'Automatically set status on close of session.'; -$string['automark_help'] = 'When session closes, automatically set status for unreported students as configured by status set.'; +$string['automark'] = 'Automatic marking'; +$string['automarkall'] = 'Yes'; +$string['automarkclose'] = 'Set unmarked at end of session'; +$string['automark_help'] = 'Allows marking to be completed automatically. +If "Yes" students will be automatically marked depending on their first access to the course. +If "Set unmarked at end of session" any students who have not marked their attendance will be set to the unmarked status selected.'; $string['automarktask'] = 'Check for closed attendance sessions that require auto marking'; $string['autorecorded'] = 'system auto recorded'; $string['averageattendance'] = 'Average attendance'; @@ -201,6 +205,7 @@ $string['newdate'] = 'New date'; $string['newduration'] = 'New duration'; $string['newstatusset'] = 'New set of statuses'; $string['noattendanceusers'] = 'It is not possible to export any data as there are no students enrolled in the course.'; +$string['noautomark'] = 'Disabled'; $string['noattforuser'] = 'No attendance records exist for the user'; $string['nodescription'] = 'Regular class session'; $string['nogroups'] = 'You can\'t add group sessions. No groups exists in course.'; diff --git a/locallib.php b/locallib.php index 5a82d32..b7ba6e5 100644 --- a/locallib.php +++ b/locallib.php @@ -39,6 +39,9 @@ define('ATT_SORT_DEFAULT', 0); define('ATT_SORT_LASTNAME', 1); define('ATT_SORT_FIRSTNAME', 2); +define('ATTENDANCE_AUTOMARK_DISABLED', 0); +define('ATTENDANCE_AUTOMARK_ALL', 1); +define('ATTENDANCE_AUTOMARK_CLOSE', 2); /** * Get statuses, * @@ -618,7 +621,7 @@ function attendance_construct_sessions_data_for_add($formdata, mod_attendance_st $sess->studentscanmark = 1; if (!empty($formdata->randompassword)) { $sess->studentpassword = attendance_random_string(); - } else { + } else if (!empty($formdata->studentpassword)) { $sess->studentpassword = $formdata->studentpassword; } if (!empty($formdata->usedefaultsubnet)) { diff --git a/settings.php b/settings.php index a2d0032..41131fd 100644 --- a/settings.php +++ b/settings.php @@ -84,8 +84,13 @@ if ($ADMIN->fulltree) { $settings->add(new admin_setting_configcheckbox('attendance/studentscanmark_default', get_string('studentscanmark', 'attendance'), '', 0)); - $settings->add(new admin_setting_configcheckbox('attendance/automark_default', - get_string('automark', 'attendance'), '', 0)); + $options = array( + ATTENDANCE_AUTOMARK_DISABLED => get_string('noautomark', 'attendance'), + ATTENDANCE_AUTOMARK_ALL => get_string('automarkall', 'attendance'), + ATTENDANCE_AUTOMARK_CLOSE => get_string('automarkclose', 'attendance')); + + $settings->add(new admin_setting_configselect('attendance/automark_default', + get_string('automark', 'attendance'), '', 0, $options)); $settings->add(new admin_setting_configcheckbox('attendance/randompassword_default', get_string('randompassword', 'attendance'), '', 0)); diff --git a/update_form.php b/update_form.php index 0197dc6..a1dfe53 100644 --- a/update_form.php +++ b/update_form.php @@ -111,7 +111,12 @@ class mod_attendance_update_form extends moodleform { $mform->addElement('checkbox', 'studentscanmark', '', get_string('studentscanmark', 'attendance')); $mform->addHelpButton('studentscanmark', 'studentscanmark', 'attendance'); - $mform->addElement('checkbox', 'automark', get_string('automark', 'attendance')); + $options2 = array( + ATTENDANCE_AUTOMARK_DISABLED => get_string('noautomark', 'attendance'), + ATTENDANCE_AUTOMARK_ALL => get_string('automarkall', 'attendance'), + ATTENDANCE_AUTOMARK_CLOSE => get_string('automarkclose', 'attendance')); + + $mform->addElement('select', 'automark', get_string('automark', 'attendance'), $options2); $mform->setType('automark', PARAM_INT); $mform->addHelpButton('automark', 'automark', 'attendance'); $mform->disabledif('automark', 'studentscanmark', 'notchecked'); @@ -120,6 +125,8 @@ class mod_attendance_update_form extends moodleform { $mform->setType('studentpassword', PARAM_TEXT); $mform->addHelpButton('studentpassword', 'passwordgrp', 'attendance'); $mform->disabledif('studentpassword', 'studentscanmark', 'notchecked'); + $mform->disabledif('studentpassword', 'automark', 'eq', ATTENDANCE_AUTOMARK_ALL); + $mform->disabledif('randompassword', 'automark', 'eq', ATTENDANCE_AUTOMARK_ALL); $mgroup = array(); $mgroup[] = & $mform->createElement('text', 'subnet', get_string('requiresubnet', 'attendance')); diff --git a/version.php b/version.php index 375a1cd..0d790ae 100644 --- a/version.php +++ b/version.php @@ -23,9 +23,9 @@ */ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2016121313; +$plugin->version = 2016121314; $plugin->requires = 2016111800; -$plugin->release = '3.2.11'; +$plugin->release = '3.2.12'; $plugin->maturity = MATURITY_STABLE; $plugin->cron = 0; $plugin->component = 'mod_attendance';