|
|
@@ -23,6 +23,9 @@
|
|
|
* query loops are one GROUP BY query per user, and "next word" no longer
|
|
|
* prints blanks (or warns) when every word already has a usage
|
|
|
* - removed the leftover rss_url*() methods copied from the RSS plugin
|
|
|
+ * - new "Student Limits" screen: edit each student's row in the `timekeeper` table
|
|
|
+ * (play window, free minutes, questions needed, daily maximum) and add a student,
|
|
|
+ * instead of editing the table in phpMyAdmin
|
|
|
*/
|
|
|
|
|
|
|
|
|
@@ -81,11 +84,18 @@ class timekeeper_admin extends e_admin_dispatcher {
|
|
|
'ui' => 'tk_geography_form_ui',
|
|
|
'uipath' => null
|
|
|
),
|
|
|
+ 'students' => array(
|
|
|
+ 'controller' => 'tk_Students_ui',
|
|
|
+ 'path' => null,
|
|
|
+ 'ui' => 'tk_Students_form_ui',
|
|
|
+ 'uipath' => null
|
|
|
+ ),
|
|
|
);
|
|
|
protected $adminMenu = array(
|
|
|
'main/list' => array('caption' => 'List Questions', 'perm' => '0'),
|
|
|
'main/create' => array('caption' => LAN_CREATE, 'perm' => '0'),
|
|
|
'main/prefs' => array('caption' => LAN_PREFS, 'perm' => '0'),
|
|
|
+ 'Students/custom' => array('caption' => 'Student Limits', 'perm' => '0'),
|
|
|
'other0' => array('divider' => true),
|
|
|
//'subjects/list' => array('caption'=> 'List Subjects', 'perm' => '0'),
|
|
|
//'subjects/create' => array('caption'=> 'Add Subjects', 'perm' => '0'),
|
|
|
@@ -396,6 +406,236 @@ class tk_geography_form_ui extends e_admin_form_ui {
|
|
|
|
|
|
}
|
|
|
|
|
|
+/*
|
|
|
+ =========================================================================
|
|
|
+ ===== Student limits (timekeeper table) interface
|
|
|
+ =========================================================================
|
|
|
+ */
|
|
|
+
|
|
|
+class tk_Students_ui extends e_admin_ui {
|
|
|
+
|
|
|
+ protected $pluginTitle = 'TimeKeeper';
|
|
|
+ protected $pluginName = 'timekeeper';
|
|
|
+ protected $table = 'timekeeper';
|
|
|
+ protected $pid = 'username';
|
|
|
+ protected $perPage = 10;
|
|
|
+ protected $fields = array(
|
|
|
+ 'username' => array('title' => "User Name", 'type' => 'text', 'data' => 'str', 'readonly' => 1, 'width' => 'auto', 'class' => 'left', 'thclass' => 'left',),
|
|
|
+ 'enabled' => array('title' => "Limits on", 'type' => 'bool', 'data' => 'int', 'width' => 'auto', 'class' => 'left', 'thclass' => 'left',),
|
|
|
+ 'freeMins' => array('title' => "Free minutes", 'type' => 'number', 'data' => 'int', 'width' => 'auto', 'class' => 'left', 'thclass' => 'left',),
|
|
|
+ 'enableTime' => array('title' => "Play from", 'type' => 'text', 'data' => 'str', 'width' => 'auto', 'class' => 'left', 'thclass' => 'left',),
|
|
|
+ 'disableTime' => array('title' => "Play until", 'type' => 'text', 'data' => 'str', 'width' => 'auto', 'class' => 'left', 'thclass' => 'left',),
|
|
|
+ 'minQuestionCount' => array('title' => "Questions needed", 'type' => 'number', 'data' => 'int', 'width' => 'auto', 'class' => 'left', 'thclass' => 'left',),
|
|
|
+ 'maxDailyPlayTime' => array('title' => "Max minutes a day", 'type' => 'number', 'data' => 'int', 'width' => 'auto', 'class' => 'left', 'thclass' => 'left',),
|
|
|
+ );
|
|
|
+ protected $fieldpref = array('username', 'enabled', 'freeMins', 'enableTime', 'disableTime', 'minQuestionCount', 'maxDailyPlayTime');
|
|
|
+
|
|
|
+ private function esc($value) {
|
|
|
+ return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * "Student Limits" page: edit every student's row in one form, and add a student.
|
|
|
+ * Reached from the menu as Students/custom.
|
|
|
+ */
|
|
|
+ public function customPage() {
|
|
|
+
|
|
|
+ $sql = e107::getDb();
|
|
|
+ $mes = e107::getMessage();
|
|
|
+
|
|
|
+ $action = filter_input(INPUT_POST, 'tk_action');
|
|
|
+ if ($action === 'save') {
|
|
|
+ $this->saveStudents($sql, $mes);
|
|
|
+ } elseif ($action === 'add') {
|
|
|
+ $this->addStudent($sql, $mes);
|
|
|
+ }
|
|
|
+
|
|
|
+ $text = $this->studentsForm($sql) . $this->addForm();
|
|
|
+
|
|
|
+ e107::getRender()->tablerender('Student Limits', $mes->render() . $text);
|
|
|
+ }
|
|
|
+
|
|
|
+ private function loadStudents($sql) {
|
|
|
+ $rows = array();
|
|
|
+ $sql->gen("SELECT * FROM `#timekeeper` ORDER BY `username`");
|
|
|
+ while ($row = $sql->fetch()) {
|
|
|
+ $rows[] = $row;
|
|
|
+ }
|
|
|
+ return $rows;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function intValue($entry, $key, $min, $max, $label, &$errors) {
|
|
|
+ $value = isset($entry[$key]) ? trim((string) $entry[$key]) : '';
|
|
|
+ if (!ctype_digit($value) || (int) $value < $min || (int) $value > $max) {
|
|
|
+ $errors[] = $label . ' must be a whole number from ' . $min . ' to ' . $max . '.';
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return (int) $value;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function timeValue($entry, $key, $label, &$errors) {
|
|
|
+ $value = isset($entry[$key]) ? trim((string) $entry[$key]) : '';
|
|
|
+ if (!preg_match('/^(\d{1,2})\.(\d{2})$/', $value, $match) || (int) $match[1] > 24 || (int) $match[2] > 59 || ((int) $match[1] === 24 && (int) $match[2] > 0)) {
|
|
|
+ $errors[] = $label . ' must look like 9.30 or 22.00 (hours, a dot, then two digits for the minutes).';
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return (float) $value;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Save the edited rows. A row with any invalid field is left unchanged
|
|
|
+ * (and reported); the others are saved.
|
|
|
+ */
|
|
|
+ private function saveStudents($sql, $mes) {
|
|
|
+ $posted = (isset($_POST['tk']) && is_array($_POST['tk'])) ? $_POST['tk'] : array();
|
|
|
+
|
|
|
+ $existing = array();
|
|
|
+ foreach ($this->loadStudents($sql) as $row) {
|
|
|
+ $existing[$row['username']] = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ $saved = 0;
|
|
|
+ foreach ($posted as $entry) {
|
|
|
+ if (!is_array($entry) || !isset($entry['username'])) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $username = (string) $entry['username'];
|
|
|
+ if (!isset($existing[$username])) {
|
|
|
+ continue; // only students that are already in the table
|
|
|
+ }
|
|
|
+
|
|
|
+ $errors = array();
|
|
|
+ $freeMins = $this->intValue($entry, 'freeMins', 0, 1440, 'Free minutes', $errors);
|
|
|
+ $minQuestionCount = $this->intValue($entry, 'minQuestionCount', 0, 100000, 'Questions needed', $errors);
|
|
|
+ $maxDailyPlayTime = $this->intValue($entry, 'maxDailyPlayTime', 0, 1440, 'Max minutes a day', $errors);
|
|
|
+ $enableTime = $this->timeValue($entry, 'enableTime', 'Play from', $errors);
|
|
|
+ $disableTime = $this->timeValue($entry, 'disableTime', 'Play until', $errors);
|
|
|
+
|
|
|
+ if (!empty($errors)) {
|
|
|
+ foreach ($errors as $error) {
|
|
|
+ $mes->addError($this->esc($username) . ': ' . $error . ' Not saved.');
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $sql->update('timekeeper', array(
|
|
|
+ 'enabled' => isset($entry['enabled']) ? 1 : 0,
|
|
|
+ 'freeMins' => $freeMins,
|
|
|
+ 'enableTime' => $enableTime,
|
|
|
+ 'disableTime' => $disableTime,
|
|
|
+ 'minQuestionCount' => $minQuestionCount,
|
|
|
+ 'maxDailyPlayTime' => $maxDailyPlayTime,
|
|
|
+ 'WHERE' => "username = '" . $sql->escape($username) . "'",
|
|
|
+ ));
|
|
|
+ $saved++;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($saved > 0) {
|
|
|
+ $mes->addSuccess('Saved the limits for ' . $saved . ($saved === 1 ? ' student.' : ' students.'));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Add a student with the same starting limits timer.php uses.
|
|
|
+ */
|
|
|
+ private function addStudent($sql, $mes) {
|
|
|
+ $username = trim((string) filter_input(INPUT_POST, 'tk_new_user'));
|
|
|
+
|
|
|
+ if (!preg_match('/^[A-Za-z0-9_.-]{1,50}$/', $username)) {
|
|
|
+ $mes->addError('Enter an e107 login name (letters, numbers, . _ - only).');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if ($sql->count('user', '(*)', 'user_loginname="' . $sql->escape($username) . '"') < 1) {
|
|
|
+ $mes->addError('There is no e107 user with the login name "' . $this->esc($username) . '".');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if ($sql->count('timekeeper', '(*)', 'username="' . $sql->escape($username) . '"') > 0) {
|
|
|
+ $mes->addError($this->esc($username) . ' already has limits.');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $sql->insert('timekeeper', array(
|
|
|
+ 'username' => $username,
|
|
|
+ 'enabled' => 1,
|
|
|
+ 'freeMins' => 10,
|
|
|
+ 'enableTime' => 10.00,
|
|
|
+ 'disableTime' => 22.00,
|
|
|
+ 'minQuestionCount' => 100,
|
|
|
+ 'usedMinutes' => 0,
|
|
|
+ 'lastDate' => 0,
|
|
|
+ 'maxDailyPlayTime' => 1440,
|
|
|
+ ));
|
|
|
+ $mes->addSuccess('Added ' . $this->esc($username) . ' with the starting limits. Change them below.');
|
|
|
+ }
|
|
|
+
|
|
|
+ private function studentsForm($sql) {
|
|
|
+ $rows = $this->loadStudents($sql);
|
|
|
+ if (empty($rows)) {
|
|
|
+ return '<p>No students have limits yet. They are created automatically the first time the enforcement client asks about a student, or you can add one below.</p>';
|
|
|
+ }
|
|
|
+
|
|
|
+ $frm = e107::getForm();
|
|
|
+
|
|
|
+ $text = $frm->open('tkStudents', 'post');
|
|
|
+ $text .= $frm->hidden('tk_action', 'save');
|
|
|
+ $text .= '<table class="table table-striped table-bordered"><thead><tr>'
|
|
|
+ . '<th>Student</th><th>Limits on</th><th>Free minutes a day</th><th>Play from</th><th>Play until</th>'
|
|
|
+ . '<th>Questions needed today</th><th>Max minutes a day</th><th>Used today</th><th>Earned minutes</th>'
|
|
|
+ . '</tr></thead><tbody>';
|
|
|
+
|
|
|
+ foreach ($rows as $i => $row) {
|
|
|
+ $name = $row['username'];
|
|
|
+ $key = 'tk[' . $i . ']';
|
|
|
+
|
|
|
+ $usedToday = ((int) $row['lastDate'] > 0 && date('Ymd', (int) $row['lastDate']) === date('Ymd')) ? (int) $row['usedMinutes'] : 0;
|
|
|
+ $earned = (int) $sql->retrieve('timekeeperScore', 'timekeeper_minutes', 'timekeeper_username="' . $sql->escape($name) . '"');
|
|
|
+
|
|
|
+ $text .= '<tr>'
|
|
|
+ . '<td>' . $this->esc($name) . $frm->hidden($key . '[username]', $name) . '</td>'
|
|
|
+ . '<td>' . $frm->checkbox($key . '[enabled]', 1, !empty($row['enabled'])) . '</td>'
|
|
|
+ . '<td>' . $frm->text($key . '[freeMins]', (int) $row['freeMins'], 6, array('size' => 'mini')) . '</td>'
|
|
|
+ . '<td>' . $frm->text($key . '[enableTime]', number_format((float) $row['enableTime'], 2, '.', ''), 5, array('size' => 'mini')) . '</td>'
|
|
|
+ . '<td>' . $frm->text($key . '[disableTime]', number_format((float) $row['disableTime'], 2, '.', ''), 5, array('size' => 'mini')) . '</td>'
|
|
|
+ . '<td>' . $frm->text($key . '[minQuestionCount]', (int) $row['minQuestionCount'], 6, array('size' => 'mini')) . '</td>'
|
|
|
+ . '<td>' . $frm->text($key . '[maxDailyPlayTime]', (int) $row['maxDailyPlayTime'], 6, array('size' => 'mini')) . '</td>'
|
|
|
+ . '<td>' . $usedToday . '</td>'
|
|
|
+ . '<td>' . $earned . '</td>'
|
|
|
+ . '</tr>';
|
|
|
+ }
|
|
|
+
|
|
|
+ $text .= '</tbody></table>';
|
|
|
+ $text .= '<div class="buttons-bar center">' . $frm->button('tk_save_button', 'Save changes') . '</div>';
|
|
|
+ $text .= $frm->close();
|
|
|
+
|
|
|
+ $text .= '<p class="smalltext">'
|
|
|
+ . '<b>Limits on</b> off = no limit for that student. '
|
|
|
+ . '<b>Free minutes</b> are available every day without earning them. '
|
|
|
+ . '<b>Play from / until</b> are hours.minutes, e.g. 9.30 or 22.00 (always two digits for the minutes: 9.5 means 9:50). '
|
|
|
+ . '<b>Questions needed</b> is how many questions must be answered today before earned minutes can be used. '
|
|
|
+ . '<b>Max minutes a day</b> is the most play per day, free and earned (0 = no play). '
|
|
|
+ . '<b>Used today</b> and <b>Earned minutes</b> are shown for information; change earned minutes on the Score screen.'
|
|
|
+ . '</p>';
|
|
|
+
|
|
|
+ return $text;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function addForm() {
|
|
|
+ $frm = e107::getForm();
|
|
|
+
|
|
|
+ return '<h4>Add a student</h4>'
|
|
|
+ . $frm->open('tkAddStudent', 'post')
|
|
|
+ . $frm->hidden('tk_action', 'add')
|
|
|
+ . $frm->text('tk_new_user', '', 50, array('placeholder' => 'e107 login name'))
|
|
|
+ . ' ' . $frm->button('tk_add_button', 'Add student')
|
|
|
+ . $frm->close();
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+class tk_Students_form_ui extends e_admin_form_ui {
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
new timekeeper_admin();
|
|
|
|
|
|
require_once(e_ADMIN . "auth.php");
|