diff --git a/add_form.php b/add_form.php index 9fc813a..a9550a3 100644 --- a/add_form.php +++ b/add_form.php @@ -103,6 +103,10 @@ class mod_attendance_add_form extends moodleform { $mform->addElement('checkbox', 'addmultiply', '', get_string('createmultiplesessions', 'attendance')); $mform->addHelpButton('addmultiply', 'createmultiplesessions', 'attendance'); + // Students can mark own attendance. + $mform->addElement('checkbox', 'studentscanmark', '', get_string('studentscanmark','attendance')); + $mform->addHelpButton('studentscanmark', 'studentscanmark', 'attendance'); + $mform->addElement('date_time_selector', 'sessiondate', get_string('sessiondate', 'attendance')); for ($i=0; $i<=23; $i++) { diff --git a/attendance.php b/attendance.php new file mode 100644 index 0000000..539354c --- /dev/null +++ b/attendance.php @@ -0,0 +1,84 @@ +. + +/** + * Prints attendance info for particular user + * + * @package mod + * @subpackage attendance + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(__FILE__).'/../../config.php'); +require_once(dirname(__FILE__).'/locallib.php'); +require_once(dirname(__FILE__).'/student_attenance_form.php'); + +$pageparams = new att_sessions_page_params(); + +// Check that the required parameters are present. +$id = required_param('sessid', PARAM_INT); +$attendance_session_id = required_param('sessid', PARAM_INT); + + +$attforsession = $DB->get_record('attendance_sessions', array('id' => $id), '*', MUST_EXIST); +$attendance = $DB->get_record('attendance', array('id' => $attforsession->attendanceid), '*', MUST_EXIST); +$cm = get_coursemodule_from_instance('attendance', $attendance->id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + +// Require the user is logged in. +require_login($course, true, $cm); + +$pageparams->sessionid = $id; +$att = new attendance($attendance, $cm, $course, $PAGE->context, $pageparams); + +// Require that a session key is passed to this page. +require_sesskey(); + +// Create the form. +$mform = new mod_attendance_student_attendance_form(null, + array('course' => $course, 'cm' => $cm, 'modcontext' => $PAGE->context, 'session' => $attforsession, 'attendance' => $att)); + +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)); + redirect($url); +} else if ($fromform = $mform->get_data()) { + 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 for the block. + redirect($url); + } else { + print_error ('attendance_already_submitted', 'mod_attendance', $url); + } + } + + // The form did not validate correctly so we will set it to display the data they submitted. + $mform->set_data($fromform); +} + +$PAGE->set_url($att->url_sessions()); +$PAGE->set_title($course->shortname. ": ".$att->name); +$PAGE->set_heading($course->fullname); +$PAGE->set_cacheable(true); +$PAGE->navbar->add($att->name); + +$output = $PAGE->get_renderer('mod_attendance'); +echo $output->header(); +$mform->display(); +echo $output->footer(); diff --git a/backup/moodle2/backup_attendance_activity_task.class.php b/backup/moodle2/backup_attendance_activity_task.class.php index 87b5c89..df3b4a0 100644 --- a/backup/moodle2/backup_attendance_activity_task.class.php +++ b/backup/moodle2/backup_attendance_activity_task.class.php @@ -60,11 +60,11 @@ class backup_attendance_activity_task extends backup_activity_task { // Link to attendance view by moduleid. $search = "/(" . $base . "\/mod\/attendance\/view.php\?id\=)([0-9]+)/"; - $content= preg_replace($search, '$@ATTFORBLOCKVIEWBYID*$2@$', $content); + $content= preg_replace($search, '$@ATTENDANCEVIEWBYID*$2@$', $content); // Link to attendance view by moduleid and studentid. $search = "/(" . $base . "\/mod\/attendance\/view.php\?id\=)([0-9]+)\&studentid\=([0-9]+)/"; - $content= preg_replace($search, '$@ATTFORBLOCKVIEWBYIDSTUD*$2*$3@$', $content); + $content= preg_replace($search, '$@ATTENDANCEVIEWBYIDSTUD*$2*$3@$', $content); return $content; } diff --git a/backup/moodle2/restore_attendance_activity_task.class.php b/backup/moodle2/restore_attendance_activity_task.class.php index 5ca7794..629cecd 100644 --- a/backup/moodle2/restore_attendance_activity_task.class.php +++ b/backup/moodle2/restore_attendance_activity_task.class.php @@ -67,9 +67,9 @@ class restore_attendance_activity_task extends restore_activity_task { static public function define_decode_rules() { $rules = array(); - $rules[] = new restore_decode_rule('ATTFORBLOCKVIEWBYID', + $rules[] = new restore_decode_rule('ATTENDANCEVIEWBYID', '/mod/attendance/view.php?id=$1', 'course_module'); - $rules[] = new restore_decode_rule('ATTFORBLOCKVIEWBYIDSTUD', + $rules[] = new restore_decode_rule('ATTENDANCEVIEWBYIDSTUD', '/mod/attendance/view.php?id=$1&studentid=$2', array('course_module', 'user')); return $rules; diff --git a/db/install.xml b/db/install.xml index 1bc5a05..ca4e6b0 100644 --- a/db/install.xml +++ b/db/install.xml @@ -30,6 +30,7 @@ + diff --git a/db/upgrade.php b/db/upgrade.php index c00926f..579f80f 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -34,6 +34,18 @@ function xmldb_attendance_upgrade($oldversion=0) { $result = true; + if ($oldversion < 2013082901) { + $table = new xmldb_table('attendance_sessions'); + + $field = new xmldb_field('studentscanmark'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + upgrade_mod_savepoint($result, 2013082901, 'attendance'); + } + if ($oldversion < 2013082902) { // Replace values that reference old module "attforblock" to "attendance". $sql = "UPDATE {grade_items} diff --git a/export.php b/export.php index a0a690a..9b6fb99 100644 --- a/export.php +++ b/export.php @@ -88,6 +88,14 @@ if ($mform->is_submitted()) { if (isset($formdata->ident['uname'])) { $data->tabhead[] = get_string('username'); } + + $optional = array('idnumber', 'institution', 'department'); + foreach ($optional as $opt) { + if (isset($formdata->ident[$opt])) { + $data->tabhead[] = get_string($opt); + } + } + $data->tabhead[] = get_string('lastname'); $data->tabhead[] = get_string('firstname'); $groupmode = groups_get_activity_groupmode($cm, $course); @@ -95,7 +103,6 @@ if ($mform->is_submitted()) { $data->tabhead[] = get_string('groups'); } - if (count($reportdata->sessions) > 0) { foreach ($reportdata->sessions as $sess) { $text = userdate($sess->sessdate, get_string('strftimedmyhm', 'attendance')); @@ -122,6 +129,14 @@ if ($mform->is_submitted()) { if (isset($formdata->ident['uname'])) { $data->table[$i][] = $user->username; } + + $optional_row = array('idnumber', 'institution', 'department'); + foreach ($optional_row as $opt) { + if (isset($formdata->ident[$opt])) { + $data->table[$i][] = $user->$opt; + } + } + $data->table[$i][] = $user->lastname; $data->table[$i][] = $user->firstname; if (!empty($groupmode)) { diff --git a/export_form.php b/export_form.php index 994c5e1..766e863 100644 --- a/export_form.php +++ b/export_form.php @@ -62,8 +62,14 @@ class mod_attendance_export_form extends moodleform { $ident = array(); $ident[] =& $mform->createElement('checkbox', 'id', '', get_string('studentid', 'attendance')); - $ident[] =& $mform->createElement('checkbox', 'uname', '', get_string('username')); + + $optional = array('idnumber', 'institution', 'department'); + foreach ($optional as $opt) { + $ident[] =& $mform->createElement('checkbox', $opt, '', get_string($opt)); + $mform->setType($opt, PARAM_NOTAGS); + } + $mform->addGroup($ident, 'ident', get_string('identifyby', 'attendance'), array('
'), true); $mform->setDefaults(array('ident[id]' => true, 'ident[uname]' => true)); $mform->setType('id', PARAM_INT); diff --git a/lang/en/attendance.php b/lang/en/attendance.php index 4c8df1f..ee35575 100755 --- a/lang/en/attendance.php +++ b/lang/en/attendance.php @@ -216,3 +216,11 @@ $string['viewmode'] = 'View mode'; $string['week'] = 'week(s)'; $string['weeks'] = 'Weeks'; $string['youcantdo'] = 'You can\'t do anything'; +// New strings. +$string['studentscanmark'] = 'Allow students to record own attendance'; +$string['studentscanmark_help'] = 'If checked students will be able to change their own attendance status for the session.'; +$string['set_by_student'] = 'Self-recorded'; +$string['attendance_already_submitted'] = 'You may not self register attendance that has already been set.'; +$string['lowgrade'] = 'Low grade'; +$string['submitattendance'] = 'Submit attendance'; +$string['attendancenotset'] = 'You must set your attendance'; diff --git a/locallib.php b/locallib.php index ace0f42..f30c292 100644 --- a/locallib.php +++ b/locallib.php @@ -33,6 +33,7 @@ define('ATT_VIEW_WEEKS', 2); define('ATT_VIEW_MONTHS', 3); define('ATT_VIEW_ALLPAST', 4); define('ATT_VIEW_ALL', 5); +define('ATT_VIEW_NOTPRESENT', 6); define('ATT_SORT_LASTNAME', 1); define('ATT_SORT_FIRSTNAME', 2); @@ -701,6 +702,8 @@ class attendance { if ($this->pageparams->startdate && $this->pageparams->enddate) { $where = "attendanceid = :aid AND sessdate >= :csdate AND sessdate >= :sdate AND sessdate < :edate"; + } else if ($this->pageparams->enddate) { + $where = "attendanceid = :aid AND sessdate >= :csdate AND sessdate < :edate"; } else { $where = "attendanceid = :aid AND sessdate >= :csdate"; } @@ -807,6 +810,7 @@ class attendance { } $i++; } + add_to_log($this->course->id, 'attendance', 'sessions added', $this->url_manage(), implode(',', $info_array), $this->cm->id); } @@ -833,6 +837,65 @@ class attendance { add_to_log($this->course->id, 'attendance', 'session updated', $url, $info, $this->cm->id); } + /** + * Used to record attendance submitted by the student. + * + * @global type $DB + * @global type $USER + * @param type $mformdata + * @return boolean + */ + public function take_from_student($mformdata) { + global $DB, $USER; + + $statuses = implode(',', array_keys( (array)$this->get_statuses() )); + $now = time(); + + $record = new stdClass(); + $record->studentid = $USER->id; + $record->statusid = $mformdata->status; + $record->statusset = $statuses; + $record->remarks = get_string('set_by_student', 'mod_attendance'); + $record->sessionid = $mformdata->sessid; + $record->timetaken = $now; + $record->takenby = $USER->id; + + $dbsesslog = $this->get_session_log($mformdata->sessid); + if (array_key_exists($record->studentid, $dbsesslog)) { + // Already recorded do not save. + return false; + } + else { + $DB->insert_record('attendance_log', $record, false); + } + + // Update the session to show that a register has been taken, or staff may overwrite records. + $rec = new object(); + $rec->id = $mformdata->sessid; + $rec->lasttaken = $now; + $rec->lasttakenby = $USER->id; + $DB->update_record('attendance_sessions', $rec); + + // Update the users grade. + $this->update_users_grade(array($USER->id)); + + /* create url for link in log screen + * need to set grouptype to 0 to allow take attendance page to be called + * from report/log page */ + + $params = array( + 'sessionid' => $this->pageparams->sessionid, + 'grouptype' => 0); + + $url = $this->url_take($params); + $logurl = att_log_convert_url($url); + + // Log the change. + add_to_log($this->course->id, 'attendance', 'taken by student', $logurl, '', $this->cm->id); + + return true; + } + public function take_from_form_data($formdata) { global $DB, $USER; // TODO: WARNING - $formdata is unclean - comes from direct $_POST - ideally needs a rewrite but we do some cleaning below. @@ -880,11 +943,16 @@ class attendance { $this->update_users_grade(array_keys($sesslog)); } + // create url for link in log screen $params = array( 'sessionid' => $this->pageparams->sessionid, 'grouptype' => $this->pageparams->grouptype); + $url = $this->url_take($params); - add_to_log($this->course->id, 'attendance', 'taken', $url, '', $this->cm->id); + $logurl = att_log_convert_url($url); + + // Log the change. + add_to_log($this->course->id, 'attendance', 'taken', $logurl, '', $this->cm->id); $group = 0; if ($this->pageparams->grouptype != attendance::SESSION_COMMON) { @@ -916,12 +984,12 @@ class attendance { global $DB, $CFG; // Fields we need from the user table. - $userfields = user_picture::fields('u', array('username')); + $userfields = user_picture::fields('u', array('username' , 'idnumber' , 'institution' , 'department')); if (isset($this->pageparams->sort) and ($this->pageparams->sort == ATT_SORT_FIRSTNAME)) { - $orderby = "u.firstname ASC, u.lastname ASC"; + $orderby = "u.firstname ASC, u.lastname ASC, u.idnumber ASC, u.institution ASC, u.department ASC"; } else { - $orderby = "u.lastname ASC, u.firstname ASC"; + $orderby = "u.lastname ASC, u.firstname ASC, u.idnumber ASC, u.institution ASC, u.department ASC"; } if ($page) { @@ -1082,13 +1150,36 @@ class attendance { return $this->usertakensesscount[$userid]; } - public function get_user_statuses_stat($userid) { + /** + * + * @global type $DB + * @param type $userid + * @param type $filters - An array things to filter by. For now only enddate is valid. + * @return type + */ + public function get_user_statuses_stat($userid, array $filters = null) { global $DB; $params = array( 'aid' => $this->id, 'cstartdate' => $this->course->startdate, 'uid' => $userid); + $processed_filters = array(); + + // We test for any valid filters sent. + if (isset($filters['enddate'])) { + $processed_filters[] = 'ats.sessdate <= :enddate'; + $params['enddate'] = $filters['enddate']; + } + + // Make the filter array into a SQL string. + if (!empty($processed_filters)) { + $processed_filters = ' AND '.implode(' AND ', $processed_filters); + } else { + $processed_filters = ''; + } + + $period = ''; if (!empty($this->pageparams->startdate) && !empty($this->pageparams->enddate)) { $period = ' AND ats.sessdate >= :sdate AND ats.sessdate < :edate '; @@ -1096,38 +1187,47 @@ class attendance { $params['edate'] = $this->pageparams->enddate; } - if (!array_key_exists($userid, $this->userstatusesstat)) { - if ($this->get_group_mode()) { - $qry = "SELECT al.statusid, count(al.statusid) AS stcnt - FROM {attendance_log} al - JOIN {attendance_sessions} ats ON al.sessionid = ats.id - LEFT JOIN {groups_members} gm ON gm.userid = al.studentid AND gm.groupid = ats.groupid - WHERE ats.attendanceid = :aid AND - ats.sessdate >= :cstartdate AND - al.studentid = :uid AND - (ats.groupid = 0 or gm.id is NOT NULL)".$period." - GROUP BY al.statusid"; - } else { - $qry = "SELECT al.statusid, count(al.statusid) AS stcnt - FROM {attendance_log} al - JOIN {attendance_sessions} ats - ON al.sessionid = ats.id - WHERE ats.attendanceid = :aid AND - ats.sessdate >= :cstartdate AND - al.studentid = :uid".$period." - GROUP BY al.statusid"; - - } - - + if ($this->get_group_mode()) { + $qry = "SELECT al.statusid, count(al.statusid) AS stcnt + FROM {attendance_log} al + JOIN {attendance_sessions} ats ON al.sessionid = ats.id + LEFT JOIN {groups_members} gm ON gm.userid = al.studentid AND gm.groupid = ats.groupid + WHERE ats.attendanceid = :aid AND + ats.sessdate >= :cstartdate AND + al.studentid = :uid AND + (ats.groupid = 0 or gm.id is NOT NULL)".$period.$processed_filters." + GROUP BY al.statusid"; + } else { + $qry = "SELECT al.statusid, count(al.statusid) AS stcnt + FROM {attendance_log} al + JOIN {attendance_sessions} ats + ON al.sessionid = ats.id + WHERE ats.attendanceid = :aid AND + ats.sessdate >= :cstartdate AND + al.studentid = :uid".$period.$processed_filters." + GROUP BY al.statusid"; + } + + // We do not want to cache, or use a cached version of the results when a filter is set. + if ($filters !== null) { + return $DB->get_records_sql($qry, $params); + } else if (!array_key_exists($userid, $this->userstatusesstat)) { + // Not filtered so if we do not already have them do the query. $this->userstatusesstat[$userid] = $DB->get_records_sql($qry, $params); } + // Return the cached stats. return $this->userstatusesstat[$userid]; } - public function get_user_grade($userid) { - return att_get_user_grade($this->get_user_statuses_stat($userid), $this->get_statuses()); + /** + * + * @param type $userid + * @param type $filters - An array things to filter by. For now only enddate is valid. + * @return type + */ + public function get_user_grade($userid, array $filters = null) { + return att_get_user_grade($this->get_user_statuses_stat($userid, $filters), $this->get_statuses()); } // For getting sessions count implemented simplest method - taken sessions. @@ -1215,7 +1315,7 @@ class attendance { // It would be better as a UNION query butunfortunatly MS SQL does not seem to support doing a DISTINCT on a the description field. $id = $DB->sql_concat(':value', 'ats.id'); if ($this->get_group_mode()) { - $sql = "SELECT $id, ats.id, ats.groupid, ats.sessdate, ats.duration, ats.description, al.statusid, al.remarks + $sql = "SELECT $id, ats.id, ats.groupid, ats.sessdate, ats.duration, ats.description, al.statusid, al.remarks, ats.studentscanmark FROM {attendance_sessions} ats RIGHT JOIN {attendance_log} al ON ats.id = al.sessionid AND al.studentid = :uid @@ -1223,7 +1323,7 @@ class attendance { WHERE $where AND (ats.groupid = 0 or gm.id is NOT NULL) ORDER BY ats.sessdate ASC"; } else { - $sql = "SELECT $id, ats.id, ats.groupid, ats.sessdate, ats.duration, ats.description, al.statusid, al.remarks + $sql = "SELECT $id, ats.id, ats.groupid, ats.sessdate, ats.duration, ats.description, al.statusid, al.remarks, ats.studentscanmark FROM {attendance_sessions} ats RIGHT JOIN {attendance_log} al ON ats.id = al.sessionid AND al.studentid = :uid @@ -1253,7 +1353,7 @@ class attendance { $where = "ats.attendanceid = :aid AND ats.sessdate >= :csdate AND ats.groupid $gsql"; } - $sql = "SELECT $id, ats.id, ats.groupid, ats.sessdate, ats.duration, ats.description, al.statusid, al.remarks + $sql = "SELECT $id, ats.id, ats.groupid, ats.sessdate, ats.duration, ats.description, al.statusid, al.remarks, ats.studentscanmark FROM {attendance_sessions} ats LEFT JOIN {attendance_log} al ON ats.id = al.sessionid AND al.studentid = :uid @@ -1295,7 +1395,7 @@ class attendance { $sess->timemodified = $now; $DB->update_record('attendance_sessions', $sess); } - $sessions->close(); + $sessions->close(); add_to_log($this->course->id, 'attendance', 'sessions duration updated', $this->url_manage(), get_string('sessionsids', 'attendance').implode(', ', $sessionsids), $this->cm->id); } @@ -1353,9 +1453,9 @@ class attendance { add_to_log($this->course->id, 'attendance', 'status updated', $this->url_preferences(), implode(' ', $updated), $this->cm->id); } + } - function att_get_statuses($attid, $onlyvisible=true) { global $DB; diff --git a/renderables.php b/renderables.php index 373bdc0..75c5324 100644 --- a/renderables.php +++ b/renderables.php @@ -109,19 +109,23 @@ class attendance_filter_controls implements renderable { public $prevcur; public $nextcur; public $curdatetxt; + public $reportcontrol; private $urlpath; private $urlparams; private $att; - public function __construct(attendance $att) { + public function __construct(attendance $att, $report = false) { global $PAGE; $this->pageparams = $att->pageparams; $this->cm = $att->cm; + // This is a report control only if $reports is true and the attendance block can be graded. + $this->reportcontrol = $report && ($att->grade > 0); + $this->curdate = $att->pageparams->curdate; $date = usergetdate($att->pageparams->curdate); @@ -456,6 +460,12 @@ class attendance_report_data implements renderable { global $CFG; $this->perm = $att->perm; + + $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); @@ -473,16 +483,28 @@ class attendance_report_data implements renderable { $this->decimalpoints = $CFG->grade_decimalpoints; } - foreach ($this->users as $user) { - $this->usersgroups[$user->id] = groups_get_all_groups($att->course->id, $user->id); + $maxgrade = att_get_user_max_grade(count($this->sessions), $this->statuses); - $this->sessionslog[$user->id] = $att->get_user_filtered_sessions_log($user->id); + foreach ($this->users as $key => $user) { + $grade = 0; + if ($this->gradable) { + $grade = $att->get_user_grade($user->id, array('enddate' => $currenttime)); + $totalgrade = $att->get_user_grade($user->id); + } - $this->usersstats[$user->id] = $att->get_user_statuses_stat($user->id); + if ($att->pageparams->view != ATT_VIEW_NOTPRESENT || $grade < $maxgrade) { + $this->usersgroups[$user->id] = groups_get_all_groups($att->course->id, $user->id); - if ($this->gradable) { - $this->grades[$user->id] = $att->get_user_grade($user->id); - $this->maxgrades[$user->id] = $att->get_user_max_grade($user->id); + $this->sessionslog[$user->id] = $att->get_user_filtered_sessions_log($user->id); + + $this->usersstats[$user->id] = $att->get_user_statuses_stat($user->id); + + if ($this->gradable) { + $this->grades[$user->id] = $totalgrade; + $this->maxgrades[$user->id] = $att->get_user_max_grade($user->id);; + } + } else { + unset($this->users[$key]); } } diff --git a/renderer.php b/renderer.php old mode 100755 new mode 100644 index f8ce347..9f43f0d --- a/renderer.php +++ b/renderer.php @@ -104,12 +104,12 @@ class mod_attendance_renderer extends plugin_renderer_base { } $totalusers = count_enrolled_users(context_module::instance($fcontrols->cm->id), 'mod/attendance:canbelisted', $group); - $usersperpage = $fcontrols->pageparams->perpage; - if (empty($fcontrols->pageparams->page) || !$fcontrols->pageparams->page || !$totalusers || !$usersperpage) { + + if (empty($fcontrols->pageparams->page) || !$fcontrols->pageparams->page || !$totalusers || empty($fcontrols->pageparams->perpage)) { return $paging_controls; } - $numberofpages = ceil($totalusers / $usersperpage); + $numberofpages = ceil($totalusers / $fcontrols->pageparams->perpage); if ($fcontrols->pageparams->page > 1) { $paging_controls .= html_writer::link($fcontrols->url(array('curdate' => $fcontrols->nextcur, 'page' => $fcontrols->pageparams->page - 1)), @@ -173,6 +173,9 @@ 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) { + $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'); @@ -734,8 +737,17 @@ class mod_attendance_renderer extends plugin_renderer_base { $cell->colspan = 2; $row->cells[] = $cell; } else { - $row->cells[] = '?'; - $row->cells[] = ''; + if (!empty($sess->studentscanmark)) { // Student can mark their own attendance. + // URL to the page that lets the student modify their attendance. + $url = new moodle_url('/mod/attendance/attendance.php', + array('sessid' => $sess->id, 'sesskey' => sesskey())); + $cell = new html_table_cell(html_writer::link($url, get_string('submitattendance', 'attendance'))); + $cell->colspan = 2; + $row->cells[] = $cell; + } else { // Student cannot mark their own attendace. + $row->cells[] = '?'; + $row->cells[] = ''; + } } $table->data[] = $row; @@ -751,6 +763,14 @@ class mod_attendance_renderer extends plugin_renderer_base { } protected function render_attendance_report_data(attendance_report_data $reportdata) { + global $PAGE; + + // Initilise Javascript used to (un)check all checkboxes. + $this->page->requires->js_init_call('M.mod_attendance.init_manage'); + + // 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); + $table = new html_table(); $table->attributes['class'] = 'generaltable attwidth'; @@ -792,13 +812,20 @@ class mod_attendance_renderer extends plugin_renderer_base { $table->align[] = 'center'; $table->size[] = '1px'; } - + if ($reportdata->sessionslog) { $table->head[] = get_string('remarks', 'attendance'); $table->align[] = 'center'; $table->size[] = '200px'; } - + + if ($bulkmessagecapability) { // Display the table header for bulk messaging. + // The checkbox must have an id of cb_selector so that the JavaScript will pick it up. + $table->head[] = html_writer::checkbox('cb_selector', 0, false, '', array('id' => 'cb_selector')); + $table->align[] = 'center'; + $table->size[] = '1px'; + } + foreach ($reportdata->users as $user) { $row = new html_table_row(); @@ -827,6 +854,11 @@ class mod_attendance_renderer extends plugin_renderer_base { $row->cells[] = ''; } } + + if ($bulkmessagecapability) { // Create the checkbox for bulk messaging. + $row->cells[] = html_writer::checkbox('user'.$user->id, 'on', false); + } + $table->data[] = $row; } @@ -837,7 +869,9 @@ class mod_attendance_renderer extends plugin_renderer_base { foreach ($reportdata->sessions as $sess) { foreach ($reportdata->users as $user) { foreach($reportdata->statuses as $status) { - if ($reportdata->sessionslog[$user->id][$sess->id]->statusid == $status->id) $sessionstats[$status->id]++; + if (!empty($reportdata->sessionslog[$user->id][$sess->id])) { + if ($reportdata->sessionslog[$user->id][$sess->id]->statusid == $status->id) $sessionstats[$status->id]++; + } } } @@ -849,8 +883,23 @@ class mod_attendance_renderer extends plugin_renderer_base { } $table->data[] = $statrow; - - return html_writer::table($table).html_writer::tag('div', get_string('users').': '.count($reportdata->users)); + + if ($bulkmessagecapability) { // Require that the user can bulk message users. + // Display check boxes that will allow the user to send a message to the students that have been checked. + $output = html_writer::empty_tag('input', array('name' => 'sesskey', 'type' => 'hidden', 'value' => sesskey())); + $output .= html_writer::empty_tag('input', array('name' => 'formaction', 'type' => 'hidden', 'value' => 'messageselect.php')); + $output .= html_writer::empty_tag('input', array('name' => 'id', 'type' => 'hidden', 'value' => $GLOBALS['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::tag('div', + html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('messageselectadd'))), + array('class' => 'buttons')); + $url = new moodle_url('/user/action_redir.php'); + return html_writer::tag('form', $output, array('action' => $url->out(), 'method' => 'post')); + } else { + return html_writer::table($table).html_writer::tag('div', get_string('users').': '.count($reportdata->users)); + } + } protected function render_attendance_preferences_data(attendance_preferences_data $prefdata) { diff --git a/report.php b/report.php index 8a4d4cf..b9d37eb 100644 --- a/report.php +++ b/report.php @@ -57,7 +57,7 @@ $PAGE->navbar->add(get_string('report', 'attendance')); $output = $PAGE->get_renderer('mod_attendance'); $tabs = new attendance_tabs($att, attendance_tabs::TAB_REPORT); -$filtercontrols = new attendance_filter_controls($att); +$filtercontrols = new attendance_filter_controls($att, true); $reportdata = new attendance_report_data($att); add_to_log($course->id, 'attendance', 'report viewed', '/mod/attendance/report.php?id='.$id, '', $cm->id); diff --git a/sessions.php b/sessions.php index b03b3df..fda84fe 100644 --- a/sessions.php +++ b/sessions.php @@ -220,6 +220,9 @@ function construct_sessions_data_for_add($formdata) { $sess->description = $formdata->sdescription['text']; $sess->descriptionformat = $formdata->sdescription['format']; $sess->timemodified = $now; + if (isset($formdata->studentscanmark)) { // Students will be able to mark their own attendance. + $sess->studentscanmark = 1; + } fill_groupid($formdata, $sessions, $sess); } @@ -237,6 +240,9 @@ function construct_sessions_data_for_add($formdata) { $sess->description = $formdata->sdescription['text']; $sess->descriptionformat = $formdata->sdescription['format']; $sess->timemodified = $now; + if (isset($formdata->studentscanmark)) { // Students will be able to mark their own attendance. + $sess->studentscanmark = 1; + } fill_groupid($formdata, $sessions, $sess); } diff --git a/student_attenance_form.php b/student_attenance_form.php new file mode 100644 index 0000000..5f7d7d7 --- /dev/null +++ b/student_attenance_form.php @@ -0,0 +1,63 @@ +. + +require_once($CFG->libdir.'/formslib.php'); + +class mod_attendance_student_attendance_form extends moodleform { + public function definition() { + global $CFG, $USER; + + $mform =& $this->_form; + + $course = $this->_customdata['course']; + $cm = $this->_customdata['cm']; + $modcontext = $this->_customdata['modcontext']; + $attforsession = $this->_customdata['session']; + $attblock = $this->_customdata['attendance']; + + $statuses = $attblock->get_statuses(); + + $mform->addElement('hidden', 'sessid', null); + $mform->setType('sessid', PARAM_INT); + $mform->setConstant('sessid', $attforsession->id); + + $mform->addElement('hidden', 'sesskey', null); + $mform->setType('sesskey', PARAM_INT); + $mform->setConstant('sesskey', sesskey()); + + // Set a title as the date and time of the session. + $sesstiontitle = userdate($attforsession->sessdate, get_string('strftimedate')).' ' + .userdate($attforsession->sessdate, get_string('strftimehm', 'mod_attendance')); + + $mform->addElement('header', 'session', $sesstiontitle); + + // If a session description is set display it. + if (!empty($attforsession->description)) { + $mform->addElement('html', $attforsession->description); + } + + // Create radio buttons for setting the attendance status. + $radioarray = array(); + foreach ($statuses as $status) { + $radioarray[] =& $mform->createElement('radio', 'status', '', $status->description, $status->id, array()); + } + // Add the radio buttons as a control with the user's name in front. + $mform->addGroup($radioarray, 'statusarray', $USER->firstname.' '.$USER->lastname.':', array(''), false); + $mform->addRule('statusarray', get_string('attendancenotset', 'attendance'), 'required', '', 'client', false, false); + + $this->add_action_buttons(); + } +} \ No newline at end of file diff --git a/tests/behat/attendance_mod.feature b/tests/behat/attendance_mod.feature new file mode 100644 index 0000000..34c04c9 --- /dev/null +++ b/tests/behat/attendance_mod.feature @@ -0,0 +1,105 @@ +@mod @uon @mod_attendance +Feature: Teachers and Students can record session attendance + In order to record session attendance + As a student + I need to be able to mark my own attendance to a session + And as a teacher + I need to be able to mark any students attendance to a session + In order to report on session attendance + As a teacher + I need to be able to export session attendance and run reports + In order to contact students with poor attendance + As a teacher + I need the ability to message a group of students with low attendance + + Background: + Given the following "courses" exist: + | fullname | shortname | summary | category | + | Course 1 | C101 | Prove the attendance activity works | 0 | + And the following "users" exist: + | username | firstname | lastname | email | idnumber | department | institution | + | student1 | Sam | Student | student1@asd.com | 1234 | computer science | University of Nottingham | + | teacher1 | Teacher | One | teacher1@asd.com | 5678 | computer science | University of Nottingham | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C101 | student | + | teacher1 | C101 | editingteacher | + And I log in as "teacher1" + And I follow "Course 1" + And I turn editing mode on + And I add a "Attendance" to section "1" + And I press "Save and display" + And I log out + + Scenario: Students can mark their own attendance + When I log in as "teacher1" + And I follow "Course 1" + And I follow "Attendance" + And I follow "Add" + And I check "Allow students to record own attendance" + And I set the following fields to these values: + | id_sessiondate_hour | 23 | + And I click on "id_submitbutton" "button" + And I follow "Continue" + And I log out + When I log in as "student1" + And I follow "Course 1" + And I follow "Attendance" + And I follow "Submit attendance" + And I check "Present" + And I press "Save changes" + Then I should see "Self-recorded" + And I log out + When I log in as "teacher1" + And I follow "Course 1" + And I expand "Reports" node + And I follow "Logs" + 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 + When I log in as "teacher1" + And I follow "Course 1" + And I follow "Attendance" + And I follow "Add" + And I set the following fields to these values: + | id_sessiondate_hour | 01 | + And I click on "id_submitbutton" "button" + And I follow "Continue" + And I follow "Report" + And I follow "Low grade" + And I check "user3" + And I click on "Send a message" "button" + Then I should see "Message body" + And I should see "student1@asd.com" + And I expand "Reports" node + And I follow "Logs" + And I click on "Get these logs" "button" + Then "attendance report viewed" "link" should exist + + # Dependency - selenium running with firefox profile with auto saving of txt files to $CFG->behat_download. + @javascript @_file_download + Scenario: Export report includes id number, department and institution + When I log in as "teacher1" + And I follow "Course 1" + And I follow "Attendance" + And I follow "Add" + And I set the following fields to these values: + | id_sessiondate_hour | 01 | + And I click on "id_submitbutton" "button" + And I follow "Continue" + And I follow "Export" + Then the "id_ident_idnumber" checkbox should not be checked + And the "id_ident_institution" checkbox should not be checked + And the "id_ident_department" checkbox should not be checked + And I check "id_ident_idnumber" + And I check "id_ident_institution" + And I check "id_ident_department" + And I set the following fields to these values: + | format | Download in text format | + And I click on "OK" "button" + Then attendance export file is ok + And I should see "ID number" as "1234" in the file + And I should see "Department" as "computer science" in the file + And I should see "Institution" as "University of Nottingham" in the file + diff --git a/tests/behat/behat_mod_attendance.php b/tests/behat/behat_mod_attendance.php new file mode 100644 index 0000000..cbcd3b0 --- /dev/null +++ b/tests/behat/behat_mod_attendance.php @@ -0,0 +1,116 @@ +. + + +// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. + +require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); + +use Behat\Mink\Exception\ExpectationException as ExpectationException, + Behat\Behat\Exception\PendingException as PendingException; + +/** + * Attendance steps definitions. + * + * @package mod + * @subpackage attendance + * @category test + * @copyright 2014 University of Nottingham + * @author Joseph Baxter (joseph.baxter@nottingham.ac.uk) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_mod_attendance extends behat_base { + + protected $file_contents; + + /** + * @Then /^attendance export file is ok$/ + */ + public function attendance_export_file_is_ok() { + + global $CFG; + + $check = true; + + // Location selenium will download to. + $dir = $CFG->behat_download; + $files = scandir($dir, 1); + $filename = $files[0]; + $file = fopen($dir . $filename, "r"); + + $count = 0; + $header = null; + + // The file is tab seperated but not exactly a tsv. + while (($row = fgetcsv($file, 0, "\t")) !== FALSE) { + + // Ignore unwanted information at the start of the file. + if ($count < 3) { + $count++; + continue; + } + + if (!$header) { + $header = $row; + } else { + $this->file_contents = array_combine($header, $row); + } + + $count++; + } + + fclose($file); + unlink($dir . $filename); + + // Check if data rows exist. + if ($count < 2) { + $check = false; + } + + if ($check) { + + return true; + + } else { + + throw new ExpectationException('Attendance export file not ok', $this->getSession()); + } + + } + + /** + * @Given /^I should see "([^"]*)" as "([^"]*)" in the file$/ + */ + public function i_should_see_as_in_the_file($field, $value) { + + foreach ($this->file_contents as $array_field => $array_value) { + + if ($field == $array_field) { + + if ($value == $array_value) { + + return true; + + } else { + + throw new PendingException(); + + } + } + } + } + +}