mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
fix(api): handle Date values for completedChallenge.completedDate (#60400)
This commit is contained in:
committed by
GitHub
parent
c4a40f7a07
commit
f5b0071a68
@@ -20,7 +20,7 @@ type File {
|
||||
|
||||
type CompletedChallenge {
|
||||
challengeType Int? @db.Int // Null | Undefined
|
||||
completedDate Float // TODO(Post-MVP): Change to DateTime
|
||||
completedDate Json // DateTime | Float, but not, as far as we know, Null
|
||||
files File[]
|
||||
githubLink String? // Undefined
|
||||
id String
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
import {
|
||||
certSlugTypeMap,
|
||||
certIds
|
||||
} from '../../../../shared/config/certification-settings';
|
||||
import { normalizeDate } from '../../utils/normalize';
|
||||
|
||||
const {
|
||||
legacyInfosecQaId,
|
||||
@@ -41,12 +43,16 @@ export function isKnownCertSlug(
|
||||
* @returns The latest certification date or the completed date if no certification is found.
|
||||
*/
|
||||
export function getFallbackFullStackDate(
|
||||
completedChallenges: { id: string; completedDate: number }[],
|
||||
completedDate: number
|
||||
) {
|
||||
completedChallenges: { id: string; completedDate: Prisma.JsonValue }[],
|
||||
completedDate: Prisma.JsonValue
|
||||
): number {
|
||||
const latestCertDate = completedChallenges
|
||||
.filter(chal => fullStackCertificateIds.includes(chal.id))
|
||||
.map(chal => ({
|
||||
...chal,
|
||||
completedDate: normalizeDate(chal.completedDate)
|
||||
}))
|
||||
.sort((a, b) => b.completedDate - a.completedDate)[0]?.completedDate;
|
||||
|
||||
return latestCertDate ?? completedDate;
|
||||
return latestCertDate ?? normalizeDate(completedDate);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
verifyTrophyWithMicrosoft
|
||||
} from '../helpers/challenge-helpers';
|
||||
import { UpdateReqType } from '../../utils';
|
||||
import { normalizeDate } from '../../utils/normalize';
|
||||
|
||||
interface JwtPayload {
|
||||
userToken: string;
|
||||
@@ -263,13 +264,12 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
|
||||
'User tried to submit a codeRoad cert project before completing the required challenges'
|
||||
);
|
||||
void reply.code(403);
|
||||
return {
|
||||
return reply.send({
|
||||
type: 'error',
|
||||
message:
|
||||
'You have to complete the project before you can submit a URL.'
|
||||
} as const;
|
||||
});
|
||||
}
|
||||
|
||||
const challenge = {
|
||||
challengeType,
|
||||
solution,
|
||||
@@ -287,13 +287,13 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
|
||||
challenge
|
||||
);
|
||||
|
||||
return {
|
||||
reply.send({
|
||||
alreadyCompleted,
|
||||
// TODO(Post-MVP): audit the client and remove this if the client does
|
||||
// not use it.
|
||||
completedDate,
|
||||
completedDate: normalizeDate(completedDate),
|
||||
points: alreadyCompleted ? points : points + 1
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -685,11 +685,11 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
|
||||
completedChallenge
|
||||
);
|
||||
|
||||
return {
|
||||
reply.send({
|
||||
alreadyCompleted,
|
||||
points: getPoints(progressTimestamps) + (alreadyCompleted ? 0 : 1),
|
||||
completedDate
|
||||
};
|
||||
completedDate: normalizeDate(completedDate)
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error, 'Error submitting Microsoft trophy challenge');
|
||||
fastify.Sentry.captureException(error);
|
||||
@@ -808,7 +808,10 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
|
||||
}
|
||||
|
||||
const newCompletedChallenges: CompletedChallenge[] =
|
||||
completedChallenges;
|
||||
completedChallenges.map(c => {
|
||||
const { completedDate, ...rest } = c;
|
||||
return { completedDate: normalizeDate(completedDate), ...rest };
|
||||
});
|
||||
const newCompletedExams: CompletedExam[] = completedExams;
|
||||
const newProgressTimeStamps = progressTimestamps as ProgressTimestamp[];
|
||||
const completedDate = Date.now();
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
getFallbackFullStackDate,
|
||||
isKnownCertSlug
|
||||
} from '../helpers/certificate-utils';
|
||||
import { normalizeDate } from '../../utils/normalize';
|
||||
|
||||
/**
|
||||
* Plugin for the unprotected certificate endpoints.
|
||||
@@ -228,7 +229,7 @@ export const unprotectedCertificateRoutes: FastifyPluginCallbackTypebox = (
|
||||
certSlug,
|
||||
certTitle,
|
||||
username,
|
||||
date: completedDate,
|
||||
date: normalizeDate(completedDate),
|
||||
completionTime
|
||||
});
|
||||
}
|
||||
@@ -239,7 +240,7 @@ export const unprotectedCertificateRoutes: FastifyPluginCallbackTypebox = (
|
||||
certTitle,
|
||||
username,
|
||||
name,
|
||||
date: completedDate,
|
||||
date: normalizeDate(completedDate),
|
||||
completionTime
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FastifyInstance } from 'fastify';
|
||||
import { omit, pick } from 'lodash';
|
||||
import { challengeTypes } from '../../../shared/config/challenge-types';
|
||||
import { getChallenges } from './get-challenges';
|
||||
import { normalizeDate } from './normalize';
|
||||
|
||||
export const jsCertProjectIds = [
|
||||
'aaa48de84e1ecc7c742e1124',
|
||||
@@ -149,7 +150,8 @@ export async function updateUserChallengeData(
|
||||
'path',
|
||||
'ext'
|
||||
]) as CompletedChallengeFile
|
||||
)
|
||||
),
|
||||
completedDate: normalizeDate(_completedChallenge.completedDate)
|
||||
};
|
||||
} else {
|
||||
completedChallenge = omit(_completedChallenge, ['files']);
|
||||
@@ -171,7 +173,7 @@ export async function updateUserChallengeData(
|
||||
const finalChallenge = alreadyCompleted
|
||||
? {
|
||||
...completedChallenge,
|
||||
completedDate: oldChallenge.completedDate
|
||||
completedDate: normalizeDate(oldChallenge.completedDate)
|
||||
}
|
||||
: completedChallenge;
|
||||
|
||||
@@ -180,7 +182,11 @@ export async function updateUserChallengeData(
|
||||
// check and update some property of the user record such that the same update
|
||||
// can't be applied twice.
|
||||
const userCompletedChallenges = alreadyCompleted
|
||||
? completedChallenges.map(x => (x.id === challengeId ? finalChallenge : x))
|
||||
? completedChallenges.map(x =>
|
||||
x.id === challengeId
|
||||
? finalChallenge
|
||||
: { ...x, completedDate: normalizeDate(x.completedDate) }
|
||||
)
|
||||
: { push: finalChallenge };
|
||||
|
||||
// We can't use push, because progressTimestamps is a JSON blob and, until
|
||||
|
||||
@@ -2,7 +2,8 @@ import {
|
||||
normalizeTwitter,
|
||||
normalizeProfileUI,
|
||||
normalizeChallenges,
|
||||
normalizeFlags
|
||||
normalizeFlags,
|
||||
normalizeDate
|
||||
} from './normalize';
|
||||
|
||||
describe('normalize', () => {
|
||||
@@ -156,4 +157,22 @@ describe('normalize', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateDate', () => {
|
||||
it('should return the date as a number', () => {
|
||||
expect(normalizeDate(1)).toEqual(1);
|
||||
expect(normalizeDate({ $date: '2023-10-01T00:00:00Z' })).toEqual(
|
||||
1696118400000
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the date is not in the expected shape', () => {
|
||||
expect(() => normalizeDate('2023-10-01T00:00:00Z')).toThrow(
|
||||
'Unexpected date value: "2023-10-01T00:00:00Z"'
|
||||
);
|
||||
expect(() => normalizeDate({ date: '123' })).toThrow(
|
||||
'Unexpected date value: {"date":"123"}'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/* This module's job is to parse the database output and prepare it for
|
||||
serialization */
|
||||
import {
|
||||
import type {
|
||||
ProfileUI,
|
||||
CompletedChallenge,
|
||||
ExamResults,
|
||||
type Survey
|
||||
Survey,
|
||||
Prisma
|
||||
} from '@prisma/client';
|
||||
import _ from 'lodash';
|
||||
|
||||
@@ -39,6 +40,27 @@ export const normalizeTwitter = (
|
||||
return url ?? handleOrUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalizes a date value to a timestamp number.
|
||||
*
|
||||
* @param date An object with a $date string or a number.
|
||||
* @returns The date as a timestamp number.
|
||||
*/
|
||||
export const normalizeDate = (date?: Prisma.JsonValue): number => {
|
||||
if (typeof date === 'number') {
|
||||
return date;
|
||||
} else if (
|
||||
date &&
|
||||
typeof date === 'object' &&
|
||||
'$date' in date &&
|
||||
typeof date.$date === 'string'
|
||||
) {
|
||||
return new Date(date.$date).getTime();
|
||||
} else {
|
||||
throw Error('Unexpected date value: ' + JSON.stringify(date));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Ensure that the user's profile UI settings are valid.
|
||||
*
|
||||
@@ -114,7 +136,15 @@ export const normalizeChallenges = (
|
||||
return { ...rest, files: noNullFiles };
|
||||
});
|
||||
|
||||
return noNullPath;
|
||||
const withNumberDates = noNullPath.map(challenge => {
|
||||
const { completedDate, ...rest } = challenge;
|
||||
return {
|
||||
...rest,
|
||||
completedDate: normalizeDate(completedDate)
|
||||
};
|
||||
});
|
||||
|
||||
return withNumberDates;
|
||||
};
|
||||
|
||||
type NormalizedSurvey = {
|
||||
|
||||
Reference in New Issue
Block a user