mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-20 12:03:11 -04:00
feat(client/api): add C# survey (#51682)
This commit is contained in:
@@ -428,6 +428,11 @@
|
||||
"type": "hasMany",
|
||||
"model": "MsUsername",
|
||||
"foreignKey": "userId"
|
||||
},
|
||||
"surveys": {
|
||||
"type": "hasMany",
|
||||
"model": "Survey",
|
||||
"foreignKey": "userId"
|
||||
}
|
||||
},
|
||||
"acls": [
|
||||
|
||||
@@ -30,3 +30,5 @@ export const fixPartiallyCompletedChallengeItem = obj =>
|
||||
|
||||
export const fixCompletedExamItem = obj =>
|
||||
pick(obj, ['id', 'completedDate', 'challengeType', 'examResults']);
|
||||
|
||||
export const fixCompletedSurveyItem = obj => pick(obj, ['title', 'responses']);
|
||||
|
||||
@@ -7,6 +7,7 @@ import fetch from 'node-fetch';
|
||||
import {
|
||||
fixCompletedChallengeItem,
|
||||
fixCompletedExamItem,
|
||||
fixCompletedSurveyItem,
|
||||
fixPartiallyCompletedChallengeItem,
|
||||
fixSavedChallengeItem
|
||||
} from '../../common/utils';
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
encodeUserToken
|
||||
} from '../middlewares/user-token';
|
||||
import { createDeleteMsUsername } from '../middlewares/ms-username';
|
||||
import { validateSurvey, createDeleteUserSurveys } from '../middlewares/survey';
|
||||
import { deprecatedEndpoint } from '../utils/disabled-endpoints';
|
||||
|
||||
const log = debugFactory('fcc:boot:user');
|
||||
@@ -39,6 +41,8 @@ function bootUser(app) {
|
||||
const deleteUserToken = createDeleteUserToken(app);
|
||||
const postMsUsername = createPostMsUsername(app);
|
||||
const deleteMsUsername = createDeleteMsUsername(app);
|
||||
const postSubmitSurvey = createPostSubmitSurvey(app);
|
||||
const deleteUserSurveys = createDeleteUserSurveys(app);
|
||||
|
||||
api.get('/account', sendNonUserToHome, deprecatedEndpoint);
|
||||
api.get('/account/unlink/:social', sendNonUserToHome, getUnlinkSocial);
|
||||
@@ -48,6 +52,7 @@ function bootUser(app) {
|
||||
ifNoUser401,
|
||||
deleteUserToken,
|
||||
deleteMsUsername,
|
||||
deleteUserSurveys,
|
||||
postDeleteAccount
|
||||
);
|
||||
api.post(
|
||||
@@ -55,6 +60,7 @@ function bootUser(app) {
|
||||
ifNoUser401,
|
||||
deleteUserToken,
|
||||
deleteMsUsername,
|
||||
deleteUserSurveys,
|
||||
postResetProgress
|
||||
);
|
||||
api.post(
|
||||
@@ -80,6 +86,13 @@ function bootUser(app) {
|
||||
deleteMsUsernameResponse
|
||||
);
|
||||
|
||||
api.post(
|
||||
'/user/submit-survey',
|
||||
ifNoUser401,
|
||||
validateSurvey,
|
||||
postSubmitSurvey
|
||||
);
|
||||
|
||||
app.use(api);
|
||||
}
|
||||
|
||||
@@ -206,8 +219,50 @@ function deleteMsUsernameResponse(req, res) {
|
||||
return res.send({ msUsername: null });
|
||||
}
|
||||
|
||||
function createPostSubmitSurvey(app) {
|
||||
const { Survey } = app.models;
|
||||
|
||||
return async function postSubmitSurvey(req, res) {
|
||||
const { user, body } = req;
|
||||
const { surveyResults } = body;
|
||||
const { completedSurveys = [] } = user;
|
||||
const { title } = surveyResults;
|
||||
|
||||
const surveyAlreadyTaken = completedSurveys.some(s => s.title === title);
|
||||
if (surveyAlreadyTaken) {
|
||||
return res.status(400).json({
|
||||
type: 'error',
|
||||
message: 'flash.survey.err-2'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const newSurvey = {
|
||||
...surveyResults,
|
||||
userId: user.id
|
||||
};
|
||||
|
||||
const createdSurvey = await Survey.create(newSurvey);
|
||||
if (!createdSurvey) {
|
||||
throw new Error('Error creating survey');
|
||||
}
|
||||
|
||||
return res.json({
|
||||
type: 'success',
|
||||
message: 'flash.survey.success'
|
||||
});
|
||||
} catch (e) {
|
||||
log(e);
|
||||
return res.status(500).json({
|
||||
type: 'error',
|
||||
message: 'flash.survey.err-3'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createReadSessionUser(app) {
|
||||
const { MsUsername, UserToken } = app.models;
|
||||
const { MsUsername, Survey, UserToken } = app.models;
|
||||
|
||||
return async function getSessionUser(req, res, next) {
|
||||
const queryUser = req.user;
|
||||
@@ -240,6 +295,18 @@ function createReadSessionUser(app) {
|
||||
return next(e);
|
||||
}
|
||||
|
||||
let completedSurveys;
|
||||
try {
|
||||
const userId = queryUser?.id;
|
||||
completedSurveys = userId
|
||||
? await Survey.find({
|
||||
where: { userId }
|
||||
})
|
||||
: [];
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
|
||||
if (!queryUser || !queryUser.toJSON().username) {
|
||||
// TODO: This should return an error status
|
||||
return res.json({ user: {}, result: '' });
|
||||
@@ -282,7 +349,8 @@ function createReadSessionUser(app) {
|
||||
...normaliseUserFields(user),
|
||||
joinDate: user.id.getTimestamp(),
|
||||
userToken: encodedUserToken,
|
||||
msUsername
|
||||
msUsername,
|
||||
completedSurveys: completedSurveys.map(fixCompletedSurveyItem)
|
||||
}
|
||||
},
|
||||
result: user.username
|
||||
|
||||
41
api-server/src/server/middlewares/survey.js
Normal file
41
api-server/src/server/middlewares/survey.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import debugFactory from 'debug';
|
||||
const log = debugFactory('fcc:boot:user');
|
||||
|
||||
const allowedTitles = ['Foundational C# with Microsoft Survey'];
|
||||
|
||||
export function validateSurvey(req, res, next) {
|
||||
const {
|
||||
surveyResults: { title = '', responses = [] }
|
||||
} = req.body;
|
||||
|
||||
if (
|
||||
!allowedTitles.includes(title) ||
|
||||
!Array.isArray(responses) ||
|
||||
!responses.every(
|
||||
r => typeof r.question === 'string' && typeof r.response === 'string'
|
||||
)
|
||||
) {
|
||||
return res.status(400).json({
|
||||
type: 'error',
|
||||
message: 'flash.survey.err-1'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
export function createDeleteUserSurveys(app) {
|
||||
const { Survey } = app.models;
|
||||
|
||||
return async function deleteUserSurveys(req, res, next) {
|
||||
try {
|
||||
await Survey.destroyAll({ userId: req.user.id });
|
||||
req.userSurveysDeleted = true;
|
||||
} catch (e) {
|
||||
req.userSurveysDeleted = false;
|
||||
log(`An error occurred deleting Surveys for user with id ${req.user.id}`);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
@@ -47,6 +47,10 @@
|
||||
"dataSource": "db",
|
||||
"public": false
|
||||
},
|
||||
"Survey": {
|
||||
"dataSource": "db",
|
||||
"public": false
|
||||
},
|
||||
"RoleMapping": {
|
||||
"dataSource": "db",
|
||||
"public": false
|
||||
|
||||
41
api-server/src/server/models/survey.json
Normal file
41
api-server/src/server/models/survey.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "Survey",
|
||||
"description": "Survey responses from campers",
|
||||
"base": "PersistedModel",
|
||||
"idInjection": true,
|
||||
"options": {
|
||||
"strict": true
|
||||
},
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"type": [
|
||||
{
|
||||
"question": "string",
|
||||
"response": "string"
|
||||
}
|
||||
],
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"validations": [],
|
||||
"relations": {
|
||||
"user": {
|
||||
"type": "belongsTo",
|
||||
"model": "user",
|
||||
"foreignKey": "userId"
|
||||
}
|
||||
},
|
||||
"acls": [
|
||||
{
|
||||
"accessType": "*",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "DENY"
|
||||
}
|
||||
],
|
||||
"methods": {}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ export const publicUserProps = [
|
||||
'calendar',
|
||||
'completedChallenges',
|
||||
'completedExams',
|
||||
'completedSurveys',
|
||||
'githubProfile',
|
||||
'isApisMicroservicesCert',
|
||||
'isBackEndCert',
|
||||
|
||||
@@ -745,6 +745,12 @@
|
||||
"verified": "Your trophy from Microsoft's learning platform was verified."
|
||||
}
|
||||
},
|
||||
"survey": {
|
||||
"err-1": "The survey submitted is not in the correct format.",
|
||||
"err-2": "It looks like you have already completed this survey.",
|
||||
"err-3": "Something went wrong trying to save your survey.",
|
||||
"success": "Thank you. Your survey was submitted."
|
||||
},
|
||||
"classroom-mode-updated": "We have updated your classroom mode settings"
|
||||
},
|
||||
"validation": {
|
||||
@@ -960,5 +966,31 @@
|
||||
"p2": "We thank you for reporting bugs that you encounter and help in making freeCodeCamp.org better.",
|
||||
"p3": "Your progress MAY NOT be saved on your next visit, and any certifications claimed on this deployment are not valid. Learn more by <0>following this link</0>.",
|
||||
"certain": "Accept and Dismiss"
|
||||
},
|
||||
"survey": {
|
||||
"foundational-c-sharp": {
|
||||
"title": "Foundational C# with Microsoft Survey",
|
||||
"q1": {
|
||||
"q": "Please describe your role:",
|
||||
"o1": "Student developer",
|
||||
"o2": "Beginner developer (less than 2 years experience)",
|
||||
"o3": "Intermediate developer (between 2 and 5 years experience)",
|
||||
"o4": "Experienced developer (more than 5 years experience)"
|
||||
},
|
||||
"q2": {
|
||||
"q": "Prior to this course, how experienced were you with .NET and C#?",
|
||||
"o1": "Novice (no prior experience)",
|
||||
"o2": "Beginner",
|
||||
"o3": "Intermediate",
|
||||
"o4": "Advanced",
|
||||
"o5": "Expert"
|
||||
}
|
||||
},
|
||||
"misc": {
|
||||
"take": "Take the survey",
|
||||
"submit": "Submit the survey",
|
||||
"exit": "Exit the survey",
|
||||
"two-questions": "Congratulations on getting this far. Before you can start the exam, please answer these two short survey questions."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ export enum FlashMessages {
|
||||
ReportSent = 'flash.report-sent',
|
||||
SigninSuccess = 'flash.signin-success',
|
||||
StartProjectErr = 'flash.start-project-err',
|
||||
SurveyErr1 = 'flash.survey.err-1',
|
||||
SurveyErr2 = 'flash.survey.err-2',
|
||||
SurveyErr3 = 'flash.survey.err-3',
|
||||
SurveySuccess = 'flash.survey.success',
|
||||
TimelinePrivate = 'flash.timeline-private',
|
||||
TokenDeleted = 'flash.token-deleted',
|
||||
UpdatedAboutMe = 'flash.updated-about-me',
|
||||
|
||||
@@ -31,6 +31,8 @@ export const actionTypes = createTypes(
|
||||
'setMsUsername',
|
||||
'setIsProcessing',
|
||||
'submitComplete',
|
||||
'submitSurvey',
|
||||
'submitSurveyComplete',
|
||||
'updateComplete',
|
||||
'updateFailed',
|
||||
'updateDonationFormState',
|
||||
|
||||
@@ -106,6 +106,11 @@ export const linkMsUsername = createAction(actionTypes.linkMsUsername);
|
||||
export const unlinkMsUsername = createAction(actionTypes.unlinkMsUsername);
|
||||
export const setMsUsername = createAction(actionTypes.setMsUsername);
|
||||
|
||||
export const submitSurvey = createAction(actionTypes.submitSurvey);
|
||||
export const submitSurveyComplete = createAction(
|
||||
actionTypes.submitSurveyComplete
|
||||
);
|
||||
|
||||
export const setIsProcessing = createAction(actionTypes.setIsProcessing);
|
||||
|
||||
export const closeSignoutModal = createAction(actionTypes.closeSignoutModal);
|
||||
|
||||
@@ -23,6 +23,7 @@ import { createShowCertSaga } from './show-cert-saga';
|
||||
import updateCompleteEpic from './update-complete-epic';
|
||||
import { createUserTokenSaga } from './user-token-saga';
|
||||
import { createMsUsernameSaga } from './ms-username-saga';
|
||||
import { createSurveySaga } from './survey-saga';
|
||||
|
||||
const defaultFetchState = {
|
||||
pending: true,
|
||||
@@ -91,7 +92,8 @@ export const sagas = [
|
||||
...createReportUserSaga(actionTypes),
|
||||
...createUserTokenSaga(actionTypes),
|
||||
...createSaveChallengeSaga(actionTypes),
|
||||
...createMsUsernameSaga(actionTypes)
|
||||
...createMsUsernameSaga(actionTypes),
|
||||
...createSurveySaga(actionTypes)
|
||||
];
|
||||
|
||||
function spreadThePayloadOnUser(state, payload) {
|
||||
@@ -432,6 +434,23 @@ export const reducer = handleActions(
|
||||
}
|
||||
};
|
||||
},
|
||||
[actionTypes.submitSurveyComplete]: (
|
||||
state,
|
||||
{ payload: { surveyResults } }
|
||||
) => {
|
||||
const { appUsername } = state;
|
||||
const { completedSurveys = [] } = state.user[appUsername];
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
[appUsername]: {
|
||||
...state.user[appUsername],
|
||||
completedSurveys: [...completedSurveys, surveyResults]
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
[challengeTypes.challengeMounted]: (state, { payload }) => ({
|
||||
...state,
|
||||
currentChallengeId: payload
|
||||
|
||||
@@ -210,6 +210,7 @@ export type User = {
|
||||
about: string;
|
||||
acceptedPrivacyTerms: boolean;
|
||||
completedChallenges: CompletedChallenge[];
|
||||
completedSurveys: SurveyResults[];
|
||||
currentChallengeId: string;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
@@ -412,3 +413,14 @@ export interface GeneratedExamResults {
|
||||
passed: boolean;
|
||||
examTimeInSeconds: number;
|
||||
}
|
||||
|
||||
// Survey related types
|
||||
export interface SurveyResponse {
|
||||
question: string;
|
||||
response: string;
|
||||
}
|
||||
|
||||
export interface SurveyResults {
|
||||
title: string;
|
||||
responses: SurveyResponse[];
|
||||
}
|
||||
|
||||
@@ -91,6 +91,9 @@ export const msUsernameSelector = state => {
|
||||
return userSelector(state).msUsername;
|
||||
};
|
||||
|
||||
export const completedSurveysSelector = state =>
|
||||
userSelector(state).completedSurveys || [];
|
||||
|
||||
export const isProcessingSelector = state => {
|
||||
return state[MainApp].isProcessing;
|
||||
};
|
||||
|
||||
40
client/src/redux/survey-saga.js
Normal file
40
client/src/redux/survey-saga.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||
|
||||
import { createFlashMessage } from '../components/Flash/redux';
|
||||
import { FlashMessages } from '../components/Flash/redux/flash-messages';
|
||||
import { postSubmitSurvey } from '../utils/ajax';
|
||||
import { closeModal } from '../templates/Challenges/redux/actions';
|
||||
import { submitSurveyComplete, setIsProcessing } from './actions';
|
||||
|
||||
const submitSurveyErr = {
|
||||
type: 'danger',
|
||||
message: FlashMessages.SurveyErr3
|
||||
};
|
||||
|
||||
function* submitSurveySaga({ payload }) {
|
||||
const surveyResults = payload;
|
||||
|
||||
try {
|
||||
const { data } = yield call(postSubmitSurvey, { surveyResults });
|
||||
const { type } = data;
|
||||
|
||||
if (type === 'success') {
|
||||
yield put(submitSurveyComplete({ surveyResults }));
|
||||
yield put(createFlashMessage(data));
|
||||
yield put(closeModal('survey'));
|
||||
yield put(setIsProcessing(false));
|
||||
} else {
|
||||
yield put(createFlashMessage(data));
|
||||
yield put(closeModal('survey'));
|
||||
yield put(setIsProcessing(false));
|
||||
}
|
||||
} catch {
|
||||
yield put(createFlashMessage(submitSurveyErr));
|
||||
yield put(closeModal('survey'));
|
||||
yield put(setIsProcessing(false));
|
||||
}
|
||||
}
|
||||
|
||||
export function createSurveySaga(types) {
|
||||
return [takeEvery(types.submitSurvey, submitSurveySaga)];
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Button, Panel } from '@freecodecamp/react-bootstrap';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { openModal } from '../../redux/actions';
|
||||
import Spacer from '../../../../components/helpers/spacer';
|
||||
import SurveyModal from './survey-modal';
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
bindActionCreators(
|
||||
{
|
||||
openSurveyModal: () => openModal('survey')
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
interface FoudationalCSharpSurveyAlertProps {
|
||||
openSurveyModal: () => void;
|
||||
}
|
||||
|
||||
function FoudationalCSharpSurveyAlert({
|
||||
openSurveyModal
|
||||
}: FoudationalCSharpSurveyAlertProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Panel data-cy='c-sharp-survey-alert' bsStyle='info'>
|
||||
<Panel.Heading>{t('survey.foundational-c-sharp.title')}</Panel.Heading>
|
||||
<Panel.Body className='text-center'>
|
||||
<p>{t('survey.misc.two-questions')}</p>
|
||||
<Spacer size='small' />
|
||||
<Button
|
||||
block={true}
|
||||
bsSize='md'
|
||||
bsStyle='info'
|
||||
data-cy='start-csharp-survey-btn'
|
||||
className='btn-invert'
|
||||
onClick={openSurveyModal}
|
||||
type='button'
|
||||
>
|
||||
{t('survey.misc.take')}
|
||||
</Button>
|
||||
<SurveyModal />
|
||||
</Panel.Body>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
FoudationalCSharpSurveyAlert.displayName = 'FoundationalCSharpSurveyAlert';
|
||||
|
||||
export default connect(null, mapDispatchToProps)(FoudationalCSharpSurveyAlert);
|
||||
@@ -0,0 +1,198 @@
|
||||
import { Button, Modal } from '@freecodecamp/react-bootstrap';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import type { Dispatch } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { SurveyResults, SurveyResponse } from '../../../../redux/prop-types';
|
||||
import { Spacer } from '../../../../components/helpers';
|
||||
import { setIsProcessing, submitSurvey } from '../../../../redux/actions';
|
||||
import { closeModal } from '../../redux/actions';
|
||||
import { isProcessingSelector } from '../../../../redux/selectors';
|
||||
|
||||
interface FoundationalCSharpSurveyProps {
|
||||
closeSurveyModal: () => void;
|
||||
submitSurvey: (arg0: SurveyResults) => void;
|
||||
isProcessing: boolean;
|
||||
setIsProcessing: (arg0: boolean) => void;
|
||||
}
|
||||
|
||||
interface SurveyState {
|
||||
questionIndex: number;
|
||||
responseIndex: null | number;
|
||||
}
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
isProcessingSelector,
|
||||
(isProcessing: boolean) => ({
|
||||
isProcessing
|
||||
})
|
||||
);
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
bindActionCreators(
|
||||
{
|
||||
closeSurveyModal: () => closeModal('survey'),
|
||||
setIsProcessing,
|
||||
submitSurvey
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
function FoundationalCSharpSurvey({
|
||||
closeSurveyModal,
|
||||
submitSurvey,
|
||||
setIsProcessing,
|
||||
isProcessing
|
||||
}: FoundationalCSharpSurveyProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// submit English values to server and save those to database
|
||||
const englishTitle = t('survey.foundational-c-sharp.title', { lng: 'en' });
|
||||
const englishSurvey = [
|
||||
{
|
||||
question: t('survey.foundational-c-sharp.q1.q', { lng: 'en' }),
|
||||
options: [
|
||||
t('survey.foundational-c-sharp.q1.o1', { lng: 'en' }),
|
||||
t('survey.foundational-c-sharp.q1.o2', { lng: 'en' }),
|
||||
t('survey.foundational-c-sharp.q1.o3', { lng: 'en' }),
|
||||
t('survey.foundational-c-sharp.q1.o4', { lng: 'en' })
|
||||
]
|
||||
},
|
||||
{
|
||||
question: t('survey.foundational-c-sharp.q2.q', { lng: 'en' }),
|
||||
options: [
|
||||
t('survey.foundational-c-sharp.q2.o1', { lng: 'en' }),
|
||||
t('survey.foundational-c-sharp.q2.o2', { lng: 'en' }),
|
||||
t('survey.foundational-c-sharp.q2.o3', { lng: 'en' }),
|
||||
t('survey.foundational-c-sharp.q2.o4', { lng: 'en' }),
|
||||
t('survey.foundational-c-sharp.q2.o5', { lng: 'en' })
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// display survey in i18n
|
||||
const i18nSurvey = [
|
||||
{
|
||||
question: t('survey.foundational-c-sharp.q1.q'),
|
||||
options: [
|
||||
t('survey.foundational-c-sharp.q1.o1'),
|
||||
t('survey.foundational-c-sharp.q1.o2'),
|
||||
t('survey.foundational-c-sharp.q1.o3'),
|
||||
t('survey.foundational-c-sharp.q1.o4')
|
||||
]
|
||||
},
|
||||
{
|
||||
question: t('survey.foundational-c-sharp.q2.q'),
|
||||
options: [
|
||||
t('survey.foundational-c-sharp.q2.o1'),
|
||||
t('survey.foundational-c-sharp.q2.o2'),
|
||||
t('survey.foundational-c-sharp.q2.o3'),
|
||||
t('survey.foundational-c-sharp.q2.o4'),
|
||||
t('survey.foundational-c-sharp.q2.o5')
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const emptySurvey: SurveyState[] = i18nSurvey.map((question, i) => ({
|
||||
questionIndex: i,
|
||||
responseIndex: null
|
||||
}));
|
||||
|
||||
const [surveyResponses, setSurveyResponses] = useState(emptySurvey);
|
||||
|
||||
function handleOptionChange(questionIndex: number, responseIndex: number) {
|
||||
const newSurveyResponses = Array.from(surveyResponses);
|
||||
newSurveyResponses[questionIndex].responseIndex = responseIndex;
|
||||
setSurveyResponses(newSurveyResponses);
|
||||
}
|
||||
|
||||
function createSurveyResults() {
|
||||
setIsProcessing(true);
|
||||
|
||||
// convert responses to English before submitting
|
||||
const englishResponses: SurveyResponse[] = surveyResponses.map(r => ({
|
||||
question: englishSurvey[r.questionIndex].question,
|
||||
response:
|
||||
englishSurvey[r.questionIndex].options[r.responseIndex as number]
|
||||
}));
|
||||
|
||||
const surveyResults = {
|
||||
title: englishTitle,
|
||||
responses: englishResponses
|
||||
};
|
||||
|
||||
submitSurvey(surveyResults);
|
||||
}
|
||||
|
||||
const cantSubmitSurvey = surveyResponses.some(q => q.responseIndex === null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header data-cy='c-sharp-survey-modal' closeButton={true}>
|
||||
<Modal.Title className='text-center'>
|
||||
{t('survey.foundational-c-sharp.title')}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body className='reset-modal-body'>
|
||||
{i18nSurvey.map((question, i) => (
|
||||
<div key={i}>
|
||||
<Spacer size='medium' />
|
||||
<div>{question.question}</div>
|
||||
<Spacer size='small' />
|
||||
<div className='video-quiz-options'>
|
||||
{question.options.map((option, j) => (
|
||||
<label className='video-quiz-option-label' key={j}>
|
||||
<input
|
||||
aria-label={t('aria.answer')}
|
||||
checked={surveyResponses[i].responseIndex === j}
|
||||
className='sr-only'
|
||||
name={question.question}
|
||||
onChange={() => handleOptionChange(i, j)}
|
||||
type='radio'
|
||||
value={option}
|
||||
/>{' '}
|
||||
<span className='video-quiz-input-visible'>
|
||||
{surveyResponses[i].responseIndex === j ? (
|
||||
<span className='video-quiz-selected-input' />
|
||||
) : null}
|
||||
</span>
|
||||
{option}{' '}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Modal.Body>
|
||||
<Modal.Footer className='reset-modal-footer'>
|
||||
<Button
|
||||
block={true}
|
||||
bsSize='medium'
|
||||
bsStyle='primary'
|
||||
data-cy='submit-csharp-survey-btn'
|
||||
disabled={cantSubmitSurvey || isProcessing}
|
||||
onClick={createSurveyResults}
|
||||
>
|
||||
{t('survey.misc.submit')}
|
||||
</Button>
|
||||
<Button
|
||||
block={true}
|
||||
bsSize='medium'
|
||||
bsStyle='primary'
|
||||
disabled={isProcessing}
|
||||
onClick={closeSurveyModal}
|
||||
>
|
||||
{t('survey.misc.exit')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
FoundationalCSharpSurvey.displayName = 'FoundationalCSharpSurvey';
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(FoundationalCSharpSurvey);
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Alert } from '@freecodecamp/react-bootstrap';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Spacer from '../../../../components/helpers/spacer';
|
||||
import { PrerequisiteChallenge } from '../../../../redux/prop-types';
|
||||
|
||||
interface MissingPrerequisitesProps {
|
||||
missingPrerequisites: PrerequisiteChallenge[];
|
||||
}
|
||||
|
||||
function MissingPrerequisites({
|
||||
missingPrerequisites
|
||||
}: MissingPrerequisitesProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Alert data-cy='missing-prerequisites-alert' bsStyle='danger'>
|
||||
<p>{t('learn.exam.not-qualified')}</p>
|
||||
<Spacer size='small' />
|
||||
<ul>
|
||||
{missingPrerequisites.map(({ title, id }) => (
|
||||
<li key={id}>{title}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
MissingPrerequisites.displayName = 'MissingPrerequisites';
|
||||
|
||||
export default MissingPrerequisites;
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { closeModal } from '../../redux/actions';
|
||||
import { isSurveyModalOpenSelector } from '../../redux/selectors';
|
||||
import { isProcessingSelector } from '../../../../redux/selectors';
|
||||
import FoundationalCSharpSurvey from './foundational-c-sharp-survey';
|
||||
|
||||
interface SurveyModalProps {
|
||||
closeSurveyModal: () => void;
|
||||
isProcessing: boolean;
|
||||
isSurveyModalOpen: boolean;
|
||||
}
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
isProcessingSelector,
|
||||
isSurveyModalOpenSelector,
|
||||
(isProcessing: boolean, isSurveyModalOpen: boolean) => ({
|
||||
isProcessing,
|
||||
isSurveyModalOpen
|
||||
})
|
||||
);
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
bindActionCreators(
|
||||
{
|
||||
closeSurveyModal: () => closeModal('survey')
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
function SurveyModal({
|
||||
closeSurveyModal,
|
||||
isSurveyModalOpen,
|
||||
isProcessing
|
||||
}: SurveyModalProps): JSX.Element {
|
||||
return (
|
||||
<Modal
|
||||
animation={false}
|
||||
dialogClassName='survey-modal'
|
||||
keyboard={true}
|
||||
onHide={() => (isProcessing ? '' : closeSurveyModal())}
|
||||
show={isSurveyModalOpen}
|
||||
>
|
||||
<FoundationalCSharpSurvey />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
SurveyModal.displayName = 'SurveyModal';
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SurveyModal);
|
||||
@@ -24,6 +24,7 @@ import Hotkeys from '../components/hotkeys';
|
||||
import { clearExamResults, startExam, stopExam } from '../../../redux/actions';
|
||||
import {
|
||||
completedChallengesSelector,
|
||||
completedSurveysSelector,
|
||||
isSignedInSelector,
|
||||
examInProgressSelector,
|
||||
examResultsSelector
|
||||
@@ -47,30 +48,38 @@ import {
|
||||
UserExamQuestion,
|
||||
UserExam,
|
||||
GeneratedExamResults,
|
||||
GeneratedExamQuestion
|
||||
GeneratedExamQuestion,
|
||||
PrerequisiteChallenge,
|
||||
SurveyResults
|
||||
} from '../../../redux/prop-types';
|
||||
import { FlashMessages } from '../../../components/Flash/redux/flash-messages';
|
||||
import { formatSecondsToTime } from '../../../utils/format-seconds';
|
||||
import ExitExamModal from './components/exit-exam-modal';
|
||||
import FinishExamModal from './components/finish-exam-modal';
|
||||
import ExamResults from './components/exam-results';
|
||||
import MissingPrerequisites from './components/missing-prerequisites';
|
||||
import FoundationCSharpSurveyAlert from './components/foundational-c-sharp-survey-alert';
|
||||
|
||||
import './exam.css';
|
||||
|
||||
// Redux
|
||||
const mapStateToProps = createSelector(
|
||||
completedChallengesSelector,
|
||||
completedSurveysSelector,
|
||||
isChallengeCompletedSelector,
|
||||
isSignedInSelector,
|
||||
examInProgressSelector,
|
||||
examResultsSelector,
|
||||
(
|
||||
completedChallenges: CompletedChallenge[],
|
||||
completedSurveys: SurveyResults[],
|
||||
isChallengeCompleted: boolean,
|
||||
isSignedIn: boolean,
|
||||
examInProgress: boolean,
|
||||
examResults: GeneratedExamResults | null
|
||||
) => ({
|
||||
completedChallenges,
|
||||
completedSurveys,
|
||||
isChallengeCompleted,
|
||||
isSignedIn,
|
||||
examInProgress,
|
||||
@@ -102,6 +111,7 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
interface ShowExamProps {
|
||||
challengeMounted: (arg0: string) => void;
|
||||
completedChallenges: CompletedChallenge[];
|
||||
completedSurveys: SurveyResults[];
|
||||
clearExamResults: () => void;
|
||||
createFlashMessage: typeof createFlashMessage;
|
||||
data: { challengeNode: ChallengeNode };
|
||||
@@ -334,6 +344,7 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
examInProgress,
|
||||
examResults,
|
||||
completedChallenges,
|
||||
completedSurveys,
|
||||
isChallengeCompleted,
|
||||
openExitExamModal,
|
||||
openFinishExamModal,
|
||||
@@ -350,12 +361,7 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
userExamQuestions
|
||||
} = this.state;
|
||||
|
||||
type Prerequisite = {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
let missingPrerequisites: Prerequisite[] = [];
|
||||
let missingPrerequisites: PrerequisiteChallenge[] = [];
|
||||
if (prerequisites) {
|
||||
missingPrerequisites = prerequisites?.filter(
|
||||
prerequisite =>
|
||||
@@ -363,7 +369,11 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
);
|
||||
}
|
||||
|
||||
const qualifiedForExam = missingPrerequisites.length === 0;
|
||||
const surveyCompleted = completedSurveys.some(
|
||||
s => s.title === 'Foundational C# with Microsoft Survey'
|
||||
);
|
||||
const prerequisitesComplete = missingPrerequisites.length === 0;
|
||||
const qualifiedForExam = prerequisitesComplete && surveyCompleted;
|
||||
|
||||
const blockNameTitle = `${t(
|
||||
`intro:${superBlock}.blocks.${block}.title`
|
||||
@@ -391,7 +401,10 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
{title}
|
||||
</div>
|
||||
<span>|</span>
|
||||
<div data-playwright-test-label='exam-show-question-time'>
|
||||
<div
|
||||
data-cy='exam-time'
|
||||
data-playwright-test-label='exam-show-question-time'
|
||||
>
|
||||
{t('learn.exam.time', {
|
||||
t: formatSecondsToTime(examTimeInSeconds)
|
||||
})}
|
||||
@@ -456,7 +469,7 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
className='exam-button'
|
||||
disabled={currentQuestionIndex <= 0}
|
||||
bsStyle='primary'
|
||||
data-cy='previous-exam-question'
|
||||
data-cy='previous-exam-question-btn'
|
||||
onClick={this.goToPreviousQuestion}
|
||||
>
|
||||
{t('buttons.previous-question')}
|
||||
@@ -471,7 +484,7 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
}
|
||||
className='exam-button'
|
||||
bsStyle='primary'
|
||||
data-cy='finish-exam'
|
||||
data-cy='finish-exam-btn'
|
||||
onClick={openFinishExamModal}
|
||||
>
|
||||
{t('buttons.finish-exam')}
|
||||
@@ -484,7 +497,7 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
}
|
||||
className='exam-button'
|
||||
bsStyle='primary'
|
||||
data-cy='next-exam-question'
|
||||
data-cy='next-exam-question-btn'
|
||||
onClick={this.goToNextQuestion}
|
||||
>
|
||||
{t('buttons.next-question')}
|
||||
@@ -499,7 +512,7 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
block={true}
|
||||
className='exam-button'
|
||||
bsStyle='primary'
|
||||
data-cy='exit-exam'
|
||||
data-cy='exit-exam-btn'
|
||||
onClick={openExitExamModal}
|
||||
>
|
||||
{t('buttons.exit-exam')}
|
||||
@@ -532,19 +545,19 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
<Spacer size='medium' />
|
||||
|
||||
{qualifiedForExam ? (
|
||||
<Alert id='qualified-for-exam' variant='info'>
|
||||
<Alert data-cy='qualified-for-exam-alert' variant='info'>
|
||||
<p>{t('learn.exam.qualified')}</p>
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert id='not-qualified-for-exam' variant='danger'>
|
||||
<p>{t('learn.exam.not-qualified')}</p>
|
||||
<Spacer size='small' />
|
||||
<ul>
|
||||
{missingPrerequisites.map(({ title, id }) => (
|
||||
<li key={id}>{title}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Alert>
|
||||
<>
|
||||
{!prerequisitesComplete ? (
|
||||
<MissingPrerequisites
|
||||
missingPrerequisites={missingPrerequisites}
|
||||
/>
|
||||
) : (
|
||||
<FoundationCSharpSurveyAlert />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<PrismFormatted text={description} />
|
||||
@@ -554,7 +567,7 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
<Button
|
||||
block={true}
|
||||
bsStyle='primary'
|
||||
data-cy='start-exam'
|
||||
data-cy='start-exam-btn'
|
||||
disabled={!qualifiedForExam}
|
||||
onClick={this.runExam}
|
||||
>
|
||||
|
||||
@@ -43,6 +43,7 @@ const initialState = {
|
||||
exitExam: false,
|
||||
finishExam: false,
|
||||
examResults: false,
|
||||
survey: false,
|
||||
projectPreview: false,
|
||||
shortcuts: false
|
||||
},
|
||||
|
||||
@@ -31,6 +31,7 @@ export const isResetModalOpenSelector = state => state[ns].modal.reset;
|
||||
export const isExitExamModalOpenSelector = state => state[ns].modal.exitExam;
|
||||
export const isFinishExamModalOpenSelector = state =>
|
||||
state[ns].modal.finishExam;
|
||||
export const isSurveyModalOpenSelector = state => state[ns].modal.survey;
|
||||
export const isExamResultsModalOpenSelector = state =>
|
||||
state[ns].modal.examResults;
|
||||
export const isProjectPreviewModalOpenSelector = state =>
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
GenerateExamResponseWithData,
|
||||
SavedChallenge,
|
||||
SavedChallengeFile,
|
||||
SurveyResults,
|
||||
User
|
||||
} from '../redux/prop-types';
|
||||
|
||||
@@ -284,6 +285,12 @@ export function postSaveChallenge(body: {
|
||||
return post('/save-challenge', body);
|
||||
}
|
||||
|
||||
export function postSubmitSurvey(body: {
|
||||
surveyResults: SurveyResults;
|
||||
}): Promise<ResponseWithData<void>> {
|
||||
return post('/user/submit-survey', body);
|
||||
}
|
||||
|
||||
/** PUT **/
|
||||
|
||||
interface MyAbout {
|
||||
|
||||
@@ -62,6 +62,10 @@ const toneUrls = {
|
||||
[FlashMessages.ReportSent]: CHAL_COMP,
|
||||
[FlashMessages.SigninSuccess]: CHAL_COMP,
|
||||
[FlashMessages.StartProjectErr]: TRY_AGAIN,
|
||||
[FlashMessages.SurveyErr1]: TRY_AGAIN,
|
||||
[FlashMessages.SurveyErr2]: TRY_AGAIN,
|
||||
[FlashMessages.SurveyErr3]: TRY_AGAIN,
|
||||
[FlashMessages.SurveySuccess]: CHAL_COMP,
|
||||
[FlashMessages.TimelinePrivate]: TRY_AGAIN,
|
||||
[FlashMessages.TokenDeleted]: CHAL_COMP,
|
||||
[FlashMessages.UpdatedAboutMe]: CHAL_COMP,
|
||||
|
||||
@@ -6,6 +6,18 @@ function seed(args = []) {
|
||||
return execSync('node tools/scripts/seed/seed-demo-user ' + args.join(' '));
|
||||
}
|
||||
|
||||
function seedExams() {
|
||||
return execSync('node tools/scripts/seed-exams/create-exams.js');
|
||||
}
|
||||
|
||||
function seedSurveys() {
|
||||
return execSync('node tools/scripts/seed/seed-surveys.js');
|
||||
}
|
||||
|
||||
function deleteSurveys() {
|
||||
return execSync('node tools/scripts/seed/seed-surveys.js delete-only');
|
||||
}
|
||||
|
||||
module.exports = defineConfig({
|
||||
e2e: {
|
||||
baseUrl: 'http://localhost:8000',
|
||||
@@ -35,7 +47,10 @@ module.exports = defineConfig({
|
||||
}
|
||||
});
|
||||
on('task', {
|
||||
seed
|
||||
seed,
|
||||
seedExams,
|
||||
seedSurveys,
|
||||
deleteSurveys
|
||||
});
|
||||
|
||||
config.env.API_LOCATION = 'http://localhost:3000';
|
||||
|
||||
109
cypress/e2e/default/learn/challenges/c-sharp-exam.ts
Normal file
109
cypress/e2e/default/learn/challenges/c-sharp-exam.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
const examUrl =
|
||||
'/learn/foundational-c-sharp-with-microsoft/foundational-c-sharp-with-microsoft-certification-exam/foundational-c-sharp-with-microsoft-certification-exam';
|
||||
|
||||
const el = {
|
||||
qualifiedAlert: "[data-cy='qualified-for-exam-alert']",
|
||||
prerequisitesAlert: "[data-cy='missing-prerequisites-alert']",
|
||||
surveyAlert: "[data-cy='c-sharp-survey-alert']",
|
||||
startSurveyBtn: "[data-cy='start-csharp-survey-btn']",
|
||||
submitSurveyBtn: "[data-cy='submit-csharp-survey-btn']",
|
||||
surveyModal: "[data-cy='c-sharp-survey-modal']",
|
||||
startExamBtn: "[data-cy='start-exam-btn']",
|
||||
examTime: "[data-cy='exam-time']",
|
||||
examInput: '.exam-answer-input-visible',
|
||||
prevQuestionBtn: "[data-cy='previous-exam-question-btn']",
|
||||
nextQuestionBtn: "[data-cy='next-exam-question-btn']",
|
||||
exitExamBtn: "[data-cy='exit-exam-btn']",
|
||||
finishExamBtn: "[data-cy='finish-exam-btn']"
|
||||
};
|
||||
|
||||
describe('C# Exam Challenge', () => {
|
||||
describe('Before completing prerequisite challenges', () => {
|
||||
before(() => {
|
||||
cy.task('seed');
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('Should show prerequisites alert and have "start exam" button disabled', () => {
|
||||
cy.visit(examUrl);
|
||||
cy.get(el.qualifiedAlert).should('not.exist');
|
||||
cy.get(el.prerequisitesAlert).should('be.visible');
|
||||
cy.contains('Trophy - Write Your First Code Using C#').should(
|
||||
'be.visible'
|
||||
);
|
||||
cy.get(el.surveyAlert).should('not.exist');
|
||||
cy.get(el.startExamBtn).should('be.disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('After completing prerequisite challenges', () => {
|
||||
before(() => {
|
||||
cy.task('seed', ['--seed-trophy-challenges']);
|
||||
cy.task('deleteSurveys');
|
||||
cy.login('trophy-user');
|
||||
cy.visit(examUrl);
|
||||
});
|
||||
|
||||
it('Should show the survey alert and be able to complete the survey', () => {
|
||||
cy.contains('Trophy - Write Your First Code Using C#').should(
|
||||
'not.exist'
|
||||
);
|
||||
cy.get(el.qualifiedAlert).should('not.exist');
|
||||
cy.get(el.prerequisitesAlert).should('not.exist');
|
||||
cy.get(el.surveyAlert).should('be.visible');
|
||||
cy.get(el.startExamBtn).should('be.disabled');
|
||||
cy.get(el.startSurveyBtn).click();
|
||||
cy.get(el.surveyModal).should('be.visible');
|
||||
cy.get(el.submitSurveyBtn).should('be.disabled');
|
||||
cy.contains('Student developer').click();
|
||||
cy.contains('Novice (no prior experience').click();
|
||||
cy.get(el.submitSurveyBtn).should('be.enabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('After completing the survey,', () => {
|
||||
beforeEach(() => {
|
||||
cy.task('seed', ['certified-user']);
|
||||
cy.task('seedExams');
|
||||
cy.task('seedSurveys');
|
||||
cy.login('certified-user');
|
||||
cy.visit(examUrl);
|
||||
});
|
||||
|
||||
it('Should be able to start and complete the exam', () => {
|
||||
cy.get(el.qualifiedAlert).should('be.visible');
|
||||
cy.get(el.surveyAlert).should('not.exist');
|
||||
cy.get(el.prerequisitesAlert).should('not.exist');
|
||||
cy.get(el.startExamBtn).click();
|
||||
cy.get('.exam-wrapper').should('be.visible');
|
||||
cy.contains('Foundational C# with Microsoft Certification Exam').should(
|
||||
'be.visible'
|
||||
);
|
||||
cy.get(el.examTime).should('be.visible');
|
||||
cy.contains('Question 1 of 5').should('be.visible');
|
||||
cy.get(el.prevQuestionBtn).should('be.disabled');
|
||||
cy.get(el.nextQuestionBtn).should('be.disabled');
|
||||
cy.get(el.finishExamBtn).should('not.exist');
|
||||
cy.get(el.exitExamBtn).should('be.visible');
|
||||
|
||||
// answer the first exam question
|
||||
cy.get(el.examInput).eq(0).click();
|
||||
cy.get(el.nextQuestionBtn).click();
|
||||
cy.get(el.prevQuestionBtn).should('be.enabled');
|
||||
cy.get(el.nextQuestionBtn).should('be.disabled');
|
||||
cy.contains('Question 2 of 5').should('be.visible');
|
||||
|
||||
// answer the rest of the questions
|
||||
cy.get(el.examInput).eq(0).click();
|
||||
cy.get(el.nextQuestionBtn).click();
|
||||
cy.get(el.examInput).eq(0).click();
|
||||
cy.get(el.nextQuestionBtn).click();
|
||||
cy.get(el.examInput).eq(0).click();
|
||||
cy.get(el.nextQuestionBtn).click();
|
||||
cy.get(el.examInput).eq(0).click();
|
||||
cy.get(el.finishExamBtn).click();
|
||||
cy.contains('Yes, I am finished').click();
|
||||
cy.get('.exam-results-wrapper').should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -69,9 +69,10 @@
|
||||
"preseed": "npm-run-all create:shared",
|
||||
"playwright:install-build-tools": "cd ./e2e && npm i && npx playwright install && npx playwright install-deps",
|
||||
"playwright:install-build-tools-linux": "sh ./playwright-install.sh",
|
||||
"seed": "cross-env DEBUG=fcc:* node ./tools/scripts/seed/seed-demo-user",
|
||||
"seed:certified-user": "cross-env DEBUG=fcc:* node ./tools/scripts/seed/seed-demo-user certified-user",
|
||||
"seed": "cross-env DEBUG=fcc:* node ./tools/scripts/seed/seed-demo-user && pnpm seed:surveys",
|
||||
"seed:certified-user": "cross-env DEBUG=fcc:* node ./tools/scripts/seed/seed-demo-user certified-user && pnpm seed:surveys",
|
||||
"seed:exams": "cd tools/scripts/seed-exams && cross-env node ./create-exams.js",
|
||||
"seed:surveys": "cross-env DEBUG=fcc:* node ./tools/scripts/seed/seed-surveys",
|
||||
"serve:client": "cd ./client && pnpm run serve",
|
||||
"serve:client-ci": "cd ./client && pnpm run serve-ci",
|
||||
"start": "npm-run-all create:shared -p develop:server serve:client",
|
||||
|
||||
@@ -42,7 +42,7 @@ function handleError(err, client) {
|
||||
const seed = async () => {
|
||||
for (const filename of examFilenames) {
|
||||
try {
|
||||
const examPath = join('./exams', filename);
|
||||
const examPath = join(__dirname, 'exams', filename);
|
||||
const examFile = readFileSync(examPath, { encoding: 'utf-8' });
|
||||
const examJson = yaml.load(examFile);
|
||||
const validExam = validateExamSchema(examJson);
|
||||
|
||||
@@ -10,6 +10,7 @@ const allowedArgs = [
|
||||
'--donor',
|
||||
'--top-contributor',
|
||||
'--unset-privacy-terms',
|
||||
'--seed-trophy-challenges',
|
||||
'certified-user'
|
||||
];
|
||||
|
||||
@@ -45,6 +46,45 @@ function handleError(err, client) {
|
||||
}
|
||||
}
|
||||
|
||||
const trophyChallenges = [
|
||||
{
|
||||
id: '647f85d407d29547b3bee1bb',
|
||||
solution:
|
||||
'https://learn.microsoft.com/api/gamestatus/achievements/learn.wwl.get-started-c-sharp-part-1.trophy?username=moT01&locale=en-us',
|
||||
completedDate: 1695064765244
|
||||
},
|
||||
{
|
||||
id: '647f87dc07d29547b3bee1bf',
|
||||
solution:
|
||||
'https://learn.microsoft.com/api/gamestatus/achievements/learn.wwl.get-started-c-sharp-part-2.trophy?username=moT01&locale=en-us',
|
||||
completedDate: 1695064900926
|
||||
},
|
||||
{
|
||||
id: '647f882207d29547b3bee1c0',
|
||||
solution:
|
||||
'https://learn.microsoft.com/api/gamestatus/achievements/learn.wwl.get-started-c-sharp-part-3.trophy?username=moT01&locale=en-us',
|
||||
completedDate: 1695064949460
|
||||
},
|
||||
{
|
||||
id: '647f867a07d29547b3bee1bc',
|
||||
solution:
|
||||
'https://learn.microsoft.com/api/gamestatus/achievements/learn.wwl.get-started-c-sharp-part-4.trophy?username=moT01&locale=en-us',
|
||||
completedDate: 1695064986634
|
||||
},
|
||||
{
|
||||
id: '647f877f07d29547b3bee1be',
|
||||
solution:
|
||||
'https://learn.microsoft.com/api/gamestatus/achievements/learn.wwl.get-started-c-sharp-part-5.trophy?username=moT01&locale=en-us',
|
||||
completedDate: 1695065026465
|
||||
},
|
||||
{
|
||||
id: '647f86ff07d29547b3bee1bd',
|
||||
solution:
|
||||
'https://learn.microsoft.com/api/gamestatus/achievements/learn.wwl.get-started-c-sharp-part-6.trophy?username=moT01&locale=en-us',
|
||||
completedDate: 1695065060157
|
||||
}
|
||||
];
|
||||
|
||||
const demoUser = {
|
||||
_id: new ObjectId('5bd30e0f1caf6ac3ddddddb5'),
|
||||
email: 'foo@bar.com',
|
||||
@@ -80,7 +120,9 @@ const demoUser = {
|
||||
isRelationalDatabaseCertV8: false,
|
||||
isCollegeAlgebraPyCertV8: false,
|
||||
isFoundationalCSharpCertV8: false,
|
||||
completedChallenges: [],
|
||||
completedChallenges: args.includes('--seed-trophy-challenges')
|
||||
? trophyChallenges
|
||||
: [],
|
||||
portfolio: [],
|
||||
yearsTopContributor: args.includes('--top-contributor')
|
||||
? ['2017', '2018', '2019']
|
||||
|
||||
99
tools/scripts/seed/seed-surveys.js
Normal file
99
tools/scripts/seed/seed-surveys.js
Normal file
@@ -0,0 +1,99 @@
|
||||
const path = require('path');
|
||||
const debug = require('debug');
|
||||
require('dotenv').config({ path: path.resolve(__dirname, '../../../.env') });
|
||||
const { MongoClient, ObjectId } = require('mongodb');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
const allowedArgs = ['delete-only'];
|
||||
|
||||
// Check for invalid arguments
|
||||
args.forEach(arg => {
|
||||
if (!allowedArgs.includes(arg))
|
||||
throw new Error(
|
||||
`Invalid argument ${arg}. Allowed arguments are ${allowedArgs.join(', ')}`
|
||||
);
|
||||
});
|
||||
|
||||
const log = debug('fcc:tools:seedSurveyInfo');
|
||||
const { MONGOHQ_URL } = process.env;
|
||||
|
||||
function handleError(err, client) {
|
||||
if (err) {
|
||||
console.error('Oh noes!! Error seeding survey info.');
|
||||
console.error(err);
|
||||
try {
|
||||
client.close();
|
||||
} catch (e) {
|
||||
// no-op
|
||||
} finally {
|
||||
/* eslint-disable-next-line no-process-exit */
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const surveyIds = [
|
||||
new ObjectId('651c5a2a5f9b639b584028bc'),
|
||||
new ObjectId('651c5a4c5f9b639b584028bd')
|
||||
];
|
||||
|
||||
const defaultUserSurvey = {
|
||||
_id: surveyIds[0],
|
||||
title: 'Foundational C# with Microsoft Survey',
|
||||
responses: [
|
||||
{
|
||||
question: 'Please describe your role:',
|
||||
response: 'Beginner developer (less than 2 years experience)'
|
||||
},
|
||||
{
|
||||
question:
|
||||
'Prior to this course, how experienced were you with .NET and C#?',
|
||||
response: 'Novice (no prior experience)'
|
||||
}
|
||||
],
|
||||
userId: new ObjectId('5bd30e0f1caf6ac3ddddddb5')
|
||||
};
|
||||
|
||||
const certifiedUserSurvey = {
|
||||
_id: surveyIds[1],
|
||||
title: 'Foundational C# with Microsoft Survey',
|
||||
responses: [
|
||||
{
|
||||
question: 'Please describe your role:',
|
||||
response: 'Experienced developer (more than 5 years experience)'
|
||||
},
|
||||
{
|
||||
question:
|
||||
'Prior to this course, how experienced were you with .NET and C#?',
|
||||
response: 'Expert'
|
||||
}
|
||||
],
|
||||
userId: new ObjectId('5fa2db00a25c1c1fa49ce067')
|
||||
};
|
||||
|
||||
const client = new MongoClient(MONGOHQ_URL, { useNewUrlParser: true });
|
||||
|
||||
log('Connected successfully to mongo');
|
||||
|
||||
const db = client.db('freecodecamp');
|
||||
const survey = db.collection('Survey');
|
||||
|
||||
const run = async () => {
|
||||
await survey.deleteMany({
|
||||
_id: {
|
||||
$in: surveyIds
|
||||
}
|
||||
});
|
||||
log('Survey info deleted');
|
||||
|
||||
if (!args.includes('delete-only')) {
|
||||
await survey.insertOne(defaultUserSurvey);
|
||||
await survey.insertOne(certifiedUserSurvey);
|
||||
log('Survey info seeded');
|
||||
}
|
||||
};
|
||||
|
||||
run()
|
||||
.then(() => client.close())
|
||||
.catch(err => handleError(err, client));
|
||||
Reference in New Issue
Block a user