close()) between the * two students' reports, which would likely have broken the second one * - reporting window is explicit integer timestamps (yesterday, midnight to * midnight; see $windowStart/$windowEnd). The old FROM_UNIXTIME(col) >= * DATE_ADD(CURDATE(), INTERVAL -1 DAY) condition had no upper bound, could * overlap the previous email, and couldn't use an index * - "minutes studied" is now the sum of the gaps between answers, ignoring * breaks over $breakSeconds. It used last-minus-first, which counted a * morning and an evening session as ten hours, and on a day with no * answers produced about 166 million minutes * - the overall percentage is correct answers / questions, not the average * of the per-subject percentages (1 question at 100% outweighed 50 at 50%) * - questions, answers, subjects and names are HTML-escaped in the email * - correct/incorrect are text symbols (the paths were relative, which * never worked in an email client) * - names and addresses are in one config block instead of repeated; the kid * is addressed by their own name * - `e107.e107_tkReport` -> MPREFIX, user escaped */ class timekeeper_cron { // Who gets which report. The parent address receives every report. private $parentEmail = 'gregory@pfip.au'; private $parentName = 'Gregory'; private $children = [ 'elliot' => 'elliot@pfip.au', 'silas' => 'silas@pfip.au', ]; // Reporting window (strtotime strings). Default: yesterday, midnight to midnight. // If you send the email in the evening and want "today so far", use // 'today midnight' and 'tomorrow midnight' and adjust the wording below. private $windowStart = 'yesterday midnight'; private $windowEnd = 'today midnight'; // A gap between two answers longer than this (seconds) counts as a break. private $breakSeconds = 600; function config() { // Setup $cron = array(); $cron[] = array( 'name' => "Send Reports", // Displayed in admin area. . 'function' => "sendReportsEmail", // Name of the function which is defined below. 'category' => 'mail', // Choose between: mail, user, content, notify, or backup 'description' => "Send Timekeeper reports out to receivers." // Displayed in admin area. ); return $cron; } function sendReportsEmail() { require_once(e_HANDLER . "mail.php"); foreach ($this->children as $user => $childEmail) { $mailContent = $this->generateEmail($user); $eml = array( 'subject' => $mailContent['subject'], 'body' => $mailContent['message'], 'sender_email' => 'service@pfip.au' ); e107::getEmail()->sendEmail($mailContent['recipients'], $this->parentName, $eml); e107::getEmail()->sendEmail($childEmail, ucfirst($user), $eml); } } public function generateEmail($user) { $returnArray['subject'] = 'TimeKeeper Report for ' . $user; $returnArray['recipients'] = $this->parentEmail; $sql = e107::getDb(); $start = (int) strtotime($this->windowStart); $end = (int) strtotime($this->windowEnd); $coverPageData = $this->coverPage($sql, $user, $start, $end); $questionsPageData = $this->questionsTable($sql, $user, $start, $end); $returnArray['message'] = $this->template($coverPageData, $questionsPageData); return $returnArray; } private function coverPage($sql, $user, $start, $end) { $sql->gen("SELECT `subject`, COUNT(`correct`) AS total, SUM(`correct`) AS num_correct, CAST((SUM(correct)/COUNT(id)*100) AS DECIMAL(5,2)) AS percent FROM `" . MPREFIX . "tkReport` WHERE `answereddatestamp` >= $start AND `answereddatestamp` < $end AND `username` = '" . $sql->escape($user) . "' GROUP BY `subject` ORDER BY `subject`"); $scores = []; $questions = 0; $correct = 0; while ($row = $sql->fetch()) { $scores[] = ['subject' => $row['subject'], 'total' => (int) $row['total'], 'percent' => $row['percent']]; $questions += (int) $row['total']; $correct += (int) $row['num_correct']; } $totalPercent = ($questions > 0) ? round($correct / $questions * 100, 1) : 0; return ['user' => $user, 'TotalPercent' => $totalPercent, 'Questions' => $questions, 'Scores' => $scores]; } private function questionsTable($sql, $user, $start, $end) { $sql->gen("SELECT `question`, `answereddatestamp`, `userAnswer`, `correct`, `subject` FROM `" . MPREFIX . "tkReport` WHERE username = '" . $sql->escape($user) . "' AND `answereddatestamp` >= $start AND `answereddatestamp` < $end ORDER BY subject, answereddatestamp"); $questionsData = ['questions' => []]; $times = []; while ($row = $sql->fetch()) { $questionsData['questions'][$row['subject']][] = [ 'question' => $row['question'], 'userAnswer' => $row['userAnswer'], 'correct' => $row['correct'], ]; $times[] = (int) $row['answereddatestamp']; } // Active study time: gaps between answers, ignoring breaks. sort($times); $active = 0; for ($i = 1; $i < count($times); $i++) { $active += min($times[$i] - $times[$i - 1], $this->breakSeconds); } $questionsData['activeSeconds'] = $active; return $questionsData; } private function esc($value) { return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); } private function correctOrNot($state) { if ($state) { return '✔'; } return '✘'; } private $css = 'body { background-color: #1a1a1a; color: #ffffff; font-family: Arial, sans-serif; } #sum { font-size: 32px; text-align: center; color: #ff9900; } #styTi { font-size: 14px; text-align: center; color: #cccccc; } #coverP, #questions { font-family: Arial, sans-serif; border-collapse: collapse; width: 100%; margin-top: 20px; } #coverP td, #coverP th, #questions td, #questions th { border: 1px solid #444; padding: 12px; text-align: left; } #coverP tr:nth-child(even), #questions tr:nth-child(even) { background-color: #2a2a2a; } #coverP tr:hover, #questions tr:hover { background-color: #333; } #coverP th, #questions th { padding-top: 12px; padding-bottom: 12px; text-align: left; background-color: #ff9900; color: #1a1a1a; font-weight: bold; } #coverP td, #questions td { color: #ffffff; }'; private function template($coverPageData, $questionsPageData) { $user = $this->esc(ucfirst($coverPageData['user'])); $emailTemplate = ''; if ($coverPageData['Questions'] === 0) { $emailTemplate .= '
' . $user . ' did not answer any questions yesterday.
'; return $emailTemplate; } $minutes = round($questionsPageData['activeSeconds'] / 60, 1); $emailTemplate .= '
' . $user . '\'s overall score for yesterday is ' . $this->esc($coverPageData['TotalPercent']) . '%.
' . '
About ' . $this->esc($minutes) . ' minutes were spent studying yesterday (breaks over ' . (int) ($this->breakSeconds / 60) . ' minutes not counted).
' . '
'; foreach ($coverPageData['Scores'] as $score) { $emailTemplate .= ''; } $emailTemplate .= '
SubjectCountPercentage
' . $this->esc(ucfirst($score['subject'])) . '' . (int) $score['total'] . '' . $this->esc($score['percent']) . '%

'; foreach ($questionsPageData['questions'] as $subject => $subjectQuestions) { $emailTemplate .= ''; foreach ($subjectQuestions as $question) { $emailTemplate .= ''; } } $emailTemplate .= '
QuestionAnsweredCorrect?
' . $this->esc(ucfirst($subject)) . '
' . $this->esc($question['question']) . '' . $this->esc($question['userAnswer']) . '' . $this->correctOrNot($question['correct']) . '
'; return $emailTemplate; } }