Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Added exam mode #3106

Open
wants to merge 61 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
1ec4ff9
Added exam mode switches for admin panel and create course dropdown
kah-seng Feb 14, 2025
1aba138
Fixed exam mode switch not updating on launch
kah-seng Feb 14, 2025
5111ee5
Hide Google Drive, GitHub, share and sessions button when exam mode e…
kah-seng Feb 14, 2025
c8d8f18
added auto url navigation to course under exam mode; fixed UI logic c…
iZUMi-kyouka Feb 16, 2025
804fdad
fixed some formatting with yarn run format
iZUMi-kyouka Feb 16, 2025
7f09b05
added enableExamMode field in backend tests; eslint fix and formatting
iZUMi-kyouka Feb 16, 2025
38b208c
Hide remote execution when exam mode enabled, shifted exam mode check…
kah-seng Feb 17, 2025
f17ff59
yarn run format and rename for standardisation
kah-seng Feb 21, 2025
bad7d50
Formatting
kah-seng Mar 4, 2025
3f4921b
Merge branch 'master' into exam_mode
RichDom2185 Mar 6, 2025
2d837c1
Remove .tool-versions
RichDom2185 Mar 6, 2025
0c7526b
Disable create course when under exam mode, hide exam mode toggle whe…
kah-seng Mar 6, 2025
fc1a803
Added isOfficialCourse field in backend tests
kah-seng Mar 6, 2025
6a20fc7
Change useTypedSelector to useSession
kah-seng Mar 6, 2025
736e624
added resume code function in admin panel
iZUMi-kyouka Mar 11, 2025
1f42ded
Added validate resume code saga
kah-seng Mar 13, 2025
6cd6727
Added basic dev tools detection, pausing of source academy and resume…
kah-seng Mar 14, 2025
b72a22b
Added resume code input validation for admin panel
kah-seng Mar 14, 2025
9f5e3a2
Restore pause overlay
kah-seng Mar 14, 2025
feacd38
Fixed resume code input validation
kah-seng Mar 14, 2025
cc13931
Added backend saga tests
kah-seng Mar 14, 2025
7d61c43
Merge branch 'master' into exam_mode
RichDom2185 Mar 16, 2025
23415c9
added disable-devtool library; configured PauseAcademyOverlay to work…
iZUMi-kyouka Mar 18, 2025
0c68331
fixed repeated request to pause user by using useRef
iZUMi-kyouka Mar 20, 2025
ad49ef5
Added documentation tab and merge block_dev_tools_library branch
kah-seng Mar 24, 2025
c52f215
Fixed tests
kah-seng Mar 24, 2025
d8e3d6f
used native sicp component instead of loading another SA web app in a…
iZUMi-kyouka Mar 25, 2025
a9cfd33
fixed styling issues; added home button to control iframe
iZUMi-kyouka Mar 25, 2025
15a9e62
fixed text and index search failure on assessment SICP tab
iZUMi-kyouka Mar 25, 2025
666f50a
Formatting and added caching of documentation pages
kah-seng Mar 25, 2025
1bdac8e
fixed home button for sicp native component with callback functions
iZUMi-kyouka Mar 25, 2025
bedcc4d
commented unusued console.log
iZUMi-kyouka Mar 27, 2025
0b28757
added maxheight to the SICP component
iZUMi-kyouka Mar 27, 2025
aa1e266
Fix format
RichDom2185 Mar 31, 2025
d91e7af
Merge branch 'master' of https://github.com/source-academy/frontend i…
RichDom2185 Mar 31, 2025
685184d
Use `useSession`
RichDom2185 Mar 31, 2025
5caf874
Remove unused code
RichDom2185 Mar 31, 2025
0e3101f
Simplify code
RichDom2185 Mar 31, 2025
d6c2ca3
Added pausing when browser tab change detected, and simplified code
kah-seng Apr 1, 2025
3f3b75d
Fixed bug where sometimes refreshing bypasses overlay, modified valid…
kah-seng Apr 1, 2025
a525978
Formatting
kah-seng Apr 1, 2025
ec9c372
fixed styling to allow resizing of documentation iframe; added sagas …
iZUMi-kyouka Apr 2, 2025
2777ebf
fix formatting
iZUMi-kyouka Apr 2, 2025
bab2a81
fixed sicp js sidebar docs styling; improved button styling
iZUMi-kyouka Apr 2, 2025
669a347
formatting
iZUMi-kyouka Apr 2, 2025
bb4c32d
Changed overlay to show when focus regained instead
kah-seng Apr 3, 2025
3e660d9
Changed pause overlay to notification when focus lost
kah-seng Apr 3, 2025
50eb143
Change alert to notifications
kah-seng Apr 3, 2025
87feefe
Alert to notification
kah-seng Apr 3, 2025
80d57bf
Removed useless button
kah-seng Apr 3, 2025
ef6f91c
added applyEnableExamMode to exclude staffs and admins from exam mode…
iZUMi-kyouka Apr 3, 2025
1447ed5
added applyEnableExamMode to exclude staffs and admins from exam mode…
iZUMi-kyouka Apr 3, 2025
39ec7f8
Added local preview of exam mode for admins
kah-seng Apr 4, 2025
37c947e
auto resized docs sidebar to reasonable size; fixed sicp sidebar bott…
iZUMi-kyouka Apr 4, 2025
95469a5
fixed bug where clicking home on other docs tab also navigates sicp d…
iZUMi-kyouka Apr 4, 2025
a8b1b4b
Report focus gain on launch
kah-seng Apr 5, 2025
8a7671b
hide preview exam mode button on non official course
iZUMi-kyouka Apr 5, 2025
a244844
Resume code input always visible, can no longer be empty no matter ex…
kah-seng Apr 6, 2025
5289a9d
Trim resume code input
kah-seng Apr 6, 2025
3cb1957
Formatting
kah-seng Apr 6, 2025
52635e8
Fixed bug where resume code does not update when switching courses
kah-seng Apr 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"classnames": "^2.3.2",
"conductor": "https://github.com/source-academy/conductor.git#0.2.1",
"dayjs": "^1.11.13",
"disable-devtool": "^0.3.8",
"dompurify": "^3.2.4",
"flexboxgrid": "^6.3.1",
"flexboxgrid-helpers": "^1.1.3",
Expand Down
104 changes: 97 additions & 7 deletions src/commons/application/Application.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import disableDevtool from 'disable-devtool';
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { Outlet } from 'react-router-dom';
import Messages, {
Expand All @@ -8,16 +9,19 @@ import Messages, {
} from 'src/features/vscode/messages';

import NavigationBar from '../navigationBar/NavigationBar';
import { PauseAcademyOverlay } from '../pauseAcademyOverlay/PauseAcademyOverlay';
import Constants from '../utils/Constants';
import { useLocalStorageState, useSession } from '../utils/Hooks';
import { showDangerMessage, showWarningMessage } from '../utils/notifications/NotificationsHelper';
import WorkspaceActions from '../workspace/WorkspaceActions';
import { defaultWorkspaceSettings, WorkspaceSettingsContext } from '../WorkspaceSettingsContext';
import SessionActions from './actions/SessionActions';
import VscodeActions from './actions/VscodeActions';
import { Role } from './ApplicationTypes';

const Application: React.FC = () => {
const dispatch = useDispatch();
const { isLoggedIn } = useSession();
const { isLoggedIn, isPaused, enableExamMode } = useSession();

// Used in the mobile/PWA experience (e.g. separate handling of orientation changes on Andriod & iOS due to unique browser behaviours)
const isMobile = /iPhone|iPad|Android/.test(navigator.userAgent);
Expand All @@ -29,6 +33,16 @@ const Application: React.FC = () => {
defaultWorkspaceSettings
);

// Used for dev tools detection
const [isPreviewExamMode, _] = useLocalStorageState(
Constants.isPreviewExamModeLocalStorageKey,
false
);
const [pauseAcademy, setPauseAcademy] = useState(false);
const [pauseAcademyReason, setPauseAcademyReason] = useState('');
const hasSentPauseUserRequest = React.useRef<boolean>(false);
const { role } = useSession();

// Effect to fetch the latest user info and course configurations from the backend on refresh,
// if the user was previously logged in
React.useEffect(() => {
Expand Down Expand Up @@ -133,14 +147,90 @@ const Application: React.FC = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

// Effect for dev tools blocking/detection when exam mode enabled
React.useEffect(() => {
if (role !== Role.Student && !isPreviewExamMode) {
return;
}

const showPauseAcademyOverlay = (reason: string) => {
setPauseAcademy(true);
setPauseAcademyReason(reason);
if (hasSentPauseUserRequest.current === false) {
dispatch(SessionActions.pauseUser());
hasSentPauseUserRequest.current = true;
}
};

if (enableExamMode || isPreviewExamMode) {
dispatch(SessionActions.reportFocusRegain());

if (isPaused !== undefined && isPaused) {
showPauseAcademyOverlay('Browser was refreshed when Source Academy was paused');
} else {
hasSentPauseUserRequest.current = false;
}

// Disable/Detect dev tools
disableDevtool({
ondevtoolopen: () => {
showPauseAcademyOverlay('Developer tools detected');
}
});

document.addEventListener('contextmenu', event => event.preventDefault());
document.addEventListener('keydown', event => {
if (
event.key == 'F12' ||
((event.key == 'I' || event.key == 'J' || event.key == 'C') &&
event.ctrlKey &&
event.shiftKey)
) {
event.preventDefault();
}
});

// Detect when Source Academy tab's content are hidden (e.g., user changes tab while Source Academy is active)
document.addEventListener('visibilitychange', _ => {
if (document.visibilityState === 'hidden') {
dispatch(SessionActions.reportFocusLost());
} else {
showDangerMessage('Source Academy was out of focus.', 5000);
dispatch(SessionActions.reportFocusRegain());
}
});
}
}, [dispatch, enableExamMode, isPaused, hasSentPauseUserRequest, role, isPreviewExamMode]);

const resumeCodeSubmitHandler = (resumeCode: string) => {
if (!resumeCode || resumeCode.length === 0) {
showWarningMessage('Resume code cannot be empty.');
} else {
dispatch(
SessionActions.validateResumeCode(resumeCode, (isResumeCodeValid: boolean) => {
if (isResumeCodeValid) {
setPauseAcademy(false);
hasSentPauseUserRequest.current = false;
} else {
showWarningMessage('Resume code is invalid.');
}
})
);
}
};

return (
<WorkspaceSettingsContext.Provider value={[workspaceSettings, setWorkspaceSettings]}>
<div className="Application">
<NavigationBar />
<div className="Application__main">
<Outlet />
{pauseAcademy ? (
<PauseAcademyOverlay reason={pauseAcademyReason} onSubmit={resumeCodeSubmitHandler} />
) : (
<div className="Application">
<NavigationBar />
<div className="Application__main">
<Outlet />
</div>
</div>
</div>
)}
</WorkspaceSettingsContext.Provider>
);
};
Expand Down
5 changes: 3 additions & 2 deletions src/commons/application/ApplicationTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
import { RouterState } from './types/CommonsTypes';
import { ExternalLibraryName } from './types/ExternalTypes';
import { SessionState } from './types/SessionTypes';
import { VscodeState as VscodeState } from './types/VscodeTypes';
import { VscodeState } from './types/VscodeTypes';

export type OverallState = {
readonly router: RouterState;
Expand Down Expand Up @@ -554,7 +554,8 @@ export const defaultSession: SessionState = {
students: undefined,
teamFormationOverviews: undefined,
gradings: {},
notifications: []
notifications: [],
isPaused: undefined
};

export const defaultStories: StoriesState = {
Expand Down
9 changes: 8 additions & 1 deletion src/commons/application/actions/SessionActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,14 @@ const SessionActions = createActions('session', {
deleteUserCourseRegistration: (courseRegId: number) => ({ courseRegId }),
updateCourseResearchAgreement: (agreedToResearch: boolean) => ({ agreedToResearch }),
updateStoriesUserRole: (userId: number, role: StoriesRole) => ({ userId, role }),
deleteStoriesUserUserGroups: (userId: number) => ({ userId })
deleteStoriesUserUserGroups: (userId: number) => ({ userId }),
validateResumeCode: (resumeCode: string, callback: (isResumeCodeValid: boolean) => any) => ({
resumeCode,
callback
}),
pauseUser: () => {},
reportFocusLost: () => {},
reportFocusRegain: () => {}
});

// For compatibility with existing code (actions helper)
Expand Down
3 changes: 2 additions & 1 deletion src/commons/application/actions/__tests__/SessionActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@ test('setUser generates correct action object', () => {
role: Role.Staff,
viewable: true
}
]
],
isPaused: false
};
const action = SessionActions.setUser(user);
expect(action).toEqual({
Expand Down
3 changes: 2 additions & 1 deletion src/commons/application/reducers/__tests__/SessionReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ test('SET_USER works correctly', () => {
viewable: true,
role: Role.Staff
}
]
],
isPaused: false
};

const action = {
Expand Down
8 changes: 8 additions & 0 deletions src/commons/application/types/SessionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type SessionState = {
readonly userId?: number;
readonly name?: string;
readonly courses: UserCourse[];
readonly isPaused?: boolean;

// Course Registration
readonly courseRegId?: number;
Expand All @@ -39,10 +40,13 @@ export type SessionState = {
readonly enableAchievements?: boolean;
readonly enableSourcecast?: boolean;
readonly enableStories?: boolean;
readonly enableExamMode?: boolean;
readonly resumeCode?: string;
readonly sourceChapter?: Chapter;
readonly sourceVariant?: Variant;
readonly moduleHelpText?: string;
readonly assetsPrefix?: string;
readonly isOfficialCourse?: boolean;

readonly assessmentConfigurations?: AssessmentConfiguration[];
readonly userCourseRegistrations?: AdminPanelCourseRegistration[];
Expand Down Expand Up @@ -84,6 +88,7 @@ export type User = {
name: string;
username: string;
courses: UserCourse[];
isPaused: boolean;
};

export type CourseRegistration = {
Expand All @@ -105,10 +110,13 @@ export type CourseConfiguration = {
enableAchievements: boolean;
enableSourcecast: boolean;
enableStories: boolean;
enableExamMode: boolean;
resumeCode: string;
sourceChapter: Chapter;
sourceVariant: Variant;
moduleHelpText: string;
assetsPrefix: string;
isOfficialCourse: boolean;
};

export type AdminPanelCourseRegistration = {
Expand Down
11 changes: 11 additions & 0 deletions src/commons/assessmentWorkspace/AssessmentWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import MobileWorkspace, { MobileWorkspaceProps } from '../mobileWorkspace/Mobile
import SideContentAutograder from '../sideContent/content/SideContentAutograder';
import SideContentContestLeaderboard from '../sideContent/content/SideContentContestLeaderboard';
import SideContentContestVotingContainer from '../sideContent/content/SideContentContestVotingContainer';
import SideContentDocumentation from '../sideContent/content/SideContentDocumentation';
import SideContentToneMatrix from '../sideContent/content/SideContentToneMatrix';
import { SideContentProps } from '../sideContent/SideContent';
import { changeSideContentHeight } from '../sideContent/SideContentActions';
Expand Down Expand Up @@ -412,6 +413,7 @@ const AssessmentWorkspace: React.FC<AssessmentWorkspaceProps> = props => {
const isTeamAssessment =
assessmentOverview !== undefined ? assessmentOverview.maxTeamSize > 1 : false;
const isContestVoting = question?.type === QuestionTypes.voting;
const isPrivate = assessmentOverview?.private ?? false;
const handleContestEntryClick = (_submissionId: number, answer: string) => {
// TODO: Hardcoded to make use of the first editor tab. Refactoring is needed for this workspace to enable Folder mode.
handleEditorValueChange(0, answer);
Expand Down Expand Up @@ -557,6 +559,15 @@ const AssessmentWorkspace: React.FC<AssessmentWorkspaceProps> = props => {
});
}

if (isPrivate) {
tabs.push({
label: `Documentation`,
iconName: IconNames.BOOK,
body: <SideContentDocumentation />,
id: SideContentType.documentation
});
}

const onChangeTabs = (
newTabId: SideContentType,
prevTabId: SideContentType,
Expand Down
Loading
Loading