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 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); }