diff --git a/src/components/assessment/assessmentShape.ts b/src/components/assessment/assessmentShape.ts index 6bd85067f7..f03483655c 100644 --- a/src/components/assessment/assessmentShape.ts +++ b/src/components/assessment/assessmentShape.ts @@ -16,6 +16,7 @@ export interface IAssessmentOverview { maxXp: number; openAt: string; title: string; + reading?: string; shortSummary: string; status: AssessmentStatus; story: string | null; diff --git a/src/components/incubator/EditingWorkspace.tsx b/src/components/incubator/EditingWorkspace.tsx index 8b59643872..73a67d1649 100644 --- a/src/components/incubator/EditingWorkspace.tsx +++ b/src/components/incubator/EditingWorkspace.tsx @@ -29,7 +29,8 @@ import { DeploymentTab, GradingTab, ManageQuestionTab, - QuestionTemplateTab, + MCQQuestionTemplateTab, + ProgrammingQuestionTemplateTab, TextareaContentTab } from './editingWorkspaceSideContent'; @@ -81,6 +82,7 @@ export type DispatchProps = { interface IState { assessment: IAssessment | null; activeTab: number; + editingMode: string; hasUnsavedChanges: boolean; showResetOverlay: boolean; originalMaxGrade: number; @@ -93,6 +95,7 @@ class AssessmentWorkspace extends React.Component - {this.resetOverlay(questionId)} + {this.resetOverlay()} ); @@ -194,7 +197,7 @@ class AssessmentWorkspace extends React.Component ( + private resetOverlay = () => ( { const assessment = retrieveLocalAssessment()!; - const question = assessment.questions[questionId] as IQuestion; this.handleRefreshLibrary(); this.setState({ assessment, hasUnsavedChanges: false, showResetOverlay: false, - originalMaxGrade: question.maxGrade, - originalMaxXp: question.maxXp + originalMaxGrade: this.getMaxMarks('maxGrade'), + originalMaxXp: this.getMaxMarks('maxXp') }); }, { minimal: false, intent: Intent.DANGER } @@ -253,7 +255,7 @@ class AssessmentWorkspace extends React.Component { + private handleRefreshLibrary = (library: Library | undefined = undefined) => { const question = this.state.assessment!.questions[this.formatedQuestionId()]; - let library = - question.library.chapter === -1 ? this.state.assessment!.globalDeployment! : question.library; + if (!library) { + library = + question.library.chapter === -1 + ? this.state.assessment!.globalDeployment! + : question.library; + } if (library && library.globals.length > 0) { const globalsVal = library.globals.map((x: any) => x[0]); const symbolsVal = library.external.symbols.concat(globalsVal); @@ -299,24 +300,22 @@ class AssessmentWorkspace extends React.Component { - const questionId = this.formatedQuestionId(); - const assessment = this.state.assessment!; - const curGrade = assessment.questions[questionId].maxGrade; + const curGrade = this.getMaxMarks('maxGrade'); const changeGrade = curGrade - this.state.originalMaxGrade; - const curXp = assessment.questions[questionId].maxXp; + const curXp = this.getMaxMarks('maxXp'); const changeXp = curXp - this.state.originalMaxXp; if (changeGrade !== 0 || changeXp !== 0) { const overview = this.props.assessmentOverview; if (changeGrade !== 0) { - overview.maxGrade += changeGrade; + overview.maxGrade = curGrade; } if (changeXp !== 0) { - overview.maxXp += changeXp; + overview.maxXp = curXp; } this.setState({ originalMaxGrade: curGrade, @@ -327,6 +326,14 @@ class AssessmentWorkspace extends React.Component { + let result = 0; + const questions = this.state.assessment!.questions; + for (const question of questions) { + result += question[field]; + } + return result as number; + }; private updateEditAssessmentState = (assessmentVal: IAssessment) => { this.setState({ assessment: assessmentVal, @@ -347,108 +354,162 @@ class AssessmentWorkspace extends React.Component { + const toggle = this.state.editingMode === 'question' ? 'global' : 'question'; + this.setState({ + activeTab: 0, + editingMode: toggle + }); + }; + /** Pre-condition: IAssessment has been loaded */ private sideContentProps: (p: AssessmentWorkspaceProps, q: number) => SideContentProps = ( props: AssessmentWorkspaceProps, questionId: number ) => { const assessment = this.state.assessment!; - const tabs = [ - { - label: `Task ${questionId + 1}`, - icon: IconNames.NINJA, - body: ( - - ) - }, - { - label: `${assessment!.category} Briefing`, - icon: IconNames.BRIEFCASE, - body: ( - - ) - }, - { - label: `Question Template`, - icon: IconNames.DOCUMENT, - body: ( - - ) - }, - { - label: `Manage Question`, - icon: IconNames.WRENCH, - body: ( - - ) - }, - { - label: `Manage Global Deployment`, - icon: IconNames.GLOBE, - body: ( - - ) - }, - { - label: `Manage Local Deployment`, - icon: IconNames.HOME, - body: ( - - ) + ); + + tabs = [ + { + label: `Task ${questionId + 1}`, + icon: IconNames.NINJA, + body: ( + + ) + }, + { + label: `Question Template`, + icon: IconNames.DOCUMENT, + body: questionTemplateTab + }, + { + label: `Manage Local Deployment`, + icon: IconNames.HOME, + body: ( + + ) + }, + { + label: `Manage Local Grader Deployment`, + icon: IconNames.CONFIRM, + body: ( + + ) + }, + { + label: `Grading`, + icon: IconNames.TICK, + body: ( + + ) + } + ]; + const functionsAttached = assessment!.questions[questionId].library.external.symbols; + if (functionsAttached.includes('get_matrix')) { + tabs.push({ + label: `Tone Matrix`, + icon: IconNames.GRID_VIEW, + body: + }); } - ]; - const isGraded = assessment!.questions[questionId].grader !== null; - if (isGraded) { - tabs.push({ - label: `Grading`, - icon: IconNames.TICK, - body: ( - - ) - }); + } else { + tabs = [ + { + label: `${assessment!.category} Briefing`, + icon: IconNames.BRIEFCASE, + body: ( + + ) + }, + { + label: `Manage Question`, + icon: IconNames.WRENCH, + body: ( + + ) + }, + { + label: `Manage Global Deployment`, + icon: IconNames.GLOBE, + body: ( + + ) + }, + { + label: `Manage Global Grader Deployment`, + icon: IconNames.CONFIRM, + body: ( + + ) + } + ]; } - const functionsAttached = assessment!.questions[questionId].library.external.symbols; - if (functionsAttached.includes('get_matrix')) { - tabs.push({ - label: `Tone Matrix`, - icon: IconNames.GRID_VIEW, - body: - }); - } return { activeTab: this.state.activeTab, handleChangeActiveTab: this.handleChangeActiveTab, @@ -466,7 +527,6 @@ class AssessmentWorkspace extends React.Component { maxXp: 0, openAt: '2000-01-01T00:00+08', title: 'Insert title here', + reading: '', shortSummary: 'Insert short summary here', status: AssessmentStatuses.not_attempted, story: 'mission', @@ -51,7 +52,7 @@ export const overviewTemplate = (): IAssessmentOverview => { export const programmingTemplate = (): IProgrammingQuestion => { return { - answer: '//1st question mock solution template', + answer: '// [Marking Scheme]\n// 1 mark for correct answer', comment: '`Great Job` **young padawan**', content: 'Enter content here', id: 0, diff --git a/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx b/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx index ee51e6c447..30c5533753 100644 --- a/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx @@ -14,10 +14,12 @@ import TextareaContent from './TextareaContent'; interface IProps { assessment: IAssessment; + label: string; pathToLibrary: Array; + pathToCopy?: Array; updateAssessment: (assessment: IAssessment) => void; handleRefreshLibrary: (library: Library) => void; - isGlobalDeployment: boolean; + isOptionalDeployment: boolean; } interface IChapter { @@ -31,30 +33,33 @@ interface IExternal { symbols: string[]; } -export class DeploymentTab extends React.Component< - IProps, - { activeTab: number; deploymentEnabled: boolean } -> { +export class DeploymentTab extends React.Component { public constructor(props: IProps) { super(props); this.state = { - activeTab: 0, - deploymentEnabled: false + activeTab: 0 }; } public render() { - if (this.props.isGlobalDeployment) { - return this.deploymentTab(); + if (!this.props.isOptionalDeployment) { + return ( +
+ {this.props.label + ' Deployment'} +
+ {this.deploymentTab()} + } +
+ ); } else { return (
- {this.state.deploymentEnabled ? this.deploymentTab() : null} + {this.isEmptyLibrary() ? null : this.deploymentTab()}
); } @@ -91,7 +96,7 @@ export class DeploymentTab extends React.Component< )); - const resetLibrary = controlButton('Refresh Library', IconNames.REFRESH, () => + const resetLibrary = controlButton('Use this Library', IconNames.REFRESH, () => this.props.handleRefreshLibrary(deployment) ); @@ -237,16 +242,25 @@ export class DeploymentTab extends React.Component< private handleSwitchDeployment = () => { const assessment = this.props.assessment; - if (this.state.deploymentEnabled) { - assignToPath(this.props.pathToLibrary, emptyLibrary(), assessment); + if (this.isEmptyLibrary()) { + let library = getValueFromPath( + this.props.pathToCopy || ['globalDeployment'], + assessment + ) as Library; + if (library.chapter === -1) { + library = assessment.globalDeployment!; + } + library = JSON.parse(JSON.stringify(library)); + assignToPath(this.props.pathToLibrary, library, assessment); } else { - assignToPath(this.props.pathToLibrary.concat(['chapter']), 1, assessment); + assignToPath(this.props.pathToLibrary, emptyLibrary(), assessment); } - this.setState({ - deploymentEnabled: !this.state.deploymentEnabled - }); this.props.updateAssessment(assessment); }; + + private isEmptyLibrary = (path: Array = this.props.pathToLibrary) => { + return getValueFromPath(path.concat(['chapter']), this.props.assessment) === -1; + }; } const removeSpaces = (str: string) => { diff --git a/src/components/incubator/editingWorkspaceSideContent/QuestionTemplateTab.tsx b/src/components/incubator/editingWorkspaceSideContent/MCQQuestionTemplateTab.tsx similarity index 54% rename from src/components/incubator/editingWorkspaceSideContent/QuestionTemplateTab.tsx rename to src/components/incubator/editingWorkspaceSideContent/MCQQuestionTemplateTab.tsx index 5dbef15fa7..f1c09d7e45 100644 --- a/src/components/incubator/editingWorkspaceSideContent/QuestionTemplateTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/MCQQuestionTemplateTab.tsx @@ -1,11 +1,10 @@ import { Card } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import * as React from 'react'; -import AceEditor from 'react-ace'; import { IAssessment, IMCQQuestion } from '../../assessment/assessmentShape'; import { controlButton } from '../../commons'; -import { assignToPath, getValueFromPath, limitNumberRange } from './'; +import { limitNumberRange } from './'; import TextareaContent from './TextareaContent'; interface IProps { @@ -14,91 +13,15 @@ interface IProps { updateAssessment: (assessment: IAssessment) => void; } -interface IState { - templateValue: string; - templateFocused: boolean; -} - -export class QuestionTemplateTab extends React.Component { +export class MCQQuestionTemplateTab extends React.Component { public constructor(props: IProps) { super(props); - this.state = { - templateValue: '', - templateFocused: false - }; } public render() { - return this.questionTemplateTab(); + return this.mcqTab(); } - private questionTemplateTab = () => { - // tslint:disable-next-line:no-console - // console.dir(this.props.assessment) - const type = this.props.assessment!.questions[this.props.questionId].type; - const display = type === 'mcq' ? this.mcqTab() : this.programmingTab(); - - return display; - }; - - private programmingTab = () => { - const path = ['questions', this.props.questionId, 'answer']; - - const handleTemplateChange = (newCode: string) => { - this.setState({ - templateValue: newCode - }); - }; - - const value = this.state.templateFocused - ? this.state.templateValue - : getValueFromPath(path, this.props.assessment); - - const display = ( -
- -
- ); - - return display; - }; - - private focusEditor = (path: Array) => (e: any): void => { - if (!this.state.templateFocused) { - this.setState({ - templateValue: getValueFromPath(path, this.props.assessment), - templateFocused: true - }); - } - }; - - private unFocusEditor = (path: Array) => (e: any): void => { - if (this.state.templateFocused) { - const value = getValueFromPath(path, this.props.assessment); - if (value !== this.state.templateValue) { - const assessmentVal = this.props.assessment; - assignToPath(path, this.state.templateValue, assessmentVal); - this.props.updateAssessment(assessmentVal); - } - this.setState({ - templateValue: '', - templateFocused: false - }); - } - }; - private mcqTab = () => { const questionId = this.props.questionId; const question = this.props.assessment!.questions[questionId] as IMCQQuestion; @@ -111,6 +34,7 @@ export class QuestionTemplateTab extends React.Component { {this.textareaContent(['questions', questionId, 'choices', i, 'hint'])} )); + const deleteButton = controlButton('Delete Option', IconNames.REMOVE, this.delOption); return (
@@ -118,9 +42,13 @@ export class QuestionTemplateTab extends React.Component {
{mcqButton} Solution: - {this.textareaContent(['questions', questionId, 'solution'], true, [0, 3])} + {this.textareaContent(['questions', questionId, 'solution'], true, [ + 0, + question.choices.length + ])} +
{controlButton('Add Option', IconNames.CONFIRM, this.addOption)} - {controlButton('Delete Option', IconNames.REMOVE, this.delOption)} + {question.choices.length > 0 ? deleteButton : undefined}
@@ -179,4 +107,4 @@ export class QuestionTemplateTab extends React.Component { }; } -export default QuestionTemplateTab; +export default MCQQuestionTemplateTab; diff --git a/src/components/incubator/editingWorkspaceSideContent/ProgrammingQuestionTemplateTab.tsx b/src/components/incubator/editingWorkspaceSideContent/ProgrammingQuestionTemplateTab.tsx new file mode 100644 index 0000000000..049c0a773a --- /dev/null +++ b/src/components/incubator/editingWorkspaceSideContent/ProgrammingQuestionTemplateTab.tsx @@ -0,0 +1,146 @@ +import { Switch } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import * as React from 'react'; +import AceEditor from 'react-ace'; + +import { IAssessment } from '../../assessment/assessmentShape'; +import { controlButton } from '../../commons'; +import { assignToPath, getValueFromPath } from './'; + +interface IProps { + assessment: IAssessment; + editorValue: string | null; + questionId: number; + updateAssessment: (assessment: IAssessment) => void; + handleEditorValueChange: (val: string) => void; +} + +interface IState { + templateValue: string; + templateFocused: boolean; + isSuggestedAnswer: boolean; +} + +export class ProgrammingQuestionTemplateTab extends React.Component { + public constructor(props: IProps) { + super(props); + this.state = { + templateValue: '', + templateFocused: false, + isSuggestedAnswer: false + }; + } + + public render() { + return this.programmingTab(); + } + + private programmingTab = () => { + const qnPath = ['questions', this.props.questionId]; + const templateSwitch = ( + + ); + const path = this.state.isSuggestedAnswer + ? qnPath.concat(['answer']) + : qnPath.concat(['solutionTemplate']); + + const copyFromEditorButton = controlButton( + 'Copy from Editor', + IconNames.IMPORT, + this.handleCopyFromEditor(path) + ); + + const copyToEditorButton = controlButton( + 'Copy to Editor', + IconNames.EXPORT, + this.handleCopyToEditor(path) + ); + + const handleTemplateChange = (newCode: string) => { + this.setState({ + templateValue: newCode + }); + }; + + const value = this.state.templateFocused + ? this.state.templateValue + : getValueFromPath(path, this.props.assessment); + + const editor = ( +
+ +
+ ); + + return ( +
+ {templateSwitch} + {copyFromEditorButton} + {copyToEditorButton} +
+
+ {editor} + } +
+ ); + }; + + private focusEditor = (path: Array) => (e: any): void => { + if (!this.state.templateFocused) { + this.setState({ + templateValue: getValueFromPath(path, this.props.assessment), + templateFocused: true + }); + } + }; + + private unFocusEditor = (path: Array) => (e: any): void => { + if (this.state.templateFocused) { + const value = getValueFromPath(path, this.props.assessment); + if (value !== this.state.templateValue) { + const assessmentVal = this.props.assessment; + assignToPath(path, this.state.templateValue, assessmentVal); + this.props.updateAssessment(assessmentVal); + } + this.setState({ + templateValue: '', + templateFocused: false + }); + } + }; + + private toggleTemplateMode = () => { + this.setState({ + isSuggestedAnswer: !this.state.isSuggestedAnswer + }); + }; + + private handleCopyFromEditor = (path: Array) => (): void => { + const assessment = this.props.assessment; + assignToPath(path, this.props.editorValue, assessment); + this.props.updateAssessment(assessment); + }; + + private handleCopyToEditor = (path: Array) => (): void => { + const value = getValueFromPath(path, this.props.assessment); + this.props.handleEditorValueChange(value); + }; +} + +export default ProgrammingQuestionTemplateTab; diff --git a/src/components/incubator/editingWorkspaceSideContent/index.tsx b/src/components/incubator/editingWorkspaceSideContent/index.tsx index 6d73029c78..7aef06beae 100644 --- a/src/components/incubator/editingWorkspaceSideContent/index.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/index.tsx @@ -1,10 +1,18 @@ import DeploymentTab from './DeploymentTab'; import GradingTab from './GradingTab'; import ManageQuestionTab from './ManageQuestionTab'; -import QuestionTemplateTab from './QuestionTemplateTab'; +import MCQQuestionTemplateTab from './MCQQuestionTemplateTab'; +import ProgrammingQuestionTemplateTab from './ProgrammingQuestionTemplateTab'; import TextareaContentTab from './TextareaContent'; -export { DeploymentTab, GradingTab, ManageQuestionTab, QuestionTemplateTab, TextareaContentTab }; +export { + DeploymentTab, + GradingTab, + ManageQuestionTab, + MCQQuestionTemplateTab, + ProgrammingQuestionTemplateTab, + TextareaContentTab +}; export const getValueFromPath = (path: Array, obj: any): any => { for (const next of path) { diff --git a/src/components/workspace/ControlBar.tsx b/src/components/workspace/ControlBar.tsx index 0d2de4ee04..536a641991 100644 --- a/src/components/workspace/ControlBar.tsx +++ b/src/components/workspace/ControlBar.tsx @@ -33,11 +33,13 @@ export type ControlBarProps = { hasUnsavedChanges?: boolean; isEditorAutorun?: boolean; isRunning: boolean; + editingMode?: string; onClickNext?(): any; onClickPrevious?(): any; onClickReturn?(): any; onClickSave?(): any; onClickReset?(): any; + toggleEditMode?(): void; }; interface IChapter { @@ -138,9 +140,10 @@ class ControlBar extends React.PureComponent { const stopAutorunButton = this.props.hasEditorAutorunButton ? controlButton('Autorun', IconNames.STOP, this.props.handleToggleEditorAutorun) : undefined; - const resetButton = this.props.hasSaveButton - ? controlButton('Reset', IconNames.REPEAT, this.props.onClickReset) - : undefined; + const resetButton = + this.props.onClickReset !== null + ? controlButton('Reset', IconNames.REPEAT, this.props.onClickReset) + : undefined; return (
{this.props.isEditorAutorun ? undefined : this.props.isRunning ? stopButton : runButton} @@ -182,15 +185,30 @@ class ControlBar extends React.PureComponent { } private replControl() { + const toggleEditModeButton = + this.props.toggleEditMode !== null ? ( + + {controlButton('Switch editing mode', IconNames.REFRESH, this.props.toggleEditMode)} + + ) : ( + undefined + ); const evalButton = ( {controlButton('Eval', IconNames.CODE, this.props.handleReplEval)} ); const clearButton = controlButton('Clear', IconNames.REMOVE, this.props.handleReplOutputClear); + return (
- {this.props.isRunning ? null : evalButton} {clearButton} + {this.props.isRunning ? null : evalButton} {clearButton} {toggleEditModeButton}
); } diff --git a/src/utils/xmlParser.ts b/src/utils/xmlParser.ts index 54765a77ce..d373ad1ed4 100644 --- a/src/utils/xmlParser.ts +++ b/src/utils/xmlParser.ts @@ -84,6 +84,7 @@ const makeAssessmentOverview = ( maxXp: maxXpVal, openAt: rawOverview.startdate, title: rawOverview.title, + reading: task.READING !== null ? task.READING[0] : '', shortSummary: task.WEBSUMMARY ? task.WEBSUMMARY[0] : '', status: AssessmentStatuses.attempting, story: rawOverview.story, @@ -208,8 +209,8 @@ const makeProgramming = ( ): IProgrammingQuestion => { const result: IProgrammingQuestion = { ...question, - answer: problem.SNIPPET[0].TEMPLATE[0] as string, - solutionTemplate: problem.SNIPPET[0].SOLUTION[0] as string, + solutionTemplate: problem.SNIPPET[0].TEMPLATE[0] as string, + answer: problem.SNIPPET[0].SOLUTION[0] as string, type: 'programming' }; if (problem.SNIPPET[0].GRADER) { @@ -297,6 +298,10 @@ export const assessmentToXml = ( }; task.$ = rawOverview; + if (overview.reading && overview.reading !== '') { + task.READING = overview.reading; + } + task.WEBSUMMARY = overview.shortSummary; task.TEXT = assessment.longSummary; task.PROBLEMS = { PROBLEM: [] }; @@ -336,13 +341,12 @@ export const assessmentToXml = ( } if (question.type === 'programming') { - problem.SNIPPET.SOLUTION = question.solutionTemplate; if (question.graderTemplate) { /* tslint:disable:no-string-literal */ problem.SNIPPET['GRADER'] = question.graderTemplate; } /* tslint:disable:no-string-literal */ - problem.SNIPPET['TEMPLATE'] = question.answer; + problem.SNIPPET['TEMPLATE'] = question.solutionTemplate; } if (question.type === 'mcq') {