mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
fix(api): use challenge helper to update completed challenges (#55046)
This commit is contained in:
committed by
GitHub
parent
ee26d4333c
commit
19b5134732
@@ -550,7 +550,7 @@ describe('challengeRoutes', () => {
|
||||
|
||||
// If a challenge has already been completed, it should return the
|
||||
// original completedDate
|
||||
expect(resUpdate.body.completedDate).not.toBe(
|
||||
expect(resUpdate.body.completedDate).toBe(
|
||||
resOriginal.body.completedDate
|
||||
);
|
||||
expect(resUpdate.statusCode).toBe(200);
|
||||
@@ -1201,7 +1201,6 @@ describe('challengeRoutes', () => {
|
||||
completedDate
|
||||
});
|
||||
|
||||
// TODO: use a custom matcher for thisu
|
||||
expect(completedDate).toBeGreaterThan(now);
|
||||
expect(completedDate).toBeLessThan(now + 1000);
|
||||
expect(res.statusCode).toBe(200);
|
||||
|
||||
+43
-54
@@ -31,8 +31,6 @@ import {
|
||||
import { generateRandomExam, createExamResults } from '../utils/exam';
|
||||
import {
|
||||
canSubmitCodeRoadCertProject,
|
||||
createProject,
|
||||
updateProject,
|
||||
verifyTrophyWithMicrosoft
|
||||
} from './helpers/challenge-helpers';
|
||||
|
||||
@@ -40,6 +38,17 @@ interface JwtPayload {
|
||||
userToken: string;
|
||||
}
|
||||
|
||||
// TODO(Post-MVP): This could be narrowed down to only the fields needed by
|
||||
// specific endpoints, but that means complicating the update helper.
|
||||
const userChallengeSelect = {
|
||||
id: true,
|
||||
completedChallenges: true,
|
||||
partiallyCompletedChallenges: true,
|
||||
progressTimestamps: true,
|
||||
needsModeration: true,
|
||||
savedChallenges: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Plugin for the challenge submission endpoints.
|
||||
*
|
||||
@@ -218,11 +227,7 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
|
||||
|
||||
const user = await fastify.prisma.user.findUniqueOrThrow({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
completedChallenges: true,
|
||||
partiallyCompletedChallenges: true,
|
||||
progressTimestamps: true
|
||||
}
|
||||
select: userChallengeSelect
|
||||
});
|
||||
|
||||
if (
|
||||
@@ -237,33 +242,22 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
|
||||
} as const;
|
||||
}
|
||||
|
||||
const completedDate = Date.now();
|
||||
const oldChallenge = user.completedChallenges?.find(
|
||||
({ id }) => id === projectId
|
||||
);
|
||||
|
||||
const updatedChallenge = {
|
||||
const challenge = {
|
||||
challengeType,
|
||||
solution,
|
||||
githubLink
|
||||
};
|
||||
const newChallenge = {
|
||||
...updatedChallenge,
|
||||
githubLink,
|
||||
id: projectId,
|
||||
completedDate
|
||||
completedDate: Date.now()
|
||||
};
|
||||
const alreadyCompleted = !!oldChallenge;
|
||||
const progressTimestamps = user.progressTimestamps as ProgressTimestamp[];
|
||||
const points = getPoints(progressTimestamps);
|
||||
|
||||
const data = alreadyCompleted
|
||||
? updateProject(projectId, updatedChallenge)
|
||||
: createProject(projectId, newChallenge, progressTimestamps);
|
||||
|
||||
await fastify.prisma.user.update({
|
||||
where: { id: userId },
|
||||
data
|
||||
});
|
||||
const { alreadyCompleted, completedDate } = await updateUserChallengeData(
|
||||
fastify,
|
||||
user,
|
||||
projectId,
|
||||
challenge
|
||||
);
|
||||
|
||||
return {
|
||||
alreadyCompleted,
|
||||
@@ -290,7 +284,9 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
|
||||
},
|
||||
async req => {
|
||||
const user = await fastify.prisma.user.findUniqueOrThrow({
|
||||
where: { id: req.user?.id }
|
||||
where: { id: req.user?.id },
|
||||
|
||||
select: userChallengeSelect
|
||||
});
|
||||
const progressTimestamps = user.progressTimestamps as
|
||||
| ProgressTimestamp[]
|
||||
@@ -334,7 +330,8 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
|
||||
const { id, files, challengeType } = req.body;
|
||||
|
||||
const user = await fastify.prisma.user.findUniqueOrThrow({
|
||||
where: { id: req.user?.id }
|
||||
where: { id: req.user?.id },
|
||||
select: userChallengeSelect
|
||||
});
|
||||
const RawProgressTimestamp = user.progressTimestamps as
|
||||
| ProgressTimestamp[]
|
||||
@@ -549,36 +546,25 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
|
||||
|
||||
const user = await fastify.prisma.user.findUniqueOrThrow({
|
||||
where: { id: req.user?.id },
|
||||
select: { completedChallenges: true, progressTimestamps: true }
|
||||
select: userChallengeSelect
|
||||
});
|
||||
|
||||
const { completedChallenges } = user;
|
||||
const progressTimestamps =
|
||||
user.progressTimestamps as ProgressTimestamp[];
|
||||
const oldChallenge = completedChallenges.find(
|
||||
({ id }) => id === challengeId
|
||||
);
|
||||
const alreadyCompleted = !!oldChallenge;
|
||||
const completedDate = alreadyCompleted
|
||||
? oldChallenge.completedDate
|
||||
: Date.now();
|
||||
|
||||
if (!alreadyCompleted) {
|
||||
const newChallenge = {
|
||||
id: challengeId,
|
||||
completedDate,
|
||||
solution: msTrophyStatus.msUserAchievementsApiUrl
|
||||
};
|
||||
await fastify.prisma.user.update({
|
||||
where: { id: req.user?.id },
|
||||
data: {
|
||||
completedChallenges: {
|
||||
push: newChallenge
|
||||
},
|
||||
progressTimestamps: [...progressTimestamps, completedDate]
|
||||
}
|
||||
});
|
||||
}
|
||||
const completedChallenge = {
|
||||
id: challengeId,
|
||||
solution: msTrophyStatus.msUserAchievementsApiUrl,
|
||||
completedDate: Date.now()
|
||||
};
|
||||
|
||||
const { alreadyCompleted, completedDate } =
|
||||
await updateUserChallengeData(
|
||||
fastify,
|
||||
user,
|
||||
challengeId,
|
||||
completedChallenge
|
||||
);
|
||||
|
||||
return {
|
||||
alreadyCompleted,
|
||||
@@ -730,6 +716,9 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
|
||||
|
||||
newCompletedChallenges[alreadyCompletedIndex] = updatedChallege;
|
||||
|
||||
// TODO(Post-MVP): Try to DRY the updates.
|
||||
// updateUserChallengeData, for all its faults, handles the
|
||||
// update/insert logic well.
|
||||
await fastify.prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
|
||||
import type { ProgressTimestamp } from '../../utils/progress';
|
||||
|
||||
/**
|
||||
* Confirm that a user can submit a CodeRoad project.
|
||||
*
|
||||
@@ -26,39 +22,6 @@ export const canSubmitCodeRoadCertProject = (
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the Prisma query to update a project.
|
||||
* @param id The id of the project.
|
||||
* @param newChallenge The challenge corresponding to the project.
|
||||
* @returns A Prisma query to update the project.
|
||||
*/
|
||||
export const updateProject = (
|
||||
id: string,
|
||||
newChallenge: Prisma.CompletedChallengeUpdateInput
|
||||
) => ({
|
||||
completedChallenges: {
|
||||
updateMany: { where: { id }, data: newChallenge }
|
||||
},
|
||||
partiallyCompletedChallenges: { deleteMany: { where: { id } } }
|
||||
});
|
||||
|
||||
/**
|
||||
* Create the Prisma query to create a project.
|
||||
* @param id The id of the project.
|
||||
* @param newChallenge The challenge corresponding to the project.
|
||||
* @param progressTimestamps The user's current progress timestamps.
|
||||
* @returns A Prisma query to update the project.
|
||||
*/
|
||||
export const createProject = (
|
||||
id: string,
|
||||
newChallenge: Prisma.CompletedChallengeCreateInput,
|
||||
progressTimestamps: ProgressTimestamp[]
|
||||
) => ({
|
||||
completedChallenges: { push: newChallenge },
|
||||
partiallyCompletedChallenges: { deleteMany: { where: { id } } },
|
||||
progressTimestamps: [...progressTimestamps, newChallenge.completedDate]
|
||||
});
|
||||
|
||||
type MSProfileError = {
|
||||
type: 'error';
|
||||
message: 'flash.ms.profile.err';
|
||||
|
||||
@@ -107,21 +107,26 @@ export function saveUserChallengeData(
|
||||
/**
|
||||
* Helper function to update a user's challenge data. Used in challenge
|
||||
* submission endpoints.
|
||||
*
|
||||
* @deprecated Create specific functions for each submission endpoint.
|
||||
* TODO: Keep refactoring. This function does too much.
|
||||
* @param fastify The Fastify instance.
|
||||
* @param user The existing user record.
|
||||
* @param challengeId The id of the submitted challenge.
|
||||
* @param _completedChallenge The challenge submission.
|
||||
* @param timezone The user's timezone.
|
||||
* @returns Information about the update.
|
||||
*/
|
||||
export async function updateUserChallengeData(
|
||||
fastify: FastifyInstance,
|
||||
user: user,
|
||||
user: Pick<
|
||||
user,
|
||||
| 'id'
|
||||
| 'completedChallenges'
|
||||
| 'needsModeration'
|
||||
| 'savedChallenges'
|
||||
| 'progressTimestamps'
|
||||
| 'partiallyCompletedChallenges'
|
||||
>,
|
||||
challengeId: string,
|
||||
_completedChallenge: CompletedChallenge,
|
||||
timezone?: string // TODO: is this required as its not given as a arg anywhere
|
||||
_completedChallenge: CompletedChallenge
|
||||
) {
|
||||
const { files, completedDate: newProgressTimeStamp = Date.now() } =
|
||||
_completedChallenge;
|
||||
@@ -149,11 +154,8 @@ export async function updateUserChallengeData(
|
||||
} else {
|
||||
completedChallenge = omit(_completedChallenge, ['files']);
|
||||
}
|
||||
let finalChallenge = {} as CompletedChallenge;
|
||||
|
||||
// Since these values are destuctured for easier updating, collectively update before returning
|
||||
const {
|
||||
timezone: userTimezone,
|
||||
completedChallenges = [],
|
||||
needsModeration = false,
|
||||
savedChallenges = [],
|
||||
@@ -161,36 +163,26 @@ export async function updateUserChallengeData(
|
||||
partiallyCompletedChallenges = []
|
||||
} = user;
|
||||
|
||||
const userCompletedChallenges: CompletedChallenge[] = completedChallenges;
|
||||
const userSavedChallenges: SavedChallenge[] = savedChallenges;
|
||||
const userProgressTimestamps = progressTimestamps;
|
||||
const userPartiallyCompletedChallenges = partiallyCompletedChallenges;
|
||||
let userSavedChallenges = savedChallenges;
|
||||
|
||||
const oldIndex = userCompletedChallenges.findIndex(
|
||||
({ id }) => challengeId === id
|
||||
);
|
||||
const oldChallenge = completedChallenges.find(({ id }) => challengeId === id);
|
||||
const alreadyCompleted = !!oldChallenge;
|
||||
|
||||
const alreadyCompleted = oldIndex !== -1;
|
||||
const oldChallenge = alreadyCompleted
|
||||
? userCompletedChallenges[oldIndex]
|
||||
: null;
|
||||
const finalChallenge = alreadyCompleted
|
||||
? {
|
||||
...completedChallenge,
|
||||
completedDate: oldChallenge.completedDate
|
||||
}
|
||||
: completedChallenge;
|
||||
|
||||
if (alreadyCompleted && oldChallenge) {
|
||||
finalChallenge = {
|
||||
...completedChallenge,
|
||||
completedDate: oldChallenge.completedDate
|
||||
};
|
||||
const userCompletedChallenges = alreadyCompleted
|
||||
? completedChallenges.map(x => (x.id === challengeId ? finalChallenge : x))
|
||||
: [...completedChallenges, finalChallenge];
|
||||
|
||||
userCompletedChallenges[oldIndex] = finalChallenge;
|
||||
} else {
|
||||
finalChallenge = {
|
||||
...completedChallenge
|
||||
};
|
||||
if (userProgressTimestamps && Array.isArray(userProgressTimestamps)) {
|
||||
userProgressTimestamps.push(newProgressTimeStamp);
|
||||
}
|
||||
userCompletedChallenges.push(finalChallenge);
|
||||
}
|
||||
const userProgressTimestamps =
|
||||
!alreadyCompleted && progressTimestamps && Array.isArray(progressTimestamps)
|
||||
? [...progressTimestamps, newProgressTimeStamp]
|
||||
: progressTimestamps;
|
||||
|
||||
if (
|
||||
multifileCertProjectIds.includes(challengeId) ||
|
||||
@@ -204,37 +196,19 @@ export async function updateUserChallengeData(
|
||||
) as SavedChallengeFile[]
|
||||
};
|
||||
|
||||
const savedIndex = userSavedChallenges.findIndex(
|
||||
({ id }) => challengeId === id
|
||||
);
|
||||
const isSaved = userSavedChallenges.some(({ id }) => challengeId === id);
|
||||
|
||||
if (savedIndex >= 0) {
|
||||
userSavedChallenges[savedIndex] = challengeToSave;
|
||||
} else {
|
||||
userSavedChallenges.push(challengeToSave);
|
||||
}
|
||||
userSavedChallenges = isSaved
|
||||
? userSavedChallenges.map(x =>
|
||||
x.id === challengeId ? challengeToSave : x
|
||||
)
|
||||
: [...userSavedChallenges, challengeToSave];
|
||||
}
|
||||
|
||||
// remove from partiallyCompleted on submit
|
||||
const updatedPartiallyCompletedChallenges =
|
||||
userPartiallyCompletedChallenges.filter(
|
||||
challenge => challenge.id !== challengeId
|
||||
);
|
||||
|
||||
if (
|
||||
timezone &&
|
||||
timezone !== 'UTC' &&
|
||||
!userTimezone &&
|
||||
userTimezone === 'UTC'
|
||||
) {
|
||||
timezone = userTimezone;
|
||||
await fastify.prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
timezone
|
||||
}
|
||||
});
|
||||
}
|
||||
const userPartiallyCompletedChallenges = partiallyCompletedChallenges.filter(
|
||||
challenge => challenge.id !== challengeId
|
||||
);
|
||||
|
||||
if (needsModeration) {
|
||||
await fastify.prisma.user.update({
|
||||
@@ -252,7 +226,7 @@ export async function updateUserChallengeData(
|
||||
needsModeration,
|
||||
savedChallenges: userSavedChallenges,
|
||||
progressTimestamps: userProgressTimestamps,
|
||||
partiallyCompletedChallenges: updatedPartiallyCompletedChallenges
|
||||
partiallyCompletedChallenges: userPartiallyCompletedChallenges
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user