Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CP3108 Staff Dashboard - Assessment Uploader #1080

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
498 changes: 483 additions & 15 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
}
},
"dependencies": {
"@blueprintjs/datetime": "^3.15.2",
"@pusher/chatkit-client": "^1.5.0",
"ace-builds": "^1.4.8",
"acorn": "^5.7.4",
Expand Down
57 changes: 57 additions & 0 deletions src/actions/__tests__/groundControl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import * as actionTypes from '../actionTypes';
import {
changeDateAssessment,
deleteAssessment,
publishAssessment,
uploadAssessment
} from '../groundControl';

test('changeDateAssessment generates correct action object', () => {
const id = 10;
const openAt = '2020-01-01T00:00:00.000Z';
const closeAt = '2021-01-01T00:00:00.000Z';
const action = changeDateAssessment(id, openAt, closeAt);
expect(action).toEqual({
type: actionTypes.CHANGE_DATE_ASSESSMENT,
payload: {
id,
openAt,
closeAt
}
});
});

test('deleteAssessment generates correct action object', () => {
const id = 12;
const action = deleteAssessment(id);
expect(action).toEqual({
type: actionTypes.DELETE_ASSESSMENT,
payload: id
});
});

test('publishAssessment generates correct action object', () => {
const id = 54;
const togglePublishTo = false;
const action = publishAssessment(togglePublishTo, id);
expect(action).toEqual({
type: actionTypes.PUBLISH_ASSESSMENT,
payload: {
togglePublishTo,
id
}
});
});

test(' generates correct action object', () => {
const file = new File([''], 'testFile');
const forceUpdate = true;
const action = uploadAssessment(file, forceUpdate);
expect(action).toEqual({
type: actionTypes.UPLOAD_ASSESSMENT,
payload: {
file,
forceUpdate
}
});
});
7 changes: 7 additions & 0 deletions src/actions/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,10 @@ export const FETCH_NOTIFICATIONS = 'FETCH_NOTIFICATIONS';
export const ACKNOWLEDGE_NOTIFICATIONS = 'ACKNOWLEDGE_NOTIFICATIONS';
export const UPDATE_NOTIFICATIONS = 'UPDATE_NOTIFICATIONS';
export const NOTIFY_CHATKIT_USERS = 'NOTIFY_CHATKIT_USERS';

/** GroundControl */

export const CHANGE_DATE_ASSESSMENT = 'CHANGE_DATE_ASSESSMENT';
export const DELETE_ASSESSMENT = 'DELETE_ASSESSMENT';
export const PUBLISH_ASSESSMENT = 'PUBLISH_ASSESSMENT';
export const UPLOAD_ASSESSMENT = 'UPLOAD_ASSESSMENT';
14 changes: 14 additions & 0 deletions src/actions/groundControl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { action } from 'typesafe-actions';

import * as actionTypes from './actionTypes';

export const changeDateAssessment = (id: number, openAt: string, closeAt: string) =>
action(actionTypes.CHANGE_DATE_ASSESSMENT, { id, openAt, closeAt });

export const deleteAssessment = (id: number) => action(actionTypes.DELETE_ASSESSMENT, id);

export const publishAssessment = (togglePublishTo: boolean, id: number) =>
action(actionTypes.PUBLISH_ASSESSMENT, { id, togglePublishTo });

export const uploadAssessment = (file: File, forceUpdate: boolean) =>
action(actionTypes.UPLOAD_ASSESSMENT, { file, forceUpdate });
1 change: 1 addition & 0 deletions src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './collabEditing';
export * from './commons';
export * from './game';
export * from './groundControl';
export * from './interpreter';
export * from './material';
export * from './playground';
Expand Down
9 changes: 9 additions & 0 deletions src/components/academy/NavigationBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ const NavigationBar: React.SFC<OwnProps> = props => (
</NavbarGroup>
{props.role === Role.Admin || props.role === Role.Staff ? (
<NavbarGroup align={Alignment.RIGHT}>
<NavLink
to={'/academy/groundcontrol'}
activeClassName={Classes.ACTIVE}
className={classNames('NavigationBar__link', Classes.BUTTON, Classes.MINIMAL)}
>
<Icon icon="satellite" />
<div className="navbar-button-text hidden-xs">Ground Control</div>
</NavLink>

<NavLink
to={'/academy/sourcereel'}
activeClassName={Classes.ACTIVE}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ exports[`Grading NavLink renders for Role.Admin 1`] = `
</NavLink>
</Blueprint3.NavbarGroup>
<Blueprint3.NavbarGroup align=\\"right\\">
<NavLink to=\\"/academy/groundcontrol\\" activeClassName=\\"bp3-active\\" className=\\"NavigationBar__link bp3-button bp3-minimal\\" aria-current=\\"page\\">
<Blueprint3.Icon icon=\\"satellite\\" />
<div className=\\"navbar-button-text hidden-xs\\">
Ground Control
</div>
</NavLink>
<NavLink to=\\"/academy/sourcereel\\" activeClassName=\\"bp3-active\\" className=\\"NavigationBar__link bp3-button bp3-minimal\\" aria-current=\\"page\\">
<Blueprint3.Icon icon=\\"mobile-video\\" />
<div className=\\"navbar-button-text hidden-xs\\">
Expand Down Expand Up @@ -142,6 +148,12 @@ exports[`Grading NavLink renders for Role.Staff 1`] = `
</NavLink>
</Blueprint3.NavbarGroup>
<Blueprint3.NavbarGroup align=\\"right\\">
<NavLink to=\\"/academy/groundcontrol\\" activeClassName=\\"bp3-active\\" className=\\"NavigationBar__link bp3-button bp3-minimal\\" aria-current=\\"page\\">
<Blueprint3.Icon icon=\\"satellite\\" />
<div className=\\"navbar-button-text hidden-xs\\">
Ground Control
</div>
</NavLink>
<NavLink to=\\"/academy/sourcereel\\" activeClassName=\\"bp3-active\\" className=\\"NavigationBar__link bp3-button bp3-minimal\\" aria-current=\\"page\\">
<Blueprint3.Icon icon=\\"mobile-video\\" />
<div className=\\"navbar-button-text hidden-xs\\">
Expand Down
2 changes: 2 additions & 0 deletions src/components/academy/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Redirect, Route, RouteComponentProps, Switch } from 'react-router';
import Grading from '../../containers/academy/grading';
import AssessmentContainer from '../../containers/assessment';
import Game from '../../containers/GameContainer';
import GroundControl from '../../containers/groundControl/GroundControlContainer';
import MaterialUpload from '../../containers/material/MaterialUploadContainer';
import Sourcereel from '../../containers/sourcecast/SourcereelContainer';
import { isAcademyRe } from '../../reducers/session';
Expand Down Expand Up @@ -77,6 +78,7 @@ class Academy extends React.Component<IAcademyProps> {
render={assessmentRenderFactory(AssessmentCategories.Practical)}
/>

<Route path="/academy/groundcontrol" component={GroundControl} />
<Route path={`/academy/grading/${gradingRegExp}`} component={Grading} />
<Route path={'/academy/material'} component={MaterialUpload} />
<Route path="/academy/sourcereel" component={Sourcereel} />
Expand Down
1 change: 1 addition & 0 deletions src/components/assessment/assessmentShape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface IAssessmentOverview {
xp: number;
gradingStatus: GradingStatus;
private?: boolean;
isPublished?: boolean;
}

export enum AssessmentStatuses {
Expand Down
60 changes: 60 additions & 0 deletions src/components/groundControl/DeleteCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Classes, Dialog } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import * as React from 'react';

import { IAssessmentOverview } from '../assessment/assessmentShape';
import { controlButton } from '../commons';

interface IDeleteCellProps {
data: IAssessmentOverview;
handleDeleteAssessment: (id: number) => void;
}

interface IDeleteCellState {
dialogOpen: boolean;
}

class DeleteCell extends React.Component<IDeleteCellProps, IDeleteCellState> {
public constructor(props: IDeleteCellProps) {
super(props);
this.state = {
dialogOpen: false
};
}

public render() {
return (
<div>
{controlButton('', IconNames.TRASH, this.handleOpenDialog)}
<Dialog
icon="info-sign"
isOpen={this.state.dialogOpen}
onClose={this.handleCloseDialog}
title="Delete Assessment"
canOutsideClickClose={true}
>
<div className={Classes.DIALOG_BODY}>
{<p>Are you sure that you want to delete this Assessment?</p>}
{<p>Students' answers and submissions will be deleted as well.</p>}
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
{controlButton('Confirm Delete', IconNames.TRASH, this.handleDelete)}
{controlButton('Cancel', IconNames.CROSS, this.handleCloseDialog)}
</div>
</div>
</Dialog>
</div>
);
}

private handleCloseDialog = () => this.setState({ dialogOpen: false });
private handleOpenDialog = () => this.setState({ dialogOpen: true });
private handleDelete = () => {
const { data } = this.props;
this.props.handleDeleteAssessment(data.id);
this.handleCloseDialog();
};
}

export default DeleteCell;
151 changes: 151 additions & 0 deletions src/components/groundControl/Dropzone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { Card, Elevation, Switch } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { FlexDirectionProperty } from 'csstype';
import * as React from 'react';
import { useDropzone } from 'react-dropzone';

import { controlButton } from '../commons';

interface IDispatchProps {
handleUploadAssessment: (file: File) => void;
toggleForceUpdate: () => void;
toggleDisplayConfirmation: () => void;
}

interface IStateProps {
forceUpdate: boolean;
displayConfirmation: boolean;
}

interface IDropzoneProps extends IDispatchProps, IStateProps {}

// Dropzone styling
const dropZoneStyle = {
baseStyle: {
flex: 1,
display: 'flex',
height: '30vh',
flexDirection: 'column' as FlexDirectionProperty,
alignItems: 'center',
justifyContent: 'center',
padding: '20px',
borderWidth: 2,
borderRadius: 2,
borderColor: '#eeeeee',
borderStyle: 'dashed',
backgroundColor: '#fafafa',
color: '#bdbdbd',
outline: 'none',
transition: 'border .24s ease-in-out'
},

activeStyle: {
borderColor: '#2196f3'
},

acceptStyle: {
borderColor: '#00e676'
},

rejectStyle: {
borderColor: '#ff1744'
}
};

const MaterialDropzone: React.FC<IDropzoneProps> = props => {
const [file, setFile] = React.useState<File>();
const [title, setTitle] = React.useState<string>();
const handleConfirmUpload = () => {
props.handleUploadAssessment(file!);
setFile(undefined);
};
const handleCancelUpload = () => setFile(undefined);

const {
getRootProps,
getInputProps,
isDragActive,
isDragAccept,
isDragReject,
isFocused
} = useDropzone({
onDrop: acceptedFiles => {
setFile(acceptedFiles[0]);
setTitle(acceptedFiles[0].name);
}
});
const style = React.useMemo(
() => ({
...dropZoneStyle.baseStyle,
...(isDragActive ? dropZoneStyle.activeStyle : {}),
...(isDragAccept ? dropZoneStyle.acceptStyle : {}),
...(isDragReject ? dropZoneStyle.rejectStyle : {}),
...(isFocused ? dropZoneStyle.activeStyle : {})
}),
[isDragActive, isDragAccept, isDragReject, isFocused]
);

const handleToggleOnChange = () => {
if (!props.forceUpdate) {
props.toggleDisplayConfirmation();
props.toggleForceUpdate();
} else {
props.toggleForceUpdate();
}
};

const toggleButton = () => {
return (
<div className="toggle-button-wrapper">
<Switch checked={props.forceUpdate} onChange={handleToggleOnChange} />
</div>
);
};

const handleConfirmForceUpdate = () => {
props.toggleDisplayConfirmation();
};

const handleCancelForceUpdate = () => {
props.toggleDisplayConfirmation();
props.toggleForceUpdate();
};

const confirmationMessage = () => {
return (
<div>
<p>Are you sure that you want to force update the assessment?</p>
{controlButton('Yes', IconNames.CONFIRM, handleConfirmForceUpdate)}
{controlButton('No', IconNames.CROSS, handleCancelForceUpdate)}
</div>
);
};

return (
<>
<Card className="contentdisplay-content" elevation={Elevation.THREE}>
<div {...getRootProps({ style })}>
<input {...getInputProps()} />
<p>Drag 'n' drop some files here, or click to select files</p>
</div>
</Card>
{file && (
<Card>
<div>{title}</div>
<br />
{!props.displayConfirmation &&
controlButton('Confirm Upload', IconNames.UPLOAD, handleConfirmUpload)}
{!props.displayConfirmation &&
controlButton('Cancel Upload', IconNames.DELETE, handleCancelUpload)}
<br />
<br />
{!props.displayConfirmation && <p>Force update opened assessment</p>}
{props.displayConfirmation && confirmationMessage()}
{!props.displayConfirmation && toggleButton()}
</Card>
)}
</>
);
};

export default MaterialDropzone;
Loading