From 7d58318ef3d69f68df4beb4e4324526dbc7f720c Mon Sep 17 00:00:00 2001 From: Shuming Date: Mon, 8 Apr 2019 09:50:24 +0800 Subject: [PATCH 1/7] Added new fields to IQuestion --- .../academy/grading/GradingWorkspace.tsx | 31 ++++++++++++++++++- .../assessment/AssessmentWorkspace.tsx | 31 ++++++++++++++++++- .../__tests__/AssessmentWorkspace.tsx | 3 ++ src/components/assessment/assessmentShape.ts | 9 ++++++ .../grading/GradingWorkspaceContainer.ts | 5 ++- .../AssessmentWorkspaceContainer.ts | 3 ++ src/mocks/assessmentAPI.ts | 9 ++++++ src/reducers/states.ts | 9 +++++- src/sagas/index.ts | 4 ++- 9 files changed, 99 insertions(+), 5 deletions(-) diff --git a/src/components/academy/grading/GradingWorkspace.tsx b/src/components/academy/grading/GradingWorkspace.tsx index 0c1b413255..26f5d27caf 100755 --- a/src/components/academy/grading/GradingWorkspace.tsx +++ b/src/components/academy/grading/GradingWorkspace.tsx @@ -9,6 +9,7 @@ import { IMCQQuestion, IProgrammingQuestion, IQuestion, + ITestcase, Library, QuestionTypes } from '../../assessment/assessmentShape'; @@ -23,7 +24,10 @@ export type GradingWorkspaceProps = DispatchProps & OwnProps & StateProps; export type StateProps = { activeTab: number; grading?: Grading; + editorPrepend: string | null; editorValue: string | null; + editorPostpend: string | null; + editorTestcases: ITestcase[] | null; editorWidth: string; hasUnsavedChanges: boolean; isRunning: boolean; @@ -160,8 +164,33 @@ class GradingWorkspace extends React.Component { ? ((question as IProgrammingQuestion).answer as string) : (question as IProgrammingQuestion).solutionTemplate : null; + const editorPrepend = + question.type === QuestionTypes.programming + ? (question as IProgrammingQuestion).prepend !== null + ? (question as IProgrammingQuestion).prepend + : "" + : ""; + const editorPostpend = + question.type === QuestionTypes.programming + ? (question as IProgrammingQuestion).postpend !== null + ? (question as IProgrammingQuestion).postpend + : "" + : ""; + const editorTestcases = + question.type === QuestionTypes.programming + ? (question as IProgrammingQuestion).testcases !== null + ? (question as IProgrammingQuestion).testcases + : [] + : []; this.props.handleUpdateCurrentSubmissionId(submissionId, questionId); - this.props.handleResetWorkspace({ editorValue }); + this.props.handleResetWorkspace( + { + editorPrepend, + editorValue, + editorPostpend, + editorTestcases + } + ); this.props.handleClearContext(question.library); this.props.handleUpdateHasUnsavedChanges(false); if (editorValue) { diff --git a/src/components/assessment/AssessmentWorkspace.tsx b/src/components/assessment/AssessmentWorkspace.tsx index ddc16ce912..d2033fd6c8 100755 --- a/src/components/assessment/AssessmentWorkspace.tsx +++ b/src/components/assessment/AssessmentWorkspace.tsx @@ -16,6 +16,7 @@ import { IMCQQuestion, IProgrammingQuestion, IQuestion, + ITestcase, Library, QuestionTypes } from './assessmentShape'; @@ -26,7 +27,10 @@ export type AssessmentWorkspaceProps = DispatchProps & OwnProps & StateProps; export type StateProps = { activeTab: number; assessment?: IAssessment; + editorPrepend: string | null; editorValue: string | null; + editorPostpend: string | null; + editorTestcases: ITestcase[] | null; editorWidth: string; hasUnsavedChanges: boolean; isRunning: boolean; @@ -195,8 +199,33 @@ class AssessmentWorkspace extends React.Component< ? ((question as IProgrammingQuestion).answer as string) : (question as IProgrammingQuestion).solutionTemplate : null; + const editorPrepend = + question.type === QuestionTypes.programming + ? (question as IProgrammingQuestion).prepend !== null + ? (question as IProgrammingQuestion).prepend + : "" + : ""; + const editorPostpend = + question.type === QuestionTypes.programming + ? (question as IProgrammingQuestion).postpend !== null + ? (question as IProgrammingQuestion).postpend + : "" + : ""; + const editorTestcases = + question.type === QuestionTypes.programming + ? (question as IProgrammingQuestion).testcases !== null + ? (question as IProgrammingQuestion).testcases + : [] + : []; this.props.handleUpdateCurrentAssessmentId(assessmentId, questionId); - this.props.handleResetWorkspace({ editorValue }); + this.props.handleResetWorkspace( + { + editorPrepend, + editorValue, + editorPostpend, + editorTestcases + } + ); this.props.handleClearContext(question.library); this.props.handleUpdateHasUnsavedChanges(false); if (editorValue) { diff --git a/src/components/assessment/__tests__/AssessmentWorkspace.tsx b/src/components/assessment/__tests__/AssessmentWorkspace.tsx index e81100697c..921c5d9a7f 100644 --- a/src/components/assessment/__tests__/AssessmentWorkspace.tsx +++ b/src/components/assessment/__tests__/AssessmentWorkspace.tsx @@ -10,7 +10,10 @@ const defaultProps: AssessmentWorkspaceProps = { assessmentId: 0, notAttempted: true, closeDate: '2048-06-18T05:24:26.026Z', + editorPrepend: "", editorValue: null, + editorPostpend: "", + editorTestcases: [], editorWidth: '50%', hasUnsavedChanges: false, handleAssessmentFetch: (assessmentId: number) => {}, diff --git a/src/components/assessment/assessmentShape.ts b/src/components/assessment/assessmentShape.ts index 68313fef67..a444092d1e 100644 --- a/src/components/assessment/assessmentShape.ts +++ b/src/components/assessment/assessmentShape.ts @@ -64,10 +64,19 @@ export type AssessmentCategory = keyof typeof AssessmentCategories; export interface IProgrammingQuestion extends IQuestion { answer: string | null; + prepend: string; solutionTemplate: string; + postpend: string; + testcases: ITestcase[]; type: 'programming'; } +export interface ITestcase { + answer: string; + score: number; + program: string; +} + export interface IMCQQuestion extends IQuestion { solution: number | null; answer: number | null; diff --git a/src/containers/academy/grading/GradingWorkspaceContainer.ts b/src/containers/academy/grading/GradingWorkspaceContainer.ts index a7188cd71a..5e24f27c64 100644 --- a/src/containers/academy/grading/GradingWorkspaceContainer.ts +++ b/src/containers/academy/grading/GradingWorkspaceContainer.ts @@ -36,7 +36,10 @@ const workspaceLocation: WorkspaceLocation = 'grading'; const mapStateToProps: MapStateToProps = (state, props) => { return { activeTab: state.workspaces.grading.sideContentActiveTab, - editorValue: state.workspaces.grading.editorValue, + editorPrepend: state.workspaces.assessment.editorPrepend, + editorValue: state.workspaces.assessment.editorValue, + editorPostpend: state.workspaces.assessment.editorPostpend, + editorTestcases: state.workspaces.assessment.editorTestcases, editorWidth: state.workspaces.grading.editorWidth, grading: state.session.gradings.get(props.submissionId), hasUnsavedChanges: state.workspaces.grading.hasUnsavedChanges, diff --git a/src/containers/assessment/AssessmentWorkspaceContainer.ts b/src/containers/assessment/AssessmentWorkspaceContainer.ts index 2ba13dba2c..b5aa933e9f 100644 --- a/src/containers/assessment/AssessmentWorkspaceContainer.ts +++ b/src/containers/assessment/AssessmentWorkspaceContainer.ts @@ -36,7 +36,10 @@ const mapStateToProps: MapStateToProps = (state, p return { activeTab: state.workspaces.assessment.sideContentActiveTab, assessment: state.session.assessments.get(props.assessmentId), + editorPrepend: state.workspaces.assessment.editorPrepend, editorValue: state.workspaces.assessment.editorValue, + editorPostpend: state.workspaces.assessment.editorPostpend, + editorTestcases: state.workspaces.assessment.editorTestcases, editorWidth: state.workspaces.assessment.editorWidth, hasUnsavedChanges: state.workspaces.assessment.hasUnsavedChanges, isRunning: state.workspaces.assessment.isRunning, diff --git a/src/mocks/assessmentAPI.ts b/src/mocks/assessmentAPI.ts index d33da0c6a4..013b4fa3b1 100644 --- a/src/mocks/assessmentAPI.ts +++ b/src/mocks/assessmentAPI.ts @@ -208,6 +208,9 @@ What's your favourite dinner food? comment: null, id: 0, library: mockSoundLibrary, + prepend: "", + postpend: "", + testcases: [], solutionTemplate: '0th question mock solution template', type: 'programming', grader: { @@ -226,6 +229,9 @@ What's your favourite dinner food? content: 'Hello and welcome to this assessment! This is the 1st question.', id: 1, library: mock3DRuneLibrary, + prepend: "", + postpend: "", + testcases: [], solutionTemplate: '1st question mock solution template', type: 'programming', grader: { @@ -318,6 +324,9 @@ What's your favourite dinner food? content: 'You have reached the last question! Have some fun with the tone matrix...', id: 1, library: mockToneMatrixLibrary, + prepend: "", + postpend: "", + testcases: [], solutionTemplate: '5th question mock solution template', type: 'programming', grader: { diff --git a/src/reducers/states.ts b/src/reducers/states.ts index 97bef0603c..b6adbb7cb6 100644 --- a/src/reducers/states.ts +++ b/src/reducers/states.ts @@ -8,7 +8,8 @@ import { ExternalLibraryName, ExternalLibraryNames, IAssessment, - IAssessmentOverview + IAssessmentOverview, + ITestcase } from '../components/assessment/assessmentShape'; import { HistoryHelper } from '../utils/history'; import { createContext } from '../utils/slangHelper'; @@ -58,7 +59,10 @@ export interface IWorkspaceManagerState { export interface IWorkspaceState { readonly context: Context; + readonly editorPrepend: string | null; readonly editorValue: string | null; + readonly editorPostpend: string | null; + readonly editorTestcases: ITestcase[]; readonly editorWidth: string; readonly isEditorAutorun: boolean; readonly isRunning: boolean; @@ -197,7 +201,10 @@ export const defaultEditorValue = '// Type your program in here!'; */ export const createDefaultWorkspace = (location: WorkspaceLocation): IWorkspaceState => ({ context: createContext(latestSourceChapter, [], location), + editorPrepend: "", editorValue: location === WorkspaceLocations.playground ? defaultEditorValue : null, + editorPostpend: "", + editorTestcases: [], editorWidth: '50%', output: [], replHistory: { diff --git a/src/sagas/index.ts b/src/sagas/index.ts index 779321f5b1..af66720e52 100644 --- a/src/sagas/index.ts +++ b/src/sagas/index.ts @@ -29,7 +29,9 @@ function* workspaceSaga(): SagaIterator { yield takeEvery(actionTypes.EVAL_EDITOR, function*(action) { const location = (action as actionTypes.IAction).payload.workspaceLocation; const code: string = yield select( - (state: IState) => (state.workspaces[location] as IWorkspaceState).editorValue + (state: IState) => (state.workspaces[location] as IWorkspaceState).editorPrepend! + + (state.workspaces[location] as IWorkspaceState).editorValue! + + (state.workspaces[location] as IWorkspaceState).editorPostpend! ); const chapter: number = yield select( (state: IState) => (state.workspaces[location] as IWorkspaceState).context.chapter From bd2186572375c42eeb2f22b5019750355be647df Mon Sep 17 00:00:00 2001 From: Shuming Date: Tue, 9 Apr 2019 21:52:10 +0800 Subject: [PATCH 2/7] added autograder tab for assessmentWorkspace --- src/actions/actionTypes.ts | 2 + src/actions/interpreter.ts | 5 ++ src/actions/workspaces.ts | 5 ++ .../assessment/AssessmentWorkspace.tsx | 11 ++- .../__tests__/AssessmentWorkspace.tsx | 1 + src/components/assessment/assessmentShape.ts | 1 + .../workspace/side-content/Autograder.tsx | 25 ++++++ .../workspace/side-content/AutograderCard.tsx | 89 +++++++++++++++++++ .../AssessmentWorkspaceContainer.ts | 3 + src/reducers/workspaces.ts | 42 +++++++++ src/sagas/index.ts | 62 +++++++++++++ 11 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 src/components/workspace/side-content/Autograder.tsx create mode 100644 src/components/workspace/side-content/AutograderCard.tsx diff --git a/src/actions/actionTypes.ts b/src/actions/actionTypes.ts index b30b29db39..41cd2f5e34 100644 --- a/src/actions/actionTypes.ts +++ b/src/actions/actionTypes.ts @@ -19,6 +19,7 @@ export const BEGIN_INTERRUPT_EXECUTION = 'BEGIN_INTERRUPT_EXECUTION'; export const END_INTERRUPT_EXECUTION = 'END_INTERRUPT_EXECUTION'; export const EVAL_INTERPRETER_ERROR = 'EVAL_INTERPRETER_ERROR'; export const EVAL_INTERPRETER_SUCCESS = 'EVAL_INTERPRETER_SUCCESS'; +export const EVAL_TESTCASE_SUCCESS = 'EVAL_TESTCASE_SUCCESS'; export const HANDLE_CONSOLE_LOG = 'HANDLE_CONSOLE_LOG'; /** Workspace */ @@ -36,6 +37,7 @@ export const END_CLEAR_CONTEXT = 'END_CLEAR_CONTEXT'; export const ENSURE_LIBRARIES_LOADED = 'ENSURE_LIBRARIES_LOADED'; export const EVAL_EDITOR = 'EVAL_EDITOR'; export const EVAL_REPL = 'EVAL_REPL'; +export const EVAL_TESTCASE = 'EVAL_TESTCASE'; export const PLAYGROUND_EXTERNAL_SELECT = 'PLAYGROUND_EXTERNAL_SELECT '; export const RESET_WORKSPACE = 'RESET_WORKSPACE'; export const SEND_REPL_INPUT_TO_OUTPUT = 'SEND_REPL_INPUT_TO_OUTPUT'; diff --git a/src/actions/interpreter.ts b/src/actions/interpreter.ts index 29eed3a8a0..d084ce9fff 100644 --- a/src/actions/interpreter.ts +++ b/src/actions/interpreter.ts @@ -13,6 +13,11 @@ export const evalInterpreterSuccess = (value: Value, workspaceLocation: Workspac payload: { type: 'result', value, workspaceLocation } }); +export const evalTestcaseSuccess = (value: Value, workspaceLocation: WorkspaceLocation, index: number) => ({ + type: actionTypes.EVAL_TESTCASE_SUCCESS, + payload: { type: 'result', value, workspaceLocation, index } +}); + export const evalInterpreterError = ( errors: SourceError[], workspaceLocation: WorkspaceLocation diff --git a/src/actions/workspaces.ts b/src/actions/workspaces.ts index 615ab560e7..b9bf64acc9 100755 --- a/src/actions/workspaces.ts +++ b/src/actions/workspaces.ts @@ -161,6 +161,11 @@ export const evalRepl = (workspaceLocation: WorkspaceLocation) => ({ payload: { workspaceLocation } }); +export const evalTestcase = (workspaceLocation: WorkspaceLocation, testcaseId: number) => ({ + type: actionTypes.EVAL_TESTCASE, + payload: { workspaceLocation, testcaseId } +}); + export const updateEditorValue: ActionCreator = ( newEditorValue: string, workspaceLocation: WorkspaceLocation diff --git a/src/components/assessment/AssessmentWorkspace.tsx b/src/components/assessment/AssessmentWorkspace.tsx index d2033fd6c8..49c4ece027 100755 --- a/src/components/assessment/AssessmentWorkspace.tsx +++ b/src/components/assessment/AssessmentWorkspace.tsx @@ -10,6 +10,7 @@ import Markdown from '../commons/Markdown'; import Workspace, { WorkspaceProps } from '../workspace'; import { ControlBarProps } from '../workspace/ControlBar'; import { SideContentProps } from '../workspace/side-content'; +import Autograder from '../workspace/side-content/Autograder'; import ToneMatrix from '../workspace/side-content/ToneMatrix'; import { IAssessment, @@ -65,6 +66,7 @@ export type DispatchProps = { handleResetWorkspace: (options: Partial) => void; handleSave: (id: number, answer: number | string) => void; handleSideContentHeightChange: (heightChange: number) => void; + handleTestcaseEval: (testcaseId: number) => void; handleUpdateCurrentAssessmentId: (assessmentId: number, questionId: number) => void; handleUpdateHasUnsavedChanges: (hasUnsavedChanges: boolean) => void; }; @@ -214,7 +216,9 @@ class AssessmentWorkspace extends React.Component< const editorTestcases = question.type === QuestionTypes.programming ? (question as IProgrammingQuestion).testcases !== null - ? (question as IProgrammingQuestion).testcases + ? (question as IProgrammingQuestion).testcases.map(testcase => { + return testcase; + }) : [] : []; this.props.handleUpdateCurrentAssessmentId(assessmentId, questionId); @@ -249,6 +253,11 @@ class AssessmentWorkspace extends React.Component< label: `${props.assessment!.category} Briefing`, icon: IconNames.BRIEFCASE, body: + }, + { + label: `${props.assessment!.category} Autograder`, + icon: IconNames.AIRPLANE, + body: } ]; const isGraded = props.assessment!.questions[questionId].grader !== null; diff --git a/src/components/assessment/__tests__/AssessmentWorkspace.tsx b/src/components/assessment/__tests__/AssessmentWorkspace.tsx index 921c5d9a7f..f6abfcccee 100644 --- a/src/components/assessment/__tests__/AssessmentWorkspace.tsx +++ b/src/components/assessment/__tests__/AssessmentWorkspace.tsx @@ -32,6 +32,7 @@ const defaultProps: AssessmentWorkspaceProps = { handleResetWorkspace: () => {}, handleSave: (id: number, answer: string | number) => {}, handleSideContentHeightChange: (heightChange: number) => {}, + handleTestcaseEval: (testcaseId: number) => {}, handleUpdateHasUnsavedChanges: (hasUnsavedChanges: boolean) => {}, handleUpdateCurrentAssessmentId: (a: number, q: number) => {}, isRunning: false, diff --git a/src/components/assessment/assessmentShape.ts b/src/components/assessment/assessmentShape.ts index a444092d1e..b30dd1df84 100644 --- a/src/components/assessment/assessmentShape.ts +++ b/src/components/assessment/assessmentShape.ts @@ -75,6 +75,7 @@ export interface ITestcase { answer: string; score: number; program: string; + actual?: any; } export interface IMCQQuestion extends IQuestion { diff --git a/src/components/workspace/side-content/Autograder.tsx b/src/components/workspace/side-content/Autograder.tsx new file mode 100644 index 0000000000..945aae5ab2 --- /dev/null +++ b/src/components/workspace/side-content/Autograder.tsx @@ -0,0 +1,25 @@ + +import * as React from 'react'; +import { ITestcase } from '../../assessment/assessmentShape'; +import AutograderCard from './AutograderCard'; + +type AutograderProps = { + testcases: ITestcase[] | null; + handleTestcaseEval: (testcaseId: number) => void; +}; + + +class Autograder extends React.Component { + + public render() { + return this.props.testcases != null + ? this.props.testcases.map((testcase, index) => +
+ +
) + :
There are no testcases provided for this mission.
; + } +} + + +export default Autograder; \ No newline at end of file diff --git a/src/components/workspace/side-content/AutograderCard.tsx b/src/components/workspace/side-content/AutograderCard.tsx new file mode 100644 index 0000000000..cf4bc1b068 --- /dev/null +++ b/src/components/workspace/side-content/AutograderCard.tsx @@ -0,0 +1,89 @@ +import { + // Button, + // ButtonGroup, + Card, + // Classes, + // Collapse, + // Dialog, + Elevation, + // Icon, + // IconName + // Intent, + // NonIdealState, + // Position, + // Spinner, + // Text, + // Tooltip + } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { stringify } from 'js-slang/dist/interop'; +import * as React from 'react'; +// tslint:disable-next-line +import { controlButton } from '../../commons'; +// tslint:disable-next-line +import { ITestcase } from '../../assessment/assessmentShape'; +// tslint:disable-next-line +import CanvasOutput from '../CanvasOutput'; +// tslint:disable-next-line +import Markdown from '../../commons/Markdown'; + +// import { InterpreterOutput } from '../../../reducers/states'; + + + + +type AutograderCardProps = { + testcase: ITestcase; + index: number; + handleTestcaseEval: (testcaseId: number) => void; +}; + + +class AutograderCard extends React.Component { + + + public render() { + const renderResult = (value: any) => { + /** A class which is the output of the show() function */ + const ShapeDrawn = (window as any).ShapeDrawn; + if (typeof ShapeDrawn !== 'undefined' && value instanceof ShapeDrawn) { + return ; + } else { + return stringify(value); + } + }; + + return
+ +
+ {/* {makeOverviewCardTitle(overview, index, setBetchaAssessment, renderGradingStatus)} */} +
+
+ +
+
+
+
+ +
+
+
+
+ {'Actual Answer: '} {this.props.testcase.actual !== undefined + ?
{renderResult(this.props.testcase.actual.value)}
+ : "No Answer"} +
+
+
+
+ {controlButton("Test", IconNames.PLAY, () => this.props.handleTestcaseEval(this.props.index)) } +
+
+
+
+
; + } +} + + +export default AutograderCard; \ No newline at end of file diff --git a/src/containers/assessment/AssessmentWorkspaceContainer.ts b/src/containers/assessment/AssessmentWorkspaceContainer.ts index b5aa933e9f..e20c8e8bde 100644 --- a/src/containers/assessment/AssessmentWorkspaceContainer.ts +++ b/src/containers/assessment/AssessmentWorkspaceContainer.ts @@ -13,6 +13,7 @@ import { clearReplOutput, evalEditor, evalRepl, + evalTestcase, fetchAssessment, submitAnswer, updateEditorValue, @@ -76,6 +77,8 @@ const mapDispatchToProps: MapDispatchToProps = (dispatch: Dis handleSave: submitAnswer, handleSideContentHeightChange: (heightChange: number) => changeSideContentHeight(heightChange, workspaceLocation), + handleTestcaseEval: (testcaseId: number) => + evalTestcase(workspaceLocation, testcaseId), handleUpdateHasUnsavedChanges: (hasUnsavedChanges: boolean) => updateHasUnsavedChanges(workspaceLocation, hasUnsavedChanges), handleUpdateCurrentAssessmentId: updateCurrentAssessmentId diff --git a/src/reducers/workspaces.ts b/src/reducers/workspaces.ts index b248f2ca7c..ce9e6e0526 100644 --- a/src/reducers/workspaces.ts +++ b/src/reducers/workspaces.ts @@ -15,6 +15,8 @@ import { EVAL_INTERPRETER_ERROR, EVAL_INTERPRETER_SUCCESS, EVAL_REPL, + EVAL_TESTCASE, + EVAL_TESTCASE_SUCCESS, HANDLE_CONSOLE_LOG, IAction, LOG_OUT, @@ -52,6 +54,7 @@ export const reducer: Reducer = ( ) => { const location: WorkspaceLocation = action.payload !== undefined ? action.payload.workspaceLocation : undefined; + const index: number = action.payload !== undefined ? action.payload.index : undefined; let newOutput: InterpreterOutput[]; let lastOutput: InterpreterOutput; @@ -276,6 +279,14 @@ export const reducer: Reducer = ( isRunning: true } }; + case EVAL_TESTCASE: + return { + ...state, + [location]: { + ...state[location], + isRunning: true + } + }; case EVAL_INTERPRETER_SUCCESS: lastOutput = state[location].output.slice(-1)[0]; if (lastOutput !== undefined && lastOutput.type === 'running') { @@ -299,6 +310,37 @@ export const reducer: Reducer = ( isRunning: false } }; + case EVAL_TESTCASE_SUCCESS: + lastOutput = state[location].output.slice(-1)[0]; + if (lastOutput !== undefined && lastOutput.type === 'running') { + newOutput = state[location].output.slice(0, -1).concat({ + ...action.payload, + workspaceLocation: undefined, + consoleLogs: lastOutput.consoleLogs + }); + } else { + newOutput = state[location].output.concat({ + ...action.payload, + workspaceLocation: undefined, + consoleLogs: [] + }); + } + return { + ...state, + [location]: { + ...state[location], + editorTestcases: state[location].editorTestcases.map( + (testcase, i) => { + if (i === index) { + testcase.actual = newOutput[0]; + return testcase; + } else { + return testcase; + } + }), + isRunning: false + } + }; case EVAL_INTERPRETER_ERROR: lastOutput = state[location].output.slice(-1)[0]; if (lastOutput !== undefined && lastOutput.type === 'running') { diff --git a/src/sagas/index.ts b/src/sagas/index.ts index af66720e52..6678dd29ed 100644 --- a/src/sagas/index.ts +++ b/src/sagas/index.ts @@ -75,6 +75,43 @@ function* workspaceSaga(): SagaIterator { yield* evalCode(code, context, location); }); + yield takeEvery(actionTypes.EVAL_TESTCASE, function*(action) { + const location = (action as actionTypes.IAction).payload.workspaceLocation; + const index = (action as actionTypes.IAction).payload.testcaseId; + const code: string = yield select( + (state: IState) => (state.workspaces[location] as IWorkspaceState).editorPrepend! + '\n' + + (state.workspaces[location] as IWorkspaceState).editorValue! + '\n' + + (state.workspaces[location] as IWorkspaceState).editorPostpend! + '\n' + + (state.workspaces[location] as IWorkspaceState).editorTestcases[index].program! + ); + const chapter: number = yield select( + (state: IState) => (state.workspaces[location] as IWorkspaceState).context.chapter + ); + const symbols: string[] = yield select( + (state: IState) => (state.workspaces[location] as IWorkspaceState).context.externalSymbols + ); + const globals: Array<[string, any]> = yield select( + (state: IState) => (state.workspaces[location] as IWorkspaceState).globals + ); + const library = { + chapter, + external: { + name: ExternalLibraryNames.NONE, + symbols + }, + globals + }; + /** End any code that is running right now. */ + yield put(actions.beginInterruptExecution(location)); + /** Clear the context, with the same chapter and externalSymbols as before. */ + yield put(actions.beginClearContext(library, location)); + yield put(actions.clearReplOutput(location)); + context = yield select( + (state: IState) => (state.workspaces[location] as IWorkspaceState).context + ); + yield* evalTestCode(code, context, location, index); + }); + yield takeEvery(actionTypes.CHAPTER_SELECT, function*(action) { const location = (action as actionTypes.IAction).payload.workspaceLocation; const newChapter = (action as actionTypes.IAction).payload.chapter; @@ -281,4 +318,29 @@ function* evalCode(code: string, context: Context, location: WorkspaceLocation) } } +function* evalTestCode(code: string, context: Context, location: WorkspaceLocation, index: number) { + const { result, interrupted } = yield race({ + result: call(runInContext, code, context, { scheduler: 'preemptive' }), + /** + * A BEGIN_INTERRUPT_EXECUTION signals the beginning of an interruption, + * i.e the trigger for the interpreter to interrupt execution. + */ + interrupted: take(actionTypes.BEGIN_INTERRUPT_EXECUTION) + }); + if (result) { + if (result.status === 'finished') { + yield put(actions.evalInterpreterSuccess(result.value, location)); + yield put(actions.evalTestcaseSuccess(result.value, location, index)); + } else { + yield put(actions.evalInterpreterError(context.errors, location)); + } + } else if (interrupted) { + interrupt(context); + /* Redundancy, added ensure that interruption results in an error. */ + context.errors.push(new InterruptedError(context.runtime.nodes[0])); + yield put(actions.endInterruptExecution(location)); + yield call(showWarningMessage, 'Execution aborted by user', 750); + } +} + export default mainSaga; From 3cffcdf6d2ec66d4723f49a4a9c5490514888210 Mon Sep 17 00:00:00 2001 From: Shuming Date: Wed, 10 Apr 2019 18:05:49 +0800 Subject: [PATCH 3/7] yarn format --- src/actions/interpreter.ts | 6 +- .../academy/grading/GradingWorkspace.tsx | 28 ++--- .../assessment/AssessmentWorkspace.tsx | 35 +++--- .../__tests__/AssessmentWorkspace.tsx | 4 +- .../workspace/side-content/Autograder.tsx | 31 ++--- .../workspace/side-content/AutograderCard.tsx | 116 ++++++++++-------- .../AssessmentWorkspaceContainer.ts | 3 +- src/mocks/assessmentAPI.ts | 12 +- src/reducers/states.ts | 4 +- src/reducers/workspaces.ts | 21 ++-- src/sagas/index.ts | 19 +-- 11 files changed, 149 insertions(+), 130 deletions(-) diff --git a/src/actions/interpreter.ts b/src/actions/interpreter.ts index d084ce9fff..c249dd198b 100644 --- a/src/actions/interpreter.ts +++ b/src/actions/interpreter.ts @@ -13,7 +13,11 @@ export const evalInterpreterSuccess = (value: Value, workspaceLocation: Workspac payload: { type: 'result', value, workspaceLocation } }); -export const evalTestcaseSuccess = (value: Value, workspaceLocation: WorkspaceLocation, index: number) => ({ +export const evalTestcaseSuccess = ( + value: Value, + workspaceLocation: WorkspaceLocation, + index: number +) => ({ type: actionTypes.EVAL_TESTCASE_SUCCESS, payload: { type: 'result', value, workspaceLocation, index } }); diff --git a/src/components/academy/grading/GradingWorkspace.tsx b/src/components/academy/grading/GradingWorkspace.tsx index 26f5d27caf..dfc2a83dcc 100755 --- a/src/components/academy/grading/GradingWorkspace.tsx +++ b/src/components/academy/grading/GradingWorkspace.tsx @@ -164,33 +164,31 @@ class GradingWorkspace extends React.Component { ? ((question as IProgrammingQuestion).answer as string) : (question as IProgrammingQuestion).solutionTemplate : null; - const editorPrepend = + const editorPrepend = question.type === QuestionTypes.programming ? (question as IProgrammingQuestion).prepend !== null ? (question as IProgrammingQuestion).prepend - : "" - : ""; - const editorPostpend = + : '' + : ''; + const editorPostpend = question.type === QuestionTypes.programming ? (question as IProgrammingQuestion).postpend !== null ? (question as IProgrammingQuestion).postpend - : "" - : ""; + : '' + : ''; const editorTestcases = question.type === QuestionTypes.programming ? (question as IProgrammingQuestion).testcases !== null ? (question as IProgrammingQuestion).testcases : [] - : []; + : []; this.props.handleUpdateCurrentSubmissionId(submissionId, questionId); - this.props.handleResetWorkspace( - { - editorPrepend, - editorValue, - editorPostpend, - editorTestcases - } - ); + this.props.handleResetWorkspace({ + editorPrepend, + editorValue, + editorPostpend, + editorTestcases + }); this.props.handleClearContext(question.library); this.props.handleUpdateHasUnsavedChanges(false); if (editorValue) { diff --git a/src/components/assessment/AssessmentWorkspace.tsx b/src/components/assessment/AssessmentWorkspace.tsx index 49c4ece027..44fc0e759e 100755 --- a/src/components/assessment/AssessmentWorkspace.tsx +++ b/src/components/assessment/AssessmentWorkspace.tsx @@ -201,35 +201,33 @@ class AssessmentWorkspace extends React.Component< ? ((question as IProgrammingQuestion).answer as string) : (question as IProgrammingQuestion).solutionTemplate : null; - const editorPrepend = + const editorPrepend = question.type === QuestionTypes.programming ? (question as IProgrammingQuestion).prepend !== null ? (question as IProgrammingQuestion).prepend - : "" - : ""; - const editorPostpend = + : '' + : ''; + const editorPostpend = question.type === QuestionTypes.programming ? (question as IProgrammingQuestion).postpend !== null ? (question as IProgrammingQuestion).postpend - : "" - : ""; + : '' + : ''; const editorTestcases = question.type === QuestionTypes.programming ? (question as IProgrammingQuestion).testcases !== null ? (question as IProgrammingQuestion).testcases.map(testcase => { return testcase; - }) + }) : [] : []; this.props.handleUpdateCurrentAssessmentId(assessmentId, questionId); - this.props.handleResetWorkspace( - { - editorPrepend, - editorValue, - editorPostpend, - editorTestcases - } - ); + this.props.handleResetWorkspace({ + editorPrepend, + editorValue, + editorPostpend, + editorTestcases + }); this.props.handleClearContext(question.library); this.props.handleUpdateHasUnsavedChanges(false); if (editorValue) { @@ -257,7 +255,12 @@ class AssessmentWorkspace extends React.Component< { label: `${props.assessment!.category} Autograder`, icon: IconNames.AIRPLANE, - body: + body: ( + + ) } ]; const isGraded = props.assessment!.questions[questionId].grader !== null; diff --git a/src/components/assessment/__tests__/AssessmentWorkspace.tsx b/src/components/assessment/__tests__/AssessmentWorkspace.tsx index f6abfcccee..ba99aa88cc 100644 --- a/src/components/assessment/__tests__/AssessmentWorkspace.tsx +++ b/src/components/assessment/__tests__/AssessmentWorkspace.tsx @@ -10,9 +10,9 @@ const defaultProps: AssessmentWorkspaceProps = { assessmentId: 0, notAttempted: true, closeDate: '2048-06-18T05:24:26.026Z', - editorPrepend: "", + editorPrepend: '', editorValue: null, - editorPostpend: "", + editorPostpend: '', editorTestcases: [], editorWidth: '50%', hasUnsavedChanges: false, diff --git a/src/components/workspace/side-content/Autograder.tsx b/src/components/workspace/side-content/Autograder.tsx index 945aae5ab2..4ba00aa9e6 100644 --- a/src/components/workspace/side-content/Autograder.tsx +++ b/src/components/workspace/side-content/Autograder.tsx @@ -1,25 +1,28 @@ - import * as React from 'react'; import { ITestcase } from '../../assessment/assessmentShape'; import AutograderCard from './AutograderCard'; type AutograderProps = { - testcases: ITestcase[] | null; - handleTestcaseEval: (testcaseId: number) => void; + testcases: ITestcase[] | null; + handleTestcaseEval: (testcaseId: number) => void; }; - class Autograder extends React.Component { - public render() { - return this.props.testcases != null - ? this.props.testcases.map((testcase, index) => -
- -
) - :
There are no testcases provided for this mission.
; - } + return this.props.testcases != null ? ( + this.props.testcases.map((testcase, index) => ( +
+ +
+ )) + ) : ( +
There are no testcases provided for this mission.
+ ); + } } - -export default Autograder; \ No newline at end of file +export default Autograder; diff --git a/src/components/workspace/side-content/AutograderCard.tsx b/src/components/workspace/side-content/AutograderCard.tsx index cf4bc1b068..279bd550c1 100644 --- a/src/components/workspace/side-content/AutograderCard.tsx +++ b/src/components/workspace/side-content/AutograderCard.tsx @@ -1,20 +1,20 @@ import { - // Button, - // ButtonGroup, - Card, - // Classes, - // Collapse, - // Dialog, - Elevation, - // Icon, - // IconName - // Intent, - // NonIdealState, - // Position, - // Spinner, - // Text, - // Tooltip - } from '@blueprintjs/core'; + // Button, + // ButtonGroup, + Card, + // Classes, + // Collapse, + // Dialog, + Elevation + // Icon, + // IconName + // Intent, + // NonIdealState, + // Position, + // Spinner, + // Text, + // Tooltip +} from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import { stringify } from 'js-slang/dist/interop'; import * as React from 'react'; @@ -29,19 +29,13 @@ import Markdown from '../../commons/Markdown'; // import { InterpreterOutput } from '../../../reducers/states'; - - - type AutograderCardProps = { - testcase: ITestcase; - index: number; - handleTestcaseEval: (testcaseId: number) => void; + testcase: ITestcase; + index: number; + handleTestcaseEval: (testcaseId: number) => void; }; - class AutograderCard extends React.Component { - - public render() { const renderResult = (value: any) => { /** A class which is the output of the show() function */ @@ -53,37 +47,51 @@ class AutograderCard extends React.Component { } }; - return
- -
- {/* {makeOverviewCardTitle(overview, index, setBetchaAssessment, renderGradingStatus)} */} -
-
- -
-
-
-
- -
-
-
-
- {'Actual Answer: '} {this.props.testcase.actual !== undefined - ?
{renderResult(this.props.testcase.actual.value)}
- : "No Answer"} -
-
-
-
- {controlButton("Test", IconNames.PLAY, () => this.props.handleTestcaseEval(this.props.index)) } + return ( +
+ +
+ {/* {makeOverviewCardTitle(overview, index, setBetchaAssessment, renderGradingStatus)} */} +
+
+ +
+
+
+
+ +
+
+
+
+ {'Actual Answer: '}{' '} + {this.props.testcase.actual !== undefined ? ( +
{renderResult(this.props.testcase.actual.value)}
+ ) : ( + 'No Answer' + )} +
+
+
+
+ {controlButton('Test', IconNames.PLAY, () => + this.props.handleTestcaseEval(this.props.index) + )} +
+
-
+
- -
; + ); } } - -export default AutograderCard; \ No newline at end of file +export default AutograderCard; diff --git a/src/containers/assessment/AssessmentWorkspaceContainer.ts b/src/containers/assessment/AssessmentWorkspaceContainer.ts index e20c8e8bde..41da56166e 100644 --- a/src/containers/assessment/AssessmentWorkspaceContainer.ts +++ b/src/containers/assessment/AssessmentWorkspaceContainer.ts @@ -77,8 +77,7 @@ const mapDispatchToProps: MapDispatchToProps = (dispatch: Dis handleSave: submitAnswer, handleSideContentHeightChange: (heightChange: number) => changeSideContentHeight(heightChange, workspaceLocation), - handleTestcaseEval: (testcaseId: number) => - evalTestcase(workspaceLocation, testcaseId), + handleTestcaseEval: (testcaseId: number) => evalTestcase(workspaceLocation, testcaseId), handleUpdateHasUnsavedChanges: (hasUnsavedChanges: boolean) => updateHasUnsavedChanges(workspaceLocation, hasUnsavedChanges), handleUpdateCurrentAssessmentId: updateCurrentAssessmentId diff --git a/src/mocks/assessmentAPI.ts b/src/mocks/assessmentAPI.ts index 013b4fa3b1..c7eb8e6c8a 100644 --- a/src/mocks/assessmentAPI.ts +++ b/src/mocks/assessmentAPI.ts @@ -208,8 +208,8 @@ What's your favourite dinner food? comment: null, id: 0, library: mockSoundLibrary, - prepend: "", - postpend: "", + prepend: '', + postpend: '', testcases: [], solutionTemplate: '0th question mock solution template', type: 'programming', @@ -229,8 +229,8 @@ What's your favourite dinner food? content: 'Hello and welcome to this assessment! This is the 1st question.', id: 1, library: mock3DRuneLibrary, - prepend: "", - postpend: "", + prepend: '', + postpend: '', testcases: [], solutionTemplate: '1st question mock solution template', type: 'programming', @@ -324,8 +324,8 @@ What's your favourite dinner food? content: 'You have reached the last question! Have some fun with the tone matrix...', id: 1, library: mockToneMatrixLibrary, - prepend: "", - postpend: "", + prepend: '', + postpend: '', testcases: [], solutionTemplate: '5th question mock solution template', type: 'programming', diff --git a/src/reducers/states.ts b/src/reducers/states.ts index b6adbb7cb6..f451499ab4 100644 --- a/src/reducers/states.ts +++ b/src/reducers/states.ts @@ -201,9 +201,9 @@ export const defaultEditorValue = '// Type your program in here!'; */ export const createDefaultWorkspace = (location: WorkspaceLocation): IWorkspaceState => ({ context: createContext(latestSourceChapter, [], location), - editorPrepend: "", + editorPrepend: '', editorValue: location === WorkspaceLocations.playground ? defaultEditorValue : null, - editorPostpend: "", + editorPostpend: '', editorTestcases: [], editorWidth: '50%', output: [], diff --git a/src/reducers/workspaces.ts b/src/reducers/workspaces.ts index ce9e6e0526..c0f540a221 100644 --- a/src/reducers/workspaces.ts +++ b/src/reducers/workspaces.ts @@ -310,7 +310,7 @@ export const reducer: Reducer = ( isRunning: false } }; - case EVAL_TESTCASE_SUCCESS: + case EVAL_TESTCASE_SUCCESS: lastOutput = state[location].output.slice(-1)[0]; if (lastOutput !== undefined && lastOutput.type === 'running') { newOutput = state[location].output.slice(0, -1).concat({ @@ -329,17 +329,16 @@ export const reducer: Reducer = ( ...state, [location]: { ...state[location], - editorTestcases: state[location].editorTestcases.map( - (testcase, i) => { - if (i === index) { - testcase.actual = newOutput[0]; - return testcase; - } else { - return testcase; - } - }), + editorTestcases: state[location].editorTestcases.map((testcase, i) => { + if (i === index) { + testcase.actual = newOutput[0]; + return testcase; + } else { + return testcase; + } + }), isRunning: false - } + } }; case EVAL_INTERPRETER_ERROR: lastOutput = state[location].output.slice(-1)[0]; diff --git a/src/sagas/index.ts b/src/sagas/index.ts index 6678dd29ed..8e6ba66574 100644 --- a/src/sagas/index.ts +++ b/src/sagas/index.ts @@ -29,9 +29,10 @@ function* workspaceSaga(): SagaIterator { yield takeEvery(actionTypes.EVAL_EDITOR, function*(action) { const location = (action as actionTypes.IAction).payload.workspaceLocation; const code: string = yield select( - (state: IState) => (state.workspaces[location] as IWorkspaceState).editorPrepend! + - (state.workspaces[location] as IWorkspaceState).editorValue! + - (state.workspaces[location] as IWorkspaceState).editorPostpend! + (state: IState) => + (state.workspaces[location] as IWorkspaceState).editorPrepend! + + (state.workspaces[location] as IWorkspaceState).editorValue! + + (state.workspaces[location] as IWorkspaceState).editorPostpend! ); const chapter: number = yield select( (state: IState) => (state.workspaces[location] as IWorkspaceState).context.chapter @@ -79,10 +80,14 @@ function* workspaceSaga(): SagaIterator { const location = (action as actionTypes.IAction).payload.workspaceLocation; const index = (action as actionTypes.IAction).payload.testcaseId; const code: string = yield select( - (state: IState) => (state.workspaces[location] as IWorkspaceState).editorPrepend! + '\n' + - (state.workspaces[location] as IWorkspaceState).editorValue! + '\n' + - (state.workspaces[location] as IWorkspaceState).editorPostpend! + '\n' + - (state.workspaces[location] as IWorkspaceState).editorTestcases[index].program! + (state: IState) => + (state.workspaces[location] as IWorkspaceState).editorPrepend! + + '\n' + + (state.workspaces[location] as IWorkspaceState).editorValue! + + '\n' + + (state.workspaces[location] as IWorkspaceState).editorPostpend! + + '\n' + + (state.workspaces[location] as IWorkspaceState).editorTestcases[index].program! ); const chapter: number = yield select( (state: IState) => (state.workspaces[location] as IWorkspaceState).context.chapter From 8c1ab77ed28f40a947d0c4354b537072c1502ed7 Mon Sep 17 00:00:00 2001 From: Shuming Date: Wed, 17 Apr 2019 15:01:32 +0800 Subject: [PATCH 4/7] Merged 'master' from source-academy/cadet-frontend --- README.md | 8 +- package-lock.json | 44 +- package.json | 6 +- src/components/Application.tsx | 6 + src/components/NavigationBar.tsx | 9 + .../__snapshots__/Application.tsx.snap | 1 + .../__snapshots__/NavigationBar.tsx.snap | 12 + src/components/assessment/assessmentShape.ts | 13 +- .../missionControl/EditingOverviewCard.tsx | 285 +++++++++ .../missionControl/EditingWorkspace.tsx | 571 ++++++++++++++++++ .../ImportFromFileComponent.tsx | 104 ++++ .../missionControl/assessmentTemplates.ts | 126 ++++ .../DeploymentTab.tsx | 329 ++++++++++ .../GradingTab.tsx | 45 ++ .../MCQQuestionTemplateTab.tsx | 109 ++++ .../ManageQuestionTab.tsx | 179 ++++++ .../ProgrammingQuestionTemplateTab.tsx | 145 +++++ .../TextareaContent.tsx | 105 ++++ .../editingWorkspaceSideContent/index.tsx | 44 ++ .../exampleTestXml/mission-M1A.xml | 323 ++++++++++ .../exampleTestXml/mission-M6B.xml | 203 +++++++ src/components/missionControl/index.tsx | 110 ++++ .../missionControl/xmlParseHelper.ts | 359 +++++++++++ .../missionControl/xmlParseStrShapes.ts | 77 +++ src/components/workspace/ControlBar.tsx | 24 +- .../EditingWorkspaceContainer.ts | 82 +++ .../ImportFromFileComponentContainer.ts | 24 + src/containers/missionControl/index.ts | 30 + src/setupTests.ts | 8 + src/styles/_academy.scss | 7 + 30 files changed, 3375 insertions(+), 13 deletions(-) create mode 100644 src/components/missionControl/EditingOverviewCard.tsx create mode 100644 src/components/missionControl/EditingWorkspace.tsx create mode 100644 src/components/missionControl/ImportFromFileComponent.tsx create mode 100644 src/components/missionControl/assessmentTemplates.ts create mode 100644 src/components/missionControl/editingWorkspaceSideContent/DeploymentTab.tsx create mode 100644 src/components/missionControl/editingWorkspaceSideContent/GradingTab.tsx create mode 100644 src/components/missionControl/editingWorkspaceSideContent/MCQQuestionTemplateTab.tsx create mode 100644 src/components/missionControl/editingWorkspaceSideContent/ManageQuestionTab.tsx create mode 100644 src/components/missionControl/editingWorkspaceSideContent/ProgrammingQuestionTemplateTab.tsx create mode 100644 src/components/missionControl/editingWorkspaceSideContent/TextareaContent.tsx create mode 100644 src/components/missionControl/editingWorkspaceSideContent/index.tsx create mode 100644 src/components/missionControl/exampleTestXml/mission-M1A.xml create mode 100644 src/components/missionControl/exampleTestXml/mission-M6B.xml create mode 100644 src/components/missionControl/index.tsx create mode 100644 src/components/missionControl/xmlParseHelper.ts create mode 100644 src/components/missionControl/xmlParseStrShapes.ts mode change 100755 => 100644 src/components/workspace/ControlBar.tsx create mode 100644 src/containers/missionControl/EditingWorkspaceContainer.ts create mode 100644 src/containers/missionControl/ImportFromFileComponentContainer.ts create mode 100644 src/containers/missionControl/index.ts diff --git a/README.md b/README.md index f7a8ca69d4..1390d5a032 100644 --- a/README.md +++ b/README.md @@ -49,12 +49,6 @@ Note that this copies your files over, any future changes will not be reflected. You may try [this](https://medium.com/@alexishevia/the-magic-behind-npm-link-d94dcb3a81af) for a smoother experience. -## For Editing And Creating New Local XML Missions - -1. Use the branch 'mission-editing' in cadet-frontend -2. Run in browser with npm start -2. Go to Incubator tab. - ## Application Structure 1. `actions` contains action creators, one file per reducer, combined in index. @@ -70,4 +64,4 @@ You may try [this](https://medium.com/@alexishevia/the-magic-behind-npm-link-d94 ## TypeScript Coding Conventions -We reference [this guide](https://github.com/piotrwitek/react-redux-typescript-guide). +We reference [this guide](https://github.com/piotrwitek/react-redux-typescript-guide). \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5d5c484e66..06ed5b7600 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3648,6 +3648,15 @@ "@types/react": "16.4.11" } }, + "@types/react-textarea-autosize": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@types/react-textarea-autosize/-/react-textarea-autosize-4.3.4.tgz", + "integrity": "sha512-LLqG27BJGt8ja9x4umQXbnK9pRd0dI23X/GXBcuf476feOZ+e5QiKJYmWOHwAJC3YLl3YixDSigzfF4gzVQZ5w==", + "dev": true, + "requires": { + "@types/react": "16.4.11" + } + }, "@types/redux-mock-store": { "version": "0.0.13", "resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-0.0.13.tgz", @@ -3663,6 +3672,15 @@ "integrity": "sha512-uUSUP6XtyTclRzTH0NLkEIiEowxYXOWDeulpngrPltEceOmsGdhfrl8xr3D4QfJA7FuUUyHwFQuWWURLFg3hgg==", "dev": true }, + "@types/xml2js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.4.tgz", + "integrity": "sha512-O6Xgai01b9PB3IGA0lRIp1Ex3JBcxGDhdO0n3NIIpCyDOAjxcIGQFmkvgJpP8anTrthxOUQjBfLdRRi0Zn/TXA==", + "dev": true, + "requires": { + "@types/node": "11.13.0" + } + }, "JSONStream": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", @@ -19612,6 +19630,15 @@ "react-is": "16.4.2" } }, + "react-textarea-autosize": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-7.1.0.tgz", + "integrity": "sha512-c2FlR/fP0qbxmlrW96SdrbgP/v0XZMTupqB90zybvmDVDutytUgPl7beU35klwcTeMepUIQEpQUn3P3bdshGPg==", + "requires": { + "@babel/runtime": "7.1.2", + "prop-types": "15.6.0" + } + }, "react-transition-group": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.4.0.tgz", @@ -20285,8 +20312,7 @@ "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "scheduler": { "version": "0.10.0", @@ -23754,6 +23780,20 @@ "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", "dev": true }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": "1.2.4", + "xmlbuilder": "9.0.7" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", diff --git a/package.json b/package.json index b4f28f3907..af7ded8b4f 100755 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "react-router": "^4.2.0", "react-router-dom": "^4.2.2", "react-router-redux": "^5.0.0-alpha.9", + "react-textarea-autosize": "^7.1.0", "react-transition-group": "^2.3.1", "redux": "^3.7.2", "redux-mock-store": "^1.5.1", @@ -72,7 +73,8 @@ "sharedb-ace": "^1.0.9", "showdown": "^1.9.0", "typesafe-actions": "^3.2.1", - "utility-types": "^2.0.0" + "utility-types": "^2.0.0", + "xml2js": "^0.4.19" }, "devDependencies": { "@blueprintjs/core": "^2.1.1", @@ -101,8 +103,10 @@ "@types/react-router-dom": "^4.2.6", "@types/react-router-redux": "^5.0.13", "@types/react-test-renderer": "^16.0.1", + "@types/react-textarea-autosize": "^4.3.3", "@types/redux-mock-store": "^0.0.13", "@types/showdown": "^1.7.5", + "@types/xml2js": "^0.4.3", "babel-core": "6", "babel-runtime": "^6.23.0", "coveralls": "^3.0.1", diff --git a/src/components/Application.tsx b/src/components/Application.tsx index d1c8fe8c70..933abb22f4 100644 --- a/src/components/Application.tsx +++ b/src/components/Application.tsx @@ -5,6 +5,7 @@ import { Redirect, Route, RouteComponentProps, Switch } from 'react-router'; import Academy from '../containers/academy'; import Login from '../containers/LoginContainer'; +import MissionControlContainer from '../containers/missionControl'; import Playground from '../containers/PlaygroundContainer'; import { Role, sourceChapters } from '../reducers/states'; import { ExternalLibraryName, ExternalLibraryNames } from './assessment/assessmentShape'; @@ -30,6 +31,8 @@ export interface IDispatchProps { handlePlaygroundExternalSelect: (external: ExternalLibraryName) => void; } +const assessmentRegExp = ':assessmentId(-?\\d+)?/:questionId(\\d+)?'; + class Application extends React.Component { public componentDidMount() { parsePlayground(this.props); @@ -47,6 +50,7 @@ class Application extends React.Component {
+ @@ -86,6 +90,8 @@ const parsePlayground = (props: IApplicationProps) => { } }; +const toIncubator = (routerProps: RouteComponentProps) => ; + const parsePrgrm = (props: RouteComponentProps<{}>) => { const qsParsed = qs.parse(props.location.hash); // legacy support diff --git a/src/components/NavigationBar.tsx b/src/components/NavigationBar.tsx index 981292503a..d5606e4dc5 100644 --- a/src/components/NavigationBar.tsx +++ b/src/components/NavigationBar.tsx @@ -55,6 +55,15 @@ const NavigationBar: React.SFC = props => ( + + +
Mission-Control
+
+ + diff --git a/src/components/__tests__/__snapshots__/NavigationBar.tsx.snap b/src/components/__tests__/__snapshots__/NavigationBar.tsx.snap index 19e0a3ba4e..44d9baca9d 100644 --- a/src/components/__tests__/__snapshots__/NavigationBar.tsx.snap +++ b/src/components/__tests__/__snapshots__/NavigationBar.tsx.snap @@ -23,6 +23,12 @@ exports[`NavigationBar renders "Not logged in" correctly 1`] = ` + + +
+ Mission-Control +
+
@@ -63,6 +69,12 @@ exports[`NavigationBar renders correctly with username 1`] = ` + + +
+ Mission-Control +
+
diff --git a/src/components/assessment/assessmentShape.ts b/src/components/assessment/assessmentShape.ts index b30dd1df84..c94c239f17 100644 --- a/src/components/assessment/assessmentShape.ts +++ b/src/components/assessment/assessmentShape.ts @@ -10,12 +10,14 @@ export interface IAssessmentOverview { category: AssessmentCategory; closeAt: string; coverImage: string; + fileName?: string; grade: number; id: number; maxGrade: number; maxXp: number; openAt: string; title: string; + reading?: string; shortSummary: string; status: AssessmentStatus; story: string | null; @@ -45,6 +47,8 @@ export type GradingStatus = keyof typeof GradingStatuses; */ export interface IAssessment { category: AssessmentCategory; + globalDeployment?: Library; + graderDeployment?: Library; id: number; longSummary: string; missionPDF: string; @@ -69,6 +73,7 @@ export interface IProgrammingQuestion extends IQuestion { postpend: string; testcases: ITestcase[]; type: 'programming'; + graderTemplate?: string; } export interface ITestcase { @@ -87,10 +92,12 @@ export interface IMCQQuestion extends IQuestion { export interface IQuestion { answer: string | number | null; + editorValue?: string | null; comment: string | null; content: string; id: number; library: Library; + graderLibrary?: Library; type: QuestionType; grader: { name: string; @@ -135,5 +142,9 @@ type ExternalLibrary = { export type Library = { chapter: number; external: ExternalLibrary; - globals: Array<[string, any]>; + globals: Array<{ + 0: string; + 1: any; + 2?: string; + }>; }; diff --git a/src/components/missionControl/EditingOverviewCard.tsx b/src/components/missionControl/EditingOverviewCard.tsx new file mode 100644 index 0000000000..79a70bf3b8 --- /dev/null +++ b/src/components/missionControl/EditingOverviewCard.tsx @@ -0,0 +1,285 @@ +import { + Button, + Card, + Classes, + Dialog, + Elevation, + Icon, + IconName, + Intent, + MenuItem, + Text +} from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { ItemRenderer, Select } from '@blueprintjs/select'; +import * as React from 'react'; +import { NavLink } from 'react-router-dom'; +import Textarea from 'react-textarea-autosize'; + +import defaultCoverImage from '../../assets/default_cover_image.jpg'; +import { getPrettyDate } from '../../utils/dateHelpers'; +import { assessmentCategoryLink } from '../../utils/paramParseHelpers'; +import { exportXml, storeLocalAssessmentOverview } from './xmlParseHelper'; + +import { + AssessmentCategories, + AssessmentCategory, + IAssessmentOverview +} from '../assessment/assessmentShape'; +import { controlButton } from '../commons'; +import Markdown from '../commons/Markdown'; + +const DEFAULT_QUESTION_ID: number = 0; + +type Props = { + listingPath?: string; + overview: IAssessmentOverview; + updateEditingOverview: (overview: IAssessmentOverview) => void; +}; + +interface IState { + editingOverviewField: string; + fieldValue: any; + showOptionsOverlay: boolean; +} + +export class EditingOverviewCard extends React.Component { + public constructor(props: Props) { + super(props); + this.state = { + editingOverviewField: '', + fieldValue: '', + showOptionsOverlay: false + }; + } + + public render() { + return ( +
+ {this.optionsOverlay()} + {this.makeEditingOverviewCard(this.props.overview)} +
+ ); + } + + private saveEditOverview = (field: keyof IAssessmentOverview) => (e: any) => { + const overview = { + ...this.props.overview, + [field]: this.state.fieldValue + }; + this.setState({ + editingOverviewField: '', + fieldValue: '' + }); + storeLocalAssessmentOverview(overview); + this.props.updateEditingOverview(overview); + }; + + private handleEditOverview = () => (e: any) => { + this.setState({ + fieldValue: e.target.value + }); + }; + + private toggleEditField = (field: keyof IAssessmentOverview) => (e: any) => { + if (this.state.editingOverviewField !== field) { + this.setState({ + editingOverviewField: field, + fieldValue: this.props.overview[field] + }); + } + }; + + private toggleOptionsOverlay = () => { + this.setState({ + showOptionsOverlay: !this.state.showOptionsOverlay + }); + }; + + private handleExportXml = (e: any) => { + exportXml(); + }; + + private makeEditingOverviewTextarea = (field: keyof IAssessmentOverview) => ( +