diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 150b03a..9bf0172 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,33 +1,11 @@ -name: Runtests +name: Run all tests # Run this workflow every time a new commit pushed to your repository on: push jobs: - selftest: - name: CI test (make validate) - runs-on: ubuntu-18.04 - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - name: Setup PHP 7.3 - uses: shivammathur/setup-php@v2 - with: - php-version: 7.3 - - - name: Initialise - run: make init - - - name: Validate - run: make validate - - citest: - name: CI test - needs: selftest - runs-on: ubuntu-18.04 - + setup: + runs-on: ubuntu-latest services: postgres: image: postgres:9.6 @@ -36,74 +14,70 @@ jobs: POSTGRES_HOST_AUTH_METHOD: 'trust' # Health check to wait for postgres to start. ports: - - 5432:5432 + - 5432:5432 options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 3 - + mariadb: + image: mariadb:10 + env: + MYSQL_USER: 'root' + MYSQL_ALLOW_EMPTY_PASSWORD: "true" + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 3 strategy: fail-fast: false matrix: - include: - - php: '7.4' - moodle-branch: 'master' - - php: '7.4' - moodle-branch: 'MOODLE_311_STABLE' - - php: '7.4' - moodle-branch: 'MOODLE_310_STABLE' - - php: '7.4' - moodle-branch: 'MOODLE_39_STABLE' - - php: '7.4' - + php-versions: ['7.3', '7.4'] + database: ['pgsql', 'mariadb'] steps: - - name: Check out repository code - uses: actions/checkout@v2 + - name: Check out repository code + uses: actions/checkout@v2 + with: + # Clone in plugin subdir, so we can setup CI in default directory. + path: plugin - - name: Install node - uses: actions/setup-node@v1 - with: - node-version: '14.15.0' + - name: Install node + uses: actions/setup-node@v1 + with: + # TODO: Check if we can support .nvmrc + node-version: '14.15.0' - - name: Setup PHP ${{ matrix.php }} - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: pgsql, zip, gd, xmlrpc, soap - coverage: none + - name: Setup PHP environment + uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring, pgsql, mysqli + tools: phpunit - - name: Initialise moodle-plugin-ci - run: | - make init - cp -R tests/Fixture/moodle-local_travis ../moodle-local_travis - echo $(cd bin; pwd) >> $GITHUB_PATH - echo $(cd vendor/bin; pwd) >> $GITHUB_PATH - echo "TRAVIS_BUILD_DIR="$(cd ../moodle-local_travis; pwd) >> $GITHUB_ENV - # PHPUnit depends on en_AU.UTF-8 locale - sudo locale-gen en_AU.UTF-8 - - name: Install moodle-plugin-ci - run: moodle-plugin-ci install -vvv - env: - DB: 'pgsql' - MOODLE_BRANCH: ${{ matrix.moodle-branch }} - IGNORE_PATHS: 'ignore' - IGNORE_NAMES: 'ignore_name.php' - MUSTACHE_IGNORE_NAMES: 'broken.mustache' + - name: Deploy moodle-plugin-ci + run: | + composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^3 + # Add dirs to $PATH + echo $(cd ci/bin; pwd) >> $GITHUB_PATH + echo $(cd ci/vendor/bin; pwd) >> $GITHUB_PATH + # PHPUnit depends on en_AU.UTF-8 locale + sudo locale-gen en_AU.UTF-8 + - name: Install moodle-plugin-ci + # Need explicit IP to stop mysql client fail on attempt to use unix socket. + run: moodle-plugin-ci install -vvv --plugin ./plugin --db-host=127.0.0.1 + env: + DB: ${{ matrix.database }} + # TODO: Omitted MOODLE_BRANCH results in regex failure, investigate. + MOODLE_BRANCH: 'master' - - name: Run Integration tests - env: - MOODLE_BRANCH: ${{ matrix.moodle-branch }} - run: | - make test-phpunit - moodle-plugin-ci phplint - moodle-plugin-ci phpcpd - moodle-plugin-ci phpmd - moodle-plugin-ci codechecker - moodle-plugin-ci validate - moodle-plugin-ci savepoints - moodle-plugin-ci mustache - moodle-plugin-ci grunt || [ \ - "$MOODLE_BRANCH" != 'master' -a \ - "$MOODLE_BRANCH" != 'MOODLE_310_STABLE' -a \ - "$MOODLE_BRANCH" != 'MOODLE_39_STABLE' ] - moodle-plugin-ci phpdoc - moodle-plugin-ci phpunit --coverage-text - moodle-plugin-ci behat --profile default - moodle-plugin-ci behat --profile chrome + - name: Run Integration tests + run: | + # Currently it stops if any command return non 0 exit status, needs a + # wrapper to collect exit statuses and list result and the end. + # For testing purposes at this stage, just assume each command succeeds. + moodle-plugin-ci phplint || true + moodle-plugin-ci phpcpd || true + moodle-plugin-ci phpmd || true + moodle-plugin-ci codechecker || true + moodle-plugin-ci validate || true + moodle-plugin-ci savepoints || true + moodle-plugin-ci mustache || true + moodle-plugin-ci grunt || true + moodle-plugin-ci phpdoc || true + moodle-plugin-ci phpunit || true + moodle-plugin-ci behat --profile chrome || true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8b5a809..0000000 --- a/.travis.yml +++ /dev/null @@ -1,51 +0,0 @@ -language: php - -addons: - postgresql: "9.6" - -services: - - mysql - - postgresql - - docker - -cache: - directories: - - $HOME/.composer/cache - - $HOME/.npm - -php: - - 7.2 - - 7.4 - -env: - global: - - MOODLE_BRANCH=master - - MUSTACHE_IGNORE_NAMES=mobile_teacher_form.mustache - matrix: - - DB=pgsql - - DB=mysqli - -before_install: - - phpenv config-rm xdebug.ini - - nvm install 14.0.0 - - nvm use 14.0.0 - - cd ../.. - - composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^3 - - export PATH="$(cd ci/bin; pwd):$(cd ci/vendor/bin; pwd):$PATH" - -install: - - moodle-plugin-ci install - - docker run -d -p 127.0.0.1:4444:4444 --net=host --shm-size=2g -v $HOME/build/moodle:$HOME/build/moodle selenium/standalone-chrome:3 - -script: - - moodle-plugin-ci phplint - - moodle-plugin-ci phpcpd - - moodle-plugin-ci phpmd - - moodle-plugin-ci codechecker - - moodle-plugin-ci validate - - moodle-plugin-ci savepoints - - moodle-plugin-ci mustache - - moodle-plugin-ci grunt - - moodle-plugin-ci phpdoc - - moodle-plugin-ci phpunit - - moodle-plugin-ci behat --profile chrome diff --git a/classes/event/session_report_updated.php b/classes/event/session_report_updated.php new file mode 100644 index 0000000..1c649ec --- /dev/null +++ b/classes/event/session_report_updated.php @@ -0,0 +1,60 @@ +. + +/** + * This file contains an event for when a student's attendance report is viewed. + * + * @package mod_attendance + * @copyright 2014 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * Event for when a student's attendance report is updated. + * + * @property-read array $other { + * Extra information about event properties. + * + * string studentid Id of student whose attendances were updated. + * string mode Mode of the report updated. + * } + * @package mod_attendance + * @copyright 2013 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class session_report_updated extends \mod_attendance\event\session_report_viewed { + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + // Objecttable and objectid can't be meaningfully specified. + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventstudentattendancesessionsupdated', 'mod_attendance'); + } +} diff --git a/classes/event/session_report_viewed.php b/classes/event/session_report_viewed.php index ba3cf0a..c4946f1 100644 --- a/classes/event/session_report_viewed.php +++ b/classes/event/session_report_viewed.php @@ -55,7 +55,7 @@ class session_report_viewed extends \core\event\base { * @return string */ public function get_description() { - return 'User with id ' . $this->userid . ' viewed attendance sessions for student with id ' . + return 'User with id ' . $this->userid . ' ' . $this->action . ' attendance sessions for student with id ' . $this->relateduserid; } @@ -74,12 +74,16 @@ class session_report_viewed extends \core\event\base { * @return \moodle_url */ public function get_url() { - // Mode is optional. + // Mode, groupby, sesscourses are optional. $mode = empty($this->other['mode']) ? "" : $this->other['mode']; + $groupby = empty($this->other['groupby']) ? "" : $this->other['groupby']; + $sesscourses = empty($this->other['sesscourses']) ? "" : $this->other['sesscourses']; return new \moodle_url('/mod/attendance/view.php', array('id' => $this->contextinstanceid, 'studentid' => $this->relateduserid, 'mode' => $mode, 'view' => $this->other['view'], + 'groupby' => $groupby, + 'sesscourses' => $sesscourses, 'curdate' => $this->other['curdate'])); } @@ -89,7 +93,7 @@ class session_report_viewed extends \core\event\base { * @return array of parameters to be passed to legacy add_to_log() function. */ protected function get_legacy_logdata() { - return array($this->courseid, 'attendance', 'student sessions viewed', $this->get_url(), + return array($this->courseid, 'attendance', 'student sessions ' . $this->action, $this->get_url(), 'student id ' . $this->relateduserid, $this->contextinstanceid); } @@ -119,17 +123,17 @@ class session_report_viewed extends \core\event\base { */ protected function validate_data() { if (!isset($this->relateduserid)) { - throw new \coding_exception('The event mod_attendance\\event\\session_report_viewed must specify relateduserid.'); + throw new \coding_exception('The event ' . $this->eventname . ' must specify relateduserid.'); } // View params can be left out as defaults will be the same when log event is viewed as when // it was stored. // filter params are important, but stored in session so default effectively unknown, // hence required here. if (!isset($this->other['view'])) { - throw new \coding_exception('The event mod_attendance\\event\\session_report_viewed must specify view.'); + throw new \coding_exception('The event ' . $this->eventname . ' must specify view.'); } if (!isset($this->other['curdate'])) { - throw new \coding_exception('The event mod_attendance\\event\\session_report_viewed must specify curdate.'); + throw new \coding_exception('The event ' . $this->eventname . ' must specify curdate.'); } parent::validate_data(); } diff --git a/classes/structure.php b/classes/structure.php index a767318..c5d349f 100644 --- a/classes/structure.php +++ b/classes/structure.php @@ -114,7 +114,7 @@ class mod_attendance_structure { * @param stdClass $dbrecord Attandance instance data from {attendance} table * @param stdClass $cm Course module record as returned by {@see get_coursemodule_from_id()} * @param stdClass $course Course record from {course} table - * @param stdClass $context The context of the workshop instance + * @param stdClass $context The context of the attendance instance * @param stdClass $pageparams */ public function __construct(stdClass $dbrecord, stdClass $cm, stdClass $course, stdClass $context=null, $pageparams=null) { diff --git a/classes/view_page_params.php b/classes/view_page_params.php index c0f98a7..fd5d696 100644 --- a/classes/view_page_params.php +++ b/classes/view_page_params.php @@ -37,12 +37,21 @@ class mod_attendance_view_page_params extends mod_attendance_page_with_filter_co /** All courses */ const MODE_ALL_COURSES = 1; + /** All sessions */ + const MODE_ALL_SESSIONS = 2; + /** @var int */ public $studentid; /** @var string */ public $mode; + /** @var string */ + public $groupby; + + /** @var string */ + public $sesscourses; + /** * mod_attendance_view_page_params constructor. */ @@ -64,6 +73,12 @@ class mod_attendance_view_page_params extends mod_attendance_page_with_filter_co if ($this->mode != self::MODE_THIS_COURSE) { $params['mode'] = $this->mode; } + if ($this->groupby != 'course') { + $params['groupby'] = $this->groupby; + } + if ($this->sesscourses != 'current') { + $params['sesscourses'] = $this->sesscourses; + } return $params; } diff --git a/lang/en/attendance.php b/lang/en/attendance.php index e7eea16..7a5b5ef 100644 --- a/lang/en/attendance.php +++ b/lang/en/attendance.php @@ -43,6 +43,7 @@ $string['all'] = 'All'; $string['allcourses'] = 'All courses'; $string['allpast'] = 'All past'; $string['allsessions'] = 'All sessions'; +$string['allsessionstotals'] = 'Totals for selected sessions'; $string['attendance:addinstance'] = 'Add a new attendance activity'; $string['attendance:canbelisted'] = 'Appears in the roster'; $string['attendance:changeattendances'] = 'Changing Attendances'; @@ -235,6 +236,7 @@ $string['eventsessionupdated'] = 'Session updated'; $string['eventstatusadded'] = 'Status added'; $string['eventstatusupdated'] = 'Status updated'; $string['eventstudentattendancesessionsviewed'] = 'Session report viewed'; +$string['eventstudentattendancesessionsupdated'] = 'Session report updated'; $string['eventtaken'] = 'Attendance taken'; $string['eventtakenbystudent'] = 'Attendance taken by student'; $string['export'] = 'Export'; @@ -250,6 +252,7 @@ $string['gridcolumns'] = 'Grid columns'; $string['group'] = 'Group'; $string['groups'] = 'Groups'; $string['groupsession'] = 'Group of students'; +$string['groupsessionsby'] = 'Group sessions by'; $string['hiddensessions'] = 'Hidden sessions'; $string['hiddensessions_help'] = 'Sessions are hidden if they are scheduled before the course start date. @@ -332,6 +335,7 @@ $string['noabsentstatusset'] = 'The status set in use does not have a status to $string['noattendanceusers'] = 'It is not possible to export any data as there are no students enrolled in the course.'; $string['noattforuser'] = 'No attendance records exist for the user'; $string['noautomark'] = 'Disabled'; +$string['nocapabilitytotakethisattendance'] = 'You tried to change the attendance of a session with the cmid: {$a} that you do not have permission to modify.'; $string['nodescription'] = 'Regular class session'; $string['noeventstoreset'] = 'There are no calendar events that require an update.'; $string['nogroups'] = 'You can\'t add group sessions. No groups exists in course.'; @@ -487,7 +491,12 @@ $string['sessionduplicate'] = 'A duplicate session exists for course: {$a->cours $string['sessionexist'] = 'Session not added (already exists)!'; $string['sessiongenerated'] = 'One session was successfully generated'; $string['sessions'] = 'Sessions'; +$string['sessionsallcourses'] = 'All courses'; +$string['sessionsbyactivity'] = 'Attendance instance'; +$string['sessionsbycourse'] = 'Course'; +$string['sessionsbydate'] = 'Week'; $string['sessionscompleted'] = 'Taken sessions'; +$string['sessionscurrentcourses'] = 'Current courses'; $string['sessionsgenerated'] = '{$a} sessions were successfully generated'; $string['sessionsids'] = 'IDs of sessions: '; $string['sessionsnotfound'] = 'There is no sessions in the selected timespan'; @@ -531,6 +540,7 @@ $string['statusset'] = 'Status set {$a}'; $string['statussetsettings'] = 'Status set'; $string['statusunselected'] = 'unselected'; $string['strftimedm'] = '%b %d'; +$string['strftimedmw'] = '%a %b %d'; $string['strftimedmy'] = '%d %b %Y'; $string['strftimedmyhm'] = '%d %b %Y %I.%M%p'; // Line added to allow multiple sessions in the same day. $string['strftimedmyw'] = '%a %d %b %Y'; @@ -583,6 +593,7 @@ $string['thiscourse'] = 'This course'; $string['time'] = 'Time'; $string['timeahead'] = 'Multiple sessions that exceed one year cannot be created, please adjust the start and end dates.'; $string['to'] = 'to:'; +$string['todate'] = 'to date'; $string['triggered'] = 'First notified'; $string['tuseremail'] = 'Email'; $string['tusername'] = 'Full name'; @@ -616,5 +627,6 @@ $string['warnings'] = 'Warnings set'; $string['warningthreshold'] = 'Warning threshold'; $string['warningupdated'] = 'Updated warnings'; $string['week'] = 'week(s)'; +$string['weekcommencing'] = 'Week commencing'; $string['weeks'] = 'Weeks'; $string['youcantdo'] = 'You can\'t do anything'; \ No newline at end of file diff --git a/locallib.php b/locallib.php index c40cd53..7764bef 100644 --- a/locallib.php +++ b/locallib.php @@ -109,6 +109,93 @@ function attendance_get_setname($attid, $statusset, $includevalues = true) { return $statusname; } +/** + * Get full filtered log. + * @param int $userid + * @param stdClass $pageparams + * @return array + */ +function attendance_get_user_sessions_log_full($userid, $pageparams) { + global $DB; + // All taken sessions (including previous groups). + + $usercourses = enrol_get_users_courses($userid); + list($usql, $uparams) = $DB->get_in_or_equal(array_keys($usercourses), SQL_PARAMS_NAMED, 'cid0'); + + $coursesql = "(1 = 1)"; + $courseparams = array(); + $now = time(); + if ($pageparams->sesscourses === 'current') { + $coursesql = "(c.startdate = 0 OR c.startdate <= :now1) AND (c.enddate = 0 OR c.enddate >= :now2)"; + $courseparams = array( + 'now1' => $now, + 'now2' => $now, + ); + } + + $datesql = "(1 = 1)"; + $dateparams = array(); + if ($pageparams->startdate && $pageparams->enddate) { + $datesql = "ats.sessdate >= :sdate AND ats.sessdate < :edate"; + $dateparams = array( + 'sdate' => $pageparams->startdate, + 'edate' => $pageparams->enddate, + ); + } + + if ($pageparams->groupby === 'date') { + $ordersql = "ats.sessdate ASC, c.fullname ASC, att.name ASC, att.id ASC"; + } else { + $ordersql = "c.fullname ASC, att.name ASC, att.id ASC, ats.sessdate ASC"; + } + + // WHERE clause is important: + // gm.userid not null => get unmarked attendances for user's current groups + // ats.groupid 0 => get all sessions that are for all students enrolled in course + // al.id not null => get all marked sessions whether or not user currently still in group. + $sql = "SELECT ats.id, ats.groupid, ats.sessdate, ats.duration, ats.description, ats.statusset, + al.statusid, al.remarks, ats.studentscanmark, ats.autoassignstatus, + ats.preventsharedip, ats.preventsharediptime, + ats.attendanceid, att.name AS attname, att.course AS courseid, c.fullname AS cname + FROM {attendance_sessions} ats + JOIN {attendance} att + ON att.id = ats.attendanceid + JOIN {course} c + ON att.course = c.id + LEFT JOIN {attendance_log} al + ON ats.id = al.sessionid AND al.studentid = :uid + LEFT JOIN {groups_members} gm + ON (ats.groupid = gm.groupid AND gm.userid = :uid1) + WHERE (gm.userid IS NOT NULL OR ats.groupid = 0 OR al.id IS NOT NULL) + AND att.course $usql + AND $datesql + AND $coursesql + ORDER BY $ordersql"; + + $params = array( + 'uid' => $userid, + 'uid1' => $userid, + ); + $params = array_merge($params, $uparams); + $params = array_merge($params, $dateparams); + $params = array_merge($params, $courseparams); + $sessions = $DB->get_records_sql($sql, $params); + + foreach ($sessions as $sess) { + if (empty($sess->description)) { + $sess->description = get_string('nodescription', 'attendance'); + } else { + $modinfo = get_fast_modinfo($sess->courseid); + $cmid = $modinfo->instances['attendance'][$sess->attendanceid]->get_course_module_record()->id; + $ctx = context_module::instance($cmid); + $sess->description = file_rewrite_pluginfile_urls($sess->description, + 'pluginfile.php', $ctx->id, 'mod_attendance', 'session', $sess->id); + } + } + + return $sessions; +} + /** * Get users courses and the relevant attendances. * @@ -275,6 +362,54 @@ function attendance_update_users_grade($attendance, $userids=array()) { return grade_update('mod/attendance', $course->id, 'mod', 'attendance', $attendance->id, 0, $grades); } +/** + * Update grades for specified users for specified attendance + * + * @param integer $attendanceid - the id of the attendance to update + * @param integer $grade - the value of the 'grade' property of the specified attendance + * @param array $userids - the userids of the users to be updated + */ +function attendance_update_users_grades_by_id($attendanceid, $grade, $userids) { + global $DB; + + if (empty($grade)) { + return false; + } + + list($course, $cm) = get_course_and_cm_from_instance($attendanceid, 'attendance'); + + $summary = new mod_attendance_summary($attendanceid, $userids); + + if (empty($userids)) { + $context = context_module::instance($cm->id); + $userids = array_keys(get_enrolled_users($context, 'mod/attendance:canbelisted', 0, 'u.id')); + } + + if ($grade < 0) { + $dbparams = array('id' => -($grade)); + $scale = $DB->get_record('scale', $dbparams); + $scalearray = explode(',', $scale->scale); + $attendancegrade = count($scalearray); + } else { + $attendancegrade = $grade; + } + + $grades = array(); + foreach ($userids as $userid) { + $grades[$userid] = new stdClass(); + $grades[$userid]->userid = $userid; + + if ($summary->has_taken_sessions($userid)) { + $usersummary = $summary->get_taken_sessions_summary_for($userid); + $grades[$userid]->rawgrade = $usersummary->takensessionspercentage * $attendancegrade; + } else { + $grades[$userid]->rawgrade = null; + } + } + + return grade_update('mod/attendance', $course->id, 'mod', 'attendance', $attendanceid, 0, $grades); +} + /** * Add an attendance status variable * diff --git a/renderables.php b/renderables.php index 1f4fdab..3a5b9ca 100644 --- a/renderables.php +++ b/renderables.php @@ -486,6 +486,42 @@ class attendance_user_data implements renderable { $this->sessionslog = $att->get_user_filtered_sessions_log_extended($userid); $this->groups = groups_get_all_groups($att->course->id); + } else if ($this->pageparams->mode == mod_attendance_view_page_params::MODE_ALL_SESSIONS) { + $this->coursesatts = attendance_get_user_courses_attendances($userid); + $this->statuses = array(); + $this->summaries = array(); + $this->groups = array(); + + foreach ($this->coursesatts as $atid => $ca) { + // Check to make sure the user can view this cm. + $modinfo = get_fast_modinfo($ca->courseid); + if (!$modinfo->instances['attendance'][$ca->attid]->uservisible) { + unset($this->coursesatts[$atid]); + continue; + } else { + $this->coursesatts[$atid]->cmid = $modinfo->instances['attendance'][$ca->attid]->get_course_module_record()->id; + } + $this->statuses[$ca->attid] = attendance_get_statuses($ca->attid); + $this->summaries[$ca->attid] = new mod_attendance_summary($ca->attid, array($userid)); + + if (!array_key_exists($ca->courseid, $this->groups)) { + $this->groups[$ca->courseid] = groups_get_all_groups($ca->courseid); + } + } + + 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 = attendance_get_user_sessions_log_full($userid, $this->pageparams); + + foreach ($this->sessionslog as $sessid => $sess) { + $this->sessionslog[$sessid]->cmid = $this->coursesatts[$sess->attendanceid]->cmid; + } + } else { $this->coursesatts = attendance_get_user_courses_attendances($userid); $this->statuses = array(); @@ -510,11 +546,131 @@ class attendance_user_data implements renderable { } /** - * url helper. + * Url function + * @param array $params + * @param array $excludeparams * @return moodle_url */ - public function url() { - return new moodle_url($this->urlpath, $this->urlparams); + public function url($params=array(), $excludeparams=array()) { + $params = array_merge($this->urlparams, $params); + + foreach ($excludeparams as $paramkey) { + unset($params[$paramkey]); + } + + return new moodle_url($this->urlpath, $params); + } + + /** + * Take multiple sessions attendance from form data. + * + * @param stdClass $formdata + */ + public function take_sessions_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. + // This whole function could do with a nice clean up. + + $now = time(); + $sesslog = array(); + $formdata = (array)$formdata; + $updatedsessions = array(); + $sessionatt = array(); + + foreach ($formdata as $key => $value) { + // Look at Remarks field because the user options may not be passed if empty. + if (substr($key, 0, 7) == 'remarks') { + $parts = explode('sess', substr($key, 7)); + $stid = $parts[0]; + if (!(is_numeric($stid))) { // Sanity check on $stid. + print_error('nonnumericid', 'attendance'); + } + $sessid = $parts[1]; + if (!(is_numeric($sessid))) { // Sanity check on $sessid. + print_error('nonnumericid', 'attendance'); + } + $dbsession = $this->sessionslog[$sessid]; + + $context = context_module::instance($dbsession->cmid); + if (!has_capability('mod/attendance:takeattendances', $context)) { + // How do we tell user about this? + \core\notification::warning(get_string("nocapabilitytotakethisattendance", "attendance", $dbsession->cmid)); + continue; + } + + $formkey = 'user'.$stid.'sess'.$sessid; + $attid = $dbsession->attendanceid; + $statusset = array_filter($this->statuses[$attid], + function($x) use($dbsession) { + return $x->setnumber === $dbsession->statusset; + }); + $sessionatt[$sessid] = $attid; + $formlog = new stdClass(); + if (array_key_exists($formkey, $formdata) && is_numeric($formdata[$formkey])) { + $formlog->statusid = $formdata[$formkey]; + } + $formlog->studentid = $stid; // We check is_numeric on this above. + $formlog->statusset = implode(',', array_keys($statusset)); + $formlog->remarks = $value; + $formlog->sessionid = $sessid; + $formlog->timetaken = $now; + $formlog->takenby = $USER->id; + + if (!array_key_exists($stid, $sesslog)) { + $sesslog[$stid] = array(); + } + $sesslog[$stid][$sessid] = $formlog; + } + } + + $updateatts = array(); + foreach ($sesslog as $stid => $userlog) { + $dbstudlog = $DB->get_records('attendance_log', array('studentid' => $stid), '', + 'sessionid,statusid,remarks,id,statusset'); + foreach ($userlog as $log) { + if (array_key_exists($log->sessionid, $dbstudlog)) { + $attid = $sessionatt[$log->sessionid]; + // Check if anything important has changed before updating record. + // Don't update timetaken/takenby records if nothing has changed. + if ($dbstudlog[$log->sessionid]->remarks != $log->remarks || + $dbstudlog[$log->sessionid]->statusid != $log->statusid || + $dbstudlog[$log->sessionid]->statusset != $log->statusset) { + + $log->id = $dbstudlog[$log->sessionid]->id; + $DB->update_record('attendance_log', $log); + + $updatedsessions[$log->sessionid] = $log->sessionid; + if (!array_key_exists($attid, $updateatts)) { + $updateatts[$attid] = array(); + } + array_push($updateatts[$attid], $log->studentid); + } + } else { + $DB->insert_record('attendance_log', $log, false); + $updatedsessions[$log->sessionid] = $log->sessionid; + if (!array_key_exists($attid, $updateatts)) { + $updateatts[$attid] = array(); + } + array_push($updateatts[$attid], $log->studentid); + } + } + } + + foreach ($updatedsessions as $sessionid) { + $session = $this->sessionslog[$sessionid]; + $session->lasttaken = $now; + $session->lasttakenby = $USER->id; + $DB->update_record('attendance_sessions', $session); + } + + if (!empty($updateatts)) { + $attendancegrade = $DB->get_records_list('attendance', 'id', array_keys($updateatts), '', 'id, grade'); + foreach ($updateatts as $attid => $updateusers) { + if ($attendancegrade[$attid] != 0) { + attendance_update_users_grades_by_id($attid, $grade, $updateusers); + } + } + } } } diff --git a/renderer.php b/renderer.php index 1a19c86..00036de 100644 --- a/renderer.php +++ b/renderer.php @@ -56,21 +56,42 @@ class mod_attendance_renderer extends plugin_renderer_base { * @return string html code */ protected function render_attendance_filter_controls(attendance_filter_controls $fcontrols) { + $classes = 'attfiltercontrols'; $filtertable = new html_table(); $filtertable->attributes['class'] = ' '; $filtertable->width = '100%'; $filtertable->align = array('left', 'center', 'right', 'right'); - $filtertable->data[0][] = $this->render_sess_group_selector($fcontrols); + if (property_exists($fcontrols->pageparams, 'mode') && + $fcontrols->pageparams->mode === mod_attendance_view_page_params::MODE_ALL_SESSIONS) { + $classes .= ' float-right'; - $filtertable->data[0][] = $this->render_curdate_controls($fcontrols); + $row = array(); + $row[] = ''; + $row[] = ''; + $row[] = ''; + $row[] = $this->render_grouping_controls($fcontrols); + $filtertable->data[] = $row; - $filtertable->data[0][] = $this->render_paging_controls($fcontrols); + $row = array(); + $row[] = ''; + $row[] = ''; + $row[] = ''; + $row[] = $this->render_course_controls($fcontrols); + $filtertable->data[] = $row; + } + + $row = array(); + + $row[] = $this->render_sess_group_selector($fcontrols); + $row[] = $this->render_curdate_controls($fcontrols); + $row[] = $this->render_paging_controls($fcontrols); + $row[] = $this->render_view_controls($fcontrols); - $filtertable->data[0][] = $this->render_view_controls($fcontrols); + $filtertable->data[] = $row; $o = html_writer::table($filtertable); - $o = $this->output->container($o, 'attfiltercontrols'); + $o = $this->output->container($o, $classes); return $o; } @@ -198,6 +219,59 @@ class mod_attendance_renderer extends plugin_renderer_base { return $curdatecontrols; } + /** + * Render grouping controls (for all sessions report). + * + * @param attendance_filter_controls $fcontrols + * @return string + */ + protected function render_grouping_controls(attendance_filter_controls $fcontrols) { + if ($fcontrols->pageparams->mode === mod_attendance_view_page_params::MODE_ALL_SESSIONS) { + $groupoptions = array( + 'date' => get_string('sessionsbydate', 'attendance'), + 'activity' => get_string('sessionsbyactivity', 'attendance'), + 'course' => get_string('sessionsbycourse', 'attendance') + ); + $groupcontrols = get_string('groupsessionsby', 'attendance') . ":"; + foreach ($groupoptions as $key => $opttext) { + if ($key != $fcontrols->pageparams->groupby) { + $link = html_writer::link($fcontrols->url(array('groupby' => $key)), $opttext); + $groupcontrols .= html_writer::tag('span', $link, array('class' => 'attbtn')); + } else { + $groupcontrols .= html_writer::tag('span', $opttext, array('class' => 'attcurbtn')); + } + } + return html_writer::tag('nobr', $groupcontrols); + } + return ""; + } + + /** + * Render course controls (for all sessions report). + * + * @param attendance_filter_controls $fcontrols + * @return string + */ + protected function render_course_controls(attendance_filter_controls $fcontrols) { + if ($fcontrols->pageparams->mode === mod_attendance_view_page_params::MODE_ALL_SESSIONS) { + $courseoptions = array( + 'all' => get_string('sessionsallcourses', 'attendance'), + 'current' => get_string('sessionscurrentcourses', 'attendance') + ); + $coursecontrols = ""; + foreach ($courseoptions as $key => $opttext) { + if ($key != $fcontrols->pageparams->sesscourses) { + $link = html_writer::link($fcontrols->url(array('sesscourses' => $key)), $opttext); + $coursecontrols .= html_writer::tag('span', $link, array('class' => 'attbtn')); + } else { + $coursecontrols .= html_writer::tag('span', $opttext, array('class' => 'attcurbtn')); + } + } + return html_writer::tag('nobr', $coursecontrols); + } + return ""; + } + /** * Render view controls. * @@ -926,6 +1000,72 @@ class mod_attendance_renderer extends plugin_renderer_base { return $celldata; } + /** + * Construct take session controls. + * + * @param attendance_take_data $takedata + * @param stdClass $user + * @return array + */ + private function construct_take_session_controls(attendance_take_data $takedata, $user) { + $celldata = array(); + $celldata['remarks'] = ''; + if ($user->enrolmentend and $user->enrolmentend < $takedata->sessioninfo->sessdate) { + $celldata['text'] = get_string('enrolmentend', 'attendance', userdate($user->enrolmentend, '%d.%m.%Y')); + $celldata['colspan'] = count($takedata->statuses) + 1; + $celldata['class'] = 'userwithoutenrol'; + } else if (!$user->enrolmentend and $user->enrolmentstatus == ENROL_USER_SUSPENDED) { + // No enrolmentend and ENROL_USER_SUSPENDED. + $celldata['text'] = get_string('enrolmentsuspended', 'attendance'); + $celldata['colspan'] = count($takedata->statuses) + 1; + $celldata['class'] = 'userwithoutenrol'; + } else { + if ($takedata->updatemode and !array_key_exists($user->id, $takedata->sessionlog)) { + $celldata['class'] = 'userwithoutdata'; + } + + $celldata['text'] = array(); + foreach ($takedata->statuses as $st) { + $params = array( + 'type' => 'radio', + 'name' => 'user'.$user->id.'sess'.$takedata->sessioninfo->id, + 'class' => 'st'.$st->id, + 'value' => $st->id); + if (array_key_exists($user->id, $takedata->sessionlog) and $st->id == $takedata->sessionlog[$user->id]->statusid) { + $params['checked'] = ''; + } + + $input = html_writer::empty_tag('input', $params); + + if ($takedata->pageparams->viewmode == mod_attendance_take_page_params::SORTED_GRID) { + $input = html_writer::tag('nobr', $input . $st->acronym); + } + + $celldata['text'][] = $input; + } + $params = array( + 'type' => 'text', + 'name' => 'remarks'.$user->id.'sess'.$takedata->sessioninfo->id, + 'maxlength' => 255); + if (array_key_exists($user->id, $takedata->sessionlog)) { + $params['value'] = $takedata->sessionlog[$user->id]->remarks; + } + $input = html_writer::empty_tag('input', $params); + if ($takedata->pageparams->viewmode == mod_attendance_take_page_params::SORTED_GRID) { + $input = html_writer::empty_tag('br').$input; + } + $celldata['remarks'] = $input; + + if ($user->enrolmentstart > $takedata->sessioninfo->sessdate + $takedata->sessioninfo->duration) { + $celldata['warning'] = get_string('enrolmentstart', 'attendance', + userdate($user->enrolmentstart, '%H:%M %d.%m.%Y')); + $celldata['class'] = 'userwithoutenrol'; + } + } + + return $celldata; + } + /** * Render header. * @@ -960,7 +1100,8 @@ class mod_attendance_renderer extends plugin_renderer_base { $o = $this->render_user_report_tabs($userdata); - if ($USER->id == $userdata->user->id) { + if ($USER->id == $userdata->user->id || + $userdata->pageparams->mode === mod_attendance_view_page_params::MODE_ALL_SESSIONS) { $o .= $this->construct_user_data($userdata); @@ -993,11 +1134,14 @@ class mod_attendance_renderer extends plugin_renderer_base { $userdata->url()->out(true, array('mode' => mod_attendance_view_page_params::MODE_THIS_COURSE)), get_string('thiscourse', 'attendance')); - // Skip the 'all courses' tab for 'temporary' users. + // Skip the 'all courses' and 'all sessions' tabs for 'temporary' users. if ($userdata->user->type == 'standard') { $tabs[] = new tabobject(mod_attendance_view_page_params::MODE_ALL_COURSES, $userdata->url()->out(true, array('mode' => mod_attendance_view_page_params::MODE_ALL_COURSES)), get_string('allcourses', 'attendance')); + $tabs[] = new tabobject(mod_attendance_view_page_params::MODE_ALL_SESSIONS, + $userdata->url()->out(true, array('mode' => mod_attendance_view_page_params::MODE_ALL_SESSIONS)), + get_string('allsessions', 'attendance')); } return print_tabs(array($tabs), $userdata->pageparams->mode, null, null, true); @@ -1022,6 +1166,22 @@ class mod_attendance_renderer extends plugin_renderer_base { $o .= html_writer::empty_tag('hr'); $o .= construct_user_data_stat($userdata->summary->get_all_sessions_summary_for($userdata->user->id), $userdata->pageparams->view); + } else if ($userdata->pageparams->mode == mod_attendance_view_page_params::MODE_ALL_SESSIONS) { + $allsessions = $this->construct_user_allsessions_log($userdata); + $o .= html_writer::start_div('allsessionssummary'); + $o .= html_writer::start_div('float-left'); + $o .= html_writer::start_div('float-left'); + $o .= $this->user_picture($userdata->user, array('size' => 100, 'class' => 'userpicture float-left')); + $o .= html_writer::end_div(); + $o .= html_writer::start_div('float-right'); + $o .= $allsessions->summary; + $o .= html_writer::end_div(); + $o .= html_writer::end_div(); + $o .= html_writer::start_div('float-right'); + $o .= $this->render_attendance_filter_controls($userdata->filtercontrols); + $o .= html_writer::end_div(); + $o .= html_writer::end_div(); + $o .= $allsessions->detail; } else { $table = new html_table(); $table->head = array(get_string('course'), @@ -1226,6 +1386,658 @@ class mod_attendance_renderer extends plugin_renderer_base { return html_writer::table($table); } + /** + * Construct table showing all sessions, not limited to current course. + * + * @param attendance_user_data $userdata + * @return string + */ + private function construct_user_allsessions_log(attendance_user_data $userdata) { + global $USER; + + $allsessions = new stdClass(); + + $shortform = false; + if ($USER->id == $userdata->user->id) { + // This is a user viewing their own stuff - hide non-relevant columns. + $shortform = true; + } + + $groupby = $userdata->pageparams->groupby; + + $table = new html_table(); + $table->attributes['class'] = 'generaltable attwidth boxaligncenter allsessions'; + $table->head = array(); + $table->align = array(); + $table->size = array(); + $table->colclasses = array(); + $colcount = 0; + $summarywidth = 0; + + // If grouping by date, we need some form of date up front. + // Only need course column if we are not using course to group + // (currently date is only option which does not use course). + if ($groupby === 'date') { + $table->head[] = ''; + $table->align[] = 'left'; + $table->colclasses[] = 'grouper'; + $table->size[] = '1px'; + + $table->head[] = get_string('date'); + $table->align[] = 'left'; + $table->colclasses[] = 'datecol'; + $table->size[] = '1px'; + $colcount++; + + $table->head[] = get_string('course'); + $table->align[] = 'left'; + $table->colclasses[] = 'colcourse'; + $colcount++; + } else { + $table->head[] = ''; + $table->align[] = 'left'; + $table->colclasses[] = 'grouper'; + $table->size[] = '1px'; + if ($groupby === 'activity') { + $table->head[] = ''; + $table->align[] = 'left'; + $table->colclasses[] = 'grouper'; + $table->size[] = '1px'; + } + } + + // Need activity column unless we are using activity to group. + if ($groupby !== 'activity') { + $table->head[] = get_string('pluginname', 'mod_attendance'); + $table->align[] = 'left'; + $table->colclasses[] = 'colcourse'; + $table->size[] = '*'; + $colcount++; + } + + // If grouping by date, it belongs up front rather than here. + if ($groupby !== 'date') { + $table->head[] = get_string('date'); + $table->align[] = 'left'; + $table->colclasses[] = 'datecol'; + $table->size[] = '1px'; + $colcount++; + } + + // Use "session" instead of "description". + $table->head[] = get_string('session', 'attendance'); + $table->align[] = 'left'; + $table->colclasses[] = 'desccol'; + $table->size[] = '*'; + $colcount++; + + if (!$shortform) { + $table->head[] = get_string('sessiontypeshort', 'attendance'); + $table->align[] = ''; + $table->size[] = '*'; + $table->colclasses[] = ''; + $colcount++; + } + + if (!empty($USER->attendanceediting)) { + $table->head[] = get_string('status', 'attendance'); + $table->align[] = 'center'; + $table->colclasses[] = 'statuscol'; + $table->size[] = '*'; + $colcount++; + $summarywidth++; + + $table->head[] = get_string('remarks', 'attendance'); + $table->align[] = 'center'; + $table->colclasses[] = 'remarkscol'; + $table->size[] = '*'; + $colcount++; + $summarywidth++; + } else { + $table->head[] = get_string('status', 'attendance'); + $table->align[] = 'center'; + $table->colclasses[] = 'statuscol'; + $table->size[] = '*'; + $colcount++; + $summarywidth++; + + $table->head[] = get_string('points', 'attendance'); + $table->align[] = 'center'; + $table->colclasses[] = 'pointscol'; + $table->size[] = '1px'; + $colcount++; + $summarywidth++; + + $table->head[] = get_string('remarks', 'attendance'); + $table->align[] = 'center'; + $table->colclasses[] = 'remarkscol'; + $table->size[] = '*'; + $colcount++; + $summarywidth++; + } + + $statusmaxpoints = array(); + foreach ($userdata->statuses as $attid => $attstatuses) { + $statusmaxpoints[$attid] = attendance_get_statusset_maxpoints($attstatuses); + } + + $lastgroup = array(null, null); + $groups = array(); + $stats = array( + 'course' => array(), + 'activity' => array(), + 'date' => array(), + 'overall' => array( + 'points' => 0, + 'maxpointstodate' => 0, + 'maxpoints' => 0, + 'pcpointstodate' => null, + 'pcpoints' => null, + 'statuses' => array() + ) + ); + $group = null; + if ($userdata->sessionslog) { + foreach ($userdata->sessionslog as $sess) { + if ($groupby === 'date') { + $weekformat = date("YW", $sess->sessdate); + if ($weekformat != $lastgroup[0]) { + if ($group !== null) { + array_push($groups, $group); + } + $group = array(); + $lastgroup[0] = $weekformat; + } + if (!array_key_exists($weekformat, $stats['date'])) { + $stats['date'][$weekformat] = array( + 'points' => 0, + 'maxpointstodate' => 0, + 'maxpoints' => 0, + 'pcpointstodate' => null, + 'pcpoints' => null, + 'statuses' => array() + ); + } + $statussetmaxpoints = $statusmaxpoints[$sess->attendanceid]; + // Ensure all possible acronyms for current sess's statusset are available as + // keys in status array for period. + // + // A bit yucky because we can't tell whether we've seen statusset before, and + // we usually will have, so much wasted spinning. + foreach ($userdata->statuses[$sess->attendanceid] as $attstatus) { + if ($attstatus->setnumber === $sess->statusset) { + if (!array_key_exists($attstatus->acronym, $stats['date'][$weekformat]['statuses'])) { + $stats['date'][$weekformat]['statuses'][$attstatus->acronym] = + array('count' => 0, 'description' => $attstatus->description); + } + if (!array_key_exists($attstatus->acronym, $stats['overall']['statuses'])) { + $stats['overall']['statuses'][$attstatus->acronym] = + array('count' => 0, 'description' => $attstatus->description); + } + } + } + // The array_key_exists check is for hidden statuses. + if (isset($sess->statusid) && array_key_exists($sess->statusid, $userdata->statuses[$sess->attendanceid])) { + $status = $userdata->statuses[$sess->attendanceid][$sess->statusid]; + $stats['date'][$weekformat]['statuses'][$status->acronym]['count']++; + $stats['date'][$weekformat]['points'] += $status->grade; + $stats['date'][$weekformat]['maxpointstodate'] += $statussetmaxpoints[$sess->statusset]; + $stats['overall']['statuses'][$status->acronym]['count']++; + $stats['overall']['points'] += $status->grade; + $stats['overall']['maxpointstodate'] += $statussetmaxpoints[$sess->statusset]; + } + $stats['date'][$weekformat]['maxpoints'] += $statussetmaxpoints[$sess->statusset]; + $stats['overall']['maxpoints'] += $statussetmaxpoints[$sess->statusset]; + } else { + // By course and perhaps activity. + if ( + ($sess->courseid != $lastgroup[0]) || + ($groupby === 'activity' && $sess->cmid != $lastgroup[1]) + ) { + if ($group !== null) { + array_push($groups, $group); + } + $group = array(); + $lastgroup[0] = $sess->courseid; + $lastgroup[1] = $sess->cmid; + } + if (!array_key_exists($sess->courseid, $stats['course'])) { + $stats['course'][$sess->courseid] = array( + 'points' => 0, + 'maxpointstodate' => 0, + 'maxpoints' => 0, + 'pcpointstodate' => null, + 'pcpoints' => null, + 'statuses' => array() + ); + } + $statussetmaxpoints = $statusmaxpoints[$sess->attendanceid]; + // Ensure all possible acronyms for current sess's statusset are available as + // keys in status array for course + // + // A bit yucky because we can't tell whether we've seen statusset before, and + // we usually will have, so much wasted spinning. + foreach ($userdata->statuses[$sess->attendanceid] as $attstatus) { + if ($attstatus->setnumber === $sess->statusset) { + if (!array_key_exists($attstatus->acronym, $stats['course'][$sess->courseid]['statuses'])) { + $stats['course'][$sess->courseid]['statuses'][$attstatus->acronym] = + array('count' => 0, 'description' => $attstatus->description); + } + if (!array_key_exists($attstatus->acronym, $stats['overall']['statuses'])) { + $stats['overall']['statuses'][$attstatus->acronym] = + array('count' => 0, 'description' => $attstatus->description); + } + } + } + // The array_key_exists check is for hidden statuses. + if (isset($sess->statusid) && array_key_exists($sess->statusid, $userdata->statuses[$sess->attendanceid])) { + $status = $userdata->statuses[$sess->attendanceid][$sess->statusid]; + $stats['course'][$sess->courseid]['statuses'][$status->acronym]['count']++; + $stats['course'][$sess->courseid]['points'] += $status->grade; + $stats['course'][$sess->courseid]['maxpointstodate'] += $statussetmaxpoints[$sess->statusset]; + $stats['overall']['statuses'][$status->acronym]['count']++; + $stats['overall']['points'] += $status->grade; + $stats['overall']['maxpointstodate'] += $statussetmaxpoints[$sess->statusset]; + } + $stats['course'][$sess->courseid]['maxpoints'] += $statussetmaxpoints[$sess->statusset]; + $stats['overall']['maxpoints'] += $statussetmaxpoints[$sess->statusset]; + + if (!array_key_exists($sess->cmid, $stats['activity'])) { + $stats['activity'][$sess->cmid] = array( + 'points' => 0, + 'maxpointstodate' => 0, + 'maxpoints' => 0, + 'pcpointstodate' => null, + 'pcpoints' => null, + 'statuses' => array() + ); + } + $statussetmaxpoints = $statusmaxpoints[$sess->attendanceid]; + // Ensure all possible acronyms for current sess's statusset are available as + // keys in status array for period + // + // A bit yucky because we can't tell whether we've seen statusset before, and + // we usually will have, so much wasted spinning. + foreach ($userdata->statuses[$sess->attendanceid] as $attstatus) { + if ($attstatus->setnumber === $sess->statusset) { + if (!array_key_exists($attstatus->acronym, $stats['activity'][$sess->cmid]['statuses'])) { + $stats['activity'][$sess->cmid]['statuses'][$attstatus->acronym] = + array('count' => 0, 'description' => $attstatus->description); + } + if (!array_key_exists($attstatus->acronym, $stats['overall']['statuses'])) { + $stats['overall']['statuses'][$attstatus->acronym] = + array('count' => 0, 'description' => $attstatus->description); + } + } + } + // The array_key_exists check is for hidden statuses. + if (isset($sess->statusid) && array_key_exists($sess->statusid, $userdata->statuses[$sess->attendanceid])) { + $status = $userdata->statuses[$sess->attendanceid][$sess->statusid]; + $stats['activity'][$sess->cmid]['statuses'][$status->acronym]['count']++; + $stats['activity'][$sess->cmid]['points'] += $status->grade; + $stats['activity'][$sess->cmid]['maxpointstodate'] += $statussetmaxpoints[$sess->statusset]; + $stats['overall']['statuses'][$status->acronym]['count']++; + $stats['overall']['points'] += $status->grade; + $stats['overall']['maxpointstodate'] += $statussetmaxpoints[$sess->statusset]; + } + $stats['activity'][$sess->cmid]['maxpoints'] += $statussetmaxpoints[$sess->statusset]; + $stats['overall']['maxpoints'] += $statussetmaxpoints[$sess->statusset]; + } + array_push($group, $sess); + } + array_push($groups, $group); + } + + $points = $stats['overall']['points']; + $maxpoints = $stats['overall']['maxpointstodate']; + $summarytable = new html_table(); + $summarytable->attributes['class'] = 'generaltable table-bordered table-condensed'; + $row = new html_table_row(); + $cell = new html_table_cell(get_string('allsessionstotals', 'attendance')); + $cell->colspan = 2; + $cell->header = true; + $row->cells[] = $cell; + $summarytable->data[] = $row; + foreach ($stats['overall']['statuses'] as $acronym => $status) { + $row = new html_table_row(); + $row->cells[] = $status['description'] . ":"; + $row->cells[] = $status['count']; + $summarytable->data[] = $row; + } + + $row = new html_table_row(); + if ($maxpoints !== 0) { + $pctodate = format_float( $points * 100 / $maxpoints); + $pointsinfo = get_string('points', 'attendance') . ": " . $points . "/" . $maxpoints; + $pointsinfo .= " (" . $pctodate . "%)"; + } else { + $pointsinfo = get_string('points', 'attendance') . ": " . $points . "/" . $maxpoints; + } + $pointsinfo .= " " . get_string('todate', 'attendance'); + $cell = new html_table_cell($pointsinfo); + $cell->colspan = 2; + $row->cells[] = $cell; + $summarytable->data[] = $row; + $allsessions->summary = html_writer::table($summarytable); + + $lastgroup = array(null, null); + foreach ($groups as $group) { + + $statussetmaxpoints = $statusmaxpoints[$sess->attendanceid]; + + // For use in headings etc. + $sess = $group[0]; + + if ($groupby === 'date') { + $row = new html_table_row(); + $row->attributes['class'] = 'grouper'; + $cell = new html_table_cell(); + $cell->rowspan = count($group) + 2; + $row->cells[] = $cell; + $week = date("W", $sess->sessdate); + $year = date("Y", $sess->sessdate); + // ISO week starts on day 1, Monday. + $weekstart = date_timestamp_get(date_isodate_set(date_create(), $year, $week, 1)); + $dmywformat = get_string('strftimedmyw', 'attendance'); + $cell = new html_table_cell(get_string('weekcommencing', 'attendance') . ": " . userdate($weekstart, $dmywformat)); + $cell->colspan = $colcount - $summarywidth; + $cell->rowspan = 2; + $cell->attributes['class'] = 'groupheading'; + $row->cells[] = $cell; + $weekformat = date("YW", $sess->sessdate); + $points = $stats['date'][$weekformat]['points']; + $maxpoints = $stats['date'][$weekformat]['maxpointstodate']; + if ($maxpoints !== 0) { + $pctodate = format_float( $points * 100 / $maxpoints); + $summary = get_string('points', 'attendance') . ": " . $points . "/" . $maxpoints; + $summary .= " (" . $pctodate . "%)"; + } else { + $summary = get_string('points', 'attendance') . ": " . $points . "/" . $maxpoints; + } + $summary .= " " . get_string('todate', 'attendance'); + $cell = new html_table_cell($summary); + $cell->colspan = $summarywidth; + $row->cells[] = $cell; + $table->data[] = $row; + $row = new html_table_row(); + $row->attributes['class'] = 'grouper'; + $summary = array(); + foreach ($stats['date'][$weekformat]['statuses'] as $acronym => $status) { + array_push($summary, html_writer::tag('b', $acronym) . $status['count']); + } + $cell = new html_table_cell(implode(" ", $summary)); + $cell->colspan = $summarywidth; + $row->cells[] = $cell; + $table->data[] = $row; + $lastgroup[0] = date("YW", $weekstart); + } else { + if ($groupby === 'course' || $sess->courseid !== $lastgroup[0]) { + $row = new html_table_row(); + $row->attributes['class'] = 'grouper'; + $cell = new html_table_cell(); + $cell->rowspan = count($group) + 2; + if ($groupby === 'activity') { + $headcell = $cell; // Keep ref to be able to adjust rowspan later. + $cell->rowspan += 2; + $row->cells[] = $cell; + $cell = new html_table_cell(); + $cell->rowspan = 2; + } + $row->cells[] = $cell; + $courseurl = new moodle_url('/course/view.php', array('id' => $sess->courseid)); + $cell = new html_table_cell(get_string('course', 'attendance') . ": " . + html_writer::link($courseurl, $sess->cname)); + $cell->colspan = $colcount - $summarywidth; + $cell->rowspan = 2; + $cell->attributes['class'] = 'groupheading'; + $row->cells[] = $cell; + $points = $stats['course'][$sess->courseid]['points']; + $maxpoints = $stats['course'][$sess->courseid]['maxpointstodate']; + if ($maxpoints !== 0) { + $pctodate = format_float( $points * 100 / $maxpoints); + $summary = get_string('points', 'attendance') . ": " . $points . "/" . $maxpoints; + $summary .= " (" . $pctodate . "%)"; + } else { + $summary = get_string('points', 'attendance') . ": " . $points . "/" . $maxpoints; + } + $summary .= " " . get_string('todate', 'attendance'); + $cell = new html_table_cell($summary); + $cell->colspan = $summarywidth; + $row->cells[] = $cell; + $table->data[] = $row; + $row = new html_table_row(); + $row->attributes['class'] = 'grouper'; + $summary = array(); + foreach ($stats['course'][$sess->courseid]['statuses'] as $acronym => $status) { + array_push($summary, html_writer::tag('b', $acronym) . $status['count']); + } + $cell = new html_table_cell(implode(" ", $summary)); + $cell->colspan = $summarywidth; + $row->cells[] = $cell; + $table->data[] = $row; + } + if ($groupby === 'activity') { + if ($sess->courseid === $lastgroup[0]) { + $headcell->rowspan += count($group) + 2; + } + $row = new html_table_row(); + $row->attributes['class'] = 'grouper'; + $cell = new html_table_cell(); + $cell->rowspan = count($group) + 2; + $row->cells[] = $cell; + $attendanceurl = new moodle_url('/mod/attendance/view.php', array('id' => $sess->cmid, + 'studentid' => $userdata->user->id, + 'view' => ATT_VIEW_ALL)); + $cell = new html_table_cell(get_string('pluginname', 'mod_attendance') . + ": " . html_writer::link($attendanceurl, $sess->attname)); + $cell->colspan = $colcount - $summarywidth; + $cell->rowspan = 2; + $cell->attributes['class'] = 'groupheading'; + $row->cells[] = $cell; + $points = $stats['activity'][$sess->cmid]['points']; + $maxpoints = $stats['activity'][$sess->cmid]['maxpointstodate']; + if ($maxpoints !== 0) { + $pctodate = format_float( $points * 100 / $maxpoints); + $summary = get_string('points', 'attendance') . ": " . $points . "/" . $maxpoints; + $summary .= " (" . $pctodate . "%)"; + } else { + $summary = get_string('points', 'attendance') . ": " . $points . "/" . $maxpoints; + } + $summary .= " " . get_string('todate', 'attendance'); + $cell = new html_table_cell($summary); + $cell->colspan = $summarywidth; + $row->cells[] = $cell; + $table->data[] = $row; + $row = new html_table_row(); + $row->attributes['class'] = 'grouper'; + $summary = array(); + foreach ($stats['activity'][$sess->cmid]['statuses'] as $acronym => $status) { + array_push($summary, html_writer::tag('b', $acronym) . $status['count']); + } + $cell = new html_table_cell(implode(" ", $summary)); + $cell->colspan = $summarywidth; + $row->cells[] = $cell; + $table->data[] = $row; + } + $lastgroup[0] = $sess->courseid; + $lastgroup[1] = $sess->cmid; + } + + // Now iterate over sessions in group... + + foreach ($group as $sess) { + $row = new html_table_row(); + + // If grouping by date, we need some form of date up front. + // Only need course column if we are not using course to group + // (currently date is only option which does not use course). + if ($groupby === 'date') { + // What part of date do we want if grouped by it already? + $row->cells[] = userdate($sess->sessdate, get_string('strftimedmw', 'attendance')) . + " ". $this->construct_time($sess->sessdate, $sess->duration); + + $courseurl = new moodle_url('/course/view.php', array('id' => $sess->courseid)); + $row->cells[] = html_writer::link($courseurl, $sess->cname); + } + + // Need activity column unless we are using activity to group. + if ($groupby !== 'activity') { + $attendanceurl = new moodle_url('/mod/attendance/view.php', array('id' => $sess->cmid, + 'studentid' => $userdata->user->id, + 'view' => ATT_VIEW_ALL)); + $row->cells[] = html_writer::link($attendanceurl, $sess->attname); + } + + // If grouping by date, it belongs up front rather than here. + if ($groupby !== 'date') { + $row->cells[] = userdate($sess->sessdate, get_string('strftimedmyw', 'attendance')) . + " ". $this->construct_time($sess->sessdate, $sess->duration); + } + + $sesscontext = context_module::instance($sess->cmid); + if (has_capability('mod/attendance:takeattendances', $sesscontext)) { + $sessionurl = new moodle_url('/mod/attendance/take.php', array('id' => $sess->cmid, + 'sessionid' => $sess->id, + 'grouptype' => $sess->groupid)); + $description = html_writer::link($sessionurl, $sess->description); + } else { + $description = $sess->description; + } + $row->cells[] = $description; + + if (!$shortform) { + if ($sess->groupid) { + $sessiontypeshort = get_string('group') . ': ' . $userdata->groups[$sess->courseid][$sess->groupid]->name; + } else { + $sessiontypeshort = get_string('commonsession', 'attendance'); + } + $row->cells[] = html_writer::tag('nobr', $sessiontypeshort); + } + + if (!empty($USER->attendanceediting)) { + $context = context_module::instance($sess->cmid); + if (has_capability('mod/attendance:takeattendances', $context)) { + // Takedata needs: + // sessioninfo->sessdate + // sessioninfo->duration + // statuses + // updatemode + // sessionlog[userid]->statusid + // sessionlog[userid]->remarks + // pageparams->viewmode == mod_attendance_take_page_params::SORTED_GRID + // and urlparams to be able to use url method later. + // + // user needs: + // enrolmentstart + // enrolmentend + // enrolmentstatus + // id. + + $nastyhack = new ReflectionClass('attendance_take_data'); + $takedata = $nastyhack->newInstanceWithoutConstructor(); + $takedata->sessioninfo = $sess; + $takedata->statuses = array_filter($userdata->statuses[$sess->attendanceid], function($x) use ($sess) { + return ($x->setnumber == $sess->statusset); + }); + $takedata->updatemode = true; + $takedata->sessionlog = array($userdata->user->id => $sess); + $takedata->pageparams = new stdClass(); + $takedata->pageparams->viewmode = mod_attendance_take_page_params::SORTED_GRID; + $ucdata = $this->construct_take_session_controls($takedata, $userdata->user); + + $celltext = join($ucdata['text']); + + if (array_key_exists('warning', $ucdata)) { + $celltext .= html_writer::empty_tag('br'); + $celltext .= $ucdata['warning']; + } + if (array_key_exists('class', $ucdata)) { + $row->attributes['class'] = $ucdata['class']; + } + + $cell = new html_table_cell($celltext); + $row->cells[] = $cell; + + $celltext = empty($ucdata['remarks']) ? '' : $ucdata['remarks']; + $cell = new html_table_cell($celltext); + $row->cells[] = $cell; + + } else { + if (!empty($sess->statusid)) { + $status = $userdata->statuses[$sess->attendanceid][$sess->statusid]; + $row->cells[] = $status->description; + $row->cells[] = $sess->remarks; + } + } + + } else { + if (!empty($sess->statusid)) { + $status = $userdata->statuses[$sess->attendanceid][$sess->statusid]; + $row->cells[] = $status->description; + $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 + $sess->duration) < $userdata->user->enrolmentstart) { + $cell = new html_table_cell(get_string('enrolmentstart', 'attendance', + userdate($userdata->user->enrolmentstart, '%d.%m.%Y'))); + $cell->colspan = 3; + $row->cells[] = $cell; + } else if ($userdata->user->enrolmentend and $sess->sessdate > $userdata->user->enrolmentend) { + $cell = new html_table_cell(get_string('enrolmentend', 'attendance', + userdate($userdata->user->enrolmentend, '%d.%m.%Y'))); + $cell->colspan = 3; + $row->cells[] = $cell; + } else { + 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. + + $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 = 3; + $row->cells[] = $cell; + } else { // Student cannot mark their own attendace. + $row->cells[] = '?'; + $row->cells[] = '? / ' . format_float($statussetmaxpoints[$sess->statusset], 1, true, true); + $row->cells[] = ''; + } + } + } + + $table->data[] = $row; + } + } + + if (!empty($USER->attendanceediting)) { + $row = new html_table_row(); + $params = array( + 'type' => 'submit', + 'class' => 'btn btn-primary', + 'value' => get_string('save', 'attendance')); + $cell = new html_table_cell(html_writer::tag('center', html_writer::empty_tag('input', $params))); + $cell->colspan = $colcount + (($groupby == 'activity') ? 2 : 1); + $row->cells[] = $cell; + $table->data[] = $row; + } + + $logtext = html_writer::table($table); + + if (!empty($USER->attendanceediting)) { + $formtext = html_writer::start_div('no-overflow'); + $formtext .= $logtext; + $formtext .= html_writer::input_hidden_params($userdata->url(array('sesskey' => sesskey()))); + $formtext .= html_writer::end_div(); + // Could use userdata->urlpath if not private or userdata->url_path() if existed, but '' turns + // out to DTRT. + $logtext = html_writer::tag('form', $formtext, array('method' => 'post', 'action' => '', + 'id' => 'attendancetakeform')); + } + $allsessions->detail = $logtext; + return $allsessions; + } + /** * Construct time for display. * diff --git a/styles.css b/styles.css index 6b1a0d8..e635046 100644 --- a/styles.css +++ b/styles.css @@ -4,6 +4,7 @@ margin-left: 2px; margin-right: 2px; padding: 5px; + display: inline-block; } .path-mod-attendance .attcurbtn { @@ -16,7 +17,6 @@ margin-bottom: 10px; margin-left: auto; margin-right: auto; - width: 90%; } .path-mod-attendance .attfiltercontrols #currentdate { @@ -54,6 +54,10 @@ font-size: 0.8em; } +.path-mod-attendance div.allsessionssummary + form#attendancetakeform > div { + width: 100%; +} + .path-mod-attendance table.controls { text-align: center; width: 100%; @@ -105,9 +109,27 @@ background-color: #eee; padding: 30px 10px; } +.path-mod-attendance table.userinfobox .userpicture { + margin: 0; +} .path-mod-attendance table.attlist td.c0 { text-align: right; } +.path-mod-attendance table.allsessions tr.grouper td { + background-color: #eee; +} +.path-mod-attendance table.allsessions td.groupheading { + font-weight: bold; +} +.path-mod-attendance .allsessionssummary > * { + display: inline-block; +} +.path-mod-attendance .allsessionssummary .float-right { + float: right; +} +.path-mod-attendance .allsessionssummary .float-left { + float: left; +} #page-mod-attendance-preferences .generalbox { text-align: center; diff --git a/view.php b/view.php index dc5da51..dea964d 100644 --- a/view.php +++ b/view.php @@ -29,10 +29,13 @@ require_once(dirname(__FILE__).'/locallib.php'); $pageparams = new mod_attendance_view_page_params(); $id = required_param('id', PARAM_INT); +$edit = optional_param('edit', -1, PARAM_BOOL); $pageparams->studentid = optional_param('studentid', null, PARAM_INT); $pageparams->mode = optional_param('mode', mod_attendance_view_page_params::MODE_THIS_COURSE, PARAM_INT); $pageparams->view = optional_param('view', null, PARAM_INT); $pageparams->curdate = optional_param('curdate', null, PARAM_INT); +$pageparams->groupby = optional_param('groupby', 'course', PARAM_ALPHA); +$pageparams->sesscourses = optional_param('sesscourses', 'current', PARAM_ALPHA); $cm = get_coursemodule_from_id('attendance', $id, 0, false, MUST_EXIST); $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); @@ -60,14 +63,6 @@ if (!$pageparams->studentid) { } } -$PAGE->set_url($att->url_view()); -$PAGE->set_title($course->shortname. ": ".$att->name); -$PAGE->set_heading($course->fullname); -$PAGE->set_cacheable(true); -$PAGE->navbar->add(get_string('attendancereport', 'attendance')); - -$output = $PAGE->get_renderer('mod_attendance'); - if (isset($pageparams->studentid) && $USER->id != $pageparams->studentid) { // Only users with proper permissions should be able to see any user's individual report. require_capability('mod/attendance:viewreports', $context); @@ -77,6 +72,36 @@ if (isset($pageparams->studentid) && $USER->id != $pageparams->studentid) { $userid = $USER->id; } +$url = $att->url_view($pageparams->get_significant_params()); +$PAGE->set_url($url); + +$buttons = ''; +$capabilities = array('mod/attendance:takeattendances', 'mod/attendance:changeattendances'); +if (has_any_capability($capabilities, $context) && + $pageparams->mode == mod_attendance_view_page_params::MODE_ALL_SESSIONS) { + + if (!isset($USER->attendanceediting)) { + $USER->attendanceediting = false; + } + + if (($edit == 1) and confirm_sesskey()) { + $USER->attendanceediting = true; + } else if ($edit == 0 and confirm_sesskey()) { + $USER->attendanceediting = false; + } + + if ($USER->attendanceediting) { + $options['edit'] = 0; + $string = get_string('turneditingoff'); + } else { + $options['edit'] = 1; + $string = get_string('turneditingon'); + } + $options['sesskey'] = sesskey(); + $button = new single_button(new moodle_url($PAGE->url, $options), $string, 'post'); + $PAGE->set_button($OUTPUT->render($button)); +} + $userdata = new attendance_user_data($att, $userid); // Create url for link in log screen. @@ -87,21 +112,43 @@ $filterparams = array( 'enddate' => $userdata->pageparams->enddate ); $params = array_merge($userdata->pageparams->get_significant_params(), $filterparams); + +$header = new mod_attendance_header($att); + if (empty($userdata->pageparams->studentid)) { $relateduserid = $USER->id; } else { $relateduserid = $userdata->pageparams->studentid; } -// Trigger viewed event. -$event = \mod_attendance\event\session_report_viewed::create(array( - 'relateduserid' => $relateduserid, - 'context' => $context, - 'other' => $params)); -$event->add_record_snapshot('course_modules', $cm); -$event->trigger(); +if (($formdata = data_submitted()) && confirm_sesskey() && $edit == -1) { + $userdata->take_sessions_from_form_data($formdata); -$header = new mod_attendance_header($att); + // Trigger updated event. + $event = \mod_attendance\event\session_report_updated::create(array( + 'relateduserid' => $relateduserid, + 'context' => $context, + 'other' => $params)); + $event->add_record_snapshot('course_modules', $cm); + $event->trigger(); + + redirect($url, get_string('attendancesuccess', 'attendance')); +} else { + // Trigger viewed event. + $event = \mod_attendance\event\session_report_viewed::create(array( + 'relateduserid' => $relateduserid, + 'context' => $context, + 'other' => $params)); + $event->add_record_snapshot('course_modules', $cm); + $event->trigger(); +} + +$PAGE->set_title($course->shortname. ": ".$att->name); +$PAGE->set_heading($course->fullname); +$PAGE->set_cacheable(true); +$PAGE->navbar->add(get_string('attendancereport', 'attendance')); + +$output = $PAGE->get_renderer('mod_attendance'); echo $output->header();