Skip to content

Commit

Permalink
Merge pull request #19 from DakshChan/feature/DEV-012-aggregatedGrades
Browse files Browse the repository at this point in the history
[IBS-12] Add 3 extra features to Aggregated Grades Table
  • Loading branch information
Kianoosh76 committed Jun 28, 2023
2 parents f3107a8 + 433f83a commit 04843ec
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 59 deletions.
28 changes: 28 additions & 0 deletions backend/module/course/instructor/get.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const express = require("express");
const router = express.Router();
const client = require("../../../setup/db");
const helpers = require("../../../utilities/helpers");

router.get("/", (req, res) => {
console.log(`[+] RESPONSE Locals: ${res.locals["course_id"]}`);
if (res.locals["course_id"] === "") {
res.status(400).json({ message: "The course id is missing or invalid." });
}
let sqlCourse = "SELECT * FROM course WHERE course_id = ($1)";
client.query(sqlCourse, [res.locals["course_id"]], (err, pgRes) => {
if (err) {
res.status(404).json({ message: "Unknown error." });
} else if (pgRes.rowCount === 1) {
res
.status(200)
.json({
message: "Course details are returned.",
course: pgRes.rows[0],
});
} else {
res.status(400).json({ message: "The course id is invalid." });
}
});
});

module.exports = router;
4 changes: 4 additions & 0 deletions backend/route/instructor.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const manual_collect_submission = require("../module/submission/staff/collect_ma
const download_submissions = require("../module/submission/staff/download");
const check_submission = require("../module/submission/staff/check");
const impersonate = require("../module/impersonate/instructor/impersonate");
const get_course = require("../module/course/instructor/get");

router.use("/", function (req, res, next) {
next();
Expand All @@ -53,6 +54,9 @@ router.use("/", function (req, res, next) {
// Middleware
router.use("/course/", middleware);

// Course Content
router.use("/course/:course_id/get", get_course);

// Role
router.use("/course/:course_id/role/get", get_role);

Expand Down
36 changes: 34 additions & 2 deletions frontend/src/api/staff_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,37 @@ let getAllMarks = async (courseId) => {
}
};

/**
* Get current course's details for Admins and Instructors' use only.
* @param courseId string
* @returns {Promise<axios.AxiosResponse<any>|*|null>}
*/
const getCourseContent = async (courseId) => {
let token = sessionStorage.getItem('token');

const role = findRoleInCourse(courseId);

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

let url = '';
if (role === 'admin') {
url = `${process.env.REACT_APP_API_URL}/admin/course/get?course_id=${courseId}`;
} else if (role === 'instructor') {
url = `${process.env.REACT_APP_API_URL}/instructor/course/${courseId}/get`;
} else {
// insufficient access
return null;
}

try {
return await axios.get(url, config);
} catch (err) {
return err.response;
}
};

/**
* GET All task group (Instructors + Admins)
* @param courseId string
Expand Down Expand Up @@ -275,7 +306,7 @@ let deleteTaskGroup = async (courseId, taskGroupId) => {
return err.response;
}
};

const StaffApi = {
get_students_in_course,
getAllMarks,
Expand All @@ -286,7 +317,8 @@ const StaffApi = {
deleteTaskGroup,

getCriteriaForTask,
all_tasks
all_tasks,
getCourseContent
};

export default StaffApi;
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import FeatherIcon from 'feather-icons-react';
import CustomCheckbox from '../../FlexyMainComponents/forms/custom-elements/CustomCheckbox';
import CustomSwitch from '../../FlexyMainComponents/forms/custom-elements/CustomSwitch';
import GetMarksCsvButton from './GetMarksCsvButton';
import TableSearchbar from './TableSearchbar';

function descendingComparator(a, b, orderBy) {
if (b[orderBy] < a[orderBy]) {
Expand Down Expand Up @@ -78,20 +79,28 @@ function AggregatedGradesTableHead(props) {
padding={headCell.disablePadding ? 'none' : 'normal'}
sortDirection={orderBy === headCell.id ? order : false}
>
<TableSortLabel
active={orderBy === headCell.id}
direction={orderBy === headCell.id ? order : 'asc'}
onClick={createSortHandler(headCell.id)}
>
{headCell.id !== 'finalGrade' ? (
<TableSortLabel
active={orderBy === headCell.id}
direction={orderBy === headCell.id ? order : 'asc'}
onClick={createSortHandler(headCell.id)}
>
<Typography variant="subtitle1" fontWeight="500">
{headCell.label}
</Typography>
{orderBy === headCell.id ? (
<Box component="span" sx={visuallyHidden}>
{order === 'desc'
? 'sorted descending'
: 'sorted ascending'}
</Box>
) : null}
</TableSortLabel>
) : (
<Typography variant="subtitle1" fontWeight="500">
{headCell.label}
</Typography>
{orderBy === headCell.id ? (
<Box component="span" sx={visuallyHidden}>
{order === 'desc' ? 'sorted descending' : 'sorted ascending'}
</Box>
) : null}
</TableSortLabel>
)}
</TableCell>
))}
</TableRow>
Expand All @@ -110,53 +119,32 @@ AggregatedGradesTableHead.propTypes = {
};

const AggregatedGradesTableToolbar = (props) => {
const { numSelected } = props;
const { originalRows, setCurrRows } = props;

return (
<Toolbar
sx={{
pl: { sm: 2 },
pr: { xs: 1, sm: 1 },
...(numSelected > 0 && {
bgcolor: (theme) =>
alpha(theme.palette.primary.main, theme.palette.action.activatedOpacity)
})
pr: { xs: 1, sm: 1 }
}}
>
{numSelected > 0 ? (
<Typography
sx={{ flex: '1 1 100%' }}
color="inherit"
variant="subtitle2"
component="div"
>
{numSelected} selected
</Typography>
) : (
<Typography sx={{ flex: '1 1 100%' }} variant="h6" id="tableTitle" component="div">
Filter
</Typography>
)}

{numSelected > 0 ? (
<Tooltip title="Delete">
<IconButton>
<FeatherIcon icon="trash-2" width="18" />
</IconButton>
</Tooltip>
) : (
<Tooltip title="Filter list">
<IconButton>
<FeatherIcon icon="filter" width="18" />
</IconButton>
</Tooltip>
{originalRows.length > 0 && (
<TableSearchbar
originalRows={originalRows}
setCurrRows={setCurrRows}
placeholder="Search for student"
width="20vw"
/>
)}
</Toolbar>
);
};

AggregatedGradesTableToolbar.propTypes = {
numSelected: PropTypes.number.isRequired
// Original rows that are fetched from backend API call
originalRows: PropTypes.arrayOf(PropTypes.object).isRequired,
// To set the current state of rows
setCurrRows: PropTypes.func.isRequired
};

const AggregatedGradesTable = ({ headCells, rows, tableWidth, courseId }) => {
Expand All @@ -166,6 +154,12 @@ const AggregatedGradesTable = ({ headCells, rows, tableWidth, courseId }) => {
const [page, setPage] = React.useState(0);
const [dense, setDense] = React.useState(false);
const [rowsPerPage, setRowsPerPage] = React.useState(5);
const [currRows, setCurrRows] = React.useState(rows === undefined ? [] : rows);

React.useEffect(() => {
setCurrRows(rows);
console.log(rows);
}, [rows]);

const handleRequestSort = (event, property) => {
const isAsc = orderBy === property && order === 'asc';
Expand All @@ -175,7 +169,7 @@ const AggregatedGradesTable = ({ headCells, rows, tableWidth, courseId }) => {

const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelecteds = rows.map((n) => n.student);
const newSelecteds = currRows.map((n) => n.student);
setSelected(newSelecteds);
return;
}
Expand Down Expand Up @@ -218,15 +212,18 @@ const AggregatedGradesTable = ({ headCells, rows, tableWidth, courseId }) => {
const isSelected = (name) => selected.indexOf(name) !== -1;

// Avoid a layout jump when reaching the last page with empty rows.
const emptyRows = page > 0 ? Math.max(0, (1 + page) * rowsPerPage - rows.length) : 0;
const emptyRows = page > 0 ? Math.max(0, (1 + page) * rowsPerPage - currRows.length) : 0;

return (
<Card sx={{ width: tableWidth }}>
<CardContent>
<Box>
<Paper sx={{ width: '100%', mb: 2, mt: 1 }}>
{/*<AggregatedGradesTableToolbar numSelected={selected.length} />*/}
<GetMarksCsvButton courseId={courseId} />
<AggregatedGradesTableToolbar
originalRows={rows}
setCurrRows={setCurrRows}
/>
<TableContainer>
<Table
sx={{ minWidth: 750 }}
Expand All @@ -239,11 +236,11 @@ const AggregatedGradesTable = ({ headCells, rows, tableWidth, courseId }) => {
orderBy={orderBy}
onSelectAllClick={handleSelectAllClick}
onRequestSort={handleRequestSort}
rowCount={rows.length}
rowCount={currRows.length}
headCells={headCells}
/>
<TableBody>
{stableSort(rows, getComparator(order, orderBy))
{stableSort(currRows, getComparator(order, orderBy))
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
.map((row, index) => {
const isItemSelected = isSelected(row.student);
Expand Down Expand Up @@ -317,7 +314,7 @@ const AggregatedGradesTable = ({ headCells, rows, tableWidth, courseId }) => {
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
component="div"
count={rows.length}
count={currRows.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
Expand Down Expand Up @@ -352,7 +349,7 @@ AggregatedGradesTable.propTypes = {
// Must be in form of { id: string, student: string, <taskName>: string }
rows: PropTypes.array.isRequired,
// Adjust width of table
tableWidth: PropTypes.number.isRequired
tableWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired
};

export default AggregatedGradesTable;
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const getMarksCsvButton = ({ courseId }) => {
};

return (
<Button onClick={handleClick} startIcon={<GetAppIcon />}>
<Button sx={{ mt: 2, ml: 2 }} onClick={handleClick} startIcon={<GetAppIcon />}>
Download as CSV
</Button>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Autocomplete, IconButton, InputAdornment, Stack, TextField } from '@mui/material';

const TableSearchbar = (props) => {
const { originalRows, setCurrRows, placeholder, width } = props;

return (
<>
<Autocomplete
freeSolo
id="table-searchbar"
options={originalRows.map((option) => option.student)}
onChange={(event, newValue, reason) => {
setCurrRows((prevState) => {
// We use original rows because prevState could've been changed from previous
// autocomplete
const newState = [];
if (Array.isArray(originalRows)) {
for (const studentObj of originalRows) {
if (studentObj.student === newValue) {
newState.push(studentObj);
}
}
}
console.log(newState);
return newState;
});
if (reason === 'clear') {
setCurrRows(originalRows);
}
}}
renderInput={(params) => (
<TextField
{...params}
size="small"
placeholder={placeholder}
aria-label="Search Input"
/>
)}
sx={{
width: width
}}
/>
</>
);
};

TableSearchbar.propTypes = {
// Original rows which is fetched from backend (displays all possible rows)
originalRows: PropTypes.arrayOf(PropTypes.object).isRequired,
// setCurrRows useState callback function to set rows to be displayed on table
setCurrRows: PropTypes.func.isRequired,
// Placeholder text for the searchbar
placeholder: PropTypes.string.isRequired,
// Width of searchbar. Either string like '100%' or a numeric integer value.
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired
};

export default TableSearchbar;
Loading

0 comments on commit 04843ec

Please sign in to comment.