mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-09-24 23:01:18 +08:00
feat(client): add exam attempts to exam-download page (#61361)
Co-authored-by: Tom <20648924+moT01@users.noreply.github.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> Co-authored-by: Mrugesh Mohapatra <1884376+raisedadead@users.noreply.github.com>
This commit is contained in:
co-authored by
Tom
Oliver Eyton-Williams
Mrugesh Mohapatra
parent
6c3bc14f52
commit
35d05d2c05
@@ -1232,34 +1232,77 @@ describe('/exam-environment/', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /exam-environment/challenges/:challengeId/exam-mappings', () => {
|
||||
describe('GET /exam-environment/exam-challenge', () => {
|
||||
afterAll(async () => {
|
||||
await fastifyTestInstance.prisma.examEnvironmentChallenge.deleteMany(
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('should return 200 and an empty array if no exams are mapped to the challenge', async () => {
|
||||
it('should return 200 and an empty array if no mapping exists', async () => {
|
||||
const challengeId = mock.oid();
|
||||
const res = await superGet(
|
||||
`/exam-environment/challenges/${challengeId}/exam-mappings`
|
||||
const examId = mock.oid();
|
||||
|
||||
const res1 = await superGet(
|
||||
`/exam-environment/exam-challenge?challengeId=${challengeId}`
|
||||
);
|
||||
expect(res.body).toStrictEqual([]);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res1.body).toStrictEqual([]);
|
||||
expect(res1.status).toBe(200);
|
||||
|
||||
const res2 = await superGet(
|
||||
`/exam-environment/exam-challenge?examId=${examId}`
|
||||
);
|
||||
expect(res2.body).toStrictEqual([]);
|
||||
expect(res2.status).toBe(200);
|
||||
|
||||
const res3 = await superGet(
|
||||
`/exam-environment/exam-challenge?challengeId=${challengeId}&examId=${examId}`
|
||||
);
|
||||
expect(res3.body).toStrictEqual([]);
|
||||
expect(res3.status).toBe(200);
|
||||
});
|
||||
|
||||
it('should return 200 and a list of exams mapped to the challenge', async () => {
|
||||
it('should return 200 and a list of challenge-exam mappings if one exists', async () => {
|
||||
await fastifyTestInstance.prisma.examEnvironmentChallenge.create({
|
||||
data: mock.examEnvironmentChallenge
|
||||
});
|
||||
const res = await superGet(
|
||||
`/exam-environment/challenges/${mock.examEnvironmentChallenge.challengeId}/exam-mappings`
|
||||
const res1 = await superGet(
|
||||
`/exam-environment/exam-challenge?challengeId=${mock.examEnvironmentChallenge.challengeId}`
|
||||
);
|
||||
expect(res.body).toEqual(
|
||||
expect(res1.body).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ examId: mock.examId })
|
||||
expect.objectContaining({
|
||||
examId: mock.examId,
|
||||
challengeId: mock.examEnvironmentChallenge.challengeId
|
||||
})
|
||||
])
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res1.status).toBe(200);
|
||||
|
||||
const res2 = await superGet(
|
||||
`/exam-environment/exam-challenge?examId=${mock.examId}`
|
||||
);
|
||||
expect(res2.body).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
examId: mock.examId,
|
||||
challengeId: mock.examEnvironmentChallenge.challengeId
|
||||
})
|
||||
])
|
||||
);
|
||||
expect(res2.status).toBe(200);
|
||||
|
||||
const res3 = await superGet(
|
||||
`/exam-environment/exam-challenge?challengeId=${mock.examEnvironmentChallenge.challengeId}&examId=${mock.examId}`
|
||||
);
|
||||
expect(res3.body).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
examId: mock.examId,
|
||||
challengeId: mock.examEnvironmentChallenge.challengeId
|
||||
})
|
||||
])
|
||||
);
|
||||
expect(res3.status).toBe(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,11 +89,11 @@ export const examEnvironmentOpenRoutes: FastifyPluginCallbackTypebox = (
|
||||
tokenMetaHandler
|
||||
);
|
||||
fastify.get(
|
||||
'/exam-environment/challenges/:challengeId/exam-mappings',
|
||||
'/exam-environment/exam-challenge',
|
||||
{
|
||||
schema: schemas.examEnvironmentGetExamMappingsByChallengeId
|
||||
schema: schemas.examEnvironmentGetExamChallenge
|
||||
},
|
||||
getExamMappingsByChallengeId
|
||||
getExamChallenge
|
||||
);
|
||||
done();
|
||||
};
|
||||
@@ -1000,22 +1000,21 @@ export async function getExamAttemptsByExamIdHandler(
|
||||
/**
|
||||
* Gets all the relations for a given challenge and exam(s).
|
||||
*/
|
||||
export async function getExamMappingsByChallengeId(
|
||||
export async function getExamChallenge(
|
||||
this: FastifyInstance,
|
||||
req: UpdateReqType<
|
||||
typeof schemas.examEnvironmentGetExamMappingsByChallengeId
|
||||
>,
|
||||
req: UpdateReqType<typeof schemas.examEnvironmentGetExamChallenge>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
const logger = this.log.child({ req });
|
||||
const { challengeId } = req.params;
|
||||
const { challengeId, examId } = req.query;
|
||||
|
||||
logger.info({ challengeId });
|
||||
logger.info({ challengeId, examId });
|
||||
|
||||
const maybeData = await mapErr(
|
||||
this.prisma.examEnvironmentChallenge.findMany({
|
||||
where: {
|
||||
challengeId
|
||||
challengeId: challengeId ?? undefined,
|
||||
examId: examId ?? undefined
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Type } from '@fastify/type-provider-typebox';
|
||||
// import { STANDARD_ERROR } from '../utils/errors';
|
||||
|
||||
export const examEnvironmentGetExamMappingsByChallengeId = {
|
||||
params: Type.Object({
|
||||
challengeId: Type.String({ format: 'objectid' })
|
||||
export const examEnvironmentGetExamChallenge = {
|
||||
querystring: Type.Object({
|
||||
challengeId: Type.Optional(Type.String({ format: 'objectid' })),
|
||||
examId: Type.Optional(Type.String({ format: 'objectid' }))
|
||||
})
|
||||
// response: {
|
||||
// 200: examEnvAttempt,
|
||||
|
||||
@@ -7,4 +7,4 @@ export {
|
||||
export { examEnvironmentPostExamGeneratedExam } from './exam-environment-exam-generated-exam.js';
|
||||
export { examEnvironmentTokenMeta } from './token-meta.js';
|
||||
export { examEnvironmentExams } from './exam-environment-exams.js';
|
||||
export { examEnvironmentGetExamMappingsByChallengeId } from './challenges.js';
|
||||
export { examEnvironmentGetExamChallenge } from './challenges.js';
|
||||
|
||||
@@ -380,6 +380,14 @@
|
||||
}
|
||||
},
|
||||
"exam": {
|
||||
"attempts": "Attempts",
|
||||
"no-attempts-yet": "No attempts yet",
|
||||
"date-taken": "Date Taken",
|
||||
"score": "Score",
|
||||
"status": "Status",
|
||||
"pending": "Pending",
|
||||
"passed": "Passed",
|
||||
"failed": "Failed",
|
||||
"download-header": "Download the freeCodeCamp Exam Environment App",
|
||||
"explanation": "To earn a certification, you must take an exam to test your understanding of the material you have learned. Taking the exam is absolutely free of charge.",
|
||||
"version": "The latest version of our app is: {{version}}.",
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"@loadable/component": "5.16.3",
|
||||
"@redux-devtools/extension": "3.3.0",
|
||||
"@redux-saga/core": "^1.2.2",
|
||||
"@reduxjs/toolkit": "2.8.2",
|
||||
"@stripe/react-stripe-js": "1.16.5",
|
||||
"@stripe/stripe-js": "1.54.2",
|
||||
"algoliasearch": "4.22.1",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { createStore as reduxCreateStore, applyMiddleware } from 'redux';
|
||||
import { composeWithDevTools } from '@redux-devtools/extension';
|
||||
// import { composeWithDevTools } from '@redux-devtools/extension';
|
||||
import { createEpicMiddleware } from 'redux-observable';
|
||||
import createSagaMiddleware from 'redux-saga';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
|
||||
import envData from '../../config/env.json';
|
||||
import { isBrowser } from '../../utils';
|
||||
import { examAttempts } from '../utils/ajax';
|
||||
import rootEpic from './root-epic';
|
||||
import rootReducer from './root-reducer';
|
||||
import rootSaga from './root-saga';
|
||||
@@ -32,9 +34,9 @@ const epicMiddleware = createEpicMiddleware({
|
||||
}
|
||||
});
|
||||
|
||||
const composeEnhancers = composeWithDevTools({
|
||||
// options like actionSanitizer, stateSanitizer
|
||||
});
|
||||
// const composeEnhancers = composeWithDevTools({
|
||||
// // options like actionSanitizer, stateSanitizer
|
||||
// });
|
||||
|
||||
export const createStore = (preloadedState = {}) => {
|
||||
let store;
|
||||
@@ -42,14 +44,30 @@ export const createStore = (preloadedState = {}) => {
|
||||
store = reduxCreateStore(
|
||||
rootReducer,
|
||||
preloadedState,
|
||||
applyMiddleware(sagaMiddleware, epicMiddleware)
|
||||
// @ts-expect-error RTK uses unknown, Redux uses any
|
||||
applyMiddleware(sagaMiddleware, epicMiddleware, examAttempts.middleware)
|
||||
);
|
||||
} else {
|
||||
store = reduxCreateStore(
|
||||
rootReducer,
|
||||
preloadedState,
|
||||
composeEnhancers(applyMiddleware(sagaMiddleware, epicMiddleware))
|
||||
);
|
||||
// store = reduxCreateStore(
|
||||
// rootReducer,
|
||||
// preloadedState,
|
||||
// composeEnhancers(
|
||||
// // @ts-expect-error RTK uses unknown, Redux uses any
|
||||
// applyMiddleware(sagaMiddleware, epicMiddleware, examAttempts.middleware)
|
||||
// )
|
||||
// );
|
||||
store = configureStore({
|
||||
// @ts-expect-error RTK uses unknown, Redux uses any
|
||||
reducer: rootReducer,
|
||||
// @ts-expect-error RTK uses unknown, Redux uses any
|
||||
middleware: getDefaultMiddleware => {
|
||||
return getDefaultMiddleware()
|
||||
.concat(examAttempts.middleware)
|
||||
.concat(sagaMiddleware)
|
||||
.concat(epicMiddleware);
|
||||
},
|
||||
preloadedState
|
||||
});
|
||||
}
|
||||
sagaMiddleware.run(rootSaga);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ns as curriculumMapNameSpace,
|
||||
reducer as curriculumMap
|
||||
} from '../templates/Introduction/redux';
|
||||
import { examAttempts } from '../utils/ajax';
|
||||
import { ns as appNameSpace } from './action-types';
|
||||
import { ns as settingsNameSpace, reducer as settings } from './settings';
|
||||
import { FlashApp as flashNameSpace } from './types';
|
||||
@@ -24,5 +25,6 @@ export default combineReducers({
|
||||
[curriculumMapNameSpace]: curriculumMap,
|
||||
[flashNameSpace]: flash,
|
||||
[searchNameSpace]: search,
|
||||
[settingsNameSpace]: settings
|
||||
[settingsNameSpace]: settings,
|
||||
[examAttempts.reducerPath]: examAttempts.reducer
|
||||
});
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Table } from '@freecodecamp/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Loader } from '../../../components/helpers';
|
||||
import { examAttempts } from '../../../utils/ajax';
|
||||
|
||||
interface AttemptsProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export function Attempts({ id }: AttemptsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const examIdsQuery = examAttempts.useGetExamIdsByChallengeIdQuery(id);
|
||||
const [getAttempts, attemptsMutation] =
|
||||
examAttempts.useGetExamAttemptsByExamIdMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (!examIdsQuery.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const examId = examIdsQuery.data.at(0)!.examId;
|
||||
void getAttempts(examId);
|
||||
}, [examIdsQuery.data, getAttempts]);
|
||||
|
||||
if (examIdsQuery.isLoading || attemptsMutation.isLoading) {
|
||||
return <Loader />;
|
||||
}
|
||||
if (examIdsQuery.error || !examIdsQuery.data) {
|
||||
console.error(examIdsQuery.error);
|
||||
return <p>{t('flash.generic-error')}</p>;
|
||||
}
|
||||
|
||||
if (attemptsMutation.error) {
|
||||
console.error(attemptsMutation.error);
|
||||
return <p>{t('flash.generic-error')}</p>;
|
||||
}
|
||||
|
||||
const attempts = attemptsMutation.data;
|
||||
|
||||
if (attempts === undefined) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (attempts.length === 0) {
|
||||
return <p>{t('exam.no-attempts-yet')}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table striped>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('exam.date-taken')}</th>
|
||||
<th>{t('exam.score')} [%]</th>
|
||||
<th>{t('exam.status')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{attempts.map(attempt => (
|
||||
<tr key={attempt.startTimeInMS}>
|
||||
<td>{new Date(attempt.startTimeInMS).toTimeString()}</td>
|
||||
<td>
|
||||
{attempt.result
|
||||
? `${attempt.result.percent}%`
|
||||
: t('exam.pending')}
|
||||
</td>
|
||||
<td>
|
||||
{attempt.result
|
||||
? attempt.result.passed
|
||||
? t('exam.passed')
|
||||
: t('exam.failed')
|
||||
: t('exam.pending')}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Spacer } from '@freecodecamp/ui';
|
||||
import { graphql } from 'gatsby';
|
||||
import { Button, Dropdown, MenuItem, Spacer } from '@freecodecamp/ui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTranslation, withTranslation } from 'react-i18next';
|
||||
import { createSelector } from 'reselect';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FullWidthRow } from '../../../components/helpers';
|
||||
import useDetectOS from '../utils/use-detect-os';
|
||||
import { ChallengeNode } from '../../../redux/prop-types';
|
||||
import { isSignedInSelector } from '../../../redux/selectors';
|
||||
import { Attempts } from './attempts';
|
||||
|
||||
interface GitProps {
|
||||
tag_name: string;
|
||||
@@ -12,7 +19,26 @@ interface GitProps {
|
||||
}[];
|
||||
}
|
||||
|
||||
function ShowExamDownload(): JSX.Element {
|
||||
const mapStateToProps = createSelector(
|
||||
isSignedInSelector,
|
||||
(isSignedIn: boolean) => ({
|
||||
isSignedIn
|
||||
})
|
||||
);
|
||||
|
||||
interface ShowExamDownloadProps {
|
||||
data: { challengeNode: ChallengeNode };
|
||||
isSignedIn: boolean;
|
||||
}
|
||||
|
||||
function ShowExamDownload({
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: { id }
|
||||
}
|
||||
},
|
||||
isSignedIn
|
||||
}: ShowExamDownloadProps): JSX.Element {
|
||||
const [latestVersion, setLatestVersion] = useState<string | null>(null);
|
||||
|
||||
const [downloadLink, setDownloadLink] = useState<string | undefined>('');
|
||||
@@ -22,8 +48,8 @@ function ShowExamDownload(): JSX.Element {
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleDownloadLink = (downloadLinks: string[]) => {
|
||||
const win = downloadLinks.find(link => link.match(/\.msi/));
|
||||
function handleDownloadLink(downloadLinks: string[]) {
|
||||
const win = downloadLinks.find(link => link.match(/\.exe/));
|
||||
const macARM = downloadLinks.find(
|
||||
link => link.match(/aarch64/) && link.match(/\.dmg/)
|
||||
);
|
||||
@@ -70,41 +96,44 @@ function ShowExamDownload(): JSX.Element {
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const checkLatestVersion = async () => {
|
||||
return await fetch(
|
||||
'https://api.github.com/repos/freeCodeCamp/exam-env/releases/latest'
|
||||
);
|
||||
};
|
||||
checkLatestVersion()
|
||||
.then(response => {
|
||||
async function checkLatestVersion() {
|
||||
try {
|
||||
const response = await fetch(
|
||||
'https://api.github.com/repos/freeCodeCamp/exam-env/releases/latest'
|
||||
);
|
||||
if (response.ok) {
|
||||
void response.json().then(data => {
|
||||
const { tag_name, assets } = data as GitProps;
|
||||
setLatestVersion(tag_name);
|
||||
|
||||
setDownloadLink(
|
||||
handleDownloadLink(
|
||||
assets.map(links => links.browser_download_url)
|
||||
)
|
||||
);
|
||||
|
||||
setDownloadLinks(assets.map(links => links.browser_download_url));
|
||||
});
|
||||
const data = (await response.json()) as GitProps;
|
||||
const { tag_name, assets } = data;
|
||||
setLatestVersion(tag_name);
|
||||
const urls = assets.map(link => link.browser_download_url);
|
||||
setDownloadLink(handleDownloadLink(urls));
|
||||
setDownloadLinks(urls);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
} catch {
|
||||
setLatestVersion('...');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void checkLatestVersion();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [os]);
|
||||
|
||||
return (
|
||||
<FullWidthRow>
|
||||
<Spacer size='l' />
|
||||
<h2>{t('exam.download-header')}</h2>
|
||||
<p>{t('exam.explanation')}</p>
|
||||
<Spacer size='l' />
|
||||
{isSignedIn && (
|
||||
<>
|
||||
<h2>{t('exam.attempts')}</h2>
|
||||
<Attempts id={id} />
|
||||
<Spacer size='l' />
|
||||
</>
|
||||
)}
|
||||
<p>
|
||||
{t('exam.version', {
|
||||
version: latestVersion || '...'
|
||||
@@ -118,29 +147,48 @@ function ShowExamDownload(): JSX.Element {
|
||||
>
|
||||
{t('buttons.download-latest-version')}
|
||||
</Button>
|
||||
{!downloadLink && <strong>{t('exam.unable-to-detect-os')}</strong>}
|
||||
<Spacer size='l' />
|
||||
<details>
|
||||
<summary>{t('exam.download-details')}</summary>
|
||||
<ul>
|
||||
{!downloadLink && (
|
||||
<>
|
||||
<Spacer size='m' />
|
||||
<strong>{t('exam.unable-to-detect-os')}</strong>
|
||||
</>
|
||||
)}
|
||||
<Spacer size='m' />
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle>{t('exam.download-details')}</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{downloadLinks
|
||||
.filter(link => !link.match(/\.sig|\.json/))
|
||||
.map((link, index) => {
|
||||
return (
|
||||
<li key={index} style={{ listStyle: 'none' }}>
|
||||
<a href={link} download={link}>
|
||||
{link}
|
||||
</a>
|
||||
</li>
|
||||
<MenuItem
|
||||
href={link}
|
||||
download={link}
|
||||
key={index}
|
||||
variant='primary'
|
||||
>
|
||||
{link}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</details>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<Spacer size='l' />
|
||||
<strong>{t('exam.download-trouble')}</strong>
|
||||
<strong>{t('exam.download-trouble')}</strong>{' '}
|
||||
<a href='mailto: support@freecodecamp.org'>support@freecodecamp.org</a>
|
||||
</FullWidthRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default ShowExamDownload;
|
||||
export default connect(mapStateToProps)(withTranslation()(ShowExamDownload));
|
||||
|
||||
// GraphQL
|
||||
export const query = graphql`
|
||||
query ExamEnvironmentExam($id: String!) {
|
||||
challengeNode(id: { eq: $id }) {
|
||||
challenge {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import cookies from 'browser-cookies';
|
||||
import envData from '../../config/env.json';
|
||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import envData from '../../config/env.json';
|
||||
import type {
|
||||
ChallengeFile,
|
||||
ChallengeFiles,
|
||||
@@ -212,6 +213,32 @@ export function getGenerateExam(
|
||||
return get(`/exam/${challengeId}`);
|
||||
}
|
||||
|
||||
export interface Exam {
|
||||
id: string;
|
||||
config: {
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Attempt {
|
||||
id: string;
|
||||
examId: string;
|
||||
startTimeInMS: number;
|
||||
questionSets: unknown[];
|
||||
result?: {
|
||||
passed: boolean;
|
||||
percent: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function getExams(): Promise<ResponseWithData<{ exams: Exam[] }>> {
|
||||
return get('/user/exam-environment/exams');
|
||||
}
|
||||
|
||||
export function getExamAttempts(): Promise<ResponseWithData<Attempt[]>> {
|
||||
return get('/user/exam-environment/exam/attempts');
|
||||
}
|
||||
|
||||
/** POST **/
|
||||
|
||||
interface Donation {
|
||||
@@ -393,3 +420,31 @@ export function deleteUserToken(): Promise<ResponseWithData<void>> {
|
||||
export function deleteMsUsername(): Promise<ResponseWithData<void>> {
|
||||
return deleteRequest('/user/ms-username', {});
|
||||
}
|
||||
|
||||
/** RTK */
|
||||
|
||||
export interface ExamEnvironmentChallenge {
|
||||
id: string;
|
||||
examId: string;
|
||||
challengeId: string;
|
||||
}
|
||||
|
||||
export const examAttempts = createApi({
|
||||
reducerPath: 'exam-attempts',
|
||||
baseQuery: fetchBaseQuery({
|
||||
baseUrl: apiLocation,
|
||||
headers: {
|
||||
'CSRF-Token': getCSRFToken()
|
||||
},
|
||||
credentials: 'include'
|
||||
}),
|
||||
endpoints: build => ({
|
||||
getExamAttemptsByExamId: build.mutation<Attempt[], string>({
|
||||
query: examId => `/user/exam-environment/exams/${examId}/attempts`
|
||||
}),
|
||||
getExamIdsByChallengeId: build.query<ExamEnvironmentChallenge[], string>({
|
||||
query: challengeId =>
|
||||
`/exam-environment/exam-challenge?challengeId=${challengeId}`
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
Generated
+60
@@ -348,6 +348,9 @@ importers:
|
||||
'@redux-saga/core':
|
||||
specifier: ^1.2.2
|
||||
version: 1.3.0
|
||||
'@reduxjs/toolkit':
|
||||
specifier: 2.8.2
|
||||
version: 2.8.2(react-redux@7.2.9(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2)
|
||||
'@stripe/react-stripe-js':
|
||||
specifier: 1.16.5
|
||||
version: 1.16.5(@stripe/stripe-js@1.54.2)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)
|
||||
@@ -3764,6 +3767,17 @@ packages:
|
||||
'@redux-saga/types@1.2.1':
|
||||
resolution: {integrity: sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==}
|
||||
|
||||
'@reduxjs/toolkit@2.8.2':
|
||||
resolution: {integrity: sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==}
|
||||
peerDependencies:
|
||||
react: ^16.9.0 || ^17.0.0 || ^18 || ^19
|
||||
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
react-redux:
|
||||
optional: true
|
||||
|
||||
'@remix-run/router@1.11.0':
|
||||
resolution: {integrity: sha512-BHdhcWgeiudl91HvVa2wxqZjSHbheSgIiDvxrF1VjFzBzpTtuDPkOdOi3Iqvc08kXtFkLjhbS+ML9aM8mJS+wQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -4234,6 +4248,12 @@ packages:
|
||||
resolution: {integrity: sha512-yxLC57GBDmbDmrnH+vJxsrbV4/aYUucBONkSRLZyJIVFAl/QJH+O/h+phITHDaxVZCYZAcudYJw4ERE32BJM7g==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
'@standard-schema/spec@1.0.0':
|
||||
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
|
||||
|
||||
'@standard-schema/utils@0.3.0':
|
||||
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
||||
|
||||
'@stripe/react-stripe-js@1.16.5':
|
||||
resolution: {integrity: sha512-lVPW3IfwdacyS22pP+nBB6/GNFRRhT/4jfgAK6T2guQmtzPwJV1DogiGGaBNhiKtSY18+yS8KlHSu+PvZNclvQ==}
|
||||
peerDependencies:
|
||||
@@ -8589,6 +8609,9 @@ packages:
|
||||
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
immer@10.1.1:
|
||||
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
|
||||
|
||||
immer@8.0.1:
|
||||
resolution: {integrity: sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==}
|
||||
|
||||
@@ -11681,9 +11704,17 @@ packages:
|
||||
peerDependencies:
|
||||
redux: ^4
|
||||
|
||||
redux-thunk@3.1.0:
|
||||
resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
|
||||
peerDependencies:
|
||||
redux: ^5.0.0
|
||||
|
||||
redux@4.2.1:
|
||||
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
|
||||
|
||||
redux@5.0.1:
|
||||
resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
|
||||
|
||||
reflect.getprototypeof@1.0.10:
|
||||
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -11825,6 +11856,9 @@ packages:
|
||||
reselect@4.1.8:
|
||||
resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==}
|
||||
|
||||
reselect@5.1.1:
|
||||
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
|
||||
|
||||
resize-observer-polyfill@1.5.1:
|
||||
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
||||
|
||||
@@ -17891,6 +17925,18 @@ snapshots:
|
||||
|
||||
'@redux-saga/types@1.2.1': {}
|
||||
|
||||
'@reduxjs/toolkit@2.8.2(react-redux@7.2.9(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2)':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.0.0
|
||||
'@standard-schema/utils': 0.3.0
|
||||
immer: 10.1.1
|
||||
redux: 5.0.1
|
||||
redux-thunk: 3.1.0(redux@5.0.1)
|
||||
reselect: 5.1.1
|
||||
optionalDependencies:
|
||||
react: 17.0.2
|
||||
react-redux: 7.2.9(react-dom@17.0.2(react@17.0.2))(react@17.0.2)
|
||||
|
||||
'@remix-run/router@1.11.0': {}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.46.2':
|
||||
@@ -18581,6 +18627,10 @@ snapshots:
|
||||
'@smithy/types': 2.10.0
|
||||
tslib: 2.6.2
|
||||
|
||||
'@standard-schema/spec@1.0.0': {}
|
||||
|
||||
'@standard-schema/utils@0.3.0': {}
|
||||
|
||||
'@stripe/react-stripe-js@1.16.5(@stripe/stripe-js@1.54.2)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)':
|
||||
dependencies:
|
||||
'@stripe/stripe-js': 1.54.2
|
||||
@@ -24568,6 +24618,8 @@ snapshots:
|
||||
|
||||
ignore@7.0.5: {}
|
||||
|
||||
immer@10.1.1: {}
|
||||
|
||||
immer@8.0.1: {}
|
||||
|
||||
immutable@4.3.5: {}
|
||||
@@ -28257,10 +28309,16 @@ snapshots:
|
||||
dependencies:
|
||||
redux: 4.2.1
|
||||
|
||||
redux-thunk@3.1.0(redux@5.0.1):
|
||||
dependencies:
|
||||
redux: 5.0.1
|
||||
|
||||
redux@4.2.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.9
|
||||
|
||||
redux@5.0.1: {}
|
||||
|
||||
reflect.getprototypeof@1.0.10:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
@@ -28487,6 +28545,8 @@ snapshots:
|
||||
|
||||
reselect@4.1.8: {}
|
||||
|
||||
reselect@5.1.1: {}
|
||||
|
||||
resize-observer-polyfill@1.5.1: {}
|
||||
|
||||
resolve-alpn@1.2.1: {}
|
||||
|
||||
Reference in New Issue
Block a user