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:
Tom
2022-01-06 07:26:54 -06:00
committed by GitHub
parent ff09b2ee43
commit b061a760c1
12 changed files with 96 additions and 33 deletions

View File

@ -208,6 +208,7 @@
"solution": "string", "solution": "string",
"githubLink": "string", "githubLink": "string",
"challengeType": "number", "challengeType": "number",
"isManuallyApproved": "boolean",
"files": { "files": {
"type": [ "type": [
{ {

View File

@ -17,5 +17,6 @@ export const fixCompletedChallengeItem = obj =>
'solution', 'solution',
'githubLink', 'githubLink',
'challengeType', 'challengeType',
'files' 'files',
'isManuallyApproved'
]); ]);

View File

@ -69,7 +69,7 @@ export default async function bootChallenge(app, done) {
done(); done();
} }
const jsProjects = [ const jsCertProjectIds = [
'aaa48de84e1ecc7c742e1124', 'aaa48de84e1ecc7c742e1124',
'a7f4d8f2483413a6ce226cac', 'a7f4d8f2483413a6ce226cac',
'56533eb9ac21ba0edf2244e2', '56533eb9ac21ba0edf2244e2',
@ -77,6 +77,10 @@ const jsProjects = [
'aa2e6f85cab2ab736c9a9b24' 'aa2e6f85cab2ab736c9a9b24'
]; ];
const multiFileCertProjectIds = getChallenges()
.filter(challenge => challenge.challengeType === 14)
.map(challenge => challenge.id);
export function buildUserUpdate( export function buildUserUpdate(
user, user,
challengeId, challengeId,
@ -85,7 +89,10 @@ export function buildUserUpdate(
) { ) {
const { files, completedDate = Date.now() } = _completedChallenge; const { files, completedDate = Date.now() } = _completedChallenge;
let completedChallenge = {}; let completedChallenge = {};
if (jsProjects.includes(challengeId)) { if (
jsCertProjectIds.includes(challengeId) ||
multiFileCertProjectIds.includes(challengeId)
) {
completedChallenge = { completedChallenge = {
..._completedChallenge, ..._completedChallenge,
files: files.map(file => files: files.map(file =>
@ -223,14 +230,19 @@ export function modernChallengeCompleted(req, res, next) {
.getCompletedChallenges$() .getCompletedChallenges$()
.flatMap(() => { .flatMap(() => {
const completedDate = Date.now(); const completedDate = Date.now();
const { id, files } = req.body; const { id, files, challengeType } = req.body;
const { alreadyCompleted, updateData } = buildUserUpdate(user, id, { const data = {
id, id,
files, files,
completedDate completedDate
}); };
if (challengeType === 14) {
data.isManuallyApproved = false;
}
const { alreadyCompleted, updateData } = buildUserUpdate(user, id, data);
const points = alreadyCompleted ? user.points : user.points + 1; const points = alreadyCompleted ? user.points : user.points + 1;
const updatePromise = new Promise((resolve, reject) => const updatePromise = new Promise((resolve, reject) =>
user.updateAttributes(updateData, err => { user.updateAttributes(updateData, err => {

View File

@ -2,6 +2,7 @@ import { first } from 'lodash-es';
import React, { useState, ReactElement } from 'react'; import React, { useState, ReactElement } from 'react';
import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex'; import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex';
import { sortChallengeFiles } from '../../../../../utils/sort-challengefiles'; import { sortChallengeFiles } from '../../../../../utils/sort-challengefiles';
import { challengeTypes } from '../../../../utils/challenge-types';
import { import {
ChallengeFile, ChallengeFile,
ChallengeFiles, ChallengeFiles,
@ -14,6 +15,7 @@ type Pane = { flex: number };
interface DesktopLayoutProps { interface DesktopLayoutProps {
block: string; block: string;
challengeFiles: ChallengeFiles; challengeFiles: ChallengeFiles;
challengeType: number;
editor: ReactElement | null; editor: ReactElement | null;
hasEditableBoundaries: boolean; hasEditableBoundaries: boolean;
hasNotes: boolean; hasNotes: boolean;
@ -68,6 +70,7 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
const { const {
block, block,
challengeType,
resizeProps, resizeProps,
instructions, instructions,
editor, editor,
@ -83,11 +86,15 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
const challengeFile = getChallengeFile(); const challengeFile = getChallengeFile();
const projectBasedChallenge = hasEditableBoundaries; const projectBasedChallenge = hasEditableBoundaries;
const displayPreview = projectBasedChallenge const isMultiFileCertProject =
challengeType === challengeTypes.multiFileCertProject;
const displayPreview =
projectBasedChallenge || isMultiFileCertProject
? showPreview && hasPreview ? showPreview && hasPreview
: hasPreview; : hasPreview;
const displayNotes = projectBasedChallenge ? showNotes && hasNotes : false; const displayNotes = projectBasedChallenge ? showNotes && hasNotes : false;
const displayConsole = projectBasedChallenge ? showConsole : true; const displayConsole =
projectBasedChallenge || isMultiFileCertProject ? showConsole : true;
const { const {
codePane, codePane,
editorPane, editorPane,
@ -99,7 +106,7 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
return ( return (
<div className='desktop-layout'> <div className='desktop-layout'>
{projectBasedChallenge && ( {(projectBasedChallenge || isMultiFileCertProject) && (
<ActionRow <ActionRow
block={block} block={block}
hasNotes={hasNotes} hasNotes={hasNotes}

View File

@ -303,15 +303,25 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
const { challengeType } = this.getChallenge(); const { challengeType } = this.getChallenge();
return ( return (
challengeType === challengeTypes.html || challengeType === challengeTypes.html ||
challengeType === challengeTypes.modern challengeType === challengeTypes.modern ||
challengeType === challengeTypes.multiFileCertProject
); );
} }
renderInstructionsPanel({ showToolPanel }: { showToolPanel: boolean }) { renderInstructionsPanel({ showToolPanel }: { showToolPanel: boolean }) {
const { block, description, instructions, superBlock, translationPending } = const {
this.getChallenge(); block,
challengeType,
description,
forumTopicId,
instructions,
superBlock,
title,
translationPending
} = this.getChallenge();
const { forumTopicId, title } = this.getChallenge(); const showBreadCrumbs =
challengeType !== challengeTypes.multiFileCertProject;
return ( return (
<SidePanel <SidePanel
block={block} block={block}
@ -326,6 +336,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
<ChallengeTitle <ChallengeTitle
block={block} block={block}
isCompleted={this.props.isChallengeCompleted} isCompleted={this.props.isChallengeCompleted}
showBreadCrumbs={showBreadCrumbs}
superBlock={superBlock} superBlock={superBlock}
translationPending={translationPending} translationPending={translationPending}
> >
@ -405,6 +416,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
render() { render() {
const { const {
block, block,
challengeType,
fields: { blockName }, fields: { blockName },
forumTopicId, forumTopicId,
hasEditableBoundaries, hasEditableBoundaries,
@ -457,6 +469,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
<DesktopLayout <DesktopLayout
block={block} block={block}
challengeFiles={challengeFiles} challengeFiles={challengeFiles}
challengeType={challengeType}
editor={this.renderEditor()} editor={this.renderEditor()}
hasEditableBoundaries={hasEditableBoundaries} hasEditableBoundaries={hasEditableBoundaries}
hasNotes={!!notes} hasNotes={!!notes}

View File

@ -10,6 +10,7 @@ interface ChallengeTitleProps {
block: string; block: string;
children: string; children: string;
isCompleted: boolean; isCompleted: boolean;
showBreadCrumbs?: boolean;
superBlock: string; superBlock: string;
translationPending: boolean; translationPending: boolean;
} }
@ -18,6 +19,7 @@ function ChallengeTitle({
block, block,
children, children,
isCompleted, isCompleted,
showBreadCrumbs = true,
superBlock, superBlock,
translationPending translationPending
}: ChallengeTitleProps): JSX.Element { }: ChallengeTitleProps): JSX.Element {
@ -34,7 +36,7 @@ function ChallengeTitle({
</Link> </Link>
</> </>
)} )}
<BreadCrumb block={block} superBlock={superBlock} /> {showBreadCrumbs && <BreadCrumb block={block} superBlock={superBlock} />}
<div className='challenge-title'> <div className='challenge-title'>
<div className='title-text'> <div className='title-text'>
<b>{children}</b> <b>{children}</b>

View File

@ -71,10 +71,15 @@ function submitModern(type, state) {
const challengeFiles = challengeFilesSelector(state); const challengeFiles = challengeFilesSelector(state);
const { username } = userSelector(state); const { username } = userSelector(state);
const challengeInfo = { 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( challengeInfo.files = challengeFiles.reduce(
(acc, { fileKey, ...curr }) => [...acc, { ...curr, key: fileKey }], (acc, { fileKey, ...curr }) => [...acc, { ...curr, key: fileKey }],
[] []

View File

@ -190,7 +190,8 @@ export const challengeDataSelector = state => {
}; };
} else if ( } else if (
challengeType === challengeTypes.html || challengeType === challengeTypes.html ||
challengeType === challengeTypes.modern challengeType === challengeTypes.modern ||
challengeType === challengeTypes.multiFileCertProject
) { ) {
const { required = [], template = '' } = challengeMetaSelector(state); const { required = [], template = '' } = challengeMetaSelector(state);
challengeData = { challengeData = {

View File

@ -72,7 +72,8 @@ const buildFunctions = {
[challengeTypes.modern]: buildDOMChallenge, [challengeTypes.modern]: buildDOMChallenge,
[challengeTypes.backend]: buildBackendChallenge, [challengeTypes.backend]: buildBackendChallenge,
[challengeTypes.backEndProject]: buildBackendChallenge, [challengeTypes.backEndProject]: buildBackendChallenge,
[challengeTypes.pythonProject]: buildBackendChallenge [challengeTypes.pythonProject]: buildBackendChallenge,
[challengeTypes.multiFileCertProject]: buildDOMChallenge
}; };
export function canBuildChallenge(challengeData) { export function canBuildChallenge(challengeData) {
@ -93,7 +94,8 @@ const testRunners = {
[challengeTypes.js]: getJSTestRunner, [challengeTypes.js]: getJSTestRunner,
[challengeTypes.html]: getDOMTestRunner, [challengeTypes.html]: getDOMTestRunner,
[challengeTypes.backend]: getDOMTestRunner, [challengeTypes.backend]: getDOMTestRunner,
[challengeTypes.pythonProject]: getDOMTestRunner [challengeTypes.pythonProject]: getDOMTestRunner,
[challengeTypes.multiFileCertProject]: getDOMTestRunner
}; };
export function getTestRunner(buildData, runnerConfig, document) { export function getTestRunner(buildData, runnerConfig, document) {
const { challengeType } = buildData; const { challengeType } = buildData;
@ -146,7 +148,11 @@ export function buildDOMChallenge({
.then(checkFilesErrors) .then(checkFilesErrors)
.then(challengeFiles => ({ .then(challengeFiles => ({
challengeType: challengeTypes.html, challengeType: challengeTypes.html,
build: concatHtml({ required: finalRequires, template, challengeFiles }), build: concatHtml({
required: finalRequires,
template,
challengeFiles
}),
sources: buildSourceMap(challengeFiles), sources: buildSourceMap(challengeFiles),
loadEnzyme loadEnzyme
})); }));
@ -184,7 +190,10 @@ export function buildBackendChallenge({ url }) {
} }
export function updatePreview(buildData, document, proxyLogger) { export function updatePreview(buildData, document, proxyLogger) {
if (buildData.challengeType === challengeTypes.html) { if (
buildData.challengeType === challengeTypes.html ||
buildData.challengeType === challengeTypes.multiFileCertProject
) {
createMainPreviewFramer(document, proxyLogger)(buildData); createMainPreviewFramer(document, proxyLogger)(buildData);
} else { } else {
throw new Error( throw new Error(
@ -194,7 +203,10 @@ export function updatePreview(buildData, document, proxyLogger) {
} }
export function updateProjectPreview(buildData, document) { export function updateProjectPreview(buildData, document) {
if (buildData.challengeType === challengeTypes.html) { if (
buildData.challengeType === challengeTypes.html ||
buildData.challengeType === challengeTypes.multiFileCertProject
) {
createProjectPreviewFramer(document)(buildData); createProjectPreviewFramer(document)(buildData);
} else { } else {
throw new Error( throw new Error(
@ -206,7 +218,8 @@ export function updateProjectPreview(buildData, document) {
export function challengeHasPreview({ challengeType }) { export function challengeHasPreview({ challengeType }) {
return ( return (
challengeType === challengeTypes.html || challengeType === challengeTypes.html ||
challengeType === challengeTypes.modern challengeType === challengeTypes.modern ||
challengeType === challengeTypes.multiFileCertProject
); );
} }

View File

@ -12,6 +12,7 @@ const invalid = 9;
const pythonProject = 10; const pythonProject = 10;
const video = 11; const video = 11;
const codeally = 12; const codeally = 12;
const multiFileCertProject = 14;
// individual exports // individual exports
exports.backend = backend; exports.backend = backend;
@ -33,7 +34,8 @@ exports.challengeTypes = {
quiz, quiz,
invalid, invalid,
video, video,
codeally codeally,
multiFileCertProject
}; };
// (Oliver) I don't think we need this for codeally projects, so they're ignored // (Oliver) I don't think we need this for codeally projects, so they're ignored
@ -67,7 +69,8 @@ exports.viewTypes = {
[quiz]: 'quiz', [quiz]: 'quiz',
[backend]: 'backend', [backend]: 'backend',
[video]: 'video', [video]: 'video',
[codeally]: 'codeally' [codeally]: 'codeally',
[multiFileCertProject]: 'classic'
}; };
// determine the type of submit function to use for the challenge on completion // determine the type of submit function to use for the challenge on completion
@ -87,7 +90,8 @@ exports.submitTypes = {
[quiz]: 'quiz', [quiz]: 'quiz',
[backend]: 'backend', [backend]: 'backend',
[modern]: 'tests', [modern]: 'tests',
[video]: 'tests' [video]: 'tests',
[multiFileCertProject]: 'tests'
}; };
// determine which help forum questions should be posted to // determine which help forum questions should be posted to

View File

@ -2,7 +2,7 @@ const path = require('path');
const { createPoly } = require('../../../utils/polyvinyl'); const { createPoly } = require('../../../utils/polyvinyl');
const { dasherize } = require('../../../utils/slugs'); const { dasherize } = require('../../../utils/slugs');
const { sortChallengeFiles } = require('../../../utils/sort-challengefiles'); const { sortChallengeFiles } = require('../../../utils/sort-challengefiles');
const { viewTypes } = require('../challenge-types'); const { challengeTypes, viewTypes } = require('../challenge-types');
const backend = path.resolve( const backend = path.resolve(
__dirname, __dirname,
@ -102,7 +102,8 @@ exports.createChallengePages = function (createPage) {
}; };
function getProjectPreviewConfig(challenge, allChallengeEdges) { function getProjectPreviewConfig(challenge, allChallengeEdges) {
const { block, challengeOrder, usesMultifileEditor } = challenge; const { block, challengeOrder, challengeType, usesMultifileEditor } =
challenge;
const challengesInBlock = allChallengeEdges const challengesInBlock = allChallengeEdges
.filter(({ node: { challenge } }) => challenge.block === block) .filter(({ node: { challenge } }) => challenge.block === block)
@ -122,7 +123,10 @@ function getProjectPreviewConfig(challenge, allChallengeEdges) {
); );
return { return {
showProjectPreview: challengeOrder === 0 && usesMultifileEditor, showProjectPreview:
challengeOrder === 0 &&
usesMultifileEditor &&
challengeType !== challengeTypes.multiFileCertProject,
challengeData: { challengeData: {
challengeType: lastChallenge.challengeType, challengeType: lastChallenge.challengeType,
challengeFiles: projectPreviewChallengeFiles, challengeFiles: projectPreviewChallengeFiles,

View File

@ -28,7 +28,7 @@ const schema = Joi.object()
challengeOrder: Joi.number(), challengeOrder: Joi.number(),
removeComments: Joi.bool(), removeComments: Joi.bool(),
certification: Joi.string().regex(slugRE), certification: Joi.string().regex(slugRE),
challengeType: Joi.number().min(0).max(12).required(), challengeType: Joi.number().min(0).max(14).required(),
checksum: Joi.number(), checksum: Joi.number(),
// __commentCounts is only used to test the comment replacement // __commentCounts is only used to test the comment replacement
__commentCounts: Joi.object(), __commentCounts: Joi.object(),