diff --git a/src/commons/XMLParser/XMLParserHelper.ts b/src/commons/XMLParser/XMLParserHelper.ts index 0d67017cac..5e106fc2ba 100644 --- a/src/commons/XMLParser/XMLParserHelper.ts +++ b/src/commons/XMLParser/XMLParserHelper.ts @@ -9,7 +9,6 @@ import { AssessmentType, BaseQuestion, emptyLibrary, - GradingStatuses, IMCQQuestion, IProgrammingQuestion, Library, @@ -72,6 +71,7 @@ const makeAssessmentOverview = (result: any, maxXpVal: number): AssessmentOvervi return { type: capitalizeFirstLetter(rawOverview.kind) as AssessmentType, isManuallyGraded: true, // TODO: This is temporarily hardcoded to true. To be redone when overhauling MissionControl + isPublished: false, closeAt: rawOverview.duedate, coverImage: rawOverview.coverimage, id: EDITING_ID, @@ -84,8 +84,8 @@ const makeAssessmentOverview = (result: any, maxXpVal: number): AssessmentOvervi shortSummary: task.WEBSUMMARY ? task.WEBSUMMARY[0] : '', status: AssessmentStatuses.attempting, story: rawOverview.story, + isGradingPublished: false, xp: 0, - gradingStatus: 'none' as GradingStatuses, maxTeamSize: 1, hasVotingFeatures: false }; diff --git a/src/commons/achievement/utils/InsertFakeAchievements.ts b/src/commons/achievement/utils/InsertFakeAchievements.ts index 81dbef4901..866b549cbf 100644 --- a/src/commons/achievement/utils/InsertFakeAchievements.ts +++ b/src/commons/achievement/utils/InsertFakeAchievements.ts @@ -5,15 +5,16 @@ import { coverImageUrl } from '../../../features/achievement/AchievementConstants'; import { GoalType } from '../../../features/achievement/AchievementTypes'; -import { AssessmentConfiguration, AssessmentOverview } from '../../assessment/AssessmentTypes'; +import { + AssessmentConfiguration, + AssessmentOverview, + AssessmentStatuses +} from '../../assessment/AssessmentTypes'; import AchievementInferencer from './AchievementInferencer'; import { isExpired, isReleased } from './DateHelper'; -function assessmentCompleted(assessmentOverview: AssessmentOverview): boolean { - return ( - assessmentOverview.gradingStatus === 'graded' || - (!assessmentOverview.isManuallyGraded && assessmentOverview.status === 'submitted') - ); +function assessmentPublished(assessmentOverview: AssessmentOverview): boolean { + return assessmentOverview.isGradingPublished; } function insertFakeAchievements( @@ -34,7 +35,8 @@ function insertFakeAchievements( // Reduce clutter for achievements that cannot be earned at that point if ( !isReleased(new Date(assessmentOverview.openAt)) || - (isExpired(new Date(assessmentOverview.closeAt)) && assessmentOverview.status !== 'submitted') + (isExpired(new Date(assessmentOverview.closeAt)) && + assessmentOverview.status !== AssessmentStatuses.submitted) ) { return; } @@ -53,7 +55,7 @@ function insertFakeAchievements( requiredCompletionFrac: 0 } }, - assessmentOverview.status === 'submitted' + assessmentOverview.status === AssessmentStatuses.submitted ); // goal for assessment grading @@ -69,14 +71,14 @@ function insertFakeAchievements( requiredCompletionFrac: 0 } }, - assessmentOverview.gradingStatus === 'graded' + assessmentOverview.isGradingPublished ); } inferencer.insertFakeAchievement({ uuid: idString, title: assessmentOverview.title, - xp: assessmentCompleted(assessmentOverview) + xp: assessmentPublished(assessmentOverview) ? assessmentOverview.xp : assessmentOverview.maxXp, isVariableXp: false, @@ -98,7 +100,7 @@ function insertFakeAchievements( }); // if completed, add the uuid into the appropriate array - if (assessmentCompleted(assessmentOverview)) { + if (assessmentPublished(assessmentOverview)) { assessmentTypes.forEach((type, idx) => { if (type === assessmentOverview.type) { categorisedUuids[idx].push(idString); diff --git a/src/commons/application/actions/SessionActions.ts b/src/commons/application/actions/SessionActions.ts index c8cbc7d758..85b6098adc 100644 --- a/src/commons/application/actions/SessionActions.ts +++ b/src/commons/application/actions/SessionActions.ts @@ -1,7 +1,7 @@ import { createAction } from '@reduxjs/toolkit'; import { paginationToBackendParams, - ungradedToBackendParams + unpublishedToBackendParams } from 'src/features/grading/GradingUtils'; import { OptionType } from 'src/pages/academy/teamFormation/subcomponents/TeamFormationForm'; @@ -54,6 +54,7 @@ import { LOGOUT_GOOGLE, NotificationConfiguration, NotificationPreference, + PUBLISH_GRADING, REAUTOGRADE_ANSWER, REAUTOGRADE_SUBMISSION, REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN, @@ -74,6 +75,7 @@ import { SUBMIT_GRADING_AND_CONTINUE, TimeOption, Tokens, + UNPUBLISH_GRADING, UNSUBMIT_SUBMISSION, UPDATE_ASSESSMENT, UPDATE_ASSESSMENT_CONFIGS, @@ -134,7 +136,7 @@ export const fetchGrading = createAction(FETCH_GRADING, (submissionId: number) = /** * @param filterToGroup - param that when set to true, only shows submissions under the group * of the grader - * @param gradedFilter - backend params to filter to ungraded + * @param publishedFilter - backend params to filter to unpublished * @param pageParams - param that contains offset and pageSize, informing backend about how * many entries, starting from what offset, to get * @param filterParams - param that contains columnFilters converted into JSON for @@ -144,10 +146,10 @@ export const fetchGradingOverviews = createAction( FETCH_GRADING_OVERVIEWS, ( filterToGroup = true, - gradedFilter = ungradedToBackendParams(false), + publishedFilter = unpublishedToBackendParams(false), pageParams = paginationToBackendParams(0, 10), filterParams = {} - ) => ({ payload: { filterToGroup, gradedFilter, pageParams, filterParams } }) + ) => ({ payload: { filterToGroup, publishedFilter, pageParams, filterParams } }) ); export const fetchTeamFormationOverviews = createAction( @@ -326,6 +328,18 @@ export const unsubmitSubmission = createAction(UNSUBMIT_SUBMISSION, (submissionI payload: { submissionId } })); +/** + * Publishing / unpublishing actions + */ + +export const publishGrading = createAction(PUBLISH_GRADING, (submissionId: number) => ({ + payload: { submissionId } +})); + +export const unpublishGrading = createAction(UNPUBLISH_GRADING, (submissionId: number) => ({ + payload: { submissionId } +})); + /** * Notification actions */ diff --git a/src/commons/application/actions/__tests__/SessionActions.ts b/src/commons/application/actions/__tests__/SessionActions.ts index c3574e8edc..f83c160065 100644 --- a/src/commons/application/actions/__tests__/SessionActions.ts +++ b/src/commons/application/actions/__tests__/SessionActions.ts @@ -2,12 +2,18 @@ import { Chapter, Variant } from 'js-slang/dist/types'; import { mockStudents } from 'src/commons/mocks/UserMocks'; import { paginationToBackendParams, - ungradedToBackendParams + unpublishedToBackendParams } from 'src/features/grading/GradingUtils'; import { GradingOverviews, GradingQuery } from '../../../../features/grading/GradingTypes'; import { TeamFormationOverview } from '../../../../features/teamFormation/TeamFormationTypes'; -import { Assessment, AssessmentOverview } from '../../../assessment/AssessmentTypes'; +import { + Assessment, + AssessmentConfiguration, + AssessmentOverview, + AssessmentStatuses, + ProgressStatuses +} from '../../../assessment/AssessmentTypes'; import { Notification } from '../../../notificationBadge/NotificationBadgeTypes'; import { GameState, Role, Story } from '../../ApplicationTypes'; import { @@ -172,7 +178,7 @@ test('fetchGradingOverviews generates correct default action object', () => { type: FETCH_GRADING_OVERVIEWS, payload: { filterToGroup: true, - gradedFilter: ungradedToBackendParams(false), + publishedFilter: unpublishedToBackendParams(false), pageParams: paginationToBackendParams(0, 10), filterParams: {} } @@ -181,15 +187,15 @@ test('fetchGradingOverviews generates correct default action object', () => { test('fetchGradingOverviews generates correct action object', () => { const filterToGroup = false; - const gradedFilter = ungradedToBackendParams(true); + const publishedFilter = unpublishedToBackendParams(true); const pageParams = { offset: 123, pageSize: 456 }; const filterParams = { abc: 'xxx', def: 'yyy' }; - const action = fetchGradingOverviews(filterToGroup, gradedFilter, pageParams, filterParams); + const action = fetchGradingOverviews(filterToGroup, publishedFilter, pageParams, filterParams); expect(action).toEqual({ type: FETCH_GRADING_OVERVIEWS, payload: { filterToGroup: filterToGroup, - gradedFilter: gradedFilter, + publishedFilter: publishedFilter, pageParams: pageParams, filterParams: filterParams } @@ -328,11 +334,12 @@ test('setCourseRegistration generates correct action object', () => { }); test('setAssessmentConfigurations generates correct action object', () => { - const assesmentConfigurations = [ + const assesmentConfigurations: AssessmentConfiguration[] = [ { assessmentConfigId: 1, type: 'Mission1', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hasTokenCounter: false, hasVotingFeatures: false, @@ -343,6 +350,7 @@ test('setAssessmentConfigurations generates correct action object', () => { assessmentConfigId: 2, type: 'Mission2', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hasTokenCounter: false, hasVotingFeatures: false, @@ -353,6 +361,7 @@ test('setAssessmentConfigurations generates correct action object', () => { assessmentConfigId: 3, type: 'Mission3', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hasTokenCounter: false, hasVotingFeatures: false, @@ -533,6 +542,7 @@ test('updateAssessmentOverviews generates correct action object', () => { { type: 'Missions', isManuallyGraded: true, + isPublished: false, closeAt: 'test_string', coverImage: 'test_string', id: 0, @@ -541,10 +551,10 @@ test('updateAssessmentOverviews generates correct action object', () => { openAt: 'test_string', title: 'test_string', shortSummary: 'test_string', - status: 'not_attempted', + status: AssessmentStatuses.not_attempted, story: null, xp: 0, - gradingStatus: 'none', + isGradingPublished: false, maxTeamSize: 1, hasVotingFeatures: false } @@ -595,9 +605,10 @@ test('updateGradingOverviews generates correct action object', () => { studentUsername: 'E0123456', studentUsernames: [], submissionId: 1, - submissionStatus: 'attempting', + isGradingPublished: false, + progress: ProgressStatuses.attempting, groupName: 'group', - gradingStatus: 'excluded', + submissionStatus: AssessmentStatuses.attempting, questionCount: 6, gradedCount: 0 } @@ -772,6 +783,7 @@ test('updateAssessmentTypes generates correct action object', () => { assessmentConfigId: 1, type: 'Missions', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hasTokenCounter: false, hasVotingFeatures: false, @@ -782,6 +794,7 @@ test('updateAssessmentTypes generates correct action object', () => { assessmentConfigId: 2, type: 'Quests', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hasTokenCounter: false, hasVotingFeatures: false, @@ -791,7 +804,8 @@ test('updateAssessmentTypes generates correct action object', () => { { assessmentConfigId: 3, type: 'Paths', - isManuallyGraded: true, + isManuallyGraded: false, + isGradingAutoPublished: true, displayInDashboard: true, hasTokenCounter: false, hasVotingFeatures: false, @@ -802,6 +816,7 @@ test('updateAssessmentTypes generates correct action object', () => { assessmentConfigId: 4, type: 'Contests', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hasTokenCounter: false, hasVotingFeatures: false, @@ -810,8 +825,20 @@ test('updateAssessmentTypes generates correct action object', () => { }, { assessmentConfigId: 5, + type: 'PEs', + isManuallyGraded: false, + isGradingAutoPublished: false, + displayInDashboard: true, + hasTokenCounter: false, + hasVotingFeatures: false, + hoursBeforeEarlyXpDecay: 0, + earlySubmissionXp: 0 + }, + { + assessmentConfigId: 6, type: 'Others', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hasTokenCounter: false, hasVotingFeatures: false, @@ -831,6 +858,7 @@ test('deleteAssessmentConfig generates correct action object', () => { assessmentConfigId: 1, type: 'Mission1', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hasTokenCounter: false, hasVotingFeatures: false, diff --git a/src/commons/application/reducers/__tests__/SessionReducer.ts b/src/commons/application/reducers/__tests__/SessionReducer.ts index 80c9e86cfd..ac19971a86 100644 --- a/src/commons/application/reducers/__tests__/SessionReducer.ts +++ b/src/commons/application/reducers/__tests__/SessionReducer.ts @@ -5,7 +5,7 @@ import { Assessment, AssessmentOverview, AssessmentStatuses, - GradingStatuses + ProgressStatuses } from '../../../assessment/AssessmentTypes'; import { Notification } from '../../../notificationBadge/NotificationBadgeTypes'; import { defaultSession, GameState, Role, Story } from '../../ApplicationTypes'; @@ -158,6 +158,7 @@ test('SET_ASSESSMENT_CONFIGURATIONS works correctly', () => { hoursBeforeEarlyXpDecay: 48, earlySubmissionXp: 200, isManuallyGraded: false, + isGradingAutoPublished: false, displayInDashboard: true, hasTokenCounter: false, hasVotingFeatures: false @@ -171,6 +172,7 @@ test('SET_ASSESSMENT_CONFIGURATIONS works correctly', () => { hoursBeforeEarlyXpDecay: 48, earlySubmissionXp: 200, isManuallyGraded: false, + isGradingAutoPublished: false, displayInDashboard: true, hasTokenCounter: false, hasVotingFeatures: false @@ -184,6 +186,7 @@ test('SET_ASSESSMENT_CONFIGURATIONS works correctly', () => { hoursBeforeEarlyXpDecay: 48, earlySubmissionXp: 200, isManuallyGraded: false, + isGradingAutoPublished: false, displayInDashboard: true, hasTokenCounter: false, hasVotingFeatures: false @@ -331,6 +334,7 @@ const assessmentOverviewsTest1: AssessmentOverview[] = [ { type: 'Missions', isManuallyGraded: true, + isPublished: false, closeAt: 'test_string', coverImage: 'test_string', id: 0, @@ -342,7 +346,7 @@ const assessmentOverviewsTest1: AssessmentOverview[] = [ status: AssessmentStatuses.not_attempted, story: null, xp: 0, - gradingStatus: GradingStatuses.none, + isGradingPublished: false, maxTeamSize: 5, hasVotingFeatures: false } @@ -352,6 +356,7 @@ const assessmentOverviewsTest2: AssessmentOverview[] = [ { type: 'Contests', isManuallyGraded: true, + isPublished: false, closeAt: 'test_string_0', coverImage: 'test_string_0', fileName: 'test_sting_0', @@ -364,7 +369,7 @@ const assessmentOverviewsTest2: AssessmentOverview[] = [ status: AssessmentStatuses.attempted, story: null, xp: 1, - gradingStatus: GradingStatuses.grading, + isGradingPublished: false, maxTeamSize: 1, hasVotingFeatures: false } @@ -537,11 +542,12 @@ const gradingOverviewTest1: GradingOverview[] = [ studentUsername: 'E0123456', studentUsernames: [], submissionId: 1, - submissionStatus: 'attempting', + submissionStatus: AssessmentStatuses.attempting, + progress: ProgressStatuses.attempting, + isGradingPublished: false, groupName: 'group', - gradingStatus: 'excluded', - questionCount: 0, - gradedCount: 6 + questionCount: 4, + gradedCount: 2 } ]; @@ -562,11 +568,12 @@ const gradingOverviewTest2: GradingOverview[] = [ studentUsername: 'E0000000', studentUsernames: [], submissionId: 2, - submissionStatus: 'attempted', + submissionStatus: AssessmentStatuses.attempted, + progress: ProgressStatuses.graded, + isGradingPublished: false, groupName: 'another group', - gradingStatus: 'excluded', - questionCount: 6, - gradedCount: 0 + questionCount: 3, + gradedCount: 3 } ]; diff --git a/src/commons/application/types/SessionTypes.ts b/src/commons/application/types/SessionTypes.ts index 39eaeae823..64799cde16 100644 --- a/src/commons/application/types/SessionTypes.ts +++ b/src/commons/application/types/SessionTypes.ts @@ -34,6 +34,7 @@ export const LOGIN = 'LOGIN'; export const LOGOUT_GOOGLE = 'LOGOUT_GOOGLE'; export const LOGIN_GITHUB = 'LOGIN_GITHUB'; export const LOGOUT_GITHUB = 'LOGOUT_GITHUB'; +export const PUBLISH_GRADING = 'PUBLISH_GRADING'; export const SET_TOKENS = 'SET_TOKENS'; export const SET_USER = 'SET_USER'; export const SET_COURSE_CONFIGURATION = 'SET_COURSE_CONFIGURATION'; @@ -51,6 +52,7 @@ export const REAUTOGRADE_SUBMISSION = 'REAUTOGRADE_SUBMISSION'; export const REAUTOGRADE_ANSWER = 'REAUTOGRADE_ANSWER'; export const REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN = 'REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN'; +export const UNPUBLISH_GRADING = 'UNPUBLISH_GRADING'; export const UNSUBMIT_SUBMISSION = 'UNSUBMIT_SUBMISSION'; export const UPDATE_ASSESSMENT_OVERVIEWS = 'UPDATE_ASSESSMENT_OVERVIEWS'; export const UPDATE_TOTAL_XP = 'UPDATE_TOTAL_XP'; diff --git a/src/commons/assessment/Assessment.tsx b/src/commons/assessment/Assessment.tsx index 87ec4ba7c3..f36fb3e139 100644 --- a/src/commons/assessment/Assessment.tsx +++ b/src/commons/assessment/Assessment.tsx @@ -50,8 +50,7 @@ import { AssessmentConfiguration, AssessmentOverview, AssessmentStatuses, - AssessmentWorkspaceParams, - GradingStatuses + AssessmentWorkspaceParams } from './AssessmentTypes'; export type AssessmentProps = { @@ -161,10 +160,8 @@ const Assessment: React.FC = props => { overview: AssessmentOverview, index: number, renderAttemptButton: boolean, - renderGradingStatus: boolean + renderGradingTooltip: boolean ) => { - const showGrade = - overview.gradingStatus === 'graded' || !props.assessmentConfiguration.isManuallyGraded; return (
@@ -181,10 +178,12 @@ const Assessment: React.FC = props => { />
- {makeOverviewCardTitle(overview, index, renderGradingStatus)} + {makeOverviewCardTitle(overview, index, renderGradingTooltip)}
- {showGrade ? `XP: ${overview.xp} / ${overview.maxXp}` : `Max XP: ${overview.maxXp}`} + {overview.isGradingPublished + ? `XP: ${overview.xp} / ${overview.maxXp}` + : `Max XP: ${overview.maxXp}`}
@@ -227,7 +226,7 @@ const Assessment: React.FC = props => { const makeOverviewCardTitle = ( overview: AssessmentOverview, index: number, - renderGradingStatus: boolean + renderProgressStatus: boolean ) => (
@@ -241,7 +240,7 @@ const Assessment: React.FC = props => { ) : null} - {renderGradingStatus ? makeGradingStatus(overview.gradingStatus) : null} + {renderProgressStatus ? showGradingTooltip(overview.isGradingPublished) : null}
{makeSubmissionButton(overview, index)}
@@ -421,33 +420,20 @@ const Assessment: React.FC = props => { ); }; -const makeGradingStatus = (gradingStatus: string) => { +const showGradingTooltip = (isGradingPublished: boolean) => { let iconName: IconName; let intent: Intent; let tooltip: string; - switch (gradingStatus) { - case GradingStatuses.graded: - iconName = IconNames.TICK; - intent = Intent.SUCCESS; - tooltip = 'Fully graded'; - break; - case GradingStatuses.grading: - iconName = IconNames.TIME; - intent = Intent.WARNING; - tooltip = 'Grading in progress'; - break; - case GradingStatuses.none: - iconName = IconNames.CROSS; - intent = Intent.DANGER; - tooltip = 'Not graded yet'; - break; - default: - // Shows default icon if this assessment is ungraded - iconName = IconNames.DISABLE; - intent = Intent.PRIMARY; - tooltip = `Not applicable`; - break; + if (isGradingPublished) { + iconName = IconNames.TICK; + intent = Intent.SUCCESS; + tooltip = 'Fully graded'; + } else { + // shh, hide actual grading progress from users even if graded + iconName = IconNames.TIME; + intent = Intent.WARNING; + tooltip = 'Grading in progress'; } return ( diff --git a/src/commons/assessment/AssessmentTypes.ts b/src/commons/assessment/AssessmentTypes.ts index 6c9c8be7a4..e61ea9e5b5 100644 --- a/src/commons/assessment/AssessmentTypes.ts +++ b/src/commons/assessment/AssessmentTypes.ts @@ -13,19 +13,26 @@ export enum AssessmentStatuses { } export type AssessmentStatus = keyof typeof AssessmentStatuses; +// Devnote: If adjusting this, ensure that each status can be uniquely attributed to one set of backend parameters, and vice versa. +// This allows for a clean conversion from progress status to backend parameters, ensuring only backend pagination. +// Adjust the conversion functions in GradingUtils accordingly. +export enum ProgressStatuses { + autograded = 'autograded', + not_attempted = 'not_attempted', + attempting = 'attempting', + attempted = 'attempted', + submitted = 'submitted', + graded = 'graded', + published = 'published' +} + +export type ProgressStatus = keyof typeof ProgressStatuses; + export type AssessmentWorkspaceParams = { assessmentId?: string; questionId?: string; }; -export enum GradingStatuses { - excluded = 'excluded', - graded = 'graded', - grading = 'grading', - none = 'none' -} -export type GradingStatus = keyof typeof GradingStatuses; - export type AssessmentType = string; export enum TestcaseTypes { @@ -59,9 +66,8 @@ export type AssessmentOverview = { closeAt: string; coverImage: string; fileName?: string; // For mission control - gradingStatus: GradingStatus; id: number; - isPublished?: boolean; + isPublished?: boolean; // refers to assessment as a whole being published hasVotingFeatures: boolean; hasTokenCounter?: boolean; isVotingPublished?: boolean; @@ -70,6 +76,7 @@ export type AssessmentOverview = { number?: string; // For mission control openAt: string; private?: boolean; + isGradingPublished: boolean; // refers to specific assessment submission being published reading?: string; // For mission control shortSummary: string; status: AssessmentStatus; @@ -98,6 +105,7 @@ export type AssessmentConfiguration = { assessmentConfigId: number; type: AssessmentType; isManuallyGraded: boolean; + isGradingAutoPublished: boolean; displayInDashboard: boolean; hoursBeforeEarlyXpDecay: number; earlySubmissionXp: number; @@ -242,6 +250,7 @@ export const overviewTemplate = (): AssessmentOverview => { closeAt: '2100-12-01T00:00+08', coverImage: 'https://fakeimg.pl/300/', id: -1, + isPublished: false, maxXp: 0, earlySubmissionXp: 0, openAt: '2000-01-01T00:00+08', @@ -250,8 +259,8 @@ export const overviewTemplate = (): AssessmentOverview => { shortSummary: 'Insert short summary here', status: AssessmentStatuses.not_attempted, story: 'mission', + isGradingPublished: false, xp: 0, - gradingStatus: 'none', maxTeamSize: 1, hasVotingFeatures: false }; diff --git a/src/commons/assessment/__tests__/Assessment.tsx b/src/commons/assessment/__tests__/Assessment.tsx index 1863ffe91e..739d97b941 100644 --- a/src/commons/assessment/__tests__/Assessment.tsx +++ b/src/commons/assessment/__tests__/Assessment.tsx @@ -16,6 +16,7 @@ const mockAssessmentProps = assertType()({ assessmentConfigId: 1, type: 'Missions', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hasTokenCounter: false, hasVotingFeatures: false, diff --git a/src/commons/assessmentWorkspace/__tests__/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/__tests__/AssessmentWorkspace.tsx index 34206febe5..7892a814d6 100644 --- a/src/commons/assessmentWorkspace/__tests__/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/__tests__/AssessmentWorkspace.tsx @@ -25,6 +25,7 @@ const defaultProps = assertType()({ assessmentConfigId: 1, type: 'Missions', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hasTokenCounter: false, hasVotingFeatures: false, diff --git a/src/commons/editingWorkspaceSideContent/EditingWorkspaceSideContentAutograderTab.tsx b/src/commons/editingWorkspaceSideContent/EditingWorkspaceSideContentAutograderTab.tsx index 0cb676c1b8..f8f056aee8 100644 --- a/src/commons/editingWorkspaceSideContent/EditingWorkspaceSideContentAutograderTab.tsx +++ b/src/commons/editingWorkspaceSideContent/EditingWorkspaceSideContentAutograderTab.tsx @@ -43,7 +43,6 @@ export const AutograderTab: React.FC = props => {
- {/* {makeOverviewCardTitle(overview, index, setBetchaAssessment, renderGradingStatus)} */}
Test Program: diff --git a/src/commons/mocks/AssessmentMocks.ts b/src/commons/mocks/AssessmentMocks.ts index 35e80a0d6d..4a3c8ad5dd 100644 --- a/src/commons/mocks/AssessmentMocks.ts +++ b/src/commons/mocks/AssessmentMocks.ts @@ -6,7 +6,6 @@ import { AssessmentConfiguration, AssessmentOverview, AssessmentStatuses, - GradingStatuses, IContestVotingQuestion, IMCQQuestion, IProgrammingQuestion, @@ -20,6 +19,7 @@ export const mockAssessmentConfigurations: AssessmentConfiguration[][] = [ assessmentConfigId: 1, type: 'Missions', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hoursBeforeEarlyXpDecay: 48, hasTokenCounter: false, @@ -30,6 +30,7 @@ export const mockAssessmentConfigurations: AssessmentConfiguration[][] = [ assessmentConfigId: 2, type: 'Quests', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hoursBeforeEarlyXpDecay: 48, hasTokenCounter: false, @@ -39,7 +40,8 @@ export const mockAssessmentConfigurations: AssessmentConfiguration[][] = [ { assessmentConfigId: 3, type: 'Paths', - isManuallyGraded: true, + isManuallyGraded: false, + isGradingAutoPublished: true, displayInDashboard: true, hoursBeforeEarlyXpDecay: 48, hasTokenCounter: false, @@ -50,6 +52,7 @@ export const mockAssessmentConfigurations: AssessmentConfiguration[][] = [ assessmentConfigId: 4, type: 'Contests', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hoursBeforeEarlyXpDecay: 48, hasTokenCounter: false, @@ -60,6 +63,7 @@ export const mockAssessmentConfigurations: AssessmentConfiguration[][] = [ assessmentConfigId: 5, type: 'Others', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hoursBeforeEarlyXpDecay: 48, hasTokenCounter: false, @@ -72,6 +76,7 @@ export const mockAssessmentConfigurations: AssessmentConfiguration[][] = [ assessmentConfigId: 1, type: 'Mission Impossible', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hoursBeforeEarlyXpDecay: 48, hasTokenCounter: false, @@ -82,6 +87,7 @@ export const mockAssessmentConfigurations: AssessmentConfiguration[][] = [ assessmentConfigId: 2, type: 'Data Structures', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hoursBeforeEarlyXpDecay: 48, hasTokenCounter: false, @@ -92,6 +98,7 @@ export const mockAssessmentConfigurations: AssessmentConfiguration[][] = [ assessmentConfigId: 3, type: 'Algorithm Frenzy', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hoursBeforeEarlyXpDecay: 48, hasTokenCounter: false, @@ -108,6 +115,7 @@ const mockUnopenedAssessmentsOverviews: AssessmentOverview[] = [ closeAt: '2048-06-18T05:24:26.026Z', coverImage: 'https://fakeimg.pl/300/', id: 1, + isPublished: false, maxXp: 1000, earlySubmissionXp: 0, openAt: '2038-06-18T05:24:26.026Z', @@ -117,7 +125,7 @@ const mockUnopenedAssessmentsOverviews: AssessmentOverview[] = [ status: AssessmentStatuses.not_attempted, story: 'mission-1', xp: 0, - gradingStatus: GradingStatuses.none, + isGradingPublished: false, maxTeamSize: 1, hasVotingFeatures: false } @@ -130,6 +138,7 @@ const mockOpenedAssessmentsOverviews: AssessmentOverview[] = [ closeAt: '2048-06-18T05:24:26.026Z', coverImage: 'https://fakeimg.pl/300/', id: 2, + isPublished: false, maxXp: 1000, earlySubmissionXp: 0, openAt: '2018-06-18T05:24:26.026Z', @@ -151,13 +160,14 @@ const mockOpenedAssessmentsOverviews: AssessmentOverview[] = [ status: AssessmentStatuses.attempted, story: 'mission-1', xp: 1, - gradingStatus: GradingStatuses.none, + isGradingPublished: false, maxTeamSize: 4, hasVotingFeatures: false }, { type: 'Missions', isManuallyGraded: true, + isPublished: false, closeAt: '2048-06-18T05:24:26.026Z', coverImage: 'https://fakeimg.pl/350x200/?text=World&font=lobster', id: 3, @@ -170,13 +180,14 @@ const mockOpenedAssessmentsOverviews: AssessmentOverview[] = [ status: AssessmentStatuses.attempting, story: 'mission-2', xp: 2, - gradingStatus: GradingStatuses.none, + isGradingPublished: false, maxTeamSize: 1, hasVotingFeatures: false }, { type: 'Quests', isManuallyGraded: true, + isPublished: false, closeAt: '2048-06-18T05:24:26.026Z', coverImage: 'https://fakeimg.pl/350x200/?text=Hello', id: 4, @@ -189,13 +200,14 @@ const mockOpenedAssessmentsOverviews: AssessmentOverview[] = [ status: AssessmentStatuses.not_attempted, story: 'sidequest-2.1', xp: 3, - gradingStatus: GradingStatuses.none, + isGradingPublished: false, maxTeamSize: 2, hasVotingFeatures: false }, { type: 'Paths', isManuallyGraded: true, + isPublished: false, closeAt: '2069-04-20T01:23:45.111Z', coverImage: 'https://fakeimg.pl/700x400/417678,64/?text=%E3%83%91%E3%82%B9&font=noto', id: 5, @@ -208,13 +220,14 @@ const mockOpenedAssessmentsOverviews: AssessmentOverview[] = [ status: AssessmentStatuses.not_attempted, story: null, xp: 0, - gradingStatus: GradingStatuses.excluded, + isGradingPublished: false, maxTeamSize: 2, hasVotingFeatures: false }, { type: 'Others', isManuallyGraded: false, + isPublished: false, closeAt: '2048-06-18T05:24:26.026Z', coverImage: 'https://fakeimg.pl/350x200/?text=Hello', id: 6, @@ -227,9 +240,9 @@ const mockOpenedAssessmentsOverviews: AssessmentOverview[] = [ status: AssessmentStatuses.not_attempted, story: 'sidequest-2.1', xp: 3, - gradingStatus: GradingStatuses.none, private: true, maxTeamSize: 1, + isGradingPublished: false, hasVotingFeatures: false } ]; @@ -238,6 +251,7 @@ const mockClosedAssessmentOverviews: AssessmentOverview[] = [ { type: 'Missions', isManuallyGraded: true, + isPublished: false, closeAt: '2008-06-18T05:24:26.026Z', coverImage: 'https://fakeimg.pl/350x200/ff0000/000', id: 7, @@ -250,13 +264,14 @@ const mockClosedAssessmentOverviews: AssessmentOverview[] = [ status: AssessmentStatuses.submitted, story: 'mission-3', xp: 800, - gradingStatus: GradingStatuses.grading, + isGradingPublished: false, maxTeamSize: 1, hasVotingFeatures: false }, { type: 'Quests', isManuallyGraded: true, + isPublished: false, closeAt: '2008-06-18T05:24:26.026Z', coverImage: 'https://fakeimg.pl/350x200/ff0000,128/000,255', id: 8, @@ -265,17 +280,18 @@ const mockClosedAssessmentOverviews: AssessmentOverview[] = [ openAt: '2007-07-18T05:24:26.026Z', title: 'Closed (not graded) Sidequest', shortSummary: - 'This is a test for the grading status tooltip when the assessment is not graded. It should render as a red cross.', + 'This is a test for the grading status tooltip when the assessment is not published. It should render as a yellow waiting clock.', status: AssessmentStatuses.submitted, story: null, xp: 500, - gradingStatus: GradingStatuses.none, + isGradingPublished: false, maxTeamSize: 1, hasVotingFeatures: false }, { type: 'Quests', isManuallyGraded: true, + isPublished: true, closeAt: '2008-06-18T05:24:26.026Z', coverImage: 'https://fakeimg.pl/350x200/ff0000,128/000,255', id: 9, @@ -283,18 +299,19 @@ const mockClosedAssessmentOverviews: AssessmentOverview[] = [ earlySubmissionXp: 0, openAt: '2007-07-18T05:24:26.026Z', title: 'Closed (fully graded) Sidequest', - shortSummary: - 'This is a test for the grading status tooltip when the assessment is fully graded. It should render as a green tick. This sidequest links to the mock Sidequest 4.', + shortSummary: `This is a test for the grading status tooltip when a manually graded assessment is fully graded but not published. + It should still render as a yellow clock. This sidequest links to the mock Sidequest 4.`, status: AssessmentStatuses.submitted, story: null, xp: 150, - gradingStatus: GradingStatuses.graded, + isGradingPublished: false, maxTeamSize: 1, hasVotingFeatures: false }, { - type: 'Quests', - isManuallyGraded: true, + type: 'Paths', + isManuallyGraded: false, + isPublished: true, closeAt: '2008-06-18T05:24:26.026Z', coverImage: 'https://fakeimg.pl/350x200/ff0000/000', id: 10, @@ -303,11 +320,11 @@ const mockClosedAssessmentOverviews: AssessmentOverview[] = [ openAt: '2007-07-18T05:24:26.026Z', title: 'Ungraded assessment', shortSummary: - 'This is a test for the grading status tooltip when the assessment does not require manual grading (e.g. paths and contests). It should render as a blue disable sign. This sidequest links to the mock Sidequest 4.', + 'This is a test for the grading status tooltip when the assessment does not require manual grading (e.g. paths and contests) but is unpublished. It should still render as a yellow clock. This sidequest links to the mock Sidequest 4.', status: AssessmentStatuses.submitted, story: null, xp: 100, - gradingStatus: GradingStatuses.excluded, + isGradingPublished: false, maxTeamSize: 1, hasVotingFeatures: false } diff --git a/src/commons/mocks/BackendMocks.ts b/src/commons/mocks/BackendMocks.ts index 14f204cbf4..59edfdf4f7 100644 --- a/src/commons/mocks/BackendMocks.ts +++ b/src/commons/mocks/BackendMocks.ts @@ -44,6 +44,7 @@ import { AssessmentOverview, AssessmentStatuses, FETCH_ASSESSMENT_OVERVIEWS, + ProgressStatuses, Question, SUBMIT_ASSESSMENT } from '../assessment/AssessmentTypes'; @@ -286,7 +287,7 @@ export function* mockBackendSaga(): SagaIterator { ); const index = overviews.data.findIndex( overview => - overview.submissionId === submissionId && overview.submissionStatus === 'submitted' + overview.submissionId === submissionId && overview.progress === ProgressStatuses.submitted ); if (index === -1) { yield call(showWarningMessage, '400: Bad Request'); @@ -294,7 +295,7 @@ export function* mockBackendSaga(): SagaIterator { } const newOverviews = overviews.data.map(overview => { if (overview.submissionId === submissionId) { - return { ...overview, submissionStatus: 'attempted' }; + overview.progress = ProgressStatuses.attempted; } return overview; }); diff --git a/src/commons/mocks/GradingMocks.ts b/src/commons/mocks/GradingMocks.ts index 139b0a4e7e..bab0279445 100644 --- a/src/commons/mocks/GradingMocks.ts +++ b/src/commons/mocks/GradingMocks.ts @@ -6,7 +6,12 @@ import { GradingQuery } from '../../features/grading/GradingTypes'; import { Role } from '../application/ApplicationTypes'; -import { Testcase, TestcaseTypes } from '../assessment/AssessmentTypes'; +import { + AssessmentStatuses, + ProgressStatuses, + Testcase, + TestcaseTypes +} from '../assessment/AssessmentTypes'; import { mockLibrary } from './AssessmentMocks'; import { mockFetchRole } from './UserMocks'; @@ -27,9 +32,10 @@ export const mockGradingOverviews: GradingOverview[] = [ studentUsername: 'E0123456', studentUsernames: [], submissionId: 1, - submissionStatus: 'submitted', + submissionStatus: AssessmentStatuses.submitted, + progress: ProgressStatuses.published, + isGradingPublished: true, groupName: '1D', - gradingStatus: 'graded', questionCount: 6, gradedCount: 6 }, @@ -49,9 +55,10 @@ export const mockGradingOverviews: GradingOverview[] = [ studentUsername: 'E0000000', studentUsernames: [], submissionId: 2, - submissionStatus: 'submitted', + submissionStatus: AssessmentStatuses.submitted, + progress: ProgressStatuses.submitted, + isGradingPublished: false, groupName: '1F', - gradingStatus: 'grading', questionCount: 6, gradedCount: 2 }, @@ -71,10 +78,11 @@ export const mockGradingOverviews: GradingOverview[] = [ studentUsername: 'E0000001', studentUsernames: [], submissionId: 3, - submissionStatus: 'submitted', + submissionStatus: AssessmentStatuses.submitted, + progress: ProgressStatuses.graded, + isGradingPublished: false, groupName: '1F', - gradingStatus: 'none', - questionCount: 6, + questionCount: 0, gradedCount: 0 } ]; diff --git a/src/commons/mocks/UserMocks.ts b/src/commons/mocks/UserMocks.ts index e7c396cc5d..523d68fc05 100644 --- a/src/commons/mocks/UserMocks.ts +++ b/src/commons/mocks/UserMocks.ts @@ -430,55 +430,34 @@ export const mockFetchStudentInfo = (accessToken: string): StudentInfo[] | null export const mockNotifications: Notification[] = [ { id: 1, - type: NotificationTypes.deadline, - assessment_id: 3, - assessment_type: 'Quests', - assessment_title: 'The Secret to Streams' - }, - { - id: 2, - type: NotificationTypes.autograded, - assessment_id: 4, - assessment_type: 'Missions', - assessment_title: 'A Closed Mission' - }, - { - id: 3, - type: NotificationTypes.graded, - assessment_id: 4, - assessment_type: 'Missions', - assessment_title: 'A Closed Mission' - }, - { - id: 4, type: NotificationTypes.new, assessment_id: 6, assessment_type: 'Paths', assessment_title: 'Basic Logic' }, { - id: 5, + id: 2, type: NotificationTypes.new, assessment_id: 7, assessment_type: 'Missions', assessment_title: 'Symphony of the Winds' }, { - id: 6, + id: 3, type: NotificationTypes.submitted, submission_id: 1, assessment_type: 'Missions', assessment_title: 'Mission 0' }, { - id: 7, + id: 4, type: NotificationTypes.submitted, submission_id: 2, assessment_type: 'Missions', assessment_title: 'Mission 1' }, { - id: 8, + id: 5, type: NotificationTypes.submitted, submission_id: 3, assessment_type: 'Missions', diff --git a/src/commons/notificationBadge/NotificationBadge.tsx b/src/commons/notificationBadge/NotificationBadge.tsx index 10625477e5..bf53700dbf 100644 --- a/src/commons/notificationBadge/NotificationBadge.tsx +++ b/src/commons/notificationBadge/NotificationBadge.tsx @@ -75,16 +75,14 @@ const makeNotificationMessage = (type: NotificationType) => { switch (type) { case NotificationTypes.new: return 'This assessment is new.'; - case NotificationTypes.deadline: - return 'This assessment is closing soon.'; - case NotificationTypes.autograded: - return 'This assessment has been autograded.'; case NotificationTypes.submitted: return 'This submission is new.'; case NotificationTypes.unsubmitted: return 'This assessment has been unsubmitted.'; - case NotificationTypes.graded: - return 'This assessment has been manually graded.'; + case NotificationTypes.published_grading: + return "This submission's grading has been published."; + case NotificationTypes.unpublished_grading: + return "This submission's grading has been unpublished."; case NotificationTypes.new_message: return 'There are new messages.'; default: diff --git a/src/commons/notificationBadge/NotificationBadgeTypes.ts b/src/commons/notificationBadge/NotificationBadgeTypes.ts index e7ed326036..79b9e88b00 100644 --- a/src/commons/notificationBadge/NotificationBadgeTypes.ts +++ b/src/commons/notificationBadge/NotificationBadgeTypes.ts @@ -9,11 +9,10 @@ export type Notification = { export enum NotificationTypes { new = 'new', - deadline = 'deadline', - autograded = 'autograded', - graded = 'graded', submitted = 'submitted', unsubmitted = 'unsubmitted', + published_grading = 'published_grading', + unpublished_grading = 'unpublished_grading', new_message = 'new_message' } diff --git a/src/commons/notificationBadge/__tests__/NotificationBadge.tsx b/src/commons/notificationBadge/__tests__/NotificationBadge.tsx index 2931c4551a..9f8904819e 100644 --- a/src/commons/notificationBadge/__tests__/NotificationBadge.tsx +++ b/src/commons/notificationBadge/__tests__/NotificationBadge.tsx @@ -20,18 +20,18 @@ const notifications: Notification[] = [ }, { id: 2, - type: 'graded', + type: 'published_grading', assessment_id: 1, assessment_type: 'Missions', - assessment_title: 'The Secret to Streams' + assessment_title: 'The Secret to Streams', + submission_id: 3 }, { id: 3, - type: 'autograded', - assessment_id: 2, + type: 'unpublished_grading', + assessment_id: 1, assessment_type: 'Missions', - assessment_title: 'The Secret to Streams', - submission_id: 3 + assessment_title: 'The Secret to Streams' }, { id: 4, diff --git a/src/commons/notificationBadge/__tests__/NotificationBadgeHelper.ts b/src/commons/notificationBadge/__tests__/NotificationBadgeHelper.ts index 63273242e7..f90fcf6b07 100644 --- a/src/commons/notificationBadge/__tests__/NotificationBadgeHelper.ts +++ b/src/commons/notificationBadge/__tests__/NotificationBadgeHelper.ts @@ -11,7 +11,7 @@ const notificationMission: Notification = { const notificationSidequest: Notification = { id: 2, - type: 'autograded', + type: 'published_grading', assessment_id: 2, assessment_type: 'Quests', assessment_title: 'Quest_1' @@ -19,7 +19,7 @@ const notificationSidequest: Notification = { const notificationPath: Notification = { id: 3, - type: 'graded', + type: 'published_grading', assessment_id: 3, assessment_type: 'Paths', assessment_title: 'Path_1' diff --git a/src/commons/profile/Profile.tsx b/src/commons/profile/Profile.tsx index 6fd1dda073..d9abf07520 100644 --- a/src/commons/profile/Profile.tsx +++ b/src/commons/profile/Profile.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; import { fetchAssessmentOverviews, fetchTotalXp } from '../application/actions/SessionActions'; -import { AssessmentStatuses, AssessmentType, GradingStatuses } from '../assessment/AssessmentTypes'; +import { AssessmentStatuses, AssessmentType } from '../assessment/AssessmentTypes'; import Constants from '../utils/Constants'; import { useSession } from '../utils/Hooks'; import ProfileCard from './ProfileCard'; @@ -126,12 +126,7 @@ const Profile: React.FC = props => { // Build condensed assessment cards from an array of assessments const summaryCallouts = assessmentOverviews! - .filter( - item => - item.status === AssessmentStatuses.submitted && - (item.gradingStatus === GradingStatuses.graded || - item.gradingStatus === GradingStatuses.excluded) - ) + .filter(item => item.isGradingPublished) .map((assessment, index) => { return ( - item.status === AssessmentStatuses.submitted && - (item.gradingStatus === 'graded' || item.gradingStatus === 'excluded') - ).length; + const numProfileCards = mockAssessmentOverviews.filter(item => item.isGradingPublished).length; [ 'profile-summary-navlink', diff --git a/src/commons/sagas/BackendSaga.ts b/src/commons/sagas/BackendSaga.ts index 62de8a52d4..eedda867fc 100644 --- a/src/commons/sagas/BackendSaga.ts +++ b/src/commons/sagas/BackendSaga.ts @@ -22,6 +22,8 @@ import { CONFIGURE_ASSESSMENT, DELETE_ASSESSMENT, PUBLISH_ASSESSMENT, + PUBLISH_GRADING_ALL, + UNPUBLISH_GRADING_ALL, UPLOAD_ASSESSMENT } from '../../features/groundControl/GroundControlTypes'; import { FETCH_SOURCECAST_INDEX } from '../../features/sourceRecorder/sourcecast/SourcecastTypes'; @@ -63,6 +65,7 @@ import { FETCH_TOTAL_XP_ADMIN, FETCH_USER_AND_COURSE, NotificationConfiguration, + PUBLISH_GRADING, REAUTOGRADE_ANSWER, REAUTOGRADE_SUBMISSION, SUBMIT_ANSWER, @@ -70,6 +73,7 @@ import { SUBMIT_GRADING_AND_CONTINUE, TimeOption, Tokens, + UNPUBLISH_GRADING, UNSUBMIT_SUBMISSION, UPDATE_ASSESSMENT_CONFIGS, UPDATE_COURSE_CONFIG, @@ -89,6 +93,7 @@ import { AssessmentOverview, AssessmentStatuses, FETCH_ASSESSMENT_OVERVIEWS, + ProgressStatuses, Question, SUBMIT_ASSESSMENT } from '../assessment/AssessmentTypes'; @@ -136,6 +141,8 @@ import { postTeams, postUnsubmit, postUploadTeams, + publishGrading, + publishGradingAll, putAssessmentConfigs, putCourseConfig, putCourseResearchAgreement, @@ -149,6 +156,8 @@ import { removeAssessmentConfig, removeTimeOptions, removeUserCourseRegistration, + unpublishGrading, + unpublishGradingAll, updateAssessment, uploadAssessment } from './RequestsSaga'; @@ -458,13 +467,13 @@ function* BackendSaga(): SagaIterator { return; } - const { filterToGroup, gradedFilter, pageParams, filterParams } = action.payload; + const { filterToGroup, publishedFilter, pageParams, filterParams } = action.payload; const gradingOverviews: GradingOverviews | null = yield call( getGradingOverviews, tokens, filterToGroup, - gradedFilter, + publishedFilter, pageParams, filterParams ); @@ -635,7 +644,7 @@ function* BackendSaga(): SagaIterator { ); const newOverviews = overviews.map(overview => { if (overview.submissionId === submissionId) { - return { ...overview, submissionStatus: 'attempted' }; + return { ...overview, progress: ProgressStatuses.attempted }; } return overview; }); @@ -651,6 +660,70 @@ function* BackendSaga(): SagaIterator { } ); + yield takeEvery( + PUBLISH_GRADING, + function* (action: ReturnType): any { + const tokens: Tokens = yield selectTokens(); + const { submissionId } = action.payload; + + const resp: Response | null = yield publishGrading(submissionId, tokens); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); + } + + const overviews: GradingOverview[] = yield select( + (state: OverallState) => state.session.gradingOverviews?.data || [] + ); + const newOverviews = overviews.map(overview => { + if (overview.submissionId === submissionId) { + return { ...overview, progress: ProgressStatuses.published }; + } + return overview; + }); + + const totalPossibleEntries = yield select( + (state: OverallState) => state.session.gradingOverviews?.count + ); + + yield call(showSuccessMessage, 'Publish grading successful', 1000); + yield put( + actions.updateGradingOverviews({ count: totalPossibleEntries, data: newOverviews }) + ); + } + ); + + yield takeEvery( + UNPUBLISH_GRADING, + function* (action: ReturnType): any { + const tokens: Tokens = yield selectTokens(); + const { submissionId } = action.payload; + + const resp: Response | null = yield unpublishGrading(submissionId, tokens); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); + } + + const overviews: GradingOverview[] = yield select( + (state: OverallState) => state.session.gradingOverviews?.data || [] + ); + const newOverviews = overviews.map(overview => { + if (overview.submissionId === submissionId) { + return { ...overview, progress: ProgressStatuses.graded }; + } + return overview; + }); + + const totalPossibleEntries = yield select( + (state: OverallState) => state.session.gradingOverviews?.count + ); + + yield call(showSuccessMessage, 'Unpublish grading successful', 1000); + yield put( + actions.updateGradingOverviews({ count: totalPossibleEntries, data: newOverviews }) + ); + } + ); + const sendGrade = function* ( action: | ReturnType @@ -1155,11 +1228,12 @@ function* BackendSaga(): SagaIterator { yield put(actions.clearStoriesUserAndGroup()); } - const placeholderAssessmentConfig = [ + const placeholderAssessmentConfig: AssessmentConfiguration[] = [ { type: 'Missions', assessmentConfigId: -1, isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hoursBeforeEarlyXpDecay: 0, hasTokenCounter: false, @@ -1318,7 +1392,7 @@ function* BackendSaga(): SagaIterator { function* (action: ReturnType): any { const tokens: Tokens = yield selectTokens(); const id = action.payload.id; - const togglePublishTo = action.payload.togglePublishTo; + const togglePublishTo = action.payload.togglePublishAssessmentTo; const resp: Response | null = yield updateAssessment( id, @@ -1409,6 +1483,38 @@ function* BackendSaga(): SagaIterator { yield call(showSuccessMessage, 'Updated successfully!', 1000); } ); + + yield takeEvery( + PUBLISH_GRADING_ALL, + function* (action: ReturnType): any { + const tokens: Tokens = yield selectTokens(); + const id = action.payload; + + const resp: Response | null = yield publishGradingAll(id, tokens); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); + } + + yield put(actions.fetchAssessmentOverviews()); + yield call(showSuccessMessage, 'Published all graded submissons successfully!', 1000); + } + ); + + yield takeEvery( + UNPUBLISH_GRADING_ALL, + function* (action: ReturnType): any { + const tokens: Tokens = yield selectTokens(); + const id = action.payload; + + const resp: Response | null = yield unpublishGradingAll(id, tokens); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); + } + + yield put(actions.fetchAssessmentOverviews()); + yield call(showSuccessMessage, 'Unpublished all submissons successfully!', 1000); + } + ); } function* handleReautogradeResponse(resp: Response | null): any { diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 50e95e8626..7b1f048fbe 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -1,4 +1,5 @@ import { call } from 'redux-saga/effects'; +import { backendParamsToProgressStatus } from 'src/features/grading/GradingUtils'; import { OptionType } from 'src/pages/academy/teamFormation/subcomponents/TeamFormationForm'; import { @@ -48,7 +49,6 @@ import { AssessmentConfiguration, AssessmentOverview, ContestEntry, - GradingStatus, IContestVotingQuestion, IProgrammingQuestion, QuestionType, @@ -429,12 +429,6 @@ export const getAssessmentOverviews = async ( const assessmentOverviews = await resp.json(); return assessmentOverviews.map((overview: any) => { - overview.gradingStatus = computeGradingStatus( - overview.isManuallyGraded, - overview.status, - overview.gradedCount, - overview.questionCount - ); delete overview.gradedCount; delete overview.questionCount; @@ -481,12 +475,6 @@ export const getUserAssessmentOverviews = async ( } const assessmentOverviews = await resp.json(); return assessmentOverviews.map((overview: any) => { - overview.gradingStatus = computeGradingStatus( - overview.isManuallyGraded, - overview.status, - overview.gradedCount, - overview.questionCount - ); delete overview.gradedCount; delete overview.questionCount; @@ -668,7 +656,7 @@ export const getGradingOverviews = async ( studentNames: overview.team ? overview.team.team_members.map((member: { name: any }) => member.name) : undefined, - studentUsername: overview.student ? overview.student.name : undefined, + studentUsername: overview.student ? overview.student.username : undefined, studentUsernames: overview.team ? overview.team.team_members.map((member: { username: any }) => member.username) : undefined, @@ -676,8 +664,14 @@ export const getGradingOverviews = async ( submissionStatus: overview.status, groupName: overview.student ? overview.student.groupName : '-', groupLeaderId: overview.student ? overview.student.groupLeaderId : undefined, - // Grading Status - gradingStatus: 'none', + isGradingPublished: overview.isGradingPublished, + progress: backendParamsToProgressStatus( + overview.assessment.isManuallyGraded, + overview.isGradingPublished, + overview.status, + overview.gradedCount, + overview.assessment.questionCount + ), questionCount: overview.assessment.questionCount, gradedCount: overview.gradedCount, // XP @@ -687,12 +681,6 @@ export const getGradingOverviews = async ( maxXp: overview.assessment.maxXp, xpBonus: overview.xpBonus }; - gradingOverview.gradingStatus = computeGradingStatus( - overview.assessment.isManuallyGraded, - gradingOverview.submissionStatus, - gradingOverview.gradedCount, - gradingOverview.questionCount - ); return gradingOverview; }) .sort((subX: GradingOverview, subY: GradingOverview) => @@ -1026,6 +1014,64 @@ export const postUnsubmit = async ( return resp; }; +/** + * POST /courses/{courseId}/admin/grading/{submissionId}/publish_grades + */ +export const publishGrading = async ( + submissionId: number, + tokens: Tokens +): Promise => { + const resp = await request(`${courseId()}/admin/grading/${submissionId}/publish_grades`, 'POST', { + ...tokens, + noHeaderAccept: true + }); + + return resp; +}; + +/** + * POST /courses/{course_id}/admin/grading/{assessmentid}/publish_all_grades + */ +export const publishGradingAll = async (id: number, tokens: Tokens): Promise => { + const resp = await request(`${courseId()}/admin/grading/${id}/publish_all_grades`, 'POST', { + ...tokens, + noHeaderAccept: true + }); + + return resp; +}; + +/** + * POST /courses/{courseId}/admin/grading/{submissionId}/unpublish_grades + */ +export const unpublishGrading = async ( + submissionId: number, + tokens: Tokens +): Promise => { + const resp = await request( + `${courseId()}/admin/grading/${submissionId}/unpublish_grades`, + 'POST', + { + ...tokens, + noHeaderAccept: true + } + ); + + return resp; +}; + +/** + * POST /courses/{course_id}/admin/grading/{assessmentid}/unpublish_all_grades + */ +export const unpublishGradingAll = async (id: number, tokens: Tokens): Promise => { + const resp = await request(`${courseId()}/admin/grading/${id}/unpublish_all_grades`, 'POST', { + ...tokens, + noHeaderAccept: true + }); + + return resp; +}; + /** * GET /courses/{courseId}/notifications */ @@ -1673,22 +1719,6 @@ export function* handleResponseError(resp: Response | null): any { yield call(showWarningMessage, respText); } -const computeGradingStatus = ( - isManuallyGraded: boolean, - submissionStatus: any, - numGraded: number, - numQuestions: number -): GradingStatus => - // isGraded refers to whether the assessment type is graded or not, as specified in - // the respective assessment configuration - isManuallyGraded && submissionStatus === 'submitted' - ? numGraded === 0 - ? 'none' - : numGraded === numQuestions - ? 'graded' - : 'grading' - : 'excluded'; - const courseId: () => string = () => { const id = store.getState().session.courseId; if (id) { diff --git a/src/commons/sagas/__tests__/BackendSaga.ts b/src/commons/sagas/__tests__/BackendSaga.ts index add54c78f8..402d6b9915 100644 --- a/src/commons/sagas/__tests__/BackendSaga.ts +++ b/src/commons/sagas/__tests__/BackendSaga.ts @@ -228,6 +228,7 @@ const mockAssessmentConfigurations: AssessmentConfiguration[] = [ assessmentConfigId: 1, type: 'Missions', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hoursBeforeEarlyXpDecay: 48, hasTokenCounter: false, @@ -238,6 +239,7 @@ const mockAssessmentConfigurations: AssessmentConfiguration[] = [ assessmentConfigId: 2, type: 'Quests', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hoursBeforeEarlyXpDecay: 48, hasTokenCounter: false, @@ -248,6 +250,7 @@ const mockAssessmentConfigurations: AssessmentConfiguration[] = [ assessmentConfigId: 3, type: 'Paths', isManuallyGraded: false, + isGradingAutoPublished: true, displayInDashboard: false, hoursBeforeEarlyXpDecay: 48, hasTokenCounter: false, @@ -258,6 +261,7 @@ const mockAssessmentConfigurations: AssessmentConfiguration[] = [ assessmentConfigId: 4, type: 'Contests', isManuallyGraded: false, + isGradingAutoPublished: false, displayInDashboard: false, hoursBeforeEarlyXpDecay: 48, hasTokenCounter: false, @@ -268,6 +272,7 @@ const mockAssessmentConfigurations: AssessmentConfiguration[] = [ assessmentConfigId: 5, type: 'Others', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: false, hoursBeforeEarlyXpDecay: 48, hasTokenCounter: false, @@ -1065,11 +1070,12 @@ describe('Test CREATE_COURSE action', () => { const user = mockUser; const courseConfiguration = mockCourseConfiguration1; const courseRegistration = mockCourseRegistration1; - const placeholderAssessmentConfig = [ + const placeholderAssessmentConfig: AssessmentConfiguration[] = [ { type: 'Missions', assessmentConfigId: -1, isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hoursBeforeEarlyXpDecay: 0, hasTokenCounter: false, diff --git a/src/features/grading/GradingTypes.ts b/src/features/grading/GradingTypes.ts index 5e531b046e..f978eb1ce9 100644 --- a/src/features/grading/GradingTypes.ts +++ b/src/features/grading/GradingTypes.ts @@ -1,8 +1,9 @@ import { + AssessmentStatus, AssessmentType, AutogradingResult, - GradingStatus, MCQChoice, + ProgressStatus, Question, Testcase } from '../../commons/assessment/AssessmentTypes'; @@ -22,16 +23,17 @@ export type GradingOverview = { xpAdjustment: number; currentXp: number; maxXp: number; + isGradingPublished: boolean; + progress: ProgressStatus; studentId: number; studentName: string | undefined; studentNames: string[] | undefined; studentUsername: string | undefined; studentUsernames: string[] | undefined; + submissionStatus: AssessmentStatus; submissionId: number; - submissionStatus: string; groupName: string; groupLeaderId?: number; - gradingStatus: GradingStatus; questionCount: number; gradedCount: number; }; diff --git a/src/features/grading/GradingUtils.ts b/src/features/grading/GradingUtils.ts index f7871dd425..37b66eb077 100644 --- a/src/features/grading/GradingUtils.ts +++ b/src/features/grading/GradingUtils.ts @@ -1,16 +1,13 @@ import { ColumnFilter } from '@tanstack/react-table'; -import { GradingStatuses } from 'src/commons/assessment/AssessmentTypes'; +import { + AssessmentStatus, + AssessmentStatuses, + ProgressStatus, + ProgressStatuses +} from 'src/commons/assessment/AssessmentTypes'; import { GradingOverview } from './GradingTypes'; -// TODO: Unused. Marked for deletion. -export const isSubmissionUngraded = (s: GradingOverview): boolean => { - const isSubmitted = s.submissionStatus === 'submitted'; - const isNotGraded = - s.gradingStatus !== GradingStatuses.graded && s.gradingStatus !== GradingStatuses.excluded; - return isSubmitted && isNotGraded; -}; - export const exportGradingCSV = (gradingOverviews: GradingOverview[] | undefined) => { if (!gradingOverviews) return; @@ -22,7 +19,7 @@ export const exportGradingCSV = (gradingOverviews: GradingOverview[] | undefined const content = new Blob( [ - '"Assessment Number","Assessment Name","Student Name","Student Username","Group","Status","Grading","Question Count","Questions Graded","Initial XP","XP Adjustment","Current XP (excl. bonus)","Max XP","Bonus XP"\n', + '"Assessment Number","Assessment Name","Student Name","Student Username","Group","Progress","Question Count","Questions Graded","Initial XP","XP Adjustment","Current XP (excl. bonus)","Max XP","Bonus XP"\n', ...gradingOverviews.map( e => [ @@ -31,8 +28,7 @@ export const exportGradingCSV = (gradingOverviews: GradingOverview[] | undefined e.studentName, e.studentUsername, e.groupName, - e.submissionStatus, - e.gradingStatus, + e.progress, e.questionCount, e.gradedCount, e.initialXp, @@ -72,9 +68,7 @@ export const exportGradingCSV = (gradingOverviews: GradingOverview[] | undefined }, 0); }; -// Cleanup work: change all references to column properties in backend saga to backend name to reduce -// un-needed hardcode conversion, ensuring that places that reference it are updated. A two-way conversion -// function would be good to implement in GradingUtils. +// TODO: Two-way conversion function for frontend-backend parameter conversion export const convertFilterToBackendParams = (column: ColumnFilter) => { switch (column.id) { case 'assessmentName': @@ -85,8 +79,8 @@ export const convertFilterToBackendParams = (column: ColumnFilter) => { return { name: column.value }; case 'studentUsername': return { username: column.value }; - case 'submissionStatus': - return { status: column.value }; + case 'progress': + return progressStatusToBackendParams(column.value as ProgressStatus); case 'groupName': return { groupName: column.value }; default: @@ -98,13 +92,77 @@ export const paginationToBackendParams = (page: number, pageSize: number) => { return { offset: page * pageSize, pageSize: pageSize }; }; -export const ungradedToBackendParams = (showAll: boolean) => { +export const unpublishedToBackendParams = (showAll: boolean) => { if (showAll) { return {}; } + return { - status: 'submitted', + status: AssessmentStatuses.submitted, isManuallyGraded: true, - notFullyGraded: true + isGradingPublished: false }; }; + +/** + * Converts multiple backend parameters into a single comprehensive grading status for use in the grading dashboard. + * @returns a ProgressStatus, defined within AssessmentTypes, useable by the grading dashboard for display and business logic + * as well as by the assessment overviews for each student to determine if grading is to be shown + */ +export const backendParamsToProgressStatus = ( + isManuallyGraded: boolean, + isGradingPublished: boolean, + submissionStatus: AssessmentStatus, + numGraded: number, + numQuestions: number +): ProgressStatus => { + if (!isManuallyGraded) { + return ProgressStatuses.autograded; + } else if (submissionStatus !== AssessmentStatuses.submitted) { + return submissionStatus; + } else if (numGraded < numQuestions) { + return ProgressStatuses.submitted; + } else if (!isGradingPublished) { + return ProgressStatuses.graded; + } else { + return ProgressStatuses.published; + } +}; + +export const progressStatusToBackendParams = (progress: ProgressStatus) => { + switch (progress) { + case ProgressStatuses.autograded: + return { + isManuallyGraded: false + }; + case ProgressStatuses.published: + return { + isManuallyGraded: true, + isGradingPublished: true, + isFullyGraded: true, + status: AssessmentStatuses.submitted + }; + case ProgressStatuses.graded: + return { + isManuallyGraded: true, + isGradingPublished: false, + isFullyGraded: true, + status: AssessmentStatuses.submitted + }; + case ProgressStatuses.submitted: + return { + isManuallyGraded: true, + isGradingPublished: false, + isFullyGraded: false, + status: AssessmentStatuses.submitted + }; + default: + // 'attempted' work may have been previously graded and then unsubmitted + // thus, isFullyGraded flag is not added + return { + isManuallyGraded: true, + isGradingPublished: false, + status: progress as AssessmentStatus + }; + } +}; diff --git a/src/features/groundControl/GroundControlActions.ts b/src/features/groundControl/GroundControlActions.ts index 0cf419a4ed..0f4b32f555 100644 --- a/src/features/groundControl/GroundControlActions.ts +++ b/src/features/groundControl/GroundControlActions.ts @@ -7,6 +7,8 @@ import { CONFIGURE_ASSESSMENT, DELETE_ASSESSMENT, PUBLISH_ASSESSMENT, + PUBLISH_GRADING_ALL, + UNPUBLISH_GRADING_ALL, UPLOAD_ASSESSMENT } from './GroundControlTypes'; @@ -24,9 +26,19 @@ export const deleteAssessment = createAction(DELETE_ASSESSMENT, (id: number) => export const publishAssessment = createAction( PUBLISH_ASSESSMENT, - (togglePublishTo: boolean, id: number) => ({ payload: { id, togglePublishTo } }) + (togglePublishAssessmentTo: boolean, id: number) => ({ + payload: { id, togglePublishAssessmentTo } + }) ); +export const publishGradingAll = createAction(PUBLISH_GRADING_ALL, (id: number) => ({ + payload: id +})); + +export const unpublishGradingAll = createAction(UNPUBLISH_GRADING_ALL, (id: number) => ({ + payload: id +})); + export const uploadAssessment = createAction( UPLOAD_ASSESSMENT, (file: File, forceUpdate: boolean, assessmentConfigId: number) => ({ diff --git a/src/features/groundControl/GroundControlTypes.ts b/src/features/groundControl/GroundControlTypes.ts index f14b1b61a3..7999c9860a 100644 --- a/src/features/groundControl/GroundControlTypes.ts +++ b/src/features/groundControl/GroundControlTypes.ts @@ -2,6 +2,8 @@ export const CHANGE_DATE_ASSESSMENT = 'CHANGE_DATE_ASSESSMENT'; export const CHANGE_TEAM_SIZE_ASSESSMENT = 'CHANGE_TEAM_SIZE_ASSESSMENT'; export const DELETE_ASSESSMENT = 'DELETE_ASSESSMENT'; export const PUBLISH_ASSESSMENT = 'PUBLISH_ASSESSMENT'; +export const PUBLISH_GRADING_ALL = 'PUBLISH_GRADING_ALL'; +export const UNPUBLISH_GRADING_ALL = 'UNPUBLISH_GRADING_ALL'; export const UPLOAD_ASSESSMENT = 'UPLOAD_ASSESSMENT'; export const CONFIGURE_ASSESSMENT = 'CONFIGURE_ASSESSMENT'; export const ASSIGN_ENTRIES_FOR_VOTING = 'ASSIGN_ENTRIES_FOR_VOTING'; diff --git a/src/pages/academy/adminPanel/subcomponents/assessmentConfigPanel/AssessmentConfigPanel.tsx b/src/pages/academy/adminPanel/subcomponents/assessmentConfigPanel/AssessmentConfigPanel.tsx index 3d10e8818b..8de1d1ed72 100644 --- a/src/pages/academy/adminPanel/subcomponents/assessmentConfigPanel/AssessmentConfigPanel.tsx +++ b/src/pages/academy/adminPanel/subcomponents/assessmentConfigPanel/AssessmentConfigPanel.tsx @@ -31,16 +31,42 @@ const AssessmentConfigPanel: React.FC = ({ }) => { const gridApi = React.useRef(); + // manually graded assessments should not be auto-published + // check and ensure that isManuallyGraded = true and isGradingAutoPublished = true cannot be set simultaneously const setIsManuallyGraded = (index: number, value: boolean) => { const temp = [...assessmentConfig.current]; temp[index] = { ...temp[index], isManuallyGraded: value }; + + // use a second spread operator if mutation of temp[index] causes issues + if (value) { + temp[index].isGradingAutoPublished = false; + gridApi.current?.getDisplayedRowAtIndex(index)?.setDataValue('isGradingAutoPublished', false); + } + setAssessmentConfig(temp); gridApi.current?.getDisplayedRowAtIndex(index)?.setDataValue('isManuallyGraded', value); }; + const setIsGradingAutoPublished = (index: number, value: boolean) => { + const temp = [...assessmentConfig.current]; + + temp[index] = { + ...temp[index], + isGradingAutoPublished: value + }; + + if (value) { + temp[index].isManuallyGraded = false; + gridApi.current?.getDisplayedRowAtIndex(index)?.setDataValue('isManuallyGraded', false); + } + + setAssessmentConfig(temp); + gridApi.current?.getDisplayedRowAtIndex(index)?.setDataValue('isGradingAutoPublished', value); + }; + const setDisplayInDashboard = (index: number, value: boolean) => { const temp = [...assessmentConfig.current]; temp[index] = { @@ -102,6 +128,7 @@ const AssessmentConfigPanel: React.FC = ({ assessmentConfigId: -1, type: 'untitled', isManuallyGraded: true, + isGradingAutoPublished: false, displayInDashboard: true, hoursBeforeEarlyXpDecay: 0, hasTokenCounter: false, @@ -141,6 +168,15 @@ const AssessmentConfigPanel: React.FC = ({ field: 'isManuallyGraded' } }, + { + headerName: 'Is Auto-published', + field: 'isGradingAutoPublished', + cellRenderer: BooleanCell, + cellRendererParams: { + setStateHandler: setIsGradingAutoPublished, + field: 'isGradingAutoPublished' + } + }, { headerName: 'Display in Dashboard', field: 'displayInDashboard', diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index af28f984c3..529758da4e 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -14,7 +14,7 @@ import { numberRegExp } from 'src/features/academy/AcademyTypes'; import { exportGradingCSV, paginationToBackendParams, - ungradedToBackendParams + unpublishedToBackendParams } from 'src/features/grading/GradingUtils'; import ContentDisplay from '../../../commons/ContentDisplay'; @@ -28,7 +28,7 @@ const groupOptions = [ ]; const showOptions = [ - { value: false, label: 'ungraded' }, + { value: false, label: 'unpublished' }, { value: true, label: 'all' } ]; @@ -55,7 +55,7 @@ const Grading: React.FC = () => { dispatch( fetchGradingOverviews( showAllGroups, - ungradedToBackendParams(showAllSubmissions), + unpublishedToBackendParams(showAllSubmissions), paginationToBackendParams(page, pageSize), filterParams ) diff --git a/src/pages/academy/grading/subcomponents/GradingActions.tsx b/src/pages/academy/grading/subcomponents/GradingActions.tsx index d10ea8f5dc..c99df8c3fd 100644 --- a/src/pages/academy/grading/subcomponents/GradingActions.tsx +++ b/src/pages/academy/grading/subcomponents/GradingActions.tsx @@ -1,20 +1,24 @@ -import { Icon as BpIcon } from '@blueprintjs/core'; +import { Button, Icon as BpIcon } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import { Flex, Icon } from '@tremor/react'; import { useDispatch } from 'react-redux'; import { Link } from 'react-router-dom'; import { + publishGrading, reautogradeSubmission, + unpublishGrading, unsubmitSubmission } from 'src/commons/application/actions/SessionActions'; +import { ProgressStatus, ProgressStatuses } from 'src/commons/assessment/AssessmentTypes'; import { showSimpleConfirmDialog } from 'src/commons/utils/DialogHelper'; import { useTypedSelector } from 'src/commons/utils/Hooks'; type Props = { submissionId: number; + progress: ProgressStatus; }; -const GradingActions: React.FC = ({ submissionId }) => { +const GradingActions: React.FC = ({ submissionId, progress }) => { const dispatch = useDispatch(); const courseId = useTypedSelector(store => store.session.courseId); @@ -45,13 +49,40 @@ const GradingActions: React.FC = ({ submissionId }) => { } }; + const handlePublishClick = async () => { + const confirm = await showSimpleConfirmDialog({ + contents: "Publish this assessment's grading?", + positiveIntent: 'primary', + positiveLabel: 'Publish' + }); + if (confirm) { + dispatch(publishGrading(submissionId)); + } + }; + + const handleUnpublishClick = async () => { + const confirm = await showSimpleConfirmDialog({ + contents: "Unpublish this assessment's grading?", + positiveIntent: 'primary', + positiveLabel: 'Unpublish' + }); + if (confirm) { + dispatch(unpublishGrading(submissionId)); + } + }; + return ( } variant="light" /> - - + +