Skip to content

Commit

Permalink
Merge pull request #32 from DakshChan/feature/IBS-49-SubmitAssignment…
Browse files Browse the repository at this point in the history
…MarksTable

[IBS-46] submit assignment marks table
  • Loading branch information
DomiVesalius authored Jul 23, 2023
2 parents f4255b6 + 0035a9a commit 2b13260
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 10 deletions.
5 changes: 5 additions & 0 deletions frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import StudentListPage from './components/Page/Student/StudentListPage';
import TaskGroupListPage from './components/General/TaskGroupList/TaskGroupListPage';
import SubmitMarks from './components/Page/Instructor/SubmitMarks';
import TaskGroupPage from './components/Page/Instructor/TaskGroupPage';
import InstructorTaskMarksPage from './components/Page/Instructor/InstructorTaskMarksPage';

function App() {
const theme = ThemeSettings();
Expand Down Expand Up @@ -90,6 +91,10 @@ function App() {
path="/instructor/course/:courseId/all-grades"
element={<AggregatedGrades role="instructor" />}
/>
<Route
path="/instructor/course/:course_id/task/:task_id/mark"
element={<InstructorTaskMarksPage />}
/>
<Route
path="/instructor/course/:course_id/impersonate"
element={<InstructorImpersonate />}
Expand Down
13 changes: 3 additions & 10 deletions frontend/src/api/instructor_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,8 @@ let submitMark = async (courseId, task, criteria, username, mark) => {
overwrite: true
};

let config = {
headers: { Authorization: `Bearer ${token}` }
};

try {
return await axios.post(
process.env.REACT_APP_API_URL + '/instructor/course/' + courseId + '/mark/submit',
data,
config
);
return await http.post(`/instructor/course/${courseId}/mark/submit`, data);
} catch (err) {
return err.response;
}
Expand Down Expand Up @@ -198,7 +190,8 @@ let InstructorApi = {
// delete_interview,

// Mark related
get_marks_csv
get_marks_csv,
submitMark
};

export default InstructorApi;
17 changes: 17 additions & 0 deletions frontend/src/api/staff_api.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios from 'axios';
import http from './client';
import { INSTRUCTOR, TA } from '../Constants/roles';

// Accessible to any of TA/Instructor/Admin

Expand Down Expand Up @@ -62,6 +63,21 @@ let all_tasks = async (course_id) => {
}
};

let getMarksForTask = async (courseId, taskId) => {
const role = findRoleInCourse(courseId);
if (role === INSTRUCTOR || role === TA) {
try {
const res = await http.get(`${role}/course/${courseId}/mark/all?task=${taskId}`);
const { marks } = res.data;
return marks;
} catch (err) {
return err.response;
}
} else {
return null;
}
};

/**
*
* @param courseId
Expand Down Expand Up @@ -345,6 +361,7 @@ let deleteTaskGroup = async (courseId, taskGroupId) => {
const StaffApi = {
get_students_in_course,
getAllMarks,
getMarksForTask,

getAllTaskGroups,
addTaskGroup,
Expand Down
179 changes: 179 additions & 0 deletions frontend/src/components/Module/Mark/TaskMarkTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import StaffApi from '../../../api/staff_api';
import useSWR from 'swr';
import { toast } from 'react-toastify';
import {
TableContainer,
Typography,
Paper,
Table,
TableHead,
TableCell,
TableRow,
TableBody,
TextField,
Button
} from '@mui/material';
import { useEffect, useState } from 'react';
import DashboardCard from '../../FlexyMainComponents/base-card/DashboardCard';
import SaveIcon from '@mui/icons-material/Save';
import InstructorApi from '../../../api/instructor_api';

function extractCriteriaNames(data) {
const criteria = [];

for (const studentData of Object.values(data)) {
for (const key of Object.keys(studentData)) {
if (criteria.includes(key)) continue;
criteria.push({ criteriaName: key, outOf: studentData[key].out_of });
}
}

return criteria;
}

function extractMarkData(data) {
return Object.keys(data).map((studentName) => {
return { name: studentName, ...data[studentName] };
});
}

const TaskMarkTable = ({ courseId, taskId }) => {
const [criteriaNames, setCriteriaNames] = useState([]);
const [taskMarkData, setTaskMarkData] = useState([]);

const markFetcher = (key) => StaffApi.getMarksForTask(courseId, taskId);
const { data, isLoading, error, mutate } = useSWR('/getMarksForTask', markFetcher, {
refreshInterval: 60000
});

useEffect(() => {
if (isLoading || error) return;

const criteriaNamesTemp = extractCriteriaNames(data);
setCriteriaNames(criteriaNamesTemp);

const markDataTemp = extractMarkData(data);
setTaskMarkData(markDataTemp);
}, [isLoading, error, data]);

if (isLoading) return <Typography variant="h1">Loading...</Typography>;
if (error) return <Typography variant="h1">Unknown error...Try again</Typography>;

const TaskMarkEntry = ({ row }) => {
const name = row.name;

const handleChange = (event, criteriaId) => {
const tempTaskMarkData = taskMarkData.map((studentTaskData) => {
if (studentTaskData.name !== name) return studentTaskData;

const newStudentTaskData = {
...studentTaskData
};

newStudentTaskData[criteriaId] = {
mark: event.target.valueAsNumber,
out_of: newStudentTaskData[criteriaId].out_of
};

return newStudentTaskData;
});
setTaskMarkData(tempTaskMarkData);
};

const handleSave = () => {
let studentTaskData = taskMarkData.filter((data) => data.name === name);
if (studentTaskData.length === 0) return;

studentTaskData = studentTaskData[0];
Object.keys(studentTaskData).map((key) => {
if (key === 'name') return;

// in this case key is a criteria id
InstructorApi.submitMark(
courseId,
taskId,
key,
name,
studentTaskData[key].mark
).catch((err) => {
toast.error(`Something went wrong. Refresh and Try again`, {
theme: 'colored'
});
});
});

toast.success('Marks updated...', {
theme: 'colored'
});

mutate();
};

return (
<>
{Object.keys(row).map((col) => {
if (col === 'name')
return (
<TableCell align="center" key={name}>
<Typography>{row.name}</Typography>
</TableCell>
);

return (
<TableCell
align="center"
key={`${name}-${col}-${row[col].mark}}-${row[col].out_of}`}
>
<TextField
id="outlined-number"
type="number"
size="small"
max={row[col].out_of}
min={0}
onChange={(event) => handleChange(event, col)}
defaultValue={row[col].mark}
/>
</TableCell>
);
})}
<TableCell>
<Button variant="contained" onClick={handleSave} startIcon={<SaveIcon />}>
Save
</Button>
</TableCell>
</>
);
};

return (
<DashboardCard title={`Marks for ${taskId}`}>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }}>
<TableHead>
<TableRow>
<TableCell align="center">
<Typography>Student</Typography>
</TableCell>
{criteriaNames.map((crit) => (
<TableCell align="center" key={`${crit.criteriaName}`}>
<Typography>
{crit.criteriaName} (/{crit.outOf})
</Typography>
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
<TableRow>
{taskMarkData.map((entry) => (
<TaskMarkEntry key={`${entry.name}`} row={entry} />
))}
</TableRow>
</TableBody>
</Table>
</TableContainer>
</DashboardCard>
);
};

export default TaskMarkTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Container, Grid } from '@mui/material';
import NavBar from '../../Module/Navigation/NavBar';
import { useParams } from 'react-router-dom';
import { useEffect } from 'react';
import TaskMarkTable from '../../Module/Mark/TaskMarkTable';

const InstructorTaskMarksPage = () => {
const { course_id, task_id } = useParams();
return (
<Grid container spacing={2}>
<Grid xs={12}>
<NavBar item page="Task Marks" role="instructor" />
</Grid>
<Grid item container spacing={2} xs={12}>
<Container>
<TaskMarkTable courseId={course_id} taskId={task_id} />
</Container>
</Grid>
</Grid>
);
};

export default InstructorTaskMarksPage;

0 comments on commit 2b13260

Please sign in to comment.