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",
"githubLink": "string",
"challengeType": "number",
"isManuallyApproved": "boolean",
"files": {
"type": [
{

View File

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

View File

@ -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 => {

View File

@ -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
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}

View File

@ -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}

View File

@ -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>

View File

@ -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 }],
[]

View File

@ -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 = {

View File

@ -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
);
}

View File

@ -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

View File

@ -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,

View File

@ -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(),