diff --git a/examples/dev_environment/install.sh b/examples/dev_environment/install.sh index a378b236..1949c482 100644 --- a/examples/dev_environment/install.sh +++ b/examples/dev_environment/install.sh @@ -9,15 +9,12 @@ python -m pip install --upgrade pip pip install jupyterhub jupyterlab echo "Installing grader_convert..." -pip install -r ../../grader_convert/requirements.txt -pip install --no-use-pep517 ../../grader_convert +pip install ../../grader_convert echo "Installing grader_service..." -pip install -r ../../grader_service/requirements.txt -pip install --no-use-pep517 ../../grader_service +pip install ../../grader_service echo "Installing grader_labextension..." -#pip install -r ../../grader_labextension/requirements.txt pip install ../../grader_labextension # no development install for grader_labextension jupyter server extension enable grader_labextension diff --git a/examples/dev_environment/jupyter_hub_config.py b/examples/dev_environment/jupyter_hub_config.py index 10d7346e..0414ad89 100644 --- a/examples/dev_environment/jupyter_hub_config.py +++ b/examples/dev_environment/jupyter_hub_config.py @@ -15,7 +15,7 @@ c.JupyterHub.authenticator_class = 'jupyterhub.auth.DummyAuthenticator' c.DummyAuthenticator.password = "admin" c.Authenticator.admin_users = {'user1'} -c.Authenticator.allowed_users = {"user1", "user2", "user3"} +c.Authenticator.allowed_users = {"user1", "user2", "user3", "user4", "user5", "user6", "user7"} ## simple setup c.JupyterHub.ip = '127.0.0.1' @@ -31,7 +31,7 @@ c.JupyterHub.load_groups = { "23wsle2:instructor": {'users': ["user1", "user2"]}, - "23wsle2:student": {'users': ["user3"]}, + "23wsle2:student": {'users': ["user3", "user4", "user5", "user6", "user7"]}, "23wsle1:instructor": {'users': ["user1", "user2"]}, "23wsle1:student": {'users': ["user3"]}, } diff --git a/grader_labextension/grader_labextension/handlers/assignment.py b/grader_labextension/grader_labextension/handlers/assignment.py index 90914ace..56a14c38 100644 --- a/grader_labextension/grader_labextension/handlers/assignment.py +++ b/grader_labextension/grader_labextension/handlers/assignment.py @@ -185,16 +185,6 @@ async def delete(self, lecture_id: int, assignment_id: int): """ try: - assignment = await self.request_service.request( - method="GET", - endpoint=f"{self.service_base_url}/lectures/{lecture_id}/assignments/{assignment_id}", - header=self.grader_authentication_header, - ) - lecture = await self.request_service.request( - "GET", - f"{self.service_base_url}/lectures/{lecture_id}", - header=self.grader_authentication_header, - ) await self.request_service.request( method="DELETE", endpoint=f"{self.service_base_url}/lectures/{lecture_id}/assignments/{assignment_id}", @@ -202,11 +192,8 @@ async def delete(self, lecture_id: int, assignment_id: int): decode_response=False ) except HTTPClientError as e: - self.log.error(e.response) raise HTTPError(e.code, reason=e.response.reason) - - self.log.warn(f'Deleting directory {self.root_dir}/{lecture["code"]}/assignments/{assignment["id"]}') - shutil.rmtree(os.path.expanduser(f'{self.root_dir}/{lecture["code"]}/assignments/{assignment["id"]}'), ignore_errors=True) + self.write("OK") diff --git a/grader_labextension/grader_labextension/handlers/base_handler.py b/grader_labextension/grader_labextension/handlers/base_handler.py index cfca7ece..8bf53f75 100644 --- a/grader_labextension/grader_labextension/handlers/base_handler.py +++ b/grader_labextension/grader_labextension/handlers/base_handler.py @@ -4,6 +4,7 @@ # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. import functools +from http.client import responses import json import traceback from typing import Optional, Awaitable @@ -86,7 +87,7 @@ def grader_authentication_header(self): @property def user_name(self): - return self.current_user.name + return self.current_user['name'] async def get_lecture(self, lecture_id) -> dict: try: @@ -111,24 +112,23 @@ async def get_assignment(self, lecture_id, assignment_id): except HTTPClientError as e: self.log.error(e.response) raise HTTPError(e.code, reason=e.response.reason) - - def write_error(self, status_code: int, **kwargs) -> None: - self.set_header('Content-Type', 'application/json') - self.set_status(status_code) - _, e, _ = kwargs.get("exc_info", (None, None, None)) - error = httputil.responses.get(status_code, "Unknown") - reason = kwargs.get("reason", None) - if e and isinstance(e, HTTPError) and e.reason: - reason = e.reason - if self.settings.get("serve_traceback") and "exc_info" in kwargs: - # in debug mode, try to send a traceback - lines = [] - for line in traceback.format_exception(*kwargs["exc_info"]): - lines.append(line) - self.finish(json.dumps( - ErrorMessage(status_code, error, self.request.path, reason, - traceback=json.dumps(lines)).to_dict())) - else: - self.finish(json.dumps(ErrorMessage(status_code, error, - self.request.path, - reason).to_dict())) + + def write_error(self, status_code, **kwargs): + """APIHandler errors are JSON, not human pages""" + self.set_header("Content-Type", "application/json") + message = responses.get(status_code, "Unknown HTTP Error") + reply: dict = { + "message": message, + } + exc_info = kwargs.get("exc_info") + if exc_info: + e = exc_info[1] + if isinstance(e, HTTPError): + reply["message"] = e.log_message or message + reply["reason"] = e.reason + else: + reply["message"] = "Unhandled error" + reply["reason"] = None + reply["traceback"] = "".join(traceback.format_exception(*exc_info)) + self.log.warning("wrote error: %r", reply["message"], exc_info=True) + self.finish(json.dumps(reply)) \ No newline at end of file diff --git a/grader_labextension/src/components/assignment/assignment.tsx b/grader_labextension/src/components/assignment/assignment.tsx index 9c4ad895..c1e4f996 100644 --- a/grader_labextension/src/components/assignment/assignment.tsx +++ b/grader_labextension/src/components/assignment/assignment.tsx @@ -8,8 +8,8 @@ import * as React from 'react'; import { Lecture } from '../../model/lecture'; import { Assignment } from '../../model/assignment'; import { Submission } from '../../model/submission'; -import { Box, Button, Chip, Stack, Tooltip, Typography } from '@mui/material'; - +import { Box, Button, Chip, IconButton, Stack, Tooltip, Typography } from '@mui/material'; +import ReplayIcon from '@mui/icons-material/Replay'; import { SubmissionList } from './submission-list'; import { AssignmentStatus } from './assignment-status'; import { Files } from './files/files'; @@ -51,6 +51,21 @@ const calculateActiveStep = (submissions: Submission[]) => { return 0; }; +interface ISubmissionsLeft{ + subLeft: number; +} +const SubmissionsLeftChip = (props: ISubmissionsLeft) =>{ + const output = props.subLeft + ' submission' + (props.subLeft === 1 ? ' left' : 's left'); + return( + } + label={output} + /> + ) +} + /** * Renders the components available in the extended assignment modal view */ @@ -79,14 +94,17 @@ export const AssignmentComponent = () => { /* Now we can divvy this into a useReducer */ const [allSubmissions, setSubmissions] = React.useState(submissions); - const [files, setFiles] = React.useState([]); const [activeStatus, setActiveStatus] = React.useState(0); + const [subLeft, setSubLeft] = React.useState(0); + React.useEffect(() => { getAllSubmissions(lecture.id, assignment.id, 'none', false).then( response => { setSubmissions(response); + if(assignment.max_submissions - response.length < 0) setSubLeft(0); + else setSubLeft(assignment.max_submissions - response.length); } ); getFiles(path).then(files => { @@ -149,6 +167,8 @@ export const AssignmentComponent = () => { response => { console.log('Submitted'); setSubmissions([response, ...allSubmissions]); + if(subLeft - 1 < 0) setSubLeft(0); + else setSubLeft(subLeft - 1); enqueueSnackbar('Successfully Submitted Assignment', { variant: 'success' }); @@ -242,7 +262,11 @@ export const AssignmentComponent = () => { const scope = permissions[lecture.code]; return scope >= Scope.tutor; }; + const [reloadFilesToggle, setReloadFiles] = React.useState(false); + const reloadFiles = () => { + setReloadFiles(!reloadFilesToggle); + }; return ( @@ -260,9 +284,16 @@ export const AssignmentComponent = () => { - + + Files + + reloadFiles()}> + + + + { Submissions - {assignment.max_submissions !== null ? ( - } - label={ - assignment.max_submissions - - submissions.length + - ' submissions left' - } - /> - ) : null} + + { assignment.max_submissions !== null + ? ( hasPermissions() + ? ( + + + + + ) + : ( + + ) + ) + : null + } + { }, [lecture, assignment, submission]); return ( - + @@ -143,7 +143,8 @@ export const Feedback = () => { Feedback Files - + + + + @@ -191,6 +224,25 @@ export const EditSubmission = () => { + setShowLogs(false)} + aria-labelledby='alert-dialog-title' + aria-describedby='alert-dialog-description' + > + {'Logs'} + + + {logs} + + + + + + ); }; diff --git a/grader_labextension/src/components/coursemanage/grading/manual-grading.tsx b/grader_labextension/src/components/coursemanage/grading/manual-grading.tsx index c75631de..e74de0ee 100644 --- a/grader_labextension/src/components/coursemanage/grading/manual-grading.tsx +++ b/grader_labextension/src/components/coursemanage/grading/manual-grading.tsx @@ -1,8 +1,11 @@ import { SectionTitle } from '../../util/section-title'; import { + Alert, + AlertTitle, Box, Button, Checkbox, FormControlLabel, IconButton, + Modal, Stack, TextField, Tooltip, Typography @@ -31,7 +34,55 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import { getAutogradeChip, getFeedbackChip, getManualChip } from './grading'; import { autogradeSubmissionsDialog, generateFeedbackDialog } from './table-toolbar'; import { showDialog } from '../../util/dialog-provider'; +import InfoIcon from '@mui/icons-material/Info'; +const style = { + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: '80%', + bgcolor: 'background.paper', + boxShadow: 3, + pt: 2, + px: 4, + pb: 3 + +}; + +const InfoModal = () =>{ + const [open, setOpen] = React.useState(false); + const handleOpen = () => { + setOpen(true); + }; + const handleClose = () => { + setOpen(false); + }; + return ( + + + + + + +

