public function mappingsAction()
 {
     echo "<pre>";
     echo "All units\n";
     print_r(MappingHelper::allUnits());
     echo "<hr>Concepts in unit 4\n";
     print_r(MappingHelper::conceptsInUnit("4"));
     echo "<hr>Questions in concept (lecture number) 1\n";
     print_r(MappingHelper::questionsInConcept("1"));
     echo "<hr>Question info for question 78.4\n";
     print_r(MappingHelper::questionInformation("78.4"));
     echo "<hr>Videos for concept 9\n";
     print_r(MappingHelper::videosForConcept("9"));
     echo "<hr>Resources for concept 1\n";
     print_r(MappingHelper::resourcesForConcept("1"));
     echo "<hr>Concepts within the past 2 weeks: \n";
     print_r(MappingHelper::conceptsWithin2Weeks());
 }
 public function scatterplotAction($scope = 'concept', $groupingId = '', $debug = false)
 {
     $this->view->disable();
     // Get our context (this takes care of starting the session, too)
     $context = $this->getDI()->getShared('ltiContext');
     if (!$context->valid) {
         echo '[{"error":"Invalid lti context"}]';
         return;
     }
     // Get the list of questions associated with concepts for the given scope and grouping ID
     $questions = [];
     switch ($scope) {
         case "concept":
             // Filter based on concept
             $questions = MappingHelper::questionsInConcept($groupingId);
             break;
         case "unit":
             // Filter based on unit
             $questions = MappingHelper::questionsInConcepts(MappingHelper::conceptsInUnit($groupingId));
             break;
         default:
             echo '[{"error":"Invalid scope option"}]';
             return;
             break;
     }
     if ($debug) {
         echo "questions for scope {$scope} and grouping {$groupingId}: \n";
         print_r($questions);
     }
     $classHelper = new ClassHelper();
     // Array of questions with more details about each
     $questionDetails = array();
     // Get some info about each question
     foreach ($questions as $question) {
         // Check that it's a valid question
         if ($question != false) {
             // Get number of attempts
             $question["attempts"] = MasteryHelper::countAttemptsForQuestion($context->getUserName(), $question["OA Quiz ID"], $question["Question Number"], $debug);
             $question["scaledAttemptScore"] = $classHelper->calculateScaledAttemptScoreForQuestion($question["attempts"], $question["OA Quiz ID"], $question["Question Number"], $debug);
             // Get amount of associated videos watched
             // Note that question ID is being used instead of assessment ID and question number, since we're searching the csv mapping and not dealing with assessment statements here
             $question["videoPercentage"] = MasteryHelper::calculateUniqueVideoPercentageForQuestion($context->getUserName(), $questionId);
             $questionDetails[] = $question;
         }
     }
     /*
     function randomPoint($group, $q) {
     	// Randomly return outliers
     	if (rand(0,30) == 5) {
     		return [$group, $q["quizNumber"], $q["questionNumber"], rand(-10000, 1000), rand(-10000, 1000)];
     	}
     	return [$group, $q["quizNumber"], $q["questionNumber"], rand(-100, 100), rand(-100, 100)];
     }
     $result = [];
     // For now, return random points based on number of questions
     $numPoints = count($questionDetails);
     //foreach ($questionDetails as $q) {
     	//$result [] = 
     //}
     //
     for ($i=0; $i<$numPoints; $i++) {
     	$result []= randomPoint("student", $questionDetails[$i]);
     	for ($j=0; $j<10; $j++) {
     		$result []= randomPoint("class", $questionDetails[$i]);
     	}
     }
     
     $xValues = array_map(function($point) { return $point[3]; }, $result);
     $yValues = array_map(function($point) { return $point[4]; }, $result);
     
     // TODO check that xValues and yValues have a length, otherwise statshelper will spit out errors
     // Perform some statistics grossness
     	// Remove any outliers for both axes, based on 1.5*IQR
     	// Cap and floor x outliers
     	$xStats = StatsHelper::boxPlotValues($xValues);
     	$result = array_map(function($point) use ($xStats) {
     		$x = $point[3];
     		// Floor upper outliers
     		if ($x > $xStats['q3'] + (1.5 * $xStats['iqr'])) {
     			$x = $xStats['q3'] + (.5 * $xStats['iqr']);
     		}
     		// Cap lower outliers
     		if ($x < $xStats['q1'] - (1.5 * $xStats['iqr'])) {
     			$x = $xStats['q1'] - (.5 * $xStats['iqr']);
     		}
     		$point[3] = $x;
     		return $point;
     	}, $result);
     	// Scale all the scores from 0 to 10
     	// TODO
     
     	// Cap and floor y outliers
     	$yStats = StatsHelper::boxPlotValues($yValues);
     	$result = array_map(function($point) use ($yStats) {
     		$y = $point[4];
     		// Floor upper outliers
     		if ($y > $yStats['q3'] + (1.5 * $yStats['iqr'])) {
     			$y = $yStats['q3'] + (.5 * $yStats['iqr']);
     		}
     		// Cap lower outliers
     		if ($y < $yStats['q1'] - (1.5 * $yStats['iqr'])) {
     			$y = $yStats['q1'] - (.5 * $yStats['iqr']);
     		}
     		$point[4] = $y;
     		return $point;
     	}, $result);
     	//print_r($result);
     	//print_r($yStats);
     
     //die();
     */
     // X = video percentage, Y = question attempts
     $headerRow = ["group", "quiz_number", "question_number", "x", "y"];
     $result = array_map(function ($q) {
         return ["student", $q["OA Quiz ID"], $q["Question Number"], $q["videoPercentage"], $q["scaledAttemptScore"]];
     }, $questionDetails);
     if ($debug) {
         echo "question details for scope {$scope} and grouping {$groupingId}: \n";
         print_r($questionDetails);
         print_r($result);
     }
     // Output data as csv so that we only have to send header information once for so many points
     if (!$debug) {
         header("Content-Type: text/csv");
     }
     $output = fopen("php://output", "w");
     fputcsv($output, $headerRow);
     foreach ($result as $row) {
         fputcsv($output, $row);
         // here you can change delimiter/enclosure
     }
     fclose($output);
 }
 public function questionRecommendationsAction($scope = 'unit', $groupingId = '3', $debug = false)
 {
     $this->view->disable();
     // Get our context (this takes care of starting the session, too)
     $context = $this->getDI()->getShared('ltiContext');
     if (!$context->valid) {
         echo '[{"error":"Invalid lti context"}]';
         return;
     }
     if (!isset($groupingId)) {
         echo '[{"error":"No scope grouping ID specified"}]';
         return;
     }
     $classHelper = new ClassHelper();
     $group1 = [];
     $group2 = [];
     $group3 = [];
     $group4 = [];
     // Get the list of questions associated with concepts for the given scope and grouping ID
     $questionRows = [];
     switch ($scope) {
         case "concept":
             // Filter based on concept
             $questionRows = MappingHelper::questionsInConcept($groupingId);
             break;
         case "unit":
             // Filter based on unit
             $questionRows = MappingHelper::questionsInConcepts(MappingHelper::conceptsInUnit($groupingId));
             break;
         default:
             // Allowing all would take too long
             echo '[{"error":"Invalid scope option"}]';
             return;
             break;
     }
     if ($debug) {
         echo "<pre>Getting information for these questions in scope {$scope} and ID {$groupingId}\n";
         print_r($questionRows);
     }
     $questions = [];
     // Get some info about each question
     foreach ($questionRows as $question) {
         // Get number of attempts and number of correct attempts
         $question["attempts"] = MasteryHelper::countAttemptsForQuestion($context->getUserName(), $question["OA Quiz ID"], $question["Question Number"], $debug);
         $question["correctAttempts"] = MasteryHelper::countCorrectAttemptsForQuestion($context->getUserName(), $question["OA Quiz ID"], $question["Question Number"], $debug);
         // Get amount of associated videos watched
         $question["videoPercentage"] = MasteryHelper::calculateUniqueVideoPercentageForQuestion($context->getUserName(), $question);
         // Variables used in the display table
         // This is one place where we're just using correct, not better correct, attempts
         $question["correct"] = $question["correctAttempts"]["correct"] > 0;
         $question["classAverageAttempts"] = $classHelper->calculateAverageAttemptsForQuestion($question["OA Quiz ID"], $question["Question Number"], $debug);
         $question["classViewedHint"] = $classHelper->calculateViewedHintPercentageForQuestion($question["OA Quiz ID"], $question["Question Number"], $debug);
         $question["classViewedAnswer"] = $classHelper->calculateViewedAnswerPercentageForQuestion($question["OA Quiz ID"], $question["Question Number"], $debug);
         $questions[] = $question;
     }
     // Fetch question texts for all questions in these assessments
     // Get the Open Assessments API endpoint from config
     $assessmentsEndpoint = $this->getDI()->getShared('config')->openassessments_endpoint;
     // Extract a list of assessment IDs from our list of questions. We'll get question texts for these.
     $assessmentIDs = ["assessment_ids" => array_values(array_unique(array_column($questions, "OA Quiz ID")))];
     $request = $assessmentsEndpoint . "api/question_text";
     //$request = "http://192.168.33.102/api/question_text";
     if ($debug) {
         echo "Fetching question texts for these assessment IDs:\n";
         print_r(array_column($questions, "OA Quiz ID"));
         print_r($assessmentIDs);
         echo $request;
     }
     #print_r($assessmentIDs);
     #echo json_encode($request);
     $session = curl_init($request);
     curl_setopt($session, CURLOPT_RETURNTRANSFER, true);
     curl_setopt($session, CURLOPT_POST, 1);
     curl_setopt($session, CURLOPT_POSTFIELDS, json_encode($assessmentIDs));
     $response = curl_exec($session);
     #echo json_encode($questionRows);
     // Catch curl errors
     if (curl_errno($session)) {
         $error = "Curl error: " . curl_error($session);
     }
     curl_close($session);
     #print_r($response);
     $questionTexts = json_decode($response, true);
     if ($debug) {
         print_r($questionTexts);
     }
     foreach ($questions as $key => $q) {
         // Make sure the question text exists before setting it
         $questions[$key]["display"] = isset($questionTexts[$q["OA Quiz ID"]][$q["Question Number"] - 1]) ? $questionTexts[$q["OA Quiz ID"]][$q["Question Number"] - 1] : "Error getting question text for #{$q["OA Quiz ID"]} # #{$q["Question Number"]}-1";
         // Avoid off-by-one error. The question id from statement object id will be 1 to n+1
         //$q["questionText"] = $questionText;
     }
     // Now go through the questions for each group and find matching questions
     foreach ($questions as $question) {
         // Group 1:
         // Questions with 0 attempts
         if ($question["attempts"] == 0) {
             $group1[] = $question;
         }
         // Group 2:
         // >0 attempts for each question
         // No correct statements without a show answer statement in the preceding minute for each question (correctAttempts < 1)
         // Watched less than half of the videos associated with each question
         if ($question["attempts"] > 0 && $question["correctAttempts"]["betterCorrect"] == 0 && $question["videoPercentage"] < 0.5) {
             $group2[] = $question;
         }
         // Group 3:
         // >0 attempts for each question
         // No correct statements without a show answer statement in the preceding minute for each question (correctAttempts < 1)
         // Watched more than half of the videos associated with each question
         if ($question["attempts"] > 0 && $question["correctAttempts"]["betterCorrect"] == 0 && $question["videoPercentage"] >= 0.5) {
             $group3[] = $question;
         }
         // Group 4:
         // >  0 correct statements without a show answer statement in the preceding minute for each question (correctAttempts > 0)
         // More than 1 attempt
         if ($question["correctAttempts"]["betterCorrect"] > 0 && $question["attempts"] > 1) {
             $group4[] = $question;
         }
     }
     if ($debug) {
         print_r($questions);
     }
     $result = ["group1" => $group1, "group2" => $group2, "group3" => $group3, "group4" => $group4];
     echo json_encode($result);
 }
 public static function calculateConceptMasteryScore($studentId, $conceptId, $debug = false)
 {
     // If the concept row was passed in, extract lecture number
     if (is_array($conceptId)) {
         $conceptId = $conceptId["Lecture Number"];
     }
     // Get questions in concept
     $questions = MappingHelper::questionsInConcept($conceptId);
     if ($debug) {
         echo "<pre><h1>Concept ID {$conceptId}: </h1>";
         print_r($questions);
         echo "<hr>";
     }
     // Array to hold information about each question (we don't care about essay questions)
     $conceptShortAnswerQuestions = array();
     $conceptMultipleChoiceQuestions = array();
     // Loop through each question and get basic information for each
     foreach ($questions as $question) {
         switch ($question["Question Type"]) {
             case "essay":
                 // Don't include essay questions in any calculations, so don't add this question to the $conceptQuestions array
                 break;
             case "short_answer":
                 // Get the number of attempts and correct (no show answer in preceding minute) attempts
                 $question["attempts"] = self::countAttemptsForQuestion($studentId, $question["OA Quiz ID"], $question["Question Number"], $debug);
                 $question["correctAttempts"] = self::countCorrectAttemptsForQuestion($studentId, $question["OA Quiz ID"], $question["Question Number"], $debug);
                 $conceptShortAnswerQuestions[] = $question;
                 break;
             case "multiple_choice":
                 // Get the number of attempts and correct (no show answer in preceding minute) attempts
                 $question["attempts"] = self::countAttemptsForQuestion($studentId, $question["OA Quiz ID"], $question["Question Number"], $debug);
                 $question["correctAttempts"] = self::countCorrectAttemptsForQuestion($studentId, $question["OA Quiz ID"], $question["Question Number"], $debug);
                 $conceptMultipleChoiceQuestions[] = $question;
                 break;
         }
     }
     // 1. Short answer questions
     // Total number of short answer questions
     $shortAnswerQuestionCount = count($conceptShortAnswerQuestions);
     // Avoid division by zero if no questions of this type (set scores to 0 for this part, and move on)
     if ($shortAnswerQuestionCount > 0) {
         // 1.a. Calculate initial: correctness factor and attempts penalty
         // Get number of correct short answer questions
         // Use array_filter to get short answer questions with correct attempts > 0
         $shortAnswerCorrectCount = count(array_filter($conceptShortAnswerQuestions, function ($question) {
             return $question["correctAttempts"]["betterCorrect"] > 0;
         }));
         // Get total number of attempts for all short answer questions
         $shortAnswerAttemptCount = array_sum(array_column($conceptShortAnswerQuestions, "attempts"));
         // TODO refactor magic numbers
         // Short answer initial = ( 10 * ( # correct SA in concept / # total SA questions in concept) - (0.2 * (total attempts for all SA questions in concept - number of SA questions in concept) / number of SA questions in concept) )
         $shortAnswerCorrectnessFactor = 10 * ($shortAnswerCorrectCount / $shortAnswerQuestionCount);
         $shortAnswerAttemptPenalty = 0.2 * max($shortAnswerAttemptCount - $shortAnswerQuestionCount, 0) / $shortAnswerQuestionCount;
         $shortAnswerInitialScore = max($shortAnswerCorrectnessFactor - $shortAnswerAttemptPenalty, 0);
         if ($debug) {
             echo "Short answer correctness and attempts penalty: ";
             // TODO avoid division by zero if no questions of this type
             // Use max to make sure we don't go below 0
             echo "(10 * ( {$shortAnswerCorrectCount} / {$shortAnswerQuestionCount}) ) - (0.2 * max({$shortAnswerAttemptCount} - {$shortAnswerQuestionCount}, 0) / {$shortAnswerQuestionCount}) = {$shortAnswerInitialScore} \n";
         }
         // 1.b. Calculate practice bonus
         // For questions with > 1 correct statement without a show answer statement in the preceding minute, then add a practice bonus equal to the attempts penalty.
         // 2.a. Short answer questions
         $shortAnswerPracticeBonus = 0;
         // Get short answer questions that have > 1 correct attempt and add a practice bonus for each of them
         foreach ($conceptShortAnswerQuestions as $question) {
             if ($question["correctAttempts"]["betterCorrect"] > 1) {
                 $shortAnswerPracticeBonus += 0.2 * ($question["attempts"] - 1) / $shortAnswerQuestionCount;
             }
         }
         if ($debug) {
             echo "Short answer practice bonus: {$shortAnswerPracticeBonus} \n";
         }
     } else {
         $shortAnswerInitialScore = 0;
         $shortAnswerPracticeBonus = 0;
     }
     // 2 Multiple choice questions
     // Total number of multiple choice questions
     $multipleChoiceQuestionCount = count($conceptMultipleChoiceQuestions);
     // Avoid division by zero if no questions of this type (set scores to 0 for this part, and move on)
     if ($multipleChoiceQuestionCount > 0) {
         // 2.a. Calculate initial: correctness factor and attempts penalty
         // Get number of correct multiple choice questions
         // Use array_filter to get multiple choice questions with correct attempts > 0
         $multipleChoiceCorrectCount = count(array_filter($conceptMultipleChoiceQuestions, function ($question) {
             return $question["correctAttempts"]["betterCorrect"] > 0;
         }));
         // Get total number of attempts for all multiple choice questions
         $multipleChoiceAttemptCount = array_sum(array_column($conceptMultipleChoiceQuestions, "attempts"));
         // TODO refactor magic numbers
         // Multiple choice initial = 10 * ( # correct MC in concept / # total MC questions in concept) - ( ( sum( (question attempts - 1) * (10 / options in question) ) ) / number of MC questions
         $multipleChoiceCorrectnessFactor = 10 * ($multipleChoiceCorrectCount / $multipleChoiceQuestionCount);
         // Attempts penalty based on number of attempts for a question as well as how many options that question has
         $multipleChoiceAttemptPenalty = 0;
         // Sum of penalties
         foreach ($conceptMultipleChoiceQuestions as $question) {
             $multipleChoiceAttemptPenalty += max($question["attempts"] - 1, 0) * (10 / $question["Multiple Choice Options"]);
         }
         // Now average penalty
         $multipleChoiceAttemptPenalty = $multipleChoiceAttemptPenalty / $multipleChoiceQuestionCount;
         $multipleChoiceInitialScore = max($multipleChoiceCorrectnessFactor - $multipleChoiceAttemptPenalty, 0);
         if ($debug) {
             echo "Multiple Choice correctness and attempts penalty: ";
             echo "(10 * ( {$multipleChoiceCorrectCount} / {$multipleChoiceQuestionCount}) ) - ({$multipleChoiceAttemptPenalty} / {$multipleChoiceQuestionCount}) = {$multipleChoiceInitialScore} \n";
         }
         // 2.b. Calculate practice bonus
         $multipleChoicePracticeBonus = 0;
         // Get number of multiple choice questions that have > 1 correct attempt and add a practice bonus for each of them
         foreach ($conceptMultipleChoiceQuestions as $question) {
             if ($question["correctAttempts"]["betterCorrect"] > 1) {
                 $multipleChoicePracticeBonus += ($question["attempts"] - 1) * (10 / $question["Multiple Choice Options"]) / $multipleChoiceQuestionCount;
             }
         }
         if ($debug) {
             echo "Multiple Choice practice bonus: {$multipleChoicePracticeBonus} \n";
         }
     } else {
         $multipleChoiceInitialScore = 0;
         $multipleChoicePracticeBonus = 0;
     }
     // 3. Calculate total mastery score for each question type: initial + bonus
     $shortAnswerScore = $shortAnswerInitialScore + $shortAnswerPracticeBonus;
     $multipleChoiceScore = $multipleChoiceInitialScore + $multipleChoicePracticeBonus;
     // Weight each question type score by number of questions.
     // Don't use total number of questions, since that will include essay. Instead use MC count + SA coconut
     // Avoid division by 0 (a concept might only have essay questions?)
     if ($shortAnswerQuestionCount + $multipleChoiceQuestionCount > 0) {
         $weightedShortAnswerScore = $shortAnswerScore * ($shortAnswerQuestionCount / ($shortAnswerQuestionCount + $multipleChoiceQuestionCount));
         $weightedMultipleChoiceScore = $multipleChoiceScore * ($multipleChoiceQuestionCount / ($shortAnswerQuestionCount + $multipleChoiceQuestionCount));
     } else {
         $weightedShortAnswerScore = 0;
         $weightedMultipleChoiceScore = 0;
     }
     // Finally!
     $conceptScore = $weightedShortAnswerScore + $weightedMultipleChoiceScore;
     if ($debug) {
         echo "Short answer score: {$shortAnswerScore} = {$shortAnswerInitialScore} + {$shortAnswerPracticeBonus} \n";
         echo "Multiple choice score: {$multipleChoiceScore} = {$multipleChoiceInitialScore} + {$multipleChoicePracticeBonus} \n";
         echo "Weighted SA: {$weightedShortAnswerScore} = {$shortAnswerScore} * ({$shortAnswerQuestionCount} / ({$shortAnswerQuestionCount} + {$multipleChoiceQuestionCount}) ) \n";
         echo "Weighted MC: {$weightedMultipleChoiceScore} = {$multipleChoiceScore} * ({$multipleChoiceQuestionCount} / ({$shortAnswerQuestionCount} + {$multipleChoiceQuestionCount}) ) \n";
         echo "Total concept score: {$conceptScore} \n";
         echo "Questions in concept {$conceptId} <hr><pre>";
         print_r($conceptShortAnswerQuestions);
         print_r($conceptMultipleChoiceQuestions);
     }
     // Round to 2 decimal places
     return round($conceptScore * 100) / 100;
 }