diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index 17189b1cba..35a39af4aa 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -3,7 +3,7 @@ import { Chapter, Language, SourceError, Variant } from 'js-slang/dist/types'; import { AcademyState } from '../../features/academy/AcademyTypes'; import { AchievementState } from '../../features/achievement/AchievementTypes'; import { DashboardState } from '../../features/dashboard/DashboardTypes'; -import { Grading } from '../../features/grading/GradingTypes'; +import { GradingQuery } from '../../features/grading/GradingTypes'; import { PlaygroundState } from '../../features/playground/PlaygroundTypes'; import { PlaybackStatus, RecordingStatus } from '../../features/sourceRecorder/SourceRecorderTypes'; import { StoriesEnvState, StoriesState } from '../../features/stories/StoriesTypes'; @@ -508,7 +508,7 @@ export const defaultSession: SessionState = { sessionId: Date.now(), githubOctokitObject: { octokit: undefined }, gradingOverviews: undefined, - gradings: new Map(), + gradings: new Map(), notifications: [] }; diff --git a/src/commons/application/actions/SessionActions.ts b/src/commons/application/actions/SessionActions.ts index 010a137d8d..baacf92381 100644 --- a/src/commons/application/actions/SessionActions.ts +++ b/src/commons/application/actions/SessionActions.ts @@ -1,6 +1,6 @@ import { action } from 'typesafe-actions'; // EDITED -import { Grading, GradingOverview } from '../../../features/grading/GradingTypes'; +import { GradingOverview, GradingQuery } from '../../../features/grading/GradingTypes'; import { Assessment, AssessmentConfiguration, @@ -209,7 +209,7 @@ export const updateGradingOverviews = (overviews: GradingOverview[]) => * An extra id parameter is included here because of * no id for Grading. */ -export const updateGrading = (submissionId: number, grading: Grading) => +export const updateGrading = (submissionId: number, grading: GradingQuery) => action(UPDATE_GRADING, { submissionId, grading diff --git a/src/commons/application/actions/__tests__/SessionActions.ts b/src/commons/application/actions/__tests__/SessionActions.ts index ff15090d99..acb2cc35c4 100644 --- a/src/commons/application/actions/__tests__/SessionActions.ts +++ b/src/commons/application/actions/__tests__/SessionActions.ts @@ -1,6 +1,6 @@ import { Chapter, Variant } from 'js-slang/dist/types'; -import { Grading, GradingOverview } from '../../../../features/grading/GradingTypes'; +import { GradingOverview, GradingQuery } from '../../../../features/grading/GradingTypes'; import { Assessment, AssessmentOverview } from '../../../assessment/AssessmentTypes'; import { Notification } from '../../../notificationBadge/NotificationBadgeTypes'; import { GameState, Role, Story } from '../../ApplicationTypes'; @@ -542,26 +542,38 @@ test('updateGradingOverviews generates correct action object', () => { test('updateGrading generates correct action object', () => { const submissionId = 3; - const grading: Grading = [ - { - question: jest.genMockFromModule('../../../../features/grading/GradingTypes'), - student: { - name: 'test student', - username: 'E0123456', - id: 234 - }, - grade: { - xp: 100, - xpAdjustment: 0, - comments: 'Well done.', - grader: { - name: 'HARTIN MENZ', - id: 100 + const grading: GradingQuery = { + answers: [ + { + question: jest.genMockFromModule('../../../../features/grading/GradingTypes'), + student: { + name: 'test student', + username: 'E0123456', + id: 234 }, - gradedAt: '2019-08-16T13:26:32+00:00' + grade: { + xp: 100, + xpAdjustment: 0, + comments: 'Well done.', + grader: { + name: 'HARTIN MENZ', + id: 100 + }, + gradedAt: '2019-08-16T13:26:32+00:00' + } } + ], + assessment: { + coverPicture: 'https://i.imgur.com/dR7zBPI.jpeg', + id: 1, + number: '5', + reading: 'reading here', + story: 'story here', + summaryLong: 'long summary here', + summaryShort: 'short summary here', + title: 'assessment title here' } - ]; + }; const action = updateGrading(submissionId, grading); expect(action).toEqual({ diff --git a/src/commons/application/reducers/__tests__/SessionReducer.ts b/src/commons/application/reducers/__tests__/SessionReducer.ts index 8d622a5a15..9db52c5aae 100644 --- a/src/commons/application/reducers/__tests__/SessionReducer.ts +++ b/src/commons/application/reducers/__tests__/SessionReducer.ts @@ -1,6 +1,6 @@ import { Chapter, Variant } from 'js-slang/dist/types'; -import { Grading, GradingOverview } from '../../../../features/grading/GradingTypes'; +import { GradingOverview, GradingQuery } from '../../../../features/grading/GradingTypes'; import { Assessment, AssessmentOverview, @@ -381,37 +381,61 @@ test('UPDATE_ASSESSMENT_OVERVIEWS works correctly in updating assessment overvie }); // Test data for UPDATE_GRADING -const gradingTest1: Grading = [ - { - question: jest.genMockFromModule('../../../../features/grading/GradingTypes'), - student: { - name: 'test student', - username: 'E0123456', - id: 234 - }, - grade: { - xp: 100, - xpAdjustment: 0, - comments: 'Well done. Please try the quest!' +const gradingTest1: GradingQuery = { + answers: [ + { + question: jest.genMockFromModule('../../../../features/grading/GradingTypes'), + student: { + name: 'test student', + username: 'E0123456', + id: 234 + }, + grade: { + xp: 100, + xpAdjustment: 0, + comments: 'Well done. Please try the quest!' + } } + ], + assessment: { + coverPicture: 'test string', + id: 1, + number: 'M1A', + reading: 'test string', + story: 'test string', + summaryLong: 'test string', + summaryShort: 'test string', + title: 'test string' } -]; +}; -const gradingTest2: Grading = [ - { - question: jest.genMockFromModule('../../../../features/grading/GradingTypes'), - student: { - name: 'another test student', - username: 'E0000000', - id: 345 - }, - grade: { - xp: 500, - xpAdjustment: 20, - comments: 'Good job! All the best for the finals.' +const gradingTest2: GradingQuery = { + answers: [ + { + question: jest.genMockFromModule('../../../../features/grading/GradingTypes'), + student: { + name: 'another test student', + username: 'E0000000', + id: 345 + }, + grade: { + xp: 500, + xpAdjustment: 20, + comments: 'Good job! All the best for the finals.' + } } + ], + assessment: { + coverPicture: 'another test string', + id: 2, + number: 'P2', + reading: 'another test string', + story: 'another test string', + summaryLong: 'another test string', + summaryShort: 'another test string', + title: 'another test string' } -]; +}; test('UPDATE_GRADING works correctly in inserting gradings', () => { const submissionId = 23; @@ -423,14 +447,14 @@ test('UPDATE_GRADING works correctly in inserting gradings', () => { } }; - const gradingMap: Map = SessionsReducer(defaultSession, action).gradings; + const gradingMap: Map = SessionsReducer(defaultSession, action).gradings; expect(gradingMap.get(submissionId)).toEqual(gradingTest1); }); test('UPDATE_GRADING works correctly in inserting gradings and retains old data', () => { const submissionId1 = 45; const submissionId2 = 56; - const gradings = new Map(); + const gradings = new Map(); gradings.set(submissionId1, gradingTest1); const newDefaultSession = { @@ -446,14 +470,14 @@ test('UPDATE_GRADING works correctly in inserting gradings and retains old data' } }; - const gradingMap: Map = SessionsReducer(newDefaultSession, action).gradings; + const gradingMap: Map = SessionsReducer(newDefaultSession, action).gradings; expect(gradingMap.get(submissionId1)).toEqual(gradingTest1); expect(gradingMap.get(submissionId2)).toEqual(gradingTest2); }); test('UPDATE_GRADING works correctly in updating gradings', () => { const submissionId = 23; - const gradings = new Map(); + const gradings = new Map(); gradings.set(submissionId, gradingTest1); const newDefaultSession = { ...defaultSession, @@ -468,7 +492,7 @@ test('UPDATE_GRADING works correctly in updating gradings', () => { } }; - const gradingMap: Map = SessionsReducer(newDefaultSession, action).gradings; + const gradingMap: Map = SessionsReducer(newDefaultSession, action).gradings; expect(gradingMap.get(submissionId)).toEqual(gradingTest2); }); diff --git a/src/commons/application/types/SessionTypes.ts b/src/commons/application/types/SessionTypes.ts index 8ce124a1d4..e1e7b1cce0 100644 --- a/src/commons/application/types/SessionTypes.ts +++ b/src/commons/application/types/SessionTypes.ts @@ -1,7 +1,7 @@ import { Octokit } from '@octokit/rest'; import { Chapter, Variant } from 'js-slang/dist/types'; -import { Grading, GradingOverview } from '../../../features/grading/GradingTypes'; +import { GradingOverview, GradingQuery } from '../../../features/grading/GradingTypes'; import { Device, DeviceSession } from '../../../features/remoteExecution/RemoteExecutionTypes'; import { Assessment, @@ -115,7 +115,7 @@ export type SessionState = { readonly assessmentOverviews?: AssessmentOverview[]; readonly assessments: Map; readonly gradingOverviews?: GradingOverview[]; - readonly gradings: Map; + readonly gradings: Map; readonly notifications: Notification[]; readonly googleUser?: string; readonly githubAssessment?: MissionRepoData; diff --git a/src/commons/mocks/BackendMocks.ts b/src/commons/mocks/BackendMocks.ts index 4680125c74..1a8c5c4110 100644 --- a/src/commons/mocks/BackendMocks.ts +++ b/src/commons/mocks/BackendMocks.ts @@ -2,7 +2,11 @@ import { SagaIterator } from 'redux-saga'; import { call, put, select, takeEvery } from 'redux-saga/effects'; import { FETCH_GROUP_GRADING_SUMMARY } from '../../features/dashboard/DashboardTypes'; -import { Grading, GradingOverview, GradingQuestion } from '../../features/grading/GradingTypes'; +import { + GradingOverview, + GradingQuery, + GradingQuestion +} from '../../features/grading/GradingTypes'; import { OverallState, Role, @@ -177,7 +181,7 @@ export function* mockBackendSaga(): SagaIterator { const accessToken = yield select((state: OverallState) => state.session.accessToken); const grading = yield call(() => mockFetchGrading(accessToken, submissionId)); if (grading !== null) { - yield put(actions.updateGrading(submissionId, [...grading])); + yield put(actions.updateGrading(submissionId, grading)); } }); @@ -217,10 +221,10 @@ export function* mockBackendSaga(): SagaIterator { const { submissionId, questionId, xpAdjustment, comments } = action.payload; // Now, update the grade for the question in the Grading in the store - const grading: Grading = yield select((state: OverallState) => + const grading: GradingQuery = yield select((state: OverallState) => state.session.gradings.get(submissionId) ); - const newGrading = grading.slice().map((gradingQuestion: GradingQuestion) => { + const newGrading = grading.answers.slice().map((gradingQuestion: GradingQuestion) => { if (gradingQuestion.question.id === questionId) { gradingQuestion.grade = { xpAdjustment, @@ -230,7 +234,9 @@ export function* mockBackendSaga(): SagaIterator { } return gradingQuestion; }); - yield put(actions.updateGrading(submissionId, newGrading)); + yield put( + actions.updateGrading(submissionId, { answers: newGrading, assessment: grading.assessment }) + ); yield call(showSuccessMessage, 'Submitted!', 1000); }; diff --git a/src/commons/mocks/GradingMocks.ts b/src/commons/mocks/GradingMocks.ts index a634679582..d44729e292 100644 --- a/src/commons/mocks/GradingMocks.ts +++ b/src/commons/mocks/GradingMocks.ts @@ -1,5 +1,10 @@ import { GradingSummary } from '../../features/dashboard/DashboardTypes'; -import { Grading, GradingOverview } from '../../features/grading/GradingTypes'; +import { + GradingAnswer, + GradingAssessment, + GradingOverview, + GradingQuery +} from '../../features/grading/GradingTypes'; import { Role } from '../application/ApplicationTypes'; import { Testcase, TestcaseTypes } from '../assessment/AssessmentTypes'; import { mockLibrary } from './AssessmentMocks'; @@ -102,7 +107,7 @@ export const mockTestcases: Testcase[] = [ { type: TestcaseTypes.opaque, program: `remainder(17, 23) === 17;`, score: 2, answer: `true` } ]; -export const mockGrading: Grading = [ +export const mockGradingAnswer: GradingAnswer = [ { question: { answer: `function remainder(n, d) { @@ -386,20 +391,47 @@ New message from **Avenger**! } ]; +export const mockGradingAssessment: GradingAssessment = { + coverPicture: 'https://i.imgur.com/dR7zBPI.jpeg', + id: 1, + number: '10', + reading: + 'This is for you to read. Read it carefully. Perhaps you will find the answer to life here.', + story: `Story: +Start of story. +End of story. +The End. + +Credits +Starring: Source Academy`, + summaryLong: + 'This is the long summary of the assessment. It is a very very very very long summary', + summaryShort: 'This is short summary', + title: 'Assessment 1: Some Title' +}; + +export const mockGradingQuery: GradingQuery = { + answers: mockGradingAnswer, + assessment: mockGradingAssessment +}; + /** * Mock for fetching a trainer/admin's student grading information. * A null value is returned for invalid token or role. * * @param accessToken a valid access token for the cadet backend. */ -export const mockFetchGrading = (accessToken: string, submissionId: number): Grading | null => { +export const mockFetchGrading = ( + accessToken: string, + submissionId: number +): GradingQuery | null => { // mocks backend role fetching const permittedRoles: Role[] = [Role.Admin, Role.Staff]; const role: Role | null = mockFetchRole(accessToken); if (role === null || !permittedRoles.includes(role)) { return null; } else { - return mockGrading; + return mockGradingQuery; } }; diff --git a/src/commons/sagas/BackendSaga.ts b/src/commons/sagas/BackendSaga.ts index f95e3d2f61..e69259495d 100644 --- a/src/commons/sagas/BackendSaga.ts +++ b/src/commons/sagas/BackendSaga.ts @@ -9,7 +9,11 @@ import { FETCH_GROUP_GRADING_SUMMARY, GradingSummary } from '../../features/dashboard/DashboardTypes'; -import { Grading, GradingOverview, GradingQuestion } from '../../features/grading/GradingTypes'; +import { + GradingOverview, + GradingQuery, + GradingQuestion +} from '../../features/grading/GradingTypes'; import { CHANGE_DATE_ASSESSMENT, DELETE_ASSESSMENT, @@ -427,7 +431,7 @@ function* BackendSaga(): SagaIterator { const tokens: Tokens = yield selectTokens(); const id = action.payload; - const grading: Grading | null = yield call(getGrading, id, tokens); + const grading: GradingQuery | null = yield call(getGrading, id, tokens); if (grading) { yield put(actions.updateGrading(id, grading)); } @@ -489,10 +493,10 @@ function* BackendSaga(): SagaIterator { yield call(showSuccessMessage, 'Submitted!', 1000); // Now, update the grade for the question in the Grading in the store - const grading: Grading = yield select((state: OverallState) => + const grading: GradingQuery = yield select((state: OverallState) => state.session.gradings.get(submissionId) ); - const newGrading = grading.slice().map((gradingQuestion: GradingQuestion) => { + const newGrading = grading.answers.slice().map((gradingQuestion: GradingQuestion) => { if (gradingQuestion.question.id === questionId) { gradingQuestion.grade = { xpAdjustment, @@ -503,7 +507,9 @@ function* BackendSaga(): SagaIterator { return gradingQuestion; }); - yield put(actions.updateGrading(submissionId, newGrading)); + yield put( + actions.updateGrading(submissionId, { answers: newGrading, assessment: grading.assessment }) + ); }; const sendGradeAndContinue = function* ( diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 7f24ea8c83..0af9b25392 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -8,7 +8,12 @@ import { GoalProgress } from '../../features/achievement/AchievementTypes'; import { GradingSummary } from '../../features/dashboard/DashboardTypes'; -import { Grading, GradingOverview, GradingQuestion } from '../../features/grading/GradingTypes'; +import { + GradingAnswer, + GradingOverview, + GradingQuery, + GradingQuestion +} from '../../features/grading/GradingTypes'; import { Device, WebSocketEndpointInformation @@ -651,7 +656,10 @@ export const getGradingOverviews = async ( /** * GET /courses/{courseId}/admin/grading/{submissionId} */ -export const getGrading = async (submissionId: number, tokens: Tokens): Promise => { +export const getGrading = async ( + submissionId: number, + tokens: Tokens +): Promise => { const resp = await request(`${courseId()}/admin/grading/${submissionId}`, 'GET', { ...tokens }); @@ -661,7 +669,7 @@ export const getGrading = async (submissionId: number, tokens: Tokens): Promise< } const gradingResult = await resp.json(); - const grading: Grading = gradingResult.map((gradingQuestion: any) => { + const grading: GradingAnswer = gradingResult.answers.map((gradingQuestion: any) => { const { student, question, grade } = gradingQuestion; const result = { question: { @@ -695,7 +703,7 @@ export const getGrading = async (submissionId: number, tokens: Tokens): Promise< return result; }); - return grading; + return { answers: grading, assessment: gradingResult.assessment }; }; /** diff --git a/src/commons/sideContent/__tests__/SideContentAutograder.tsx b/src/commons/sideContent/__tests__/SideContentAutograder.tsx index 023c9636bf..ae26fdadd4 100644 --- a/src/commons/sideContent/__tests__/SideContentAutograder.tsx +++ b/src/commons/sideContent/__tests__/SideContentAutograder.tsx @@ -3,7 +3,7 @@ import { ErrorSeverity, ErrorType, SourceError } from 'js-slang/dist/types'; import { shallowRender } from 'src/commons/utils/TestUtils'; import { AutogradingResult, Testcase, TestcaseTypes } from '../../assessment/AssessmentTypes'; -import { mockGrading } from '../../mocks/GradingMocks'; +import { mockGradingAnswer } from '../../mocks/GradingMocks'; import SideContentAutograder, { SideContentAutograderProps } from '../SideContentAutograder'; const mockErrors: SourceError[] = [ @@ -66,7 +66,8 @@ const mockSecretTestcases: Testcase[] = [ const secretTestcaseCardClasses = publicTestcaseCardClasses.map(classes => `${classes} secret`); -const mockAutogradingResults: AutogradingResult[] = mockGrading[0].question.autogradingResults; +const mockAutogradingResults: AutogradingResult[] = + mockGradingAnswer[0].question.autogradingResults; const resultCardClasses = [ 'ResultCard correct', diff --git a/src/features/grading/GradingTypes.ts b/src/features/grading/GradingTypes.ts index b2cfbe5cab..14799b6f51 100644 --- a/src/features/grading/GradingTypes.ts +++ b/src/features/grading/GradingTypes.ts @@ -42,7 +42,23 @@ export type GradingOverviewWithNotifications = { * The information fetched before * grading a submission. */ -export type Grading = GradingQuestion[]; +export type GradingAnswer = GradingQuestion[]; + +export type GradingAssessment = { + coverPicture: string; + id: number; + number: string; + reading: string; + story: string; + summaryLong: string; + summaryShort: string; + title: string; +}; + +export type GradingQuery = { + answers: GradingAnswer; + assessment: GradingAssessment; +}; /** * Encapsulates information regarding grading a diff --git a/src/pages/academy/grading/subcomponents/GradingWorkspace.tsx b/src/pages/academy/grading/subcomponents/GradingWorkspace.tsx index 3797712db1..e7eae82e75 100644 --- a/src/pages/academy/grading/subcomponents/GradingWorkspace.tsx +++ b/src/pages/academy/grading/subcomponents/GradingWorkspace.tsx @@ -166,11 +166,11 @@ const GradingWorkspace: React.FC = props => { } let questionId = props.questionId; - if (props.questionId >= grading.length) { - questionId = grading.length - 1; + if (props.questionId >= grading.answers.length) { + questionId = grading.answers.length - 1; } - const question: AnsweredQuestion = grading[questionId].question; + const question: AnsweredQuestion = grading.answers[questionId].question; let answer: string = ''; if (question.type === QuestionTypes.programming) { @@ -212,7 +212,7 @@ const GradingWorkspace: React.FC = props => { * as the function to move to the next question does not check * if that question exists */ - if (grading[questionId] === undefined) { + if (grading.answers[questionId] === undefined) { navigate(`/courses/${courseId}/grading`); } else { checkWorkspaceReset(props); @@ -233,7 +233,7 @@ const GradingWorkspace: React.FC = props => { if (storedSubmissionId === submissionId && storedQuestionId === questionId) { return; } - const question = grading![questionId].question as Question; + const question = grading!.answers[questionId].question as Question; let autogradingResults: AutogradingResult[] = []; let editorValue: string = ''; @@ -298,22 +298,24 @@ const GradingWorkspace: React.FC = props => { /* Render an editor with the xp given to the current question. */ body: ( ), @@ -322,7 +324,7 @@ const GradingWorkspace: React.FC = props => { { label: `Question ${questionId + 1}`, iconName: IconNames.NINJA, - body: , + body: , id: SideContentType.questionOverview }, { @@ -337,9 +339,23 @@ const GradingWorkspace: React.FC = props => { /> ), id: SideContentType.autograder + }, + { + label: `Briefing`, + iconName: IconNames.BRIEFCASE, + body: ( + + ), + id: SideContentType.briefing } ]; - const externalLibrary = grading![questionId].question.library.external; + const externalLibrary = grading!.answers[questionId].question.library.external; const functionsAttached = externalLibrary.symbols; if (functionsAttached.includes('get_matrix')) { tabs.push({ @@ -375,7 +391,7 @@ const GradingWorkspace: React.FC = props => { const controlBarProps: (q: number) => ControlBarProps = (questionId: number) => { const listingPath = `/courses/${courseId}/grading`; const gradingWorkspacePath = listingPath + `/${props.submissionId}`; - const questionProgress: [number, number] = [questionId + 1, grading!.length]; + const questionProgress: [number, number] = [questionId + 1, grading!.answers.length]; const onClickPrevious = () => navigate(gradingWorkspacePath + `/${(questionId - 1).toString()}`); @@ -450,9 +466,10 @@ const GradingWorkspace: React.FC = props => { } /* If questionId is out of bounds, set it to the max. */ - const questionId = props.questionId >= grading.length ? grading.length - 1 : props.questionId; + const questionId = + props.questionId >= grading.answers.length ? grading.answers.length - 1 : props.questionId; /* Get the question to be graded */ - const question = grading[questionId].question as Question; + const question = grading.answers[questionId].question as Question; const workspaceProps: WorkspaceProps = { controlBarProps: controlBarProps(questionId), editorContainerProps: