Skip to content

Commit

Permalink
Grade override (Doenet#1955)
Browse files Browse the repository at this point in the history
  • Loading branch information
kevincharles authored Mar 6, 2023
1 parent f84503d commit 1115c12
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 12 deletions.
115 changes: 115 additions & 0 deletions src/Api/saveActivityOverrideGrades.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: access');
header('Access-Control-Allow-Methods: GET');
header('Access-Control-Allow-Credentials: true');
header('Content-Type: application/json');

include 'db_connection.php';
include 'permissionsAndSettingsForOneCourseFunction.php';

date_default_timezone_set('UTC');
// America/Chicago

$jwtArray = include 'jwtArray.php';
$requestorUserId = $jwtArray['userId'];

$success = true;
$message = '';

if (!isset($_GET['userId'])) {
$success = false;
$message = 'Internal Error: missing userId';
} elseif (!isset($_GET['score'])) {
$success = false;
$message = 'Internal Error: missing score';
} elseif (!isset($_GET['doenetId'])) {
$success = false;
$message = 'Internal Error: missing doenetId';
}

if ($success) {
$userId = mysqli_real_escape_string($conn, $_REQUEST['userId']);
$score = mysqli_real_escape_string($conn, $_REQUEST['score']);
$doenetId = mysqli_real_escape_string($conn, $_REQUEST['doenetId']);

//Look up total points for assignment
$result = $conn->query(
"SELECT
totalPointsOrPercent,
courseId
FROM assignment
WHERE doenetId = '$doenetId'"
);
if ($result->num_rows > 0) {
$row = $result->fetch_assoc();
$totalPointsOrPercent = $row['totalPointsOrPercent'];
$courseId = $row['courseId'];
} else {
$success = false;
$message = "No assignment with doenetId: $doenetId";
}
}

//Check permissions
if ($success) {
$requestorPermissions = permissionsAndSettingsForOneCourseFunction(
$conn,
$requestorUserId,
$courseId
);

if ($requestorPermissions == false) {
$success = false;
$message = 'You are not authorized to view or modify grade data';
} elseif ($requestorPermissions['canViewAndModifyGrades'] != '1') {
$success = false;
$message = 'You are only allowed to view your own data';
}
}

if ($success) {
$creditOverride = $score / $totalPointsOrPercent;

// if we don't have a record for this user on the user_assignment table then we need to insert not update
$result = $conn->query(
"SELECT creditOverride
FROM user_assignment
WHERE doenetId = '$doenetId'
AND userId = '$userId'"
);

$need_insert = true;
if ($result->num_rows > 0) {
$need_insert = false;
}

if ($need_insert) {
// insert creditOverride in user_assigment
$sql = "INSERT INTO user_assignment (doenetId,userId,credit,creditOverride)
VALUES
('$doenetId','$userId','$creditOverride','$creditOverride')
";
} else {
// update creditOverride in user_assigment
$sql = "UPDATE user_assignment
SET credit='$creditOverride', creditOverride='$creditOverride'
WHERE userId = '$userId'
AND doenetId = '$doenetId'
";
}
$result = $conn->query($sql);
}

$response_arr = [
'success' => $success,
'message' => $message,
];

// set response code - 200 OK
http_response_code(200);

echo json_encode($response_arr);

$conn->close();
?>
67 changes: 61 additions & 6 deletions src/Tools/_framework/Menus/CreditAchieved.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import React, { useEffect, useState, useRef } from 'react';
import { useRecoilValue, useRecoilCallback } from 'recoil';
import { useRecoilValue, useRecoilCallback, useSetRecoilState, useRecoilState } from 'recoil';
import { searchParamAtomFamily } from '../NewToolRoot';
import axios from 'axios';
import { creditAchievedAtom, currentAttemptNumber } from '../ToolPanels/AssignmentViewer';
import styled from "styled-components";
import { itemByDoenetId } from '../../../_reactComponents/Course/CourseActions';
import { activityAttemptNumberSetUpAtom, currentPageAtom, itemWeightsAtom } from '../../../Viewer/ActivityViewer';
import { useLocation, useNavigate } from 'react-router';
import { effectivePermissionsByCourseId } from '../../../_reactComponents/PanelHeaderComponents/RoleDropdown';
import Button from '../../../_reactComponents/PanelHeaderComponents/Button';
import ButtonGroup from '../../../_reactComponents/PanelHeaderComponents/ButtonGroup';
import Textfield from '../../../_reactComponents/PanelHeaderComponents/Textfield';
import { toastType, useToast } from '../Toast';
import { overviewData } from '../ToolPanels/Gradebook';

const Line = styled.div`
border-bottom: 2px solid var(--canvastext);
Expand All @@ -23,9 +29,58 @@ const ScoreOnRight = styled.div`
const ScoreContainer = styled.div`
position: relative;
background: ${props => props.highlight ? "var(--mainGray)" : "var(--canvas)"};
cursor: ${props=> props.isLink ? "pointer" : "auto" };
cursor: ${props => props.isLink ? "pointer" : "auto"};
`

function FinalScore({ score }) {
const addToast = useToast();
let courseId = useRecoilValue(searchParamAtomFamily('courseId'));
let doenetId = useRecoilValue(searchParamAtomFamily('doenetId'));
let userId = useRecoilValue(searchParamAtomFamily('userId'));
let { canViewAndModifyGrades } = useRecoilValue(effectivePermissionsByCourseId(courseId));
const [creditAchieved, setCreditAchieved] = useRecoilState(creditAchievedAtom);


const [editMode, setEditMode] = useState(false);
const [scoreState, setScore] = useState(score);
const setOverview = useSetRecoilState(overviewData);

if (editMode) {
return <div style={{ display: "flex", flexDirection: "column", rowGap: "4px" }}>
<div>Final Score:</div>
{/* <ScoreContainer>Original Final Score: <ScoreOnRight data-test="Original Final Score">{score}</ScoreOnRight></ScoreContainer> */}
<Textfield width="menu" value={scoreState} onChange={(e) => { setScore(e.target.value) }} />
<Button value="Update" onClick={async () => {
if (isNaN(scoreState) || scoreState == "") {
addToast("Final Score needs to be a number.", toastType.ERROR);
} else {
const { data } = await axios.get(`/api/saveActivityOverrideGrades.php`, { params: { score: scoreState, doenetId, userId } });

let creditOverride = Number(scoreState) / creditAchieved?.totalPointsOrPercent;

if (data.success) {

setEditMode(false);
setCreditAchieved((prev) => {
let next = { ...prev }
next.creditForAssignment = creditOverride;
return next;
})
setOverview({ doenetId, userId, credit: `${creditOverride}` })
}
}

}} />
</div>
}

if (canViewAndModifyGrades == 1) {
return <ScoreContainer><ButtonGroup>Final Score <Button value="Edit" onClick={() => setEditMode(true)} />: <ScoreOnRight data-test="Final Score">{score}</ScoreOnRight></ButtonGroup> </ScoreContainer>

}
return <ScoreContainer>Final Score: <ScoreOnRight data-test="Final Score">{score}</ScoreOnRight></ScoreContainer>

}

export default function CreditAchieved() {
const recoilAttemptNumber = useRecoilValue(currentAttemptNumber);
Expand All @@ -37,6 +92,7 @@ export default function CreditAchieved() {
const currentPage = useRecoilValue(currentPageAtom);
const activityAttemptNumberSetUp = useRecoilValue(activityAttemptNumberSetUpAtom);


let { search } = useLocation();
let navigate = useNavigate();

Expand All @@ -45,16 +101,15 @@ export default function CreditAchieved() {

const { creditByItem, creditForAttempt, creditForAssignment, totalPointsOrPercent } = useRecoilValue(creditAchievedAtom);


const initialize = useRecoilCallback(({ set }) => async (attemptNumber, doenetId, userId, tool) => {


const { data } = await axios.get(`/api/loadAssessmentCreditAchieved.php`, { params: { attemptNumber, doenetId, userId, tool } });

if (data.success) {
const creditByItem = data.creditByItem.map(Number);
const creditForAssignment = Number(data.creditForAssignment)
const creditForAttempt = Number(data.creditForAttempt)
const creditOverride = Number(data.creditOverride_for_assignment);
const showCorrectness = data.showCorrectness === "1";
const totalPointsOrPercent = Number(data.totalPointsOrPercent)

Expand Down Expand Up @@ -109,12 +164,12 @@ export default function CreditAchieved() {
} else {
scoreDisplay = (x ? Math.round(x * 1000) / 10 : 0) + "%";
}
return <ScoreContainer key={`creditByItem${i}`} highlight={currentPage === i + 1} onClick={() => navigate(search+`#page${i+1}`)} isLink={true}>Item {i + 1}: <ScoreOnRight data-test={`Item ${i + 1} Credit`}>{scoreDisplay}</ScoreOnRight></ScoreContainer>
return <ScoreContainer key={`creditByItem${i}`} highlight={currentPage === i + 1} onClick={() => navigate(search + `#page${i + 1}`)} isLink={true}>Item {i + 1}: <ScoreOnRight data-test={`Item ${i + 1} Credit`}>{scoreDisplay}</ScoreOnRight></ScoreContainer>
})

return <div>
<ScoreContainer>Possible Points: <ScoreOnRight data-test="Possible Points">{totalPointsOrPercent}</ScoreOnRight></ScoreContainer>
<ScoreContainer>Final Score: <ScoreOnRight data-test="Final Score">{score}</ScoreOnRight></ScoreContainer>
<FinalScore score={score} />
<Line />
<b>Credit For:</b>
<ScoreContainer data-test="Attempt Container">Attempt {recoilAttemptNumber}: <ScoreOnRight data-test="Attempt Percent">{creditForAttempt ? Math.round(creditForAttempt * 1000) / 10 : 0}%</ScoreOnRight></ScoreContainer>
Expand Down
17 changes: 17 additions & 0 deletions src/Tools/_framework/ToolPanels/Gradebook.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,23 @@ export const overviewData = selector({

return overview;
},
set: ({ set }, { doenetId, userId, credit }) => {
set(overviewDataQuery, (prev) => {
let next = [];
for (let userActivityArr of prev) {
if (userActivityArr[0] == doenetId &&
userActivityArr[2] == userId
) {
let newArr = [...userActivityArr];
newArr[1] = credit;
next.push(newArr)
} else {
next.push(userActivityArr);
}
}
return next;
})
}
});

export const attemptDataQuery = atomFamily({
Expand Down
4 changes: 3 additions & 1 deletion src/Tools/_framework/ToolPanels/GradebookAssignment.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
attemptDataQuery,
studentDataQuery,
overviewDataQuery,
overviewData,
} from './Gradebook';

import {
Expand Down Expand Up @@ -303,6 +304,7 @@ export default function GradebookAssignmentView() {
let doenetId = useRecoilValue(searchParamAtomFamily('doenetId'));
let courseId = useRecoilValue(searchParamAtomFamily('courseId'));
let attempts = useRecoilValueLoadable(attemptData(doenetId));
let overview = useRecoilValueLoadable(overviewData);
let students = useRecoilValueLoadable(studentData);
let process = useRecoilValue(processGradesAtom);
const setSuppressMenus = useSetRecoilState(suppressMenusAtom);
Expand Down Expand Up @@ -442,7 +444,7 @@ export default function GradebookAssignmentView() {
// </Link>
}

let totalCredit = attempts.contents[userId]?.credit;
let totalCredit = overview?.contents?.[userId]?.assignments?.[doenetId];
let totalPointsEarned =
Math.round(totalCredit * totalPossiblePoints * 100) / 100;
row['grade'] = totalCredit ? totalPointsEarned : '0';
Expand Down
15 changes: 10 additions & 5 deletions src/Tools/_framework/ToolPanels/GradebookStudentAssignment.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { Styles, Table, studentData, attemptData, assignmentData } from "./Gradebook"
import { Styles, Table, studentData, attemptData, assignmentData, overviewData } from "./Gradebook"

import {
useSetRecoilState,
Expand Down Expand Up @@ -42,7 +42,7 @@ export default function GradebookStudentAssignmentView(){
let assignments = useRecoilValueLoadable(assignmentData);
let { canViewAndModifyGrades} = useRecoilValue(effectivePermissionsByCourseId(courseId));
const setSuppressMenus = useSetRecoilState(suppressMenusAtom);

let overview = useRecoilValueLoadable(overviewData);


const totalPointsOrPercent = Number(assignments.contents[doenetId]?.totalPointsOrPercent)
Expand Down Expand Up @@ -205,11 +205,16 @@ export default function GradebookStudentAssignmentView(){

creditRow[("a"+i)] = attemptCredit ? Math.round(attemptCredit * 1000)/10 + '%' : ""
scoreRow[("a"+i)] = attemptCredit ? Math.round(attemptCredit * 100 * totalPointsOrPercent)/100 : ""

}
let credit = overview.contents?.[userId]?.assignments?.[doenetId];

let score = totalPointsOrPercent * credit;
let percentage = Math.round(credit * 1000) / 10 + '%';

creditRow["total"] = attempts.contents[userId].credit ? Math.round(attempts.contents[userId].credit * 1000)/10 + '%' : ""
scoreRow["total"] = attempts.contents[userId].credit ? Math.round(attempts.contents[userId].credit * totalPointsOrPercent * 100)/100 : "0"
scoreRow["total"] = score
creditRow["total"] = percentage
// creditRow["total"] = attempts.contents[userId].credit ? Math.round(attempts.contents[userId].credit * 1000)/10 + '%' : ""
// scoreRow["total"] = attempts.contents[userId].credit ? Math.round(attempts.contents[userId].credit * totalPointsOrPercent * 100)/100 : "0"
}


Expand Down

0 comments on commit 1115c12

Please sign in to comment.