Browse Source

Bug fixing and SQL table updates

Gregory Huxley 2 tuần trước cách đây
mục cha
commit
a5215a3733
2 tập tin đã thay đổi với 164 bổ sung và 145 xóa
  1. 121 124
      questionGenerators/math/numeracy2.php
  2. 43 21
      timekeeper_sql.php

+ 121 - 124
questionGenerators/math/numeracy2.php

@@ -1,197 +1,194 @@
 <?php
 
-/*
- * To change this license header, choose License Headers in Project Properties.
- * To change this template file, choose Tools | Templates
- * and open the template in the editor.
- */
-
 /**
- * Description of numeracy
+ * Numeracy question generator (addition, subtraction, multiplication,
+ * division, rounding).
  *
- * @author gregory
+ * Changes from the original:
+ *  - the expected answer is stored server-side (TkAnswerStore) and the form
+ *    carries only an opaque token in `checkerData`
+ *  - difficulty() always returns ['percent' => .., 'count' => ..]
+ *  - Division() used $numbers[1]/$numbers[2] (index 2 doesn't exist, so the
+ *    divisor was null -> division by zero); now uses [0]/[1] and never divides
+ *    by zero
+ *  - Rounding() always produced 1.00 (int cast of an array); now scales with
+ *    the user's accuracy and works in whole cents
+ *  - subtraction can no longer go negative (numbers are always ordered)
+ *  - mt_rand() always receives integers (float args are deprecated in 8.1+)
+ *  - user/subject are escaped in SQL, and the hard-coded `e107.` database
+ *    name is replaced by MPREFIX
+ *  - the previous tkLastQuestion row is removed before inserting the new one
  */
 class numeracy2 {
-   
+
     private $user;
     private $subSubject;
 
     /**
-     * Setup new numbers engine
-     * 
-     * @param bool $test Enable debug mode.
+     * @param string $user User to supply the question for.
+     * @param string $sub  Sub-subject: addition|subtraction|multiplication|division|rounding
      */
-    public function __construct($user,$sub) {
-       
+    public function __construct($user, $sub) {
         $this->user = $user;
         $this->subSubject = $sub;
-
     }
 
     public function getNumeracy() {
-        
-        //print_a(['user'=>$this->user,'subSubject'=>$this->subSubject]);
         $difficulty = $this->difficulty();
         $question = $this->numeracySwitch($difficulty);
+
+        if ($question === null) {
+            e107::getRender()->tablerender('Numeracy', 'Unknown numeracy subject.');
+            return;
+        }
+
         $questionHtml = $this->htmlify($question);
-        
-        e107::getDb()->insert('tkLastQuestion', ['user'=>$this->user,'LastQuestionHtml'=>base64_encode($questionHtml)]);
-        
+
+        $db = e107::getDb();
+        $db->delete('tkLastQuestion', "user = '" . $db->escape($this->user) . "'");
+        $db->insert('tkLastQuestion', ['user' => $this->user, 'LastQuestionHtml' => base64_encode($questionHtml)]);
+
         e107::getRender()->tablerender('Numeracy', $questionHtml);
     }
-    
+
     private function numeracySwitch($difficulty) {
         switch ($this->subSubject) {
-            
             case 'addition':
-                return $this->basic('+',$difficulty);
+                return $this->basic('+', $difficulty);
             case 'subtraction':
-                return $this->basic('-',$difficulty);
+                return $this->basic('-', $difficulty);
             case 'multiplication':
-                return $this->basic('x',$difficulty);
+                return $this->basic('x', $difficulty);
             case 'rounding':
                 return $this->Rounding($difficulty);
             case 'division':
                 return $this->Division($difficulty);
         }
+        return null;
     }
 
     /**
-     * Return the difficulty based on previously answered questions.
+     * Difficulty based on the last month of answers for this sub-subject.
      *
-     * @return int Difficulty as percent.
+     * @return array ['percent' => float 0-100, 'count' => int]
      */
     private function difficulty() {
-
-        $stats = e107::getDb()->retrieve("SELECT (SUM(correct)/COUNT(correct)*100) as percent,COUNT(correct) as count
-            FROM e107.`e107_tkReport` WHERE 
-            answereddatestamp >= unix_timestamp((NOW()) - INTERVAL 1 MONTH)
-            AND username = '$this->user' AND subject = '$this->subSubject';");
-        
-        if ($stats["count"]==0) {
-            return 1;
-        } else {
-            return $stats;
+        $db = e107::getDb();
+        $stats = $db->retrieve(
+            "SELECT (SUM(correct)/COUNT(correct)*100) AS percent, COUNT(correct) AS count
+             FROM `" . MPREFIX . "tkReport`
+             WHERE answereddatestamp >= UNIX_TIMESTAMP(NOW() - INTERVAL 1 MONTH)
+             AND username = '" . $db->escape($this->user) . "'
+             AND subject = '" . $db->escape($this->subSubject) . "'"
+        );
+
+        if (!is_array($stats) || (int) $stats['count'] === 0) {
+            return ['percent' => 0, 'count' => 0];
         }
-        
+
+        return ['percent' => (float) $stats['percent'], 'count' => (int) $stats['count']];
     }
 
-    private function getNumbers($difficulty,$sign){
-//TODO add finer grain control for multiplication here
-        if($sign=='x'||$sign=='/')
-        {
-            $difficulty['count'] = (int)$difficulty['count']/4;
-            $difficulty['percent'] = (int)$difficulty['percent']/6;
-        }
-        
-        if($difficulty['count']>=300) {
-            $startingNumber = mt_rand(0, (int) pow($difficulty['count'],($difficulty['percent']/100)*2));
-            $endingNumber = mt_rand(0, (int) pow($difficulty['count'],($difficulty['percent']/100)*2));
-            $returnNumbers = [$startingNumber,$endingNumber];
-        }elseif($difficulty['count']>=200){
-            $startingNumber = mt_rand(0, (int) $difficulty['count']+($difficulty['percent']/100)*2);
-            $endingNumber = mt_rand(0, (int) $difficulty['count']+($difficulty['percent']/100)*2);
-            $returnNumbers = $this->flipNumbers($startingNumber,$endingNumber);
-        }elseif($difficulty['count']>=100){
-            $startingNumber = mt_rand(0, (int) ($difficulty['percent'])*2);
-            $endingNumber = mt_rand(0, (int) ($difficulty['percent'])*2);
-            $returnNumbers = $this->flipNumbers($startingNumber,$endingNumber);
-        }else{
-            $startingNumber = mt_rand(0, $difficulty['count']+1);
-            $endingNumber = mt_rand(0, $difficulty['count']+1);
-            $returnNumbers = $this->flipNumbers($startingNumber,$endingNumber);
+    /**
+     * Two random whole numbers, larger first.
+     */
+    private function getNumbers($difficulty, $sign) {
+        //TODO add finer grain control for multiplication here
+        $count = (float) $difficulty['count'];
+        $percent = (float) $difficulty['percent'];
+
+        if ($sign == 'x' || $sign == '/') {
+            $count = floor($count / 4);
+            $percent = $percent / 6;
         }
 
-        //print_a(['Original'=>['1'=>$startingNumber,'2'=>$endingNumber],'FlipNumbers'=>$returnNumbers,'user'=>$this->user,'diff'=>$difficulty]);
-        return $returnNumbers;
+        if ($count >= 300) {
+            $max = (int) pow($count, ($percent / 100) * 2);
+        } elseif ($count >= 200) {
+            $max = (int) ($count + ($percent / 100) * 2);
+        } elseif ($count >= 100) {
+            $max = (int) ($percent * 2);
+        } else {
+            $max = (int) $count + 1;
+        }
 
+        return $this->flipNumbers(mt_rand(0, $max), mt_rand(0, $max));
     }
 
-    private function flipNumbers($startingNumber,$endingNumber){
-        if($startingNumber>$endingNumber){
-            return [$startingNumber,$endingNumber];
-        }else{
-            return [$endingNumber,$startingNumber];
+    private function flipNumbers($startingNumber, $endingNumber) {
+        if ($startingNumber > $endingNumber) {
+            return [$startingNumber, $endingNumber];
         }
+        return [$endingNumber, $startingNumber];
     }
-    
-    private function basic($sign,$difficulty) {
-        
-        $numbers = $this->getNumbers($difficulty,$sign);
 
+    private function basic($sign, $difficulty) {
+        $numbers = $this->getNumbers($difficulty, $sign);
         $firstNumber = $numbers[0];
         $secondNumber = $numbers[1];
-        $TempQuestion = [];
-        
-        $TempQuestion[] = $firstNumber;
-        $TempQuestion[] = $sign;
-        $TempQuestion[] = $secondNumber;
-        
-        $solution['+'] = ($firstNumber + $secondNumber); 
-        $solution['-'] = ($firstNumber - $secondNumber); 
-        $solution['x'] = ($firstNumber * $secondNumber); 
-        
-        return [ 'answer1'=>$solution[$sign], 'question'=>$TempQuestion, 'type'=>$this->subSubject];
-        
+
+        $solution = [
+            '+' => $firstNumber + $secondNumber,
+            '-' => $firstNumber - $secondNumber,
+            'x' => $firstNumber * $secondNumber,
+        ];
+
+        return ['answer1' => $solution[$sign], 'question' => [$firstNumber, $sign, $secondNumber], 'type' => $this->subSubject];
     }
 
     private function Division($difficulty) {
-        
-        $numbers = $this->getNumbers($difficulty,'/');
-
-        $firstNumber = $numbers[1];
-        $secondNumber = $numbers[2];
-        $divNumber = $firstNumber * $secondNumber;
-        $TempQuestion = [];
-        
-        $TempQuestion[] = $divNumber;
-        $TempQuestion[] = '/';
-        $TempQuestion[] = $secondNumber;
-        
-        $solution['/'] = ($divNumber / $secondNumber); 
-        
-        return [ 'answer1'=>$solution['/'], 'question'=>$TempQuestion, 'type'=>$this->subSubject];
-        
+        $numbers = $this->getNumbers($difficulty, '/');
+        $quotient = max(1, $numbers[0]);
+        $divisor = max(1, $numbers[1]);
+        $dividend = $quotient * $divisor;
+
+        return ['answer1' => $quotient, 'question' => [$dividend, '/', $divisor], 'type' => $this->subSubject];
     }
-    
-    private function Rounding($difficulty) {
-        
-        $firstNumber = sprintf("%01.2f", (float) rand(100, (int) ($difficulty) * 100) / 100);
-        $TempQuestion = [];
-        
-        $TempQuestion[] = $firstNumber;
-        $TempQuestion[] = 'r';
-        
-        $solution['r'] = (sprintf("%01.2f", round($firstNumber / 5, 2) * 5)); 
-        
-        return [ 'answer1'=>$solution['r'], 'question'=>$TempQuestion, 'type'=>$this->subSubject];
 
+    /**
+     * Round an amount of money to the nearest 5 cents. The amount grows with
+     * the user's accuracy: up to $5.00 at 0%, up to $25.00 at 100%.
+     */
+    private function Rounding($difficulty) {
+        $maxCents = (5 + (int) floor($difficulty['percent'] / 10) * 2) * 100;
+        $cents = mt_rand(100, $maxCents);
+        $roundedCents = (int) (round($cents / 5) * 5);
+
+        return [
+            'answer1' => sprintf('%01.2f', $roundedCents / 100),
+            'question' => [sprintf('%01.2f', $cents / 100), 'r'],
+            'type' => $this->subSubject,
+        ];
     }
 
     private function htmlify($question) {
-        
+        // Expected answer stays on the server; the form only carries a token.
+        $token = TkAnswerStore::issue($this->user, $question);
+
+        $instruction = ($this->subSubject === 'rounding')
+            ? 'Round to the nearest 5 cents.'
+            : 'Solve the following equation.';
+
         $frm = e107::getForm();
 
         $fString = $frm->open('questionForm', 'post', '', ['autocomplete' => 'off', 'class' => 'formclass']);
-        $fString .= $frm->label('<h4>'.ucfirst($this->subSubject).'</h4>') . "</br>\n";
-        $fString .= $frm->label('Solve the following equation.') . "\n";
+        $fString .= $frm->label('<h4>' . htmlspecialchars(ucfirst($this->subSubject)) . '</h4>') . "</br>\n";
+        $fString .= $frm->label($instruction) . "\n";
         $fString .= '</br>';
-        
+
         foreach ($question['question'] as $seq) {
             $fString .= $frm->label($seq . " &nbsp; ") . "\n";
         }
-        
+
         $fString .= '</br></br>';
         $fString .= $frm->text('a1', '', 80, ['placeholder' => 'Write answer here!', 'autocomplete' => "off", 'spellcheck' => "false"]) . "\n";
-        $fString .= $frm->hidden('checkerData', TkAnswerStore::issue($this->user, $question), ['id' => 'checkerData']);
+        $fString .= $frm->hidden('checkerData', $token, ['id' => 'checkerData']);
         $fString .= $frm->hidden('subject', $this->subSubject, ['id' => 'subject']);
         $fString .= '</br>' . "\n";
-        $fString .= e107::getForm()->button('Answer', 'Check answer') . "\n";
-        $fString .= e107::getForm()->close();
+        $fString .= $frm->button('Answer', 'Check answer') . "\n";
+        $fString .= $frm->close();
         $fString .= '</br></br>';
-        
+
         return $fString;
-        
     }
-
 }

+ 43 - 21
timekeeper_sql.php

@@ -1,13 +1,12 @@
-
 CREATE TABLE `timekeeper` (
   `timekeeper_username` varchar(255) NOT NULL DEFAULT '',
   `timekeeper_Enabled` bit(1) DEFAULT NULL,
   `timekeeper_Freemins` int(10) unsigned NOT NULL DEFAULT '0',
-  `timekeeper_Enable_Time` double(2) NOT NULL DEFAULT '0',
+  `timekeeper_Enable_Time` double NOT NULL DEFAULT '0',
   `timekeeper_Disable_Time` double NOT NULL DEFAULT '0',
   `timekeeper_Min_Question_Count` int(11) DEFAULT '0',
   PRIMARY KEY (`timekeeper_username`)
-) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 CREATE TABLE `timekeeperPending` (
   `timekeeper_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
@@ -16,7 +15,7 @@ CREATE TABLE `timekeeperPending` (
   `timekeeper_answereddatestamp` int(10) unsigned DEFAULT '0',
   `timekeeper_answer` text NOT NULL,
   PRIMARY KEY (`timekeeper_id`)
-) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 CREATE TABLE `timekeeperQuestions` (
   `timekeeper_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
@@ -28,7 +27,7 @@ CREATE TABLE `timekeeperQuestions` (
   `timekeeper_answers` text NOT NULL,
   `timekeeper_correctanswer` int(3) unsigned DEFAULT '0',
   PRIMARY KEY (`timekeeper_id`)
-) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 CREATE TABLE `timekeeperReport` (
   `timekeeper_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
@@ -41,18 +40,18 @@ CREATE TABLE `timekeeperReport` (
   `timekeeper_subject` varchar(255) NOT NULL DEFAULT '',
   `timekeeper_correct` int(1) unsigned DEFAULT '0',
   PRIMARY KEY (`timekeeper_id`)
-) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 CREATE TABLE `timekeeperScore` (
-  `timekeeper_id` int(10) unsigned AUTO_INCREMENT,
+  `timekeeper_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
   `timekeeper_username` varchar(255) NOT NULL DEFAULT '',
   `timekeeper_lifetimequestions` int(10) unsigned DEFAULT '0',
   `timekeeper_lifetimecorrect` int(10) unsigned DEFAULT '0',
-  `timekeeper_minutes` int(10) unsigned DEFAULT '0',
+  `timekeeper_minutes` int(10) NOT NULL DEFAULT '0',
   `timekeeper_yearlevel` int(2) unsigned DEFAULT '0',
   `timekeeper_lastquestiondatestamp` int(10) unsigned DEFAULT '0',
   PRIMARY KEY (`timekeeper_id`)
-) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 CREATE TABLE `timekeeperStats` (
   `timekeeper_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
@@ -62,18 +61,41 @@ CREATE TABLE `timekeeperStats` (
   `timekeeper_yearLevel` int(2) unsigned DEFAULT '0',
   `timekeeper_quantity` int(10) unsigned DEFAULT '0',
   PRIMARY KEY (`timekeeper_id`)
-) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 CREATE TABLE `tkLastQuestion` (
-  `id` INT NOT NULL AUTO_INCREMENT,
-  `user` VARCHAR(100) NULL,
-  `LastQuestionHtml` LONGTEXT NULL,
-  PRIMARY KEY (`id`));
-  
-  CREATE TABLE tkPendingAnswer (
-  username varchar(100) NOT NULL,
-  token char(32) NOT NULL,
-  payload text NOT NULL,
-  created int(10) unsigned NOT NULL,
-  PRIMARY KEY (username)
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `user` varchar(100) NOT NULL DEFAULT '',
+  `LastQuestionHtml` longtext,
+  PRIMARY KEY (`id`),
+  KEY `user` (`user`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE `tkPendingAnswer` (
+  `username` varchar(100) NOT NULL,
+  `token` char(32) NOT NULL,
+  `payload` text NOT NULL,
+  `created` int(10) unsigned NOT NULL,
+  PRIMARY KEY (`username`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE `tkReport` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `username` varchar(255) NOT NULL DEFAULT '',
+  `subject` varchar(255) NOT NULL DEFAULT '',
+  `question` text NOT NULL,
+  `userAnswer` text NOT NULL,
+  `answer` text NOT NULL,
+  `correct` tinyint(1) unsigned NOT NULL DEFAULT '0',
+  `answereddatestamp` int(10) unsigned NOT NULL DEFAULT '0',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE `tk_wordList` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `word` varchar(100) NOT NULL,
+  `usage` text,
+  `year` tinyint(3) unsigned NOT NULL DEFAULT '1',
+  PRIMARY KEY (`id`),
+  KEY `year` (`year`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;