e_cron.php 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. <?php
  2. /**
  3. * e107 website system
  4. *
  5. * Copyright (C) 2008-2016 e107 Inc (e107.org)
  6. * Released under the terms and conditions of the
  7. * GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
  8. *
  9. * Daily TimeKeeper report emails.
  10. *
  11. * Changes from the current repo version:
  12. * - the shared DB connection is no longer closed ($sql->close()) between the
  13. * two students' reports, which would likely have broken the second one
  14. * - reporting window is explicit integer timestamps (yesterday, midnight to
  15. * midnight; see $windowStart/$windowEnd). The old FROM_UNIXTIME(col) >=
  16. * DATE_ADD(CURDATE(), INTERVAL -1 DAY) condition had no upper bound, could
  17. * overlap the previous email, and couldn't use an index
  18. * - "minutes studied" is now the sum of the gaps between answers, ignoring
  19. * breaks over $breakSeconds. It used last-minus-first, which counted a
  20. * morning and an evening session as ten hours, and on a day with no
  21. * answers produced about 166 million minutes
  22. * - the overall percentage is correct answers / questions, not the average
  23. * of the per-subject percentages (1 question at 100% outweighed 50 at 50%)
  24. * - questions, answers, subjects and names are HTML-escaped in the email
  25. * - correct/incorrect are text symbols (the <img> paths were relative, which
  26. * never worked in an email client)
  27. * - names and addresses are in one config block instead of repeated; the kid
  28. * is addressed by their own name
  29. * - `e107.e107_tkReport` -> MPREFIX, user escaped
  30. */
  31. class timekeeper_cron {
  32. // Who gets which report. The parent address receives every report.
  33. private $parentEmail = 'gregory@pfip.au';
  34. private $parentName = 'Gregory';
  35. private $children = [
  36. 'elliot' => 'elliot@pfip.au',
  37. 'silas' => 'silas@pfip.au',
  38. ];
  39. // Reporting window (strtotime strings). Default: yesterday, midnight to midnight.
  40. // If you send the email in the evening and want "today so far", use
  41. // 'today midnight' and 'tomorrow midnight' and adjust the wording below.
  42. private $windowStart = 'yesterday midnight';
  43. private $windowEnd = 'today midnight';
  44. // A gap between two answers longer than this (seconds) counts as a break.
  45. private $breakSeconds = 600;
  46. function config() { // Setup
  47. $cron = array();
  48. $cron[] = array(
  49. 'name' => "Send Reports", // Displayed in admin area. .
  50. 'function' => "sendReportsEmail", // Name of the function which is defined below.
  51. 'category' => 'mail', // Choose between: mail, user, content, notify, or backup
  52. 'description' => "Send Timekeeper reports out to receivers." // Displayed in admin area.
  53. );
  54. return $cron;
  55. }
  56. function sendReportsEmail() {
  57. require_once(e_HANDLER . "mail.php");
  58. foreach ($this->children as $user => $childEmail) {
  59. $mailContent = $this->generateEmail($user);
  60. $eml = array(
  61. 'subject' => $mailContent['subject'],
  62. 'body' => $mailContent['message'],
  63. 'sender_email' => 'service@pfip.au'
  64. );
  65. e107::getEmail()->sendEmail($mailContent['recipients'], $this->parentName, $eml);
  66. e107::getEmail()->sendEmail($childEmail, ucfirst($user), $eml);
  67. }
  68. }
  69. public function generateEmail($user) {
  70. $returnArray['subject'] = 'TimeKeeper Report for ' . $user;
  71. $returnArray['recipients'] = $this->parentEmail;
  72. $sql = e107::getDb();
  73. $start = (int) strtotime($this->windowStart);
  74. $end = (int) strtotime($this->windowEnd);
  75. $coverPageData = $this->coverPage($sql, $user, $start, $end);
  76. $questionsPageData = $this->questionsTable($sql, $user, $start, $end);
  77. $returnArray['message'] = $this->template($coverPageData, $questionsPageData);
  78. return $returnArray;
  79. }
  80. private function coverPage($sql, $user, $start, $end) {
  81. $sql->gen("SELECT `subject`, COUNT(`correct`) AS total, SUM(`correct`) AS num_correct,
  82. CAST((SUM(correct)/COUNT(id)*100) AS DECIMAL(5,2)) AS percent
  83. FROM `" . MPREFIX . "tkReport`
  84. WHERE `answereddatestamp` >= $start AND `answereddatestamp` < $end
  85. AND `username` = '" . $sql->escape($user) . "'
  86. GROUP BY `subject` ORDER BY `subject`");
  87. $scores = [];
  88. $questions = 0;
  89. $correct = 0;
  90. while ($row = $sql->fetch()) {
  91. $scores[] = ['subject' => $row['subject'], 'total' => (int) $row['total'], 'percent' => $row['percent']];
  92. $questions += (int) $row['total'];
  93. $correct += (int) $row['num_correct'];
  94. }
  95. $totalPercent = ($questions > 0) ? round($correct / $questions * 100, 1) : 0;
  96. return ['user' => $user, 'TotalPercent' => $totalPercent, 'Questions' => $questions, 'Scores' => $scores];
  97. }
  98. private function questionsTable($sql, $user, $start, $end) {
  99. $sql->gen("SELECT `question`, `answereddatestamp`, `userAnswer`, `correct`, `subject`
  100. FROM `" . MPREFIX . "tkReport`
  101. WHERE username = '" . $sql->escape($user) . "'
  102. AND `answereddatestamp` >= $start AND `answereddatestamp` < $end
  103. ORDER BY subject, answereddatestamp");
  104. $questionsData = ['questions' => []];
  105. $times = [];
  106. while ($row = $sql->fetch()) {
  107. $questionsData['questions'][$row['subject']][] = [
  108. 'question' => $row['question'],
  109. 'userAnswer' => $row['userAnswer'],
  110. 'correct' => $row['correct'],
  111. ];
  112. $times[] = (int) $row['answereddatestamp'];
  113. }
  114. // Active study time: gaps between answers, ignoring breaks.
  115. sort($times);
  116. $active = 0;
  117. for ($i = 1; $i < count($times); $i++) {
  118. $active += min($times[$i] - $times[$i - 1], $this->breakSeconds);
  119. }
  120. $questionsData['activeSeconds'] = $active;
  121. return $questionsData;
  122. }
  123. private function esc($value) {
  124. return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
  125. }
  126. private function correctOrNot($state) {
  127. if ($state) {
  128. return '<span style="color:#33cc33" title="Correct">&#10004;</span>';
  129. }
  130. return '<span style="color:#ff4444" title="Incorrect">&#10008;</span>';
  131. }
  132. private $css = 'body {
  133. background-color: #1a1a1a;
  134. color: #ffffff;
  135. font-family: Arial, sans-serif;
  136. }
  137. #sum {
  138. font-size: 32px;
  139. text-align: center;
  140. color: #ff9900;
  141. }
  142. #styTi {
  143. font-size: 14px;
  144. text-align: center;
  145. color: #cccccc;
  146. }
  147. #coverP, #questions {
  148. font-family: Arial, sans-serif;
  149. border-collapse: collapse;
  150. width: 100%;
  151. margin-top: 20px;
  152. }
  153. #coverP td, #coverP th, #questions td, #questions th {
  154. border: 1px solid #444;
  155. padding: 12px;
  156. text-align: left;
  157. }
  158. #coverP tr:nth-child(even), #questions tr:nth-child(even) {
  159. background-color: #2a2a2a;
  160. }
  161. #coverP tr:hover, #questions tr:hover {
  162. background-color: #333;
  163. }
  164. #coverP th, #questions th {
  165. padding-top: 12px;
  166. padding-bottom: 12px;
  167. text-align: left;
  168. background-color: #ff9900;
  169. color: #1a1a1a;
  170. font-weight: bold;
  171. }
  172. #coverP td, #questions td {
  173. color: #ffffff;
  174. }';
  175. private function template($coverPageData, $questionsPageData) {
  176. $user = $this->esc(ucfirst($coverPageData['user']));
  177. $emailTemplate = '<!DOCTYPE html><html><head><meta charset="utf-8"><style>' . $this->css . '</style></head><body>';
  178. if ($coverPageData['Questions'] === 0) {
  179. $emailTemplate .= '<div id="sum">' . $user . ' did not answer any questions yesterday.</div></body></html>';
  180. return $emailTemplate;
  181. }
  182. $minutes = round($questionsPageData['activeSeconds'] / 60, 1);
  183. $emailTemplate .= '<div id="sum">' . $user . '\'s overall score for yesterday is ' . $this->esc($coverPageData['TotalPercent']) . '%.</div>'
  184. . '<div id="styTi">About ' . $this->esc($minutes) . ' minutes were spent studying yesterday (breaks over ' . (int) ($this->breakSeconds / 60) . ' minutes not counted).</div>'
  185. . '<br/><table id="coverP"><tr><th>Subject</th><th>Count</th><th>Percentage</th></tr>';
  186. foreach ($coverPageData['Scores'] as $score) {
  187. $emailTemplate .= '<tr><td>' . $this->esc(ucfirst($score['subject'])) . '</td><td>' . (int) $score['total'] . '</td><td>' . $this->esc($score['percent']) . '%</td></tr>';
  188. }
  189. $emailTemplate .= '</table><br/><table id="questions"><tr><th>Question</th><th>Answered</th><th>Correct?</th></tr>';
  190. foreach ($questionsPageData['questions'] as $subject => $subjectQuestions) {
  191. $emailTemplate .= '<tr><td colspan="3" style="text-align: center; font-size: 16px; background-color: #333; color: #ff9900;">' . $this->esc(ucfirst($subject)) . '</td></tr>';
  192. foreach ($subjectQuestions as $question) {
  193. $emailTemplate .= '<tr><td>' . $this->esc($question['question']) . '</td><td>' . $this->esc($question['userAnswer']) . '</td><td>' . $this->correctOrNot($question['correct']) . '</td></tr>';
  194. }
  195. }
  196. $emailTemplate .= '</table></body></html>';
  197. return $emailTemplate;
  198. }
  199. }