Skip to content

Add exam mode #3106

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

Open
wants to merge 67 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
67 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
bcb9833
Merge branch 'master' into exam_mode
RichDom2185 Apr 17, 2025
a0e854d
Simplify props name
RichDom2185 Apr 17, 2025
75a9fbb
Remove commented code and unnecessary bool prop
RichDom2185 Apr 17, 2025
715aeff
Remove unnecessary underscore variable
RichDom2185 Apr 17, 2025
407052d
merge from master and resolve conflicts
iZUMi-kyouka Jun 10, 2025
1b21039
Merge branch 'master' of https://github.com/source-academy/frontend i…
RichDom2185 Jun 16, 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
2 changes: 2 additions & 0 deletions src/commons/application/types/SessionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type SessionState = {
readonly enableAchievements?: boolean;
readonly enableSourcecast?: boolean;
readonly enableStories?: boolean;
readonly enableExamMode?: boolean;
readonly sourceChapter?: Chapter;
readonly sourceVariant?: Variant;
readonly moduleHelpText?: string;
Expand Down Expand Up @@ -105,6 +106,7 @@ export type CourseConfiguration = {
enableAchievements: boolean;
enableSourcecast: boolean;
enableStories: boolean;
enableExamMode: boolean;
sourceChapter: Chapter;
sourceVariant: Variant;
moduleHelpText: string;
Expand Down
4 changes: 3 additions & 1 deletion src/commons/dropdown/DropdownCourses.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useNavigate } from 'react-router';

import { Role } from '../application/ApplicationTypes';
import { UserCourse } from '../application/types/SessionTypes';
import { useTypedSelector } from '../utils/Hooks';

type Props = {
isOpen: boolean;
Expand All @@ -15,6 +16,7 @@ type Props = {

const DropdownCourses: React.FC<Props> = ({ isOpen, onClose, courses, courseId }) => {
const navigate = useNavigate();
const enableExamMode = useTypedSelector(state => state.session.enableExamMode);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use useSession for simplicity:

Suggested change
const enableExamMode = useTypedSelector(state => state.session.enableExamMode);
const { enableExamMode } = useSession();


const options = courses.map(course => ({
value: course.courseId,
Expand Down Expand Up @@ -42,7 +44,7 @@ const DropdownCourses: React.FC<Props> = ({ isOpen, onClose, courses, courseId }
options={options}
fill
onChange={onChangeHandler}
disabled={courses.length <= 1}
disabled={courses.length <= 1 || enableExamMode}
/>
</DialogBody>
</Dialog>
Expand Down
13 changes: 13 additions & 0 deletions src/commons/dropdown/DropdownCreateCourse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const DropdownCreateCourse: React.FC<Props> = props => {
enableAchievements: true,
enableSourcecast: true,
enableStories: false,
enableExamMode: false,
sourceChapter: Chapter.SOURCE_1,
sourceVariant: Variant.DEFAULT,
moduleHelpText: ''
Expand Down Expand Up @@ -234,6 +235,18 @@ const DropdownCreateCourse: React.FC<Props> = props => {
})
}
/>

<Switch
checked={courseConfig.enableExamMode}
inline
label="Enable Exam Mode"
onChange={e =>
setCourseConfig({
...courseConfig,
enableExamMode: (e.target as HTMLInputElement).checked
})
}
/>
</div>
</div>
<div>
Expand Down
2 changes: 2 additions & 0 deletions src/commons/mocks/UserMocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ export const mockCourseConfigurations: CourseConfiguration[] = [
enableAchievements: true,
enableSourcecast: true,
enableStories: false,
enableExamMode: false,
sourceChapter: Chapter.SOURCE_1,
sourceVariant: Variant.DEFAULT,
moduleHelpText: '',
Expand All @@ -386,6 +387,7 @@ export const mockCourseConfigurations: CourseConfiguration[] = [
enableAchievements: false,
enableSourcecast: false,
enableStories: false,
enableExamMode: false,
sourceChapter: Chapter.SOURCE_2,
sourceVariant: Variant.DEFAULT,
moduleHelpText: 'Help Text!',
Expand Down
15 changes: 15 additions & 0 deletions src/commons/sagas/BackendSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,21 @@ const newBackendSagaTwo = combineSagaHandlers(sagaActions, {
assessmentConfigurations: AssessmentConfiguration[] | null;
} = yield call(getLatestCourseRegistrationAndConfiguration, tokens);

if (courseConfiguration?.enableExamMode) {
const {
user
}: {
user: User | null;
courseRegistration: CourseRegistration | null;
courseConfiguration: CourseConfiguration | null;
assessmentConfigurations: AssessmentConfiguration[] | null;
} = yield call(getUser, tokens);

if (user) {
yield put(actions.setUser(user));
}
}

if (!courseRegistration || !courseConfiguration || !assessmentConfigurations) {
yield call(showWarningMessage, `Failed to load course!`);
return yield routerNavigate('/welcome');
Expand Down
4 changes: 4 additions & 0 deletions src/commons/sagas/__tests__/BackendSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ const mockCourseConfiguration1: CourseConfiguration = {
enableAchievements: true,
enableSourcecast: true,
enableStories: false,
enableExamMode: false,
sourceChapter: Chapter.SOURCE_1,
sourceVariant: Variant.DEFAULT,
moduleHelpText: 'Help text',
Expand Down Expand Up @@ -164,6 +165,7 @@ const mockCourseConfiguration2: CourseConfiguration = {
enableAchievements: true,
enableSourcecast: true,
enableStories: false,
enableExamMode: false,
sourceChapter: Chapter.SOURCE_4,
sourceVariant: Variant.DEFAULT,
moduleHelpText: 'Help text',
Expand Down Expand Up @@ -930,6 +932,7 @@ describe('Test UPDATE_COURSE_CONFIG action', () => {
enableAchievements: false,
enableSourcecast: false,
enableStories: false,
enableExamMode: false,
sourceChapter: Chapter.SOURCE_4,
sourceVariant: Variant.DEFAULT,
moduleHelpText: 'Help',
Expand Down Expand Up @@ -1028,6 +1031,7 @@ describe('Test CREATE_COURSE action', () => {
enableAchievements: true,
enableSourcecast: true,
enableStories: false,
enableExamMode: false,
sourceChapter: Chapter.SOURCE_1,
sourceVariant: Variant.DEFAULT,
moduleHelpText: 'Help Text'
Expand Down
9 changes: 7 additions & 2 deletions src/pages/academy/Academy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux';
import { Navigate, Outlet, useNavigate, useParams } from 'react-router';
import ResearchAgreementPrompt from 'src/commons/researchAgreementPrompt/ResearchAgreementPrompt';
import Constants from 'src/commons/utils/Constants';
import { useSession } from 'src/commons/utils/Hooks';
import { useSession, useTypedSelector } from 'src/commons/utils/Hooks';
import classes from 'src/styles/Academy.module.scss';

import SessionActions from '../../commons/application/actions/SessionActions';
Expand Down Expand Up @@ -37,6 +37,7 @@ const CourseSelectingAcademy: React.FC = () => {
const { courseId } = useSession();
const { courseId: routeCourseIdStr } = useParams<{ courseId?: string }>();
const routeCourseId = routeCourseIdStr != null ? parseInt(routeCourseIdStr, 10) : undefined;
const enableExamMode = useTypedSelector(state => state.session.enableExamMode);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useSession is a wrapper around useTypedSelector(state => state.session) among other things

Suggested change
const { courseId } = useSession();
const { courseId: routeCourseIdStr } = useParams<{ courseId?: string }>();
const routeCourseId = routeCourseIdStr != null ? parseInt(routeCourseIdStr, 10) : undefined;
const enableExamMode = useTypedSelector(state => state.session.enableExamMode);
const { courseId, enableExamMode } = useSession();
const { courseId: routeCourseIdStr } = useParams<{ courseId?: string }>();
const routeCourseId = routeCourseIdStr != null ? parseInt(routeCourseIdStr, 10) : undefined;

React.useEffect(() => {
// Regex to handle case where routeCourseIdStr is not a number
Expand All @@ -47,7 +48,11 @@ const CourseSelectingAcademy: React.FC = () => {
if (routeCourseId !== undefined && !Number.isNaN(routeCourseId) && courseId !== routeCourseId) {
dispatch(SessionActions.updateLatestViewedCourse(routeCourseId));
}
}, [courseId, dispatch, routeCourseId, navigate, routeCourseIdStr]);

if (enableExamMode) {
navigate(`/courses/${courseId}`);
}
}, [courseId, dispatch, routeCourseId, navigate, routeCourseIdStr, enableExamMode]);

return Number.isNaN(routeCourseId) ? (
<Navigate to="/" />
Expand Down
3 changes: 3 additions & 0 deletions src/pages/academy/adminPanel/AdminPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const defaultCourseConfig: UpdateCourseConfiguration = {
enableAchievements: true,
enableSourcecast: true,
enableStories: false,
enableExamMode: false,
moduleHelpText: ''
};

Expand Down Expand Up @@ -62,6 +63,7 @@ const AdminPanel: React.FC = () => {
enableAchievements: session.enableAchievements,
enableSourcecast: session.enableSourcecast,
enableStories: session.enableStories,
enableExamMode: session.enableExamMode,
moduleHelpText: session.moduleHelpText
});
}, [
Expand All @@ -71,6 +73,7 @@ const AdminPanel: React.FC = () => {
session.enableGame,
session.enableSourcecast,
session.enableStories,
session.enableExamMode,
session.moduleHelpText,
session.viewable
]);
Expand Down
11 changes: 11 additions & 0 deletions src/pages/academy/adminPanel/subcomponents/CourseConfigPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const CourseConfigPanel: React.FC<Props> = props => {
enableAchievements,
enableSourcecast,
enableStories,
enableExamMode,
moduleHelpText
} = props.courseConfiguration;

Expand Down Expand Up @@ -186,6 +187,16 @@ const CourseConfigPanel: React.FC<Props> = props => {
})
}
/>
<Switch
checked={enableExamMode}
label="Enable Exam Mode"
onChange={e =>
props.setCourseConfiguration({
...props.courseConfiguration,
enableExamMode: (e.target as HTMLInputElement).checked
})
}
/>
</div>
</div>
</div>
Expand Down
16 changes: 9 additions & 7 deletions src/pages/playground/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,8 @@ const Playground: React.FC<PlaygroundProps> = props => {
sourceChapter: courseSourceChapter,
sourceVariant: courseSourceVariant,
googleUser: persistenceUser,
githubOctokitObject
githubOctokitObject,
enableExamMode
} = useTypedSelector(state => state.session);

const dispatch = useDispatch();
Expand Down Expand Up @@ -749,7 +750,7 @@ const Playground: React.FC<PlaygroundProps> = props => {
}
}

if (!isSicpEditor && !Constants.playgroundOnly) {
if (!isSicpEditor && !Constants.playgroundOnly && !enableExamMode) {
tabs.push(remoteExecutionTab);
}

Expand All @@ -765,7 +766,8 @@ const Playground: React.FC<PlaygroundProps> = props => {
shouldShowDataVisualizer,
shouldShowCseMachine,
shouldShowSubstVisualizer,
remoteExecutionTab
remoteExecutionTab,
enableExamMode
]);

// Remove Intro and Remote Execution tabs for mobile
Expand Down Expand Up @@ -972,12 +974,12 @@ const Playground: React.FC<PlaygroundProps> = props => {
controlBarProps: {
editorButtons: [
autorunButtons,
languageConfig.chapter === Chapter.FULL_JS ? null : shareButton,
languageConfig.chapter === Chapter.FULL_JS || enableExamMode ? null : shareButton,
chapterSelectButton,
isSicpEditor ? null : sessionButtons,
isSicpEditor || enableExamMode ? null : sessionButtons,
languageConfig.supports.multiFile ? toggleFolderModeButton : null,
persistenceButtons,
githubButtons,
enableExamMode ? null : persistenceButtons,
enableExamMode ? null : githubButtons,
usingSubst || usingCse || isCseVariant(languageConfig.variant)
? stepperStepLimit
: isSourceLanguage(languageConfig.chapter)
Expand Down