feat: add framework for rwd cert projects (#44505)
* feat: add rwd cert projects feat: save projects with flag revert: not needed things revert: empty line revert: empty line fix: it fix: remove log * fix: snapshot tests * fix: show bread crumbs by default * revert: snapshot fix * Update curriculum/challenges/_meta/responsive-web-design-projects/meta.json Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * fix: manuallyApproved -> isManuallyApproved * fix: add review suggestions Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@ -208,6 +208,7 @@
|
||||
"solution": "string",
|
||||
"githubLink": "string",
|
||||
"challengeType": "number",
|
||||
"isManuallyApproved": "boolean",
|
||||
"files": {
|
||||
"type": [
|
||||
{
|
||||
|
@ -17,5 +17,6 @@ export const fixCompletedChallengeItem = obj =>
|
||||
'solution',
|
||||
'githubLink',
|
||||
'challengeType',
|
||||
'files'
|
||||
'files',
|
||||
'isManuallyApproved'
|
||||
]);
|
||||
|
@ -69,7 +69,7 @@ export default async function bootChallenge(app, done) {
|
||||
done();
|
||||
}
|
||||
|
||||
const jsProjects = [
|
||||
const jsCertProjectIds = [
|
||||
'aaa48de84e1ecc7c742e1124',
|
||||
'a7f4d8f2483413a6ce226cac',
|
||||
'56533eb9ac21ba0edf2244e2',
|
||||
@ -77,6 +77,10 @@ const jsProjects = [
|
||||
'aa2e6f85cab2ab736c9a9b24'
|
||||
];
|
||||
|
||||
const multiFileCertProjectIds = getChallenges()
|
||||
.filter(challenge => challenge.challengeType === 14)
|
||||
.map(challenge => challenge.id);
|
||||
|
||||
export function buildUserUpdate(
|
||||
user,
|
||||
challengeId,
|
||||
@ -85,7 +89,10 @@ export function buildUserUpdate(
|
||||
) {
|
||||
const { files, completedDate = Date.now() } = _completedChallenge;
|
||||
let completedChallenge = {};
|
||||
if (jsProjects.includes(challengeId)) {
|
||||
if (
|
||||
jsCertProjectIds.includes(challengeId) ||
|
||||
multiFileCertProjectIds.includes(challengeId)
|
||||
) {
|
||||
completedChallenge = {
|
||||
..._completedChallenge,
|
||||
files: files.map(file =>
|
||||
@ -223,14 +230,19 @@ export function modernChallengeCompleted(req, res, next) {
|
||||
.getCompletedChallenges$()
|
||||
.flatMap(() => {
|
||||
const completedDate = Date.now();
|
||||
const { id, files } = req.body;
|
||||
const { id, files, challengeType } = req.body;
|
||||
|
||||
const { alreadyCompleted, updateData } = buildUserUpdate(user, id, {
|
||||
const data = {
|
||||
id,
|
||||
files,
|
||||
completedDate
|
||||
});
|
||||
};
|
||||
|
||||
if (challengeType === 14) {
|
||||
data.isManuallyApproved = false;
|
||||
}
|
||||
|
||||
const { alreadyCompleted, updateData } = buildUserUpdate(user, id, data);
|
||||
const points = alreadyCompleted ? user.points : user.points + 1;
|
||||
const updatePromise = new Promise((resolve, reject) =>
|
||||
user.updateAttributes(updateData, err => {
|
||||
|
@ -2,6 +2,7 @@ import { first } from 'lodash-es';
|
||||
import React, { useState, ReactElement } from 'react';
|
||||
import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex';
|
||||
import { sortChallengeFiles } from '../../../../../utils/sort-challengefiles';
|
||||
import { challengeTypes } from '../../../../utils/challenge-types';
|
||||
import {
|
||||
ChallengeFile,
|
||||
ChallengeFiles,
|
||||
@ -14,6 +15,7 @@ type Pane = { flex: number };
|
||||
interface DesktopLayoutProps {
|
||||
block: string;
|
||||
challengeFiles: ChallengeFiles;
|
||||
challengeType: number;
|
||||
editor: ReactElement | null;
|
||||
hasEditableBoundaries: boolean;
|
||||
hasNotes: boolean;
|
||||
@ -68,6 +70,7 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
|
||||
|
||||
const {
|
||||
block,
|
||||
challengeType,
|
||||
resizeProps,
|
||||
instructions,
|
||||
editor,
|
||||
@ -83,11 +86,15 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
|
||||
|
||||
const challengeFile = getChallengeFile();
|
||||
const projectBasedChallenge = hasEditableBoundaries;
|
||||
const displayPreview = projectBasedChallenge
|
||||
? showPreview && hasPreview
|
||||
: hasPreview;
|
||||
const isMultiFileCertProject =
|
||||
challengeType === challengeTypes.multiFileCertProject;
|
||||
const displayPreview =
|
||||
projectBasedChallenge || isMultiFileCertProject
|
||||
? showPreview && hasPreview
|
||||
: hasPreview;
|
||||
const displayNotes = projectBasedChallenge ? showNotes && hasNotes : false;
|
||||
const displayConsole = projectBasedChallenge ? showConsole : true;
|
||||
const displayConsole =
|
||||
projectBasedChallenge || isMultiFileCertProject ? showConsole : true;
|
||||
const {
|
||||
codePane,
|
||||
editorPane,
|
||||
@ -99,7 +106,7 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
|
||||
|
||||
return (
|
||||
<div className='desktop-layout'>
|
||||
{projectBasedChallenge && (
|
||||
{(projectBasedChallenge || isMultiFileCertProject) && (
|
||||
<ActionRow
|
||||
block={block}
|
||||
hasNotes={hasNotes}
|
||||
|
@ -303,15 +303,25 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
const { challengeType } = this.getChallenge();
|
||||
return (
|
||||
challengeType === challengeTypes.html ||
|
||||
challengeType === challengeTypes.modern
|
||||
challengeType === challengeTypes.modern ||
|
||||
challengeType === challengeTypes.multiFileCertProject
|
||||
);
|
||||
}
|
||||
|
||||
renderInstructionsPanel({ showToolPanel }: { showToolPanel: boolean }) {
|
||||
const { block, description, instructions, superBlock, translationPending } =
|
||||
this.getChallenge();
|
||||
const {
|
||||
block,
|
||||
challengeType,
|
||||
description,
|
||||
forumTopicId,
|
||||
instructions,
|
||||
superBlock,
|
||||
title,
|
||||
translationPending
|
||||
} = this.getChallenge();
|
||||
|
||||
const { forumTopicId, title } = this.getChallenge();
|
||||
const showBreadCrumbs =
|
||||
challengeType !== challengeTypes.multiFileCertProject;
|
||||
return (
|
||||
<SidePanel
|
||||
block={block}
|
||||
@ -326,6 +336,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
<ChallengeTitle
|
||||
block={block}
|
||||
isCompleted={this.props.isChallengeCompleted}
|
||||
showBreadCrumbs={showBreadCrumbs}
|
||||
superBlock={superBlock}
|
||||
translationPending={translationPending}
|
||||
>
|
||||
@ -405,6 +416,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
render() {
|
||||
const {
|
||||
block,
|
||||
challengeType,
|
||||
fields: { blockName },
|
||||
forumTopicId,
|
||||
hasEditableBoundaries,
|
||||
@ -457,6 +469,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
<DesktopLayout
|
||||
block={block}
|
||||
challengeFiles={challengeFiles}
|
||||
challengeType={challengeType}
|
||||
editor={this.renderEditor()}
|
||||
hasEditableBoundaries={hasEditableBoundaries}
|
||||
hasNotes={!!notes}
|
||||
|
@ -10,6 +10,7 @@ interface ChallengeTitleProps {
|
||||
block: string;
|
||||
children: string;
|
||||
isCompleted: boolean;
|
||||
showBreadCrumbs?: boolean;
|
||||
superBlock: string;
|
||||
translationPending: boolean;
|
||||
}
|
||||
@ -18,6 +19,7 @@ function ChallengeTitle({
|
||||
block,
|
||||
children,
|
||||
isCompleted,
|
||||
showBreadCrumbs = true,
|
||||
superBlock,
|
||||
translationPending
|
||||
}: ChallengeTitleProps): JSX.Element {
|
||||
@ -34,7 +36,7 @@ function ChallengeTitle({
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<BreadCrumb block={block} superBlock={superBlock} />
|
||||
{showBreadCrumbs && <BreadCrumb block={block} superBlock={superBlock} />}
|
||||
<div className='challenge-title'>
|
||||
<div className='title-text'>
|
||||
<b>{children}</b>
|
||||
|
@ -71,10 +71,15 @@ function submitModern(type, state) {
|
||||
const challengeFiles = challengeFilesSelector(state);
|
||||
const { username } = userSelector(state);
|
||||
const challengeInfo = {
|
||||
id
|
||||
id,
|
||||
challengeType
|
||||
};
|
||||
// Only send files to server, if it is a JS project
|
||||
if (block === 'javascript-algorithms-and-data-structures-projects') {
|
||||
|
||||
// Only send files to server, if it is a JS project or multiFile cert project
|
||||
if (
|
||||
block === 'javascript-algorithms-and-data-structures-projects' ||
|
||||
challengeType === challengeTypes.multiFileCertProject
|
||||
) {
|
||||
challengeInfo.files = challengeFiles.reduce(
|
||||
(acc, { fileKey, ...curr }) => [...acc, { ...curr, key: fileKey }],
|
||||
[]
|
||||
|
@ -190,7 +190,8 @@ export const challengeDataSelector = state => {
|
||||
};
|
||||
} else if (
|
||||
challengeType === challengeTypes.html ||
|
||||
challengeType === challengeTypes.modern
|
||||
challengeType === challengeTypes.modern ||
|
||||
challengeType === challengeTypes.multiFileCertProject
|
||||
) {
|
||||
const { required = [], template = '' } = challengeMetaSelector(state);
|
||||
challengeData = {
|
||||
|
@ -72,7 +72,8 @@ const buildFunctions = {
|
||||
[challengeTypes.modern]: buildDOMChallenge,
|
||||
[challengeTypes.backend]: buildBackendChallenge,
|
||||
[challengeTypes.backEndProject]: buildBackendChallenge,
|
||||
[challengeTypes.pythonProject]: buildBackendChallenge
|
||||
[challengeTypes.pythonProject]: buildBackendChallenge,
|
||||
[challengeTypes.multiFileCertProject]: buildDOMChallenge
|
||||
};
|
||||
|
||||
export function canBuildChallenge(challengeData) {
|
||||
@ -93,7 +94,8 @@ const testRunners = {
|
||||
[challengeTypes.js]: getJSTestRunner,
|
||||
[challengeTypes.html]: getDOMTestRunner,
|
||||
[challengeTypes.backend]: getDOMTestRunner,
|
||||
[challengeTypes.pythonProject]: getDOMTestRunner
|
||||
[challengeTypes.pythonProject]: getDOMTestRunner,
|
||||
[challengeTypes.multiFileCertProject]: getDOMTestRunner
|
||||
};
|
||||
export function getTestRunner(buildData, runnerConfig, document) {
|
||||
const { challengeType } = buildData;
|
||||
@ -146,7 +148,11 @@ export function buildDOMChallenge({
|
||||
.then(checkFilesErrors)
|
||||
.then(challengeFiles => ({
|
||||
challengeType: challengeTypes.html,
|
||||
build: concatHtml({ required: finalRequires, template, challengeFiles }),
|
||||
build: concatHtml({
|
||||
required: finalRequires,
|
||||
template,
|
||||
challengeFiles
|
||||
}),
|
||||
sources: buildSourceMap(challengeFiles),
|
||||
loadEnzyme
|
||||
}));
|
||||
@ -184,7 +190,10 @@ export function buildBackendChallenge({ url }) {
|
||||
}
|
||||
|
||||
export function updatePreview(buildData, document, proxyLogger) {
|
||||
if (buildData.challengeType === challengeTypes.html) {
|
||||
if (
|
||||
buildData.challengeType === challengeTypes.html ||
|
||||
buildData.challengeType === challengeTypes.multiFileCertProject
|
||||
) {
|
||||
createMainPreviewFramer(document, proxyLogger)(buildData);
|
||||
} else {
|
||||
throw new Error(
|
||||
@ -194,7 +203,10 @@ export function updatePreview(buildData, document, proxyLogger) {
|
||||
}
|
||||
|
||||
export function updateProjectPreview(buildData, document) {
|
||||
if (buildData.challengeType === challengeTypes.html) {
|
||||
if (
|
||||
buildData.challengeType === challengeTypes.html ||
|
||||
buildData.challengeType === challengeTypes.multiFileCertProject
|
||||
) {
|
||||
createProjectPreviewFramer(document)(buildData);
|
||||
} else {
|
||||
throw new Error(
|
||||
@ -206,7 +218,8 @@ export function updateProjectPreview(buildData, document) {
|
||||
export function challengeHasPreview({ challengeType }) {
|
||||
return (
|
||||
challengeType === challengeTypes.html ||
|
||||
challengeType === challengeTypes.modern
|
||||
challengeType === challengeTypes.modern ||
|
||||
challengeType === challengeTypes.multiFileCertProject
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ const invalid = 9;
|
||||
const pythonProject = 10;
|
||||
const video = 11;
|
||||
const codeally = 12;
|
||||
const multiFileCertProject = 14;
|
||||
|
||||
// individual exports
|
||||
exports.backend = backend;
|
||||
@ -33,7 +34,8 @@ exports.challengeTypes = {
|
||||
quiz,
|
||||
invalid,
|
||||
video,
|
||||
codeally
|
||||
codeally,
|
||||
multiFileCertProject
|
||||
};
|
||||
|
||||
// (Oliver) I don't think we need this for codeally projects, so they're ignored
|
||||
@ -67,7 +69,8 @@ exports.viewTypes = {
|
||||
[quiz]: 'quiz',
|
||||
[backend]: 'backend',
|
||||
[video]: 'video',
|
||||
[codeally]: 'codeally'
|
||||
[codeally]: 'codeally',
|
||||
[multiFileCertProject]: 'classic'
|
||||
};
|
||||
|
||||
// determine the type of submit function to use for the challenge on completion
|
||||
@ -87,7 +90,8 @@ exports.submitTypes = {
|
||||
[quiz]: 'quiz',
|
||||
[backend]: 'backend',
|
||||
[modern]: 'tests',
|
||||
[video]: 'tests'
|
||||
[video]: 'tests',
|
||||
[multiFileCertProject]: 'tests'
|
||||
};
|
||||
|
||||
// determine which help forum questions should be posted to
|
||||
|
@ -2,7 +2,7 @@ const path = require('path');
|
||||
const { createPoly } = require('../../../utils/polyvinyl');
|
||||
const { dasherize } = require('../../../utils/slugs');
|
||||
const { sortChallengeFiles } = require('../../../utils/sort-challengefiles');
|
||||
const { viewTypes } = require('../challenge-types');
|
||||
const { challengeTypes, viewTypes } = require('../challenge-types');
|
||||
|
||||
const backend = path.resolve(
|
||||
__dirname,
|
||||
@ -102,7 +102,8 @@ exports.createChallengePages = function (createPage) {
|
||||
};
|
||||
|
||||
function getProjectPreviewConfig(challenge, allChallengeEdges) {
|
||||
const { block, challengeOrder, usesMultifileEditor } = challenge;
|
||||
const { block, challengeOrder, challengeType, usesMultifileEditor } =
|
||||
challenge;
|
||||
|
||||
const challengesInBlock = allChallengeEdges
|
||||
.filter(({ node: { challenge } }) => challenge.block === block)
|
||||
@ -122,7 +123,10 @@ function getProjectPreviewConfig(challenge, allChallengeEdges) {
|
||||
);
|
||||
|
||||
return {
|
||||
showProjectPreview: challengeOrder === 0 && usesMultifileEditor,
|
||||
showProjectPreview:
|
||||
challengeOrder === 0 &&
|
||||
usesMultifileEditor &&
|
||||
challengeType !== challengeTypes.multiFileCertProject,
|
||||
challengeData: {
|
||||
challengeType: lastChallenge.challengeType,
|
||||
challengeFiles: projectPreviewChallengeFiles,
|
||||
|
@ -28,7 +28,7 @@ const schema = Joi.object()
|
||||
challengeOrder: Joi.number(),
|
||||
removeComments: Joi.bool(),
|
||||
certification: Joi.string().regex(slugRE),
|
||||
challengeType: Joi.number().min(0).max(12).required(),
|
||||
challengeType: Joi.number().min(0).max(14).required(),
|
||||
checksum: Joi.number(),
|
||||
// __commentCounts is only used to test the comment replacement
|
||||
__commentCounts: Joi.object(),
|
||||
|
Reference in New Issue
Block a user