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
-
+
+
+
Submission Files
+
+
+
@@ -191,6 +224,25 @@ export const EditSubmission = () => {
+
);
};
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
- */}