diff --git a/api/src/routes/protected/certificate.ts b/api/src/routes/protected/certificate.ts index 37fdde6ca39..a382278a064 100644 --- a/api/src/routes/protected/certificate.ts +++ b/api/src/routes/protected/certificate.ts @@ -2,7 +2,7 @@ import type { CompletedChallenge } from '@prisma/client'; import validator from 'validator'; import type { FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'; -import { getChallenges } from '../../utils/get-challenges.js'; +import { challenges, getChallenges } from '../../utils/get-challenges.js'; import { Certification, certSlugTypeMap, @@ -216,7 +216,6 @@ export const protectedCertificateRoutes: FastifyPluginCallbackTypebox = ( _options, done ) => { - const challenges = getChallenges(); const certLookup = createCertLookup(challenges); // TODO(POST_MVP): Response should not include updated user. If a client wants the updated user, it should make a separate request diff --git a/api/src/routes/protected/challenge.ts b/api/src/routes/protected/challenge.ts index b68491468c9..6e283a8fa01 100644 --- a/api/src/routes/protected/challenge.ts +++ b/api/src/routes/protected/challenge.ts @@ -22,7 +22,7 @@ import { formatCoderoadChallengeCompletedValidation, formatProjectCompletedValidation } from '../../utils/error-formatting.js'; -import { getChallenges } from '../../utils/get-challenges.js'; +import { challenges } from '../../utils/get-challenges.js'; import { ProgressTimestamp, getPoints } from '../../utils/progress.js'; import { validateExamFromDbSchema, @@ -57,8 +57,6 @@ const userChallengeSelect = { savedChallenges: true }; -const challenges = getChallenges(); - /** * Plugin for the challenge submission endpoints. * diff --git a/api/src/utils/common-challenge-functions.ts b/api/src/utils/common-challenge-functions.ts index c121326b097..dd331fcea05 100644 --- a/api/src/utils/common-challenge-functions.ts +++ b/api/src/utils/common-challenge-functions.ts @@ -2,7 +2,7 @@ import type { ExamResults, user, Prisma } from '@prisma/client'; import { FastifyInstance } from 'fastify'; import { omit, pick } from 'lodash-es'; import { challengeTypes } from '../../../shared/config/challenge-types.js'; -import { getChallenges } from './get-challenges.js'; +import { challenges, savableChallenges } from './get-challenges.js'; import { normalizeDate } from './normalize.js'; export const jsCertProjectIds = [ @@ -13,15 +13,15 @@ export const jsCertProjectIds = [ 'aa2e6f85cab2ab736c9a9b24' ]; -export const multifileCertProjectIds = getChallenges() +export const multifileCertProjectIds = challenges .filter(c => c.challengeType === challengeTypes.multifileCertProject) .map(c => c.id); -export const multifilePythonCertProjectIds = getChallenges() +export const multifilePythonCertProjectIds = challenges .filter(c => c.challengeType === challengeTypes.multifilePythonCertProject) .map(c => c.id); -export const msTrophyChallenges = getChallenges() +export const msTrophyChallenges = challenges .filter(challenge => challenge.challengeType === challengeTypes.msTrophy) .map(({ id, msTrophyId }) => ({ id, msTrophyId })); @@ -133,11 +133,7 @@ export async function updateUserChallengeData( _completedChallenge; let completedChallenge: CompletedChallenge; - if ( - jsCertProjectIds.includes(challengeId) || - multifileCertProjectIds.includes(challengeId) || - multifilePythonCertProjectIds.includes(challengeId) - ) { + if (savableChallenges.has(challengeId)) { completedChallenge = { ..._completedChallenge, files: files?.map( diff --git a/api/src/utils/get-challenges.ts b/api/src/utils/get-challenges.ts index 5716cc81572..fe241bc3dc0 100644 --- a/api/src/utils/get-challenges.ts +++ b/api/src/utils/get-challenges.ts @@ -21,6 +21,7 @@ interface Block { challengeType: number; url?: string; msTrophyId?: string; + saveSubmissionToDB?: boolean; }[]; } @@ -51,3 +52,13 @@ export function getChallenges(): Block['challenges'] { return [...acc, ...challengesForBlock.flat()]; }, []); } + +export const challenges = getChallenges(); + +export const savableChallenges = challenges.reduce((acc, curr) => { + if (curr.saveSubmissionToDB) { + acc.add(curr.id); + } + + return acc; +}, new Set()); diff --git a/client/gatsby-node.js b/client/gatsby-node.js index 9742b286812..df986c247cf 100644 --- a/client/gatsby-node.js +++ b/client/gatsby-node.js @@ -103,6 +103,7 @@ exports.createPages = async function createPages({ history fileKey } + saveSubmissionToDB solutions { contents ext @@ -334,6 +335,7 @@ exports.createSchemaCustomization = ({ actions }) => { questions: [Question] quizzes: [Quiz] required: [RequiredResource] + saveSubmissionToDB: Boolean scene: Scene solutions: [[FileContents]] suborder: Int diff --git a/client/src/redux/prop-types.ts b/client/src/redux/prop-types.ts index 991a1783c69..89ddfb0a3e2 100644 --- a/client/src/redux/prop-types.ts +++ b/client/src/redux/prop-types.ts @@ -246,6 +246,7 @@ export type ChallengeNode = { quizzes: Quiz[]; assignments: string[]; required: Required[]; + saveSubmissionToDB?: boolean; scene: FullScene; solutions: { [T: string]: FileKeyChallenge; @@ -309,6 +310,7 @@ export type DailyCodingChallengeNode = { notes: string; videoUrl?: string; translationPending: false; + saveSubmissionToDB?: boolean; }; }; @@ -534,6 +536,7 @@ export type ChallengeMeta = { helpCategory: string; disableLoopProtectTests: boolean; disableLoopProtectPreview: boolean; + saveSubmissionToDB?: boolean; } & NavigationPaths; export type NavigationPaths = { diff --git a/client/src/redux/save-challenge-saga.js b/client/src/redux/save-challenge-saga.js index 294971cb3d3..e04a9e6c9b6 100644 --- a/client/src/redux/save-challenge-saga.js +++ b/client/src/redux/save-challenge-saga.js @@ -1,6 +1,5 @@ import { call, put, select, takeEvery } from 'redux-saga/effects'; -import { canSaveToDB } from '../../../shared-dist/config/challenge-types'; import { createFlashMessage } from '../components/Flash/redux'; import { FlashMessages } from '../components/Flash/redux/flash-messages'; import { @@ -19,7 +18,9 @@ import { saveChallengeComplete } from './actions'; import { savedChallengesSelector } from './selectors'; function* saveChallengeSaga() { - const { id, challengeType } = yield select(challengeMetaSelector); + const { id, challengeType, saveSubmissionToDB } = yield select( + challengeMetaSelector + ); const { challengeFiles } = yield select(challengeDataSelector); const savedChallenges = yield select(savedChallengesSelector); const savedChallenge = savedChallenges.find(challenge => challenge.id === id); @@ -34,7 +35,7 @@ function* saveChallengeSaga() { ); } - if (canSaveToDB(challengeType)) { + if (saveSubmissionToDB) { const body = standardizeRequestBody({ id, challengeFiles, challengeType }); const bodySizeInBytes = getStringSizeInBytes(body); diff --git a/client/src/templates/Challenges/classic/editor.tsx b/client/src/templates/Challenges/classic/editor.tsx index bcd7ca4dbd5..c3f2949b444 100644 --- a/client/src/templates/Challenges/classic/editor.tsx +++ b/client/src/templates/Challenges/classic/editor.tsx @@ -34,10 +34,7 @@ import type { } from '../../../redux/prop-types'; import { editorToneOptions } from '../../../utils/tone/editor-config'; import { editorNotes } from '../../../utils/tone/editor-notes'; -import { - canSaveToDB, - challengeTypes -} from '../../../../../shared-dist/config/challenge-types'; +import { challengeTypes } from '../../../../../shared-dist/config/challenge-types'; import { executeChallenge, saveEditorContent, @@ -106,6 +103,7 @@ export interface EditorProps { resizeProps: ResizeProps; saveChallenge: () => void; saveEditorContent: () => void; + saveSubmissionToDB?: boolean; setEditorFocusability: (isFocusable: boolean) => void; submitChallenge: () => void; stopResetting: () => void; @@ -154,7 +152,10 @@ const mapStateToProps = createSelector( ( attempts: number, canFocus: boolean, - { challengeType }: { challengeType: number }, + { + challengeType, + saveSubmissionToDB + }: { challengeType: number; saveSubmissionToDB?: boolean }, open, previewOpen: boolean, isResetting: boolean, @@ -166,6 +167,7 @@ const mapStateToProps = createSelector( attempts, canFocus: open ? false : canFocus, challengeType, + saveSubmissionToDB, previewOpen, isResetting, isSignedIn, @@ -615,7 +617,7 @@ const Editor = (props: EditorProps): JSX.Element => { monaco.KeyMod.WinCtrl | monaco.KeyCode.KeyS ], run: - canSaveToDB(props.challengeType) && props.isSignedIn + props.saveSubmissionToDB && props.isSignedIn ? // save to database props.saveChallenge : // save to local storage diff --git a/client/src/templates/Challenges/classic/show.tsx b/client/src/templates/Challenges/classic/show.tsx index f18c22e8597..72376d43749 100644 --- a/client/src/templates/Challenges/classic/show.tsx +++ b/client/src/templates/Challenges/classic/show.tsx @@ -212,7 +212,8 @@ function ShowClassic({ usesMultifileEditor, notes, videoUrl, - translationPending + translationPending, + saveSubmissionToDB } } }, @@ -551,7 +552,10 @@ function ShowClassic({ superBlock={superBlock} /> - + void; isOpen: boolean; - challengeType: number; + saveSubmissionToDB?: boolean; reset: () => void; challengeTitle: string; } @@ -41,7 +40,7 @@ function withActions(...fns: Array<() => void>) { function ResetModal({ reset, close, - challengeType, + saveSubmissionToDB, isOpen, challengeTitle }: ResetModalProps): JSX.Element { @@ -56,7 +55,7 @@ function ResetModal({

- {canSaveToDB(challengeType) + {saveSubmissionToDB ? t('learn.revert-warn') : t('learn.reset-warn', { title: challengeTitle @@ -73,7 +72,7 @@ function ResetModal({ variant='danger' onClick={withActions(reset, close)} > - {canSaveToDB(challengeType) + {saveSubmissionToDB ? t('buttons.revert-to-saved-code') : t('buttons.reset-lesson')} diff --git a/client/src/templates/Challenges/components/tool-panel.tsx b/client/src/templates/Challenges/components/tool-panel.tsx index b1983dfb6e3..ffe759890ef 100644 --- a/client/src/templates/Challenges/components/tool-panel.tsx +++ b/client/src/templates/Challenges/components/tool-panel.tsx @@ -7,7 +7,6 @@ import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import { createSelector } from 'reselect'; -import { canSaveToDB } from '../../../../../shared-dist/config/challenge-types'; import { openModal, executeChallenge } from '../redux/actions'; import { challengeMetaSelector } from '../redux/selectors'; import { saveChallenge } from '../../../redux/actions'; @@ -18,11 +17,8 @@ import './tool-panel.css'; const mapStateToProps = createSelector( challengeMetaSelector, isSignedInSelector, - ( - { challengeType }: { challengeId: string; challengeType: number }, - isSignedIn - ) => ({ - challengeType, + ({ saveSubmissionToDB }: { saveSubmissionToDB?: boolean }, isSignedIn) => ({ + saveSubmissionToDB, isSignedIn }) ); @@ -39,7 +35,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => ); interface ToolPanelProps { - challengeType: number; + saveSubmissionToDB?: boolean; executeChallenge: (options?: { showCompletionModal: boolean }) => void; saveChallenge: () => void; isMobile?: boolean; @@ -52,7 +48,7 @@ interface ToolPanelProps { } function ToolPanel({ - challengeType, + saveSubmissionToDB, executeChallenge, saveChallenge, isMobile, @@ -76,7 +72,7 @@ function ToolPanel({ - {isSignedIn && canSaveToDB(challengeType) && ( + {isSignedIn && saveSubmissionToDB && ( <>