Manual Grading Information

+ + Info + If you want to manually grade an assignment, make sure to follow these steps:

+ 1.   In order to grade a submission manually, the submission must first be auto-graded. This sets meta data for manual grading. However, we're actively working towards enabling direct manual grading without the necessity of auto-grading in the future.
+ 2.   Once the meta data was set for submission, you can pull the submission.
+ 3.   From file list access submission files and grade them manually.
+ 4.   After you've completed the grading of the submission, click "FINISH MANUAL GRADING." This action will save the grading and determine the points that the student receives for their submission. +
+ +
+
+
+ ); +} export const ManualGrading = () => { const { @@ -280,7 +331,13 @@ export const ManualGrading = () => {
- Submission Files + + + Submission Files + + + + @@ -289,7 +346,21 @@ export const ManualGrading = () => { - + + {submission.auto_status !== 'automatically_graded' ? + + + + + : null} { size={'small'} variant='outlined' color='success' + disabled={submission.auto_status !== 'automatically_graded'} onClick={openFinishDialog} sx={{ whiteSpace: 'nowrap', minWidth: 'auto' }} > @@ -325,15 +397,19 @@ export const ManualGrading = () => { Edit Submission - + {submission.auto_status === 'automatically_graded' ? + + : null} + + {submission.auto_status === 'automatically_graded' ? + : null} - + diff --git a/grader_labextension/src/components/coursemanage/grading/table-toolbar.tsx b/grader_labextension/src/components/coursemanage/grading/table-toolbar.tsx index 38619d45..a1832fa0 100644 --- a/grader_labextension/src/components/coursemanage/grading/table-toolbar.tsx +++ b/grader_labextension/src/components/coursemanage/grading/table-toolbar.tsx @@ -8,7 +8,8 @@ import { InputAdornment, Stack, ToggleButton, - ToggleButtonGroup + ToggleButtonGroup, + Tooltip } from '@mui/material'; import FileDownloadIcon from '@mui/icons-material/FileDownload'; import ReplayIcon from '@mui/icons-material/Replay'; @@ -192,7 +193,15 @@ export function EnhancedTableToolbar(props: EnhancedTableToolbarProps) { }); }; - + const checkAutogradeStatus = () => { + let available = true; + selected.forEach(async id =>{ + const row = rows.find(value => value.id === id); + if(row.auto_status !== 'automatically_graded') + available = false; + }) + return available; + }; return ( @@ -266,13 +275,19 @@ export function EnhancedTableToolbar(props: EnhancedTableToolbarProps) { )} {numSelected > 0 ? ( - + + + + + {checkAutogradeStatus() === true ? + + : null} + ) : ( diff --git a/grader_labextension/src/components/coursemanage/settings/settings.tsx b/grader_labextension/src/components/coursemanage/settings/settings.tsx index 3361cfa3..99757dfe 100644 --- a/grader_labextension/src/components/coursemanage/settings/settings.tsx +++ b/grader_labextension/src/components/coursemanage/settings/settings.tsx @@ -1,7 +1,7 @@ import { Assignment } from '../../../model/assignment'; import { Submission } from '../../../model/submission'; import * as React from 'react'; -import { ErrorMessage, useFormik } from 'formik'; +import { useFormik } from 'formik'; import { Box, Button, @@ -11,15 +11,15 @@ import { MenuItem, Stack, TextField, - Tooltip, Typography + Tooltip, Typography, } from '@mui/material'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'; import { - deleteAssignment, - updateAssignment + updateAssignment, + deleteAssignment } from '../../../services/assignments.service'; import { enqueueSnackbar } from 'notistack'; import { Lecture } from '../../../model/lecture'; @@ -28,7 +28,6 @@ import { SectionTitle } from '../../util/section-title'; import { useNavigate, useRouteLoaderData } from 'react-router-dom'; import { getLateSubmissionInfo, ILateSubmissionInfo, LateSubmissionForm } from './late-submission-form'; import { FormikValues } from 'formik/dist/types'; -import { SubmissionPeriod } from '../../../model/submissionPeriod'; import moment from 'moment'; import { red } from '@mui/material/colors'; import { showDialog } from '../../util/dialog-provider'; @@ -48,7 +47,6 @@ const validationSchema = yup.object({ .required('Name is required'), due_date: yup .date() - .min(new Date(), 'Deadline must be set in the future') .nullable(), type: yup.mixed().oneOf(['user', 'group']), automatic_grading: yup.mixed().oneOf(['unassisted', 'auto', 'full_auto']), @@ -217,13 +215,18 @@ export const SettingsComponent = () => { /> { formik.setFieldValue('due_date', date); + if(new Date(date).getTime() < Date.now()){ + enqueueSnackbar("You selected date in past!",{ + variant: 'warning' + }); + } }} + /> @@ -324,25 +327,6 @@ export const SettingsComponent = () => { first!} - - {/* Not included in release 0.1 - Type - */}