From d361e128c08067b0156bfe8f888b0f8b4467868a Mon Sep 17 00:00:00 2001 From: Shaun Hamilton Date: Wed, 28 Feb 2024 16:05:23 +0200 Subject: [PATCH] breaking(curriculum): add scientific-computing to /learn (#53143) (#53146) Co-authored-by: Oliver Eyton-Williams Co-authored-by: Dario-DC <105294544+Dario-DC@users.noreply.github.com> Co-authored-by: Zaira <33151350+zairahira@users.noreply.github.com> --- api-server/src/server/boot/challenge.js | 27 +- client/src/redux/save-challenge-saga.js | 5 +- .../Challenges/classic/desktop-layout.tsx | 3 +- .../templates/Challenges/classic/editor.tsx | 13 +- .../src/templates/Challenges/classic/show.tsx | 4 +- .../Challenges/components/tool-panel.tsx | 45 +- .../Challenges/redux/completion-epic.js | 3 +- .../redux/execute-challenge-saga.js | 16 +- .../templates/Challenges/redux/selectors.js | 1 + .../src/templates/Challenges/utils/build.ts | 12 +- .../Introduction/components/block.tsx | 1 + .../__fixtures__/completed-challenges.ts | 8 +- client/src/utils/curriculum-layout.ts | 12 +- .../src/utils/solution-display-type.test.ts | 6 +- client/src/utils/solution-display-type.ts | 3 +- client/utils/gatsby/challenge-page-creator.js | 1 + .../arithmetic-formatter.md | 205 ++++- .../budget-app.md | 846 +++++++++++++++++- .../polygon-area-calculator.md | 828 ++++++++++++++++- .../probability-calculator.md | 234 ++++- .../time-calculator.md | 557 +++++++++++- .../challenge-schema.test.js.snap | 2 +- curriculum/schema/challenge-schema.js | 2 +- curriculum/test/test-challenges.js | 4 +- shared/config/challenge-types.ts | 10 +- .../browser-scripts/python-worker.ts | 1 + 26 files changed, 2633 insertions(+), 216 deletions(-) diff --git a/api-server/src/server/boot/challenge.js b/api-server/src/server/boot/challenge.js index 1fd0d5e94d8..e4f2c062d01 100644 --- a/api-server/src/server/boot/challenge.js +++ b/api-server/src/server/boot/challenge.js @@ -120,11 +120,25 @@ const jsCertProjectIds = [ ]; const multifileCertProjectIds = getChallenges() - .filter(challenge => challenge.challengeType === 14) + .filter( + challenge => challenge.challengeType === challengeTypes.multifileCertProject + ) + .map(challenge => challenge.id); + +const multifilePythonCertProjectIds = getChallenges() + .filter( + challenge => + challenge.challengeType === challengeTypes.multifilePythonCertProject + ) .map(challenge => challenge.id); const savableChallenges = getChallenges() - .filter(challenge => challenge.challengeType === 14) + .filter(challenge => { + return ( + challenge.challengeType === challengeTypes.multifileCertProject || + challenge.challengeType === challengeTypes.multifilePythonCertProject + ); + }) .map(challenge => challenge.id); const msTrophyChallenges = getChallenges() @@ -141,7 +155,8 @@ export function buildUserUpdate( let completedChallenge = {}; if ( jsCertProjectIds.includes(challengeId) || - multifileCertProjectIds.includes(challengeId) + multifileCertProjectIds.includes(challengeId) || + multifilePythonCertProjectIds.includes(challengeId) ) { completedChallenge = { ..._completedChallenge, @@ -397,7 +412,11 @@ export async function modernChallengeCompleted(req, res, next) { // We only need to know the challenge type if it's a project. If it's a // step or normal challenge we can avoid storing in the database. - if (jsCertProjectIds.includes(id) || multifileCertProjectIds.includes(id)) { + if ( + jsCertProjectIds.includes(id) || + multifileCertProjectIds.includes(id) || + multifilePythonCertProjectIds.includes(id) + ) { completedChallenge.challengeType = challengeType; } diff --git a/client/src/redux/save-challenge-saga.js b/client/src/redux/save-challenge-saga.js index f2208ac5b53..d412119e35a 100644 --- a/client/src/redux/save-challenge-saga.js +++ b/client/src/redux/save-challenge-saga.js @@ -34,7 +34,10 @@ function* saveChallengeSaga() { } // only allow saving of multifileCertProject's - if (challengeType === challengeTypes.multifileCertProject) { + if ( + challengeType === challengeTypes.multifileCertProject || + challengeType === challengeTypes.multifilePythonCertProject + ) { const body = standardizeRequestBody({ id, challengeFiles, challengeType }); const bodySizeInBytes = getStringSizeInBytes(body); diff --git a/client/src/templates/Challenges/classic/desktop-layout.tsx b/client/src/templates/Challenges/classic/desktop-layout.tsx index 82ddedc9485..87791babe4b 100644 --- a/client/src/templates/Challenges/classic/desktop-layout.tsx +++ b/client/src/templates/Challenges/classic/desktop-layout.tsx @@ -169,7 +169,8 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => { const challengeFile = getChallengeFile(); const projectBasedChallenge = hasEditableBoundaries; const isMultifileCertProject = - challengeType === challengeTypes.multifileCertProject; + challengeType === challengeTypes.multifileCertProject || + challengeType === challengeTypes.multifilePythonCertProject; const displayPreviewPane = hasPreview && showPreviewPane; const displayPreviewPortal = hasPreview && showPreviewPortal; const displayNotes = projectBasedChallenge ? showNotes && hasNotes : false; diff --git a/client/src/templates/Challenges/classic/editor.tsx b/client/src/templates/Challenges/classic/editor.tsx index 5c8c887addb..5112d857feb 100644 --- a/client/src/templates/Challenges/classic/editor.tsx +++ b/client/src/templates/Challenges/classic/editor.tsx @@ -283,7 +283,9 @@ const Editor = (props: EditorProps): JSX.Element => { selectionHighlight: false, overviewRulerBorder: false, hideCursorInOverviewRuler: true, - renderIndentGuides: props.challengeType === challengeTypes.python, + renderIndentGuides: + props.challengeType === challengeTypes.python || + props.challengeType === challengeTypes.multifilePythonCertProject, minimap: { enabled: false }, @@ -302,7 +304,11 @@ const Editor = (props: EditorProps): JSX.Element => { parameterHints: { enabled: false }, - tabSize: props.challengeType !== challengeTypes.python ? 2 : 4, + tabSize: + props.challengeType !== challengeTypes.python && + props.challengeType !== challengeTypes.multifilePythonCertProject + ? 2 + : 4, dragAndDrop: true, lightbulb: { enabled: false @@ -538,7 +544,8 @@ const Editor = (props: EditorProps): JSX.Element => { monaco.KeyMod.WinCtrl | monaco.KeyCode.KEY_S ], run: - props.challengeType === challengeTypes.multifileCertProject && + (props.challengeType === challengeTypes.multifileCertProject || + props.challengeType === challengeTypes.multifilePythonCertProject) && props.isSignedIn ? // save to database props.saveChallenge diff --git a/client/src/templates/Challenges/classic/show.tsx b/client/src/templates/Challenges/classic/show.tsx index 1c9776867d2..10aa6648de3 100644 --- a/client/src/templates/Challenges/classic/show.tsx +++ b/client/src/templates/Challenges/classic/show.tsx @@ -158,7 +158,8 @@ const StepPreview = ({ challengeType: number; xtermFitRef: React.MutableRefObject; }) => { - return challengeType === challengeTypes.python ? ( + return challengeType === challengeTypes.python || + challengeType === challengeTypes.multifilePythonCertProject ? ( ) : ( { diff --git a/client/src/templates/Challenges/components/tool-panel.tsx b/client/src/templates/Challenges/components/tool-panel.tsx index 9dbba47780f..83e10ed724a 100644 --- a/client/src/templates/Challenges/components/tool-panel.tsx +++ b/client/src/templates/Challenges/components/tool-panel.tsx @@ -77,27 +77,30 @@ function ToolPanel({ - {isSignedIn && challengeType === challengeTypes.multifileCertProject && ( - <> - - - - )} - {challengeType !== challengeTypes.multifileCertProject && ( - <> - - - - )} + {isSignedIn && + (challengeType === challengeTypes.multifileCertProject || + challengeType === challengeTypes.multifilePythonCertProject) && ( + <> + + + + )} + {challengeType !== challengeTypes.multifileCertProject && + challengeType !== challengeTypes.multifilePythonCertProject && ( + <> + + + + )} { challengeType === challengeTypes.html || challengeType === challengeTypes.modern || challengeType === challengeTypes.multifileCertProject || + challengeType === challengeTypes.multifilePythonCertProject || challengeType === challengeTypes.python ) { const { required = [], template = '' } = challengeMetaSelector(state); diff --git a/client/src/templates/Challenges/utils/build.ts b/client/src/templates/Challenges/utils/build.ts index 3fed2f24ad7..2a87dbcd8f4 100644 --- a/client/src/templates/Challenges/utils/build.ts +++ b/client/src/templates/Challenges/utils/build.ts @@ -123,7 +123,8 @@ const buildFunctions = { [challengeTypes.pythonProject]: buildBackendChallenge, [challengeTypes.multifileCertProject]: buildDOMChallenge, [challengeTypes.colab]: buildBackendChallenge, - [challengeTypes.python]: buildPythonChallenge + [challengeTypes.python]: buildPythonChallenge, + [challengeTypes.multifilePythonCertProject]: buildPythonChallenge }; export function canBuildChallenge(challengeData: BuildChallengeData): boolean { @@ -151,7 +152,8 @@ const testRunners = { [challengeTypes.backend]: getDOMTestRunner, [challengeTypes.pythonProject]: getDOMTestRunner, [challengeTypes.python]: getPyTestRunner, - [challengeTypes.multifileCertProject]: getDOMTestRunner + [challengeTypes.multifileCertProject]: getDOMTestRunner, + [challengeTypes.multifilePythonCertProject]: getPyTestRunner }; // TODO: Figure out and (hopefully) simplify the return type. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -322,7 +324,10 @@ export function buildPythonChallenge({ .then(checkFilesErrors) // Unlike the DOM challenges, there's no need to embed the files in HTML .then(challengeFiles => ({ - challengeType: challengeTypes.python, + challengeType: + challengeFiles[0].editableRegionBoundaries?.length === 0 + ? challengeTypes.multifilePythonCertProject + : challengeTypes.python, sources: buildSourceMap(challengeFiles) })) ); @@ -393,6 +398,7 @@ export function challengeHasPreview({ challengeType }: ChallengeMeta): boolean { challengeType === challengeTypes.html || challengeType === challengeTypes.modern || challengeType === challengeTypes.multifileCertProject || + challengeType === challengeTypes.multifilePythonCertProject || challengeType === challengeTypes.python ); } diff --git a/client/src/templates/Introduction/components/block.tsx b/client/src/templates/Introduction/components/block.tsx index 6da94eb13d9..aa351a9b752 100644 --- a/client/src/templates/Introduction/components/block.tsx +++ b/client/src/templates/Introduction/components/block.tsx @@ -21,6 +21,7 @@ import { playTone } from '../../../utils/tone'; import { makeExpandedBlockSelector, toggleBlock } from '../redux'; import { isGridBased, isProjectBased } from '../../../utils/curriculum-layout'; import Challenges from './challenges'; + import '../intro.css'; const { curriculumLocale, showUpcomingChanges, showNewCurriculum } = envData; diff --git a/client/src/utils/__fixtures__/completed-challenges.ts b/client/src/utils/__fixtures__/completed-challenges.ts index b0e8f38f122..b76a0b1acf6 100644 --- a/client/src/utils/__fixtures__/completed-challenges.ts +++ b/client/src/utils/__fixtures__/completed-challenges.ts @@ -1,4 +1,5 @@ import { challengeFiles } from '../../../utils/__fixtures__/challenges'; +import { challengeTypes } from '../../../../shared/config/challenge-types'; const baseChallenge = { id: '1', @@ -29,7 +30,12 @@ export const withChallenges = { export const multifileSolution = { ...withChallenges, - challengeType: 14 + challengeType: challengeTypes.multifileCertProject +} + +export const multifilePythonSolution = { + ...withChallenges, + challengeType: challengeTypes.multifilePythonCertProject } export const onlyGithubLink = { diff --git a/client/src/utils/curriculum-layout.ts b/client/src/utils/curriculum-layout.ts index 4e9bf593d65..4f9ec8394ff 100644 --- a/client/src/utils/curriculum-layout.ts +++ b/client/src/utils/curriculum-layout.ts @@ -16,10 +16,13 @@ export const isGridBased = ( ) => { // Python projects should not be displayed as a grid, but should be displayed // as a list of projects. Otherwise, if we do not do this the project will be - // shown as a single certificaton project, which is not what we want. - - if (challengeType === 10) return false; + // shown as a single certification project. + if ( + challengeType === challengeTypes.pythonProject || + challengeType === challengeTypes.multifilePythonCertProject + ) + return false; return gridBasedSuperBlocks.includes(superBlock); }; @@ -33,7 +36,8 @@ const projectBasedChallengeTypes = [ challengeTypes.codeAllyCert, challengeTypes.multifileCertProject, challengeTypes.exam, - challengeTypes.codeAllyPractice + challengeTypes.codeAllyPractice, + challengeTypes.multifilePythonCertProject ]; export const isProjectBased = ( diff --git a/client/src/utils/solution-display-type.test.ts b/client/src/utils/solution-display-type.test.ts index d38ed525540..aeb541fb167 100644 --- a/client/src/utils/solution-display-type.test.ts +++ b/client/src/utils/solution-display-type.test.ts @@ -2,6 +2,7 @@ import { bothLinks, invalidGithubLink, legacySolution, + multifilePythonSolution, multifileSolution, onlyGithubLink, onlySolution, @@ -17,14 +18,15 @@ describe('getSolutionDisplayType', () => { expect(getSolutionDisplayType(legacySolution)).toBe('showUserCode'); }); it('should handle solutions with files', () => { - expect.assertions(2); expect(getSolutionDisplayType(withChallenges)).toBe('showUserCode'); expect(getSolutionDisplayType(multifileSolution)).toBe( 'showMultifileProjectSolution' ); + expect(getSolutionDisplayType(multifilePythonSolution)).toBe( + 'showMultifileProjectSolution' + ); }); it('should handle solutions with a single valid url', () => { - expect.assertions(2); expect(getSolutionDisplayType(onlySolution)).toBe('showProjectLink'); expect(getSolutionDisplayType(invalidGithubLink)).toBe('showProjectLink'); }); diff --git a/client/src/utils/solution-display-type.ts b/client/src/utils/solution-display-type.ts index bb2f604ee4e..b28c7b9780e 100644 --- a/client/src/utils/solution-display-type.ts +++ b/client/src/utils/solution-display-type.ts @@ -19,7 +19,8 @@ export const getSolutionDisplayType = ({ }: CompletedChallenge): DisplayType => { if (examResults) return 'showExamResults'; if (challengeFiles?.length) - return challengeType === challengeTypes.multifileCertProject + return challengeType === challengeTypes.multifileCertProject || + challengeType === challengeTypes.multifilePythonCertProject ? 'showMultifileProjectSolution' : 'showUserCode'; if (!solution) return 'none'; diff --git a/client/utils/gatsby/challenge-page-creator.js b/client/utils/gatsby/challenge-page-creator.js index d88e283ef5a..8de7b0711f4 100644 --- a/client/utils/gatsby/challenge-page-creator.js +++ b/client/utils/gatsby/challenge-page-creator.js @@ -170,6 +170,7 @@ function getProjectPreviewConfig(challenge, allChallengeEdges) { challengeOrder === 0 && usesMultifileEditor && challengeType !== challengeTypes.multifileCertProject && + challengeType !== challengeTypes.multifilePythonCertProject && // TODO: revert this to enable project previews for python challenges challengeType !== challengeTypes.python, challengeData: { diff --git a/curriculum/challenges/english/07-scientific-computing-with-python/scientific-computing-with-python-projects/arithmetic-formatter.md b/curriculum/challenges/english/07-scientific-computing-with-python/scientific-computing-with-python-projects/arithmetic-formatter.md index 232fdf3cc95..3d083c39c64 100644 --- a/curriculum/challenges/english/07-scientific-computing-with-python/scientific-computing-with-python-projects/arithmetic-formatter.md +++ b/curriculum/challenges/english/07-scientific-computing-with-python/scientific-computing-with-python-projects/arithmetic-formatter.md @@ -1,22 +1,13 @@ --- id: 5e44412c903586ffb414c94c title: Arithmetic Formatter -challengeType: 10 +challengeType: 23 forumTopicId: 462359 dashedName: arithmetic-formatter --- # --description-- -You will be working on this project with our Replit starter code. - -- Start by importing the project on Replit. -- Next, you will see a `.replit` window. -- Select `Use run command` and click the `Done` button. - - -# --instructions-- - Students in primary school often arrange arithmetic problems vertically to make them easier to solve. For example, "235 + 52" becomes: ```py @@ -25,7 +16,7 @@ Students in primary school often arrange arithmetic problems vertically to make ----- ``` -Create a function that receives a list of strings that are arithmetic problems and returns the problems arranged vertically and side-by-side. The function should optionally take a second argument. When the second argument is set to `True`, the answers should be displayed. +Finish the `arithmetic_arranger` function that receives a list of strings which are arithmetic problems, and returns the problems arranged vertically and side-by-side. The function should optionally take a second argument. When the second argument is set to `True`, the answers should be displayed. ## Example @@ -62,7 +53,6 @@ Output: The function will return the correct conversion if the supplied problems are properly formatted, otherwise, it will **return** a **string** that describes an error that is meaningful to the user. - - Situations that will return an error: - If there are **too many problems** supplied to the function. The limit is **five**, anything more will return: `Error: Too many problems.` @@ -78,32 +68,185 @@ The function will return the correct conversion if the supplied problems are pro - There should be four spaces between each problem. - There should be dashes at the bottom of each problem. The dashes should run along the entire length of each problem individually. (The example above shows what this should look like.) -## Development - -Write your code in `arithmetic_arranger.py`. For development, you can use `main.py` to test your `arithmetic_arranger()` function. Click the "run" button and `main.py` will run. - -## Testing - -The unit tests for this project are in `test_module.py`. We are running the tests from `test_module.py` in `main.py` for your convenience. The tests will run automatically whenever you hit the "run" button. Alternatively you may run the tests by inputting `pytest` in the console. - -## Submitting - -Copy your project's URL and submit it to freeCodeCamp. - # --hints-- -It should correctly format an arithmetic problem and pass all tests. +`arithmetic_arranger(["3801 - 2", "123 + 49"])` should return ` 3801 123\n- 2 + 49\n------ -----`. ```js +({ + test: () => { + runPython(` +from unittest import TestCase +TestCase().assertEqual(arithmetic_arranger(["3801 - 2", "123 + 49"]), ' 3801 123\\n- 2 + 49\\n------ -----')`); + } +}) +``` + +`arithmetic_arranger(["1 + 2", "1 - 9380"])` should return ` 1 1\n+ 2 - 9380\n--- ------`. + +```js +({ + test: () => { + runPython(` +from unittest import TestCase + +TestCase().assertEqual(arithmetic_arranger(["1 + 2", "1 - 9380"]), ' 1 1\\n+ 2 - 9380\\n--- ------')`); + } +}) +``` + +`arithmetic_arranger(["3 + 855", "3801 - 2", "45 + 43", "123 + 49"])` should return ` 3 3801 45 123\n+ 855 - 2 + 43 + 49\n----- ------ ---- -----`. + +```js +({ + test: () => { + runPython(` +from unittest import TestCase + +TestCase().assertEqual(arithmetic_arranger(["3 + 855", "3801 - 2", "45 + 43", "123 + 49"]), ' 3 3801 45 123\\n+ 855 - 2 + 43 + 49\\n----- ------ ---- -----')`); + } +}) +``` + +`arithmetic_arranger(["11 + 4", "3801 - 2999", "1 + 2", "123 + 49", "1 - 9380"])` should return ` 11 3801 1 123 1\n+ 4 - 2999 + 2 + 49 - 9380\n---- ------ --- ----- ------`. + +```js +({ + test: () => { + runPython(` +from unittest import TestCase + +TestCase().assertEqual(arithmetic_arranger(["11 + 4", "3801 - 2999", "1 + 2", "123 + 49", "1 - 9380"]), ' 11 3801 1 123 1\\n+ 4 - 2999 + 2 + 49 - 9380\\n---- ------ --- ----- ------')`); + } +}) +``` + +`arithmetic_arranger(["44 + 815", "909 - 2", "45 + 43", "123 + 49", "888 + 40", "653 + 87"])` should return `Error: Too many problems.`. + +```js +({ + test: () => { + runPython(` +from unittest import TestCase + +TestCase().assertEqual(arithmetic_arranger(["44 + 815", "909 - 2", "45 + 43", "123 + 49", "888 + 40", "653 + 87"]), 'Error: Too many problems.')`); + } +}) +``` + +`arithmetic_arranger(["3 / 855", "3801 - 2", "45 + 43", "123 + 49"])` should return `Error: Operator must be '+' or '-'.`. + +```js +({ + test: () => { + runPython(` +from unittest import TestCase + +TestCase().assertEqual(arithmetic_arranger(["3 / 855", "3801 - 2", "45 + 43", "123 + 49"]), "Error: Operator must be '+' or '-'.")`); + } +}) +``` + +`arithmetic_arranger(["24 + 85215", "3801 - 2", "45 + 43", "123 + 49"])` should return `Error: Numbers cannot be more than four digits.`. + +```js +({ + test: () => { + runPython(` +from unittest import TestCase + +TestCase().assertEqual(arithmetic_arranger(["24 + 85215", "3801 - 2", "45 + 43", "123 + 49"]), "Error: Numbers cannot be more than four digits.")`); + } +}) +``` + +`arithmetic_arranger(["98 + 3g5", "3801 - 2", "45 + 43", "123 + 49"])` should return `Error: Numbers must only contain digits.`. + +```js +({ + test: () => { + runPython(` +from unittest import TestCase + +TestCase().assertEqual(arithmetic_arranger(["98 + 3g5", "3801 - 2", "45 + 43", "123 + 49"]), "Error: Numbers must only contain digits.")`); + } +}) +``` + +`arithmetic_arranger(["3 + 855", "988 + 40"], True)` should return ` 3 988\n+ 855 + 40\n----- -----\n 858 1028`. + +```js +({ + test: () => { + runPython(` +from unittest import TestCase + +TestCase().assertEqual(arithmetic_arranger(["3 + 855", "988 + 40"], True), " 3 988\\n+ 855 + 40\\n----- -----\\n 858 1028")`); + } +}) +``` + +`arithmetic_arranger(["32 - 698", "1 - 3801", "45 + 43", "123 + 49", "988 + 40"], True)` should return ` 32 1 45 123 988\n- 698 - 3801 + 43 + 49 + 40\n----- ------ ---- ----- -----\n -666 -3800 88 172 1028`. + +```js +({ + test: () => { + runPython(` +from unittest import TestCase + +TestCase().assertEqual(arithmetic_arranger(["32 - 698", "1 - 3801", "45 + 43", "123 + 49", "988 + 40"], True), " 32 1 45 123 988\\n- 698 - 3801 + 43 + 49 + 40\\n----- ------ ---- ----- -----\\n -666 -3800 88 172 1028")`); + } +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +def arithmetic_arranger(problems, show_answers=False): + + return problems + +print(f'\n{arithmetic_arranger(["32 + 698", "3801 - 2", "45 + 43", "123 + 49"])}') ``` # --solutions-- -```js -/** - Backend challenges don't need solutions, - because they would need to be tested against a full working project. - Please check our contributing guidelines to learn more. -*/ +```py +def arithmetic_arranger(problems, result=False): + + lin1 = "" + lin2 = "" + lin3 = "" + lin4 = "" + + if len(problems) > 5: + return 'Error: Too many problems.' + for problem in problems: + [num1, sym, num2] = problem.split() + sign = ['+', '-'] + if sym not in sign: + return ("Error: Operator must be '+' or '-'.") + if len(num1) > 4 or len(num2) > 4: + return ("Error: Numbers cannot be more than four digits.") + if not num1.isnumeric() or not num2.isnumeric(): + return ("Error: Numbers must only contain digits.") + + lin1 += " " + num1 + " " if len(num1) >= len( + num2) else " " * (len(num2) + 2 - len(num1)) + num1 + " " + lin2 += sym + " " + num2 + " " if len(num2) >= len( + num1) else sym + " " * (len(num1) - len(num2) + 1) + num2 + " " + nmax = (len(num1) + 2) if len(num1) >= len(num2) else (len(num2) + 2) + lin3 += "-" * nmax + " " + op = int(num1) + int(num2) if sym == "+" else int(num1) - int(num2) + lin4 += (" " * (nmax - len(str(op)))) + str(op) + " " + + arranged_problems = lin1.rstrip() + "\n" + lin2.rstrip( + ) + "\n" + lin3.rstrip() + if result: + arranged_problems += "\n" + lin4.rstrip() + + return arranged_problems ``` diff --git a/curriculum/challenges/english/07-scientific-computing-with-python/scientific-computing-with-python-projects/budget-app.md b/curriculum/challenges/english/07-scientific-computing-with-python/scientific-computing-with-python-projects/budget-app.md index 941e681c22c..70886292cec 100644 --- a/curriculum/challenges/english/07-scientific-computing-with-python/scientific-computing-with-python-projects/budget-app.md +++ b/curriculum/challenges/english/07-scientific-computing-with-python/scientific-computing-with-python-projects/budget-app.md @@ -1,23 +1,14 @@ --- id: 5e44413e903586ffb414c94e title: Budget App -challengeType: 10 +challengeType: 23 forumTopicId: 462361 dashedName: budget-app --- # --description-- -You will be working on this project with our Replit starter code. - -- Start by importing the project on Replit. -- Next, you will see a `.replit` window. -- Select `Use run command` and click the `Done` button. - - -# --instructions-- - -Complete the `Category` class in `budget.py`. It should be able to instantiate objects based on different budget categories like *food*, *clothing*, and *entertainment*. When objects are created, they are passed in the name of the category. The class should have an instance variable called `ledger` that is a list. The class should also contain the following methods: +Complete the `Category` class. It should be able to instantiate objects based on different budget categories like *food*, *clothing*, and *entertainment*. When objects are created, they are passed in the name of the category. The class should have an instance variable called `ledger` that is a list. The class should also contain the following methods: - A `deposit` method that accepts an amount and description. If no description is given, it should default to an empty string. The method should append an object to the ledger list in the form of `{"amount": amount, "description": description}`. - A `withdraw` method that is similar to the `deposit` method, but the amount passed in should be stored in the ledger as a negative number. If there are not enough funds, nothing should be added to the ledger. This method should return `True` if the withdrawal took place, and `False` otherwise. @@ -31,7 +22,19 @@ When the budget object is printed it should display: - A list of the items in the ledger. Each line should show the description and amount. The first 23 characters of the description should be displayed, then the amount. The amount should be right aligned, contain two decimal places, and display a maximum of 7 characters. - A line displaying the category total. -Here is an example of the output: +Here is an example usage: + +```py +food = Category("Food") +food.deposit(1000, "deposit") +food.withdraw(10.15, "groceries") +food.withdraw(15.89, "restaurant and more food for dessert") +clothing = Category("Clothing") +food.transfer(50, clothing) +print(food) +``` + +And here is an example of the output: ```bash *************Food************* @@ -74,34 +77,811 @@ Percentage spent by category g ``` -The unit tests for this project are in `test_module.py`. - -## Development - -Write your code in `budget.py`. For development, you can use `main.py` to test your `Category` class. Click the "run" button and `main.py` will run. - -## Testing - -We imported the tests from `test_module.py` to `main.py` for your convenience. The tests will run automatically whenever you hit the "run" button. - -## Submitting - -Copy your project's URL and submit it to freeCodeCamp. - # --hints-- -It should create a Category class and pass all tests. +The `deposit` method should create a specific object in the ledger instance variable. ```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/budget.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import budget +from importlib import reload +reload(budget) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.food = budget.Category("Food") + + def test_deposit(self): + self.food.deposit(900, "deposit") + actual = self.food.ledger[0] + expected = {"amount": 900, "description": "deposit"} + self.assertEqual(actual, expected, 'Expected "deposit" method to create a specific object in the ledger instance variable.') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +Calling the `deposit` method with no description should create a blank description. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/budget.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import budget +from importlib import reload + +reload(budget) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.food = budget.Category("Food") + + def test_deposit_no_description(self): + self.food.deposit(45.56) + actual = self.food.ledger[0] + expected = {"amount": 45.56, "description": ""} + self.assertEqual(actual, expected, 'Expected calling "deposit" method with no description to create a blank description.') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +The `withdraw` method should create a specific object in the `ledger` instance variable. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/budget.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import budget +from importlib import reload + +reload(budget) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.food = budget.Category("Food") + + def test_withdraw(self): + self.food.deposit(900, "deposit") + self.food.withdraw(45.67, "milk, cereal, eggs, bacon, bread") + actual = self.food.ledger[1] + expected = {"amount": -45.67, "description": "milk, cereal, eggs, bacon, bread"} + self.assertEqual(actual, expected, 'Expected "withdraw" method to create a specific object in the ledger instance variable.') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +Calling the `withdraw` method with no description should create a blank description. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/budget.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import budget +from importlib import reload + +reload(budget) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.food = budget.Category("Food") + + def test_withdraw_no_description(self): + self.food.deposit(900, "deposit") + good_withdraw = self.food.withdraw(45.67) + actual = self.food.ledger[1] + expected = {"amount": -45.67, "description": ""} + self.assertEqual(actual, expected, 'Expected "withdraw" method with no description to create a blank description.') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +The `withdraw` method should return `True` if the withdrawal took place. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/budget.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import budget +from importlib import reload + +reload(budget) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.food = budget.Category("Food") + + def test_withdraw_no_description(self): + self.food.deposit(900, "deposit") + good_withdraw = self.food.withdraw(45.67) + self.assertEqual(good_withdraw, True, 'Expected "withdraw" method to return "True".') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +Calling `food.deposit(900, "deposit")` and `food.withdraw(45.67, "milk, cereal, eggs, bacon, bread")` should return a balance of `854.33`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/budget.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import budget +from importlib import reload + +reload(budget) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.food = budget.Category("Food") + + def test_get_balance(self): + self.food.deposit(900, "deposit") + self.food.withdraw(45.67, "milk, cereal, eggs, bacon, bread") + actual = self.food.get_balance() + expected = 854.33 + self.assertEqual(actual, expected, 'Expected balance to be 854.33') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +Calling the `transfer` method on a category object should create a specific ledger item in that category object. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/budget.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import budget +from importlib import reload + +reload(budget) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.food = budget.Category("Food") + self.entertainment = budget.Category("Entertainment") + + def test_transfer(self): + self.food.deposit(900, "deposit") + self.food.withdraw(45.67, "milk, cereal, eggs, bacon, bread") + transfer_amount = 20 + good_transfer = self.food.transfer(transfer_amount, self.entertainment) + actual = self.food.ledger[2] + expected = {"amount": -transfer_amount, "description": "Transfer to Entertainment"} + self.assertEqual(actual, expected, 'Expected "transfer" method to create a specific ledger item in food object.') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +The `transfer` method should return `True` if the transfer took place. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/budget.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import budget +from importlib import reload + +reload(budget) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.food = budget.Category("Food") + self.entertainment = budget.Category("Entertainment") + + def test_transfer(self): + self.food.deposit(900, "deposit") + self.food.withdraw(45.67, "milk, cereal, eggs, bacon, bread") + transfer_amount = 20 + good_transfer = self.food.transfer(transfer_amount, self.entertainment) + self.assertEqual(good_transfer, True, 'Expected "transfer" method to return "True".') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +Calling `transfer` on a category object should reduce the balance in the category object. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/budget.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import budget +from importlib import reload + +reload(budget) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.food = budget.Category("Food") + self.entertainment = budget.Category("Entertainment") + + def test_transfer(self): + self.food.deposit(900, "deposit") + self.food.withdraw(45.67, "milk, cereal, eggs, bacon, bread") + transfer_amount = 20 + food_balance_before = self.food.get_balance() + good_transfer = self.food.transfer(transfer_amount, self.entertainment) + food_balance_after = self.food.get_balance() + self.assertEqual(food_balance_before - food_balance_after, transfer_amount, 'Expected "transfer" method to reduce balance in food object.') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +The `transfer` method should increase the balance of the category object passed as its argument. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/budget.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import budget +from importlib import reload + +reload(budget) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.food = budget.Category("Food") + self.entertainment = budget.Category("Entertainment") + + def test_transfer(self): + self.food.deposit(900, "deposit") + self.food.withdraw(45.67, "milk, cereal, eggs, bacon, bread") + transfer_amount = 20 + entertainment_balance_before = self.entertainment.get_balance() + good_transfer = self.food.transfer(transfer_amount, self.entertainment) + entertainment_balance_after = self.entertainment.get_balance() + self.assertEqual(entertainment_balance_after - entertainment_balance_before, transfer_amount, 'Expected "transfer" method to increase balance in entertainment object.') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +The `transfer` method should create a specific ledger item in the category object passed as its argument. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/budget.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import budget +from importlib import reload + +reload(budget) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.food = budget.Category("Food") + self.entertainment = budget.Category("Entertainment") + + def test_transfer(self): + self.food.deposit(900, "deposit") + self.food.withdraw(45.67, "milk, cereal, eggs, bacon, bread") + transfer_amount = 20 + good_transfer = self.food.transfer(transfer_amount, self.entertainment) + actual = self.entertainment.ledger[0] + expected = {"amount": transfer_amount, "description": "Transfer from Food"} + self.assertEqual(actual, expected, 'Expected "transfer" method to create a specific ledger item in entertainment object.') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +The `check_funds` method should return `False` if the amount passed to the method is greater than the category balance. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/budget.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import budget +from importlib import reload + +reload(budget) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.food = budget.Category("Food") + + def test_check_funds(self): + self.food.deposit(10, "deposit") + actual = self.food.check_funds(20) + expected = False + self.assertEqual(actual, expected, 'Expected "check_funds" method to be False') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +The `check_funds` method should return `True` if the amount passed to the method is not greater than the category balance. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/budget.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import budget +from importlib import reload + +reload(budget) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.food = budget.Category("Food") + + def test_check_funds(self): + self.food.deposit(10, "deposit") + actual = self.food.check_funds(10) + expected = True + self.assertEqual(actual, expected, 'Expected "check_funds" method to be True') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +The `withdraw` method should return `False` if the withdrawal didn't take place. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/budget.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import budget +from importlib import reload + +reload(budget) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.food = budget.Category("Food") + + def test_withdraw_no_funds(self): + self.food.deposit(100, "deposit") + good_withdraw = self.food.withdraw(100.10) + self.assertEqual(good_withdraw, False, 'Expected "withdraw" method to return "False".') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +The `transfer` method should return `False` if the transfer didn't take place. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/budget.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import budget +from importlib import reload + +reload(budget) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.food = budget.Category("Food") + self.entertainment = budget.Category("Entertainment") + + def test_transfer_no_funds(self): + self.food.deposit(100, "deposit") + good_transfer = self.food.transfer(200, self.entertainment) + self.assertEqual(good_transfer, False, 'Expected "transfer" method to return "False".') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +Printing a `Category` instance should give a different string representation of the object. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/budget.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import budget +from importlib import reload + +reload(budget) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.food = budget.Category("Food") + self.entertainment = budget.Category("Entertainment") + + def test_to_string(self): + self.food.deposit(900, "deposit") + self.food.withdraw(45.67, "milk, cereal, eggs, bacon, bread") + self.food.transfer(20, self.entertainment) + actual = str(self.food) + expected = "*************Food*************\\ndeposit 900.00\\nmilk, cereal, eggs, bac -45.67\\nTransfer to Entertainme -20.00\\nTotal: 834.33" + self.assertEqual(actual, expected, 'Expected different string representation of object.') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +` + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +`create_spend_chart` should print a different chart representation. Check that all spacing is exact. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/budget.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import budget +from importlib import reload + +reload(budget) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.food = budget.Category("Food") + self.entertainment = budget.Category("Entertainment") + self.business = budget.Category("Business") + + def test_create_spend_chart(self): + self.food.deposit(900, "deposit") + self.entertainment.deposit(900, "deposit") + self.business.deposit(900, "deposit") + self.food.withdraw(105.55) + self.entertainment.withdraw(33.40) + self.business.withdraw(10.99) + actual = budget.create_spend_chart([self.business, self.food, self.entertainment]) + expected = "Percentage spent by category\\n100| \\n 90| \\n 80| \\n 70| o \\n 60| o \\n 50| o \\n 40| o \\n 30| o \\n 20| o o \\n 10| o o \\n 0| o o o \\n ----------\\n B F E \\n u o n \\n s o t \\n i d e \\n n r \\n e t \\n s a \\n s i \\n n \\n m \\n e \\n n \\n t " + self.assertEqual(actual, expected, 'Expected different chart representation. Check that all spacing is exact.') +`); + + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +class Category: + pass + +def create_spend_chart(categories): + pass ``` # --solutions-- -```js -/** - Backend challenges don't need solutions, - because they would need to be tested against a full working project. - Please check our contributing guidelines to learn more. -*/ +```py +class Category: + + def __init__(self, name): + self.name = name + self.ledger = [] + self.balance = 0 + self.spent = 0 + + def __str__(self): + first_line = f'{self.name.center(30, "*")}\n' + lines = '' + total = f'Total: {format(self.balance, ".2f")}' + + for n in range(len(self.ledger)): + descr = self.ledger[n]["description"][:23] + am = format(float(self.ledger[n]["amount"]), ".2f")[:7] + lines = lines + f'{descr:<23}{am:>7}\n' + + return f'{first_line}{lines}{total}' + + def deposit(self, amount, description=''): + self.ledger.append({ + 'amount': float(amount), + 'description': description + }) + self.balance = self.balance + float(amount) + + def withdraw(self, amount, description=''): + if self.check_funds(amount): + self.ledger.append({ + 'amount': -float(amount), + 'description': description + }) + self.balance = self.balance - float(amount) + self.spent = self.spent + float(amount) + return True + else: + return False + + def get_balance(self): + return self.balance + + def transfer(self, amount, category): + if self.check_funds(amount): + # withdraw + self.ledger.append({ + 'amount': -float(amount), + 'description': f'Transfer to {category.name}' + }) + self.balance = self.balance - float(amount) + # deposit + category.deposit(amount, f'Transfer from {self.name}') + + return True + else: + return False + + def check_funds(self, amount): + if float(amount) > self.balance: + return False + else: + return True + + +def create_spend_chart(categories): + total_expenses = 0 + obj = {} + col1 = [] + str = [] + final_str = 'Percentage spent by category\n' + label_max_length = 0 + label_strings = [] + + for category in categories: + total_expenses = total_expenses + category.spent + obj[category.name] = {'expenses': category.spent} + obj[category.name]['label'] = list(category.name) + if len(obj[category.name]['label']) > label_max_length: + label_max_length = len(obj[category.name]['label']) + + for category in categories: + obj[category.name]['percent'] = ( + (category.spent / total_expenses * 100) // 10) * 10 + obj[category.name]['column'] = [] + for i in range(0, 110, 10): + if obj[category.name]['percent'] >= i: + obj[category.name]['column'].insert(0, 'o') + else: + obj[category.name]['column'].insert(0, ' ') + + for i in range(0, 110, 10): + col1.insert(0, i) + + for i in range(11): + str.append("") + for key in obj: + str[i] += (f'{obj[key]["column"][i]} ') + final_str += f'{col1[i]:>3}| {str[i]}\n' + final_str += f' {"-"*(1+3*len(obj))}\n ' + + for i in range(label_max_length): + label_strings.append(' ') + for k in obj: + if len(obj[k]['label']) < label_max_length: + obj[k]['label'].extend( + f'{" "*(label_max_length-len(obj[k]["label"]))}') + + label_strings[i] += f'{obj[k]["label"][i]} ' + if i < label_max_length - 1: + label_strings[i] += '\n ' + final_str += label_strings[i] + + print(final_str) + return (final_str) + ``` diff --git a/curriculum/challenges/english/07-scientific-computing-with-python/scientific-computing-with-python-projects/polygon-area-calculator.md b/curriculum/challenges/english/07-scientific-computing-with-python/scientific-computing-with-python-projects/polygon-area-calculator.md index 5fcccbd2d4b..43d920b7287 100644 --- a/curriculum/challenges/english/07-scientific-computing-with-python/scientific-computing-with-python-projects/polygon-area-calculator.md +++ b/curriculum/challenges/english/07-scientific-computing-with-python/scientific-computing-with-python-projects/polygon-area-calculator.md @@ -1,23 +1,14 @@ --- id: 5e444147903586ffb414c94f title: Polygon Area Calculator -challengeType: 10 +challengeType: 23 forumTopicId: 462363 dashedName: polygon-area-calculator --- # --description-- -You will be working on this project with our Replit starter code. - -- Start by importing the project on Replit. -- Next, you will see a `.replit` window. -- Select `Use run command` and click the `Done` button. - - -# --instructions-- - -In this project you will use object oriented programming to create a Rectangle class and a Square class. The Square class should be a subclass of Rectangle and inherit methods and attributes. +In this project you will use object oriented programming to create a `Rectangle` class and a `Square` class. The `Square` class should be a subclass of `Rectangle`, and inherit its methods and attributes. ## Rectangle class @@ -31,27 +22,27 @@ When a Rectangle object is created, it should be initialized with `width` and `h - `get_picture`: Returns a string that represents the shape using lines of "\*". The number of lines should be equal to the height and the number of "\*" in each line should be equal to the width. There should be a new line (`\n`) at the end of each line. If the width or height is larger than 50, this should return the string: "Too big for picture.". - `get_amount_inside`: Takes another shape (square or rectangle) as an argument. Returns the number of times the passed in shape could fit inside the shape (with no rotations). For instance, a rectangle with a width of 4 and a height of 8 could fit in two squares with sides of 4. -Additionally, if an instance of a Rectangle is represented as a string, it should look like: `Rectangle(width=5, height=10)` +Additionally, if an instance of a `Rectangle` is represented as a string, it should look like: `Rectangle(width=5, height=10)` ## Square class -The Square class should be a subclass of Rectangle. When a Square object is created, a single side length is passed in. The `__init__` method should store the side length in both the `width` and `height` attributes from the Rectangle class. +The `Square` class should be a subclass of `Rectangle`. When a `Square` object is created, a single side length is passed in. The `__init__` method should store the side length in both the `width` and `height` attributes from the `Rectangle` class. -The Square class should be able to access the Rectangle class methods but should also contain a `set_side` method. If an instance of a Square is represented as a string, it should look like: `Square(side=9)` +The `Square` class should be able to access the `Rectangle` class methods but should also contain a `set_side` method. If an instance of a `Square` is represented as a string, it should look like: `Square(side=9)` -Additionally, the `set_width` and `set_height` methods on the Square class should set both the width and height. +Additionally, the `set_width` and `set_height` methods on the `Square` class should set both the width and height. ## Usage example ```py -rect = shape_calculator.Rectangle(10, 5) +rect = Rectangle(10, 5) print(rect.get_area()) rect.set_height(3) print(rect.get_perimeter()) print(rect) print(rect.get_picture()) -sq = shape_calculator.Square(9) +sq = Square(9) print(sq.get_area()) sq.set_side(4) print(sq.get_diagonal()) @@ -84,34 +75,795 @@ Square(side=4) 8 ``` -The unit tests for this project are in `test_module.py`. - -## Development - -Write your code in `shape_calculator.py`. For development, you can use `main.py` to test your `shape_calculator()` function. Click the "run" button and `main.py` will run. - -## Testing - -We imported the tests from `test_module.py` to `main.py` for your convenience. The tests will run automatically whenever you hit the "run" button. - -## Submitting - -Copy your project's URL and submit it to freeCodeCamp. - # --hints-- -It should create a Rectangle class and Square class and pass all tests. +The `Square` class should be a subclass of the `Rectangle` class. ```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/shape_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import shape_calculator +from importlib import reload +reload(shape_calculator) +class UnitTests(unittest.TestCase): + maxDiff = None + + def test_subclass(self): + actual = issubclass(shape_calculator.Square, shape_calculator.Rectangle) + expected = True + self.assertEqual(actual, expected, 'Expected Square class to be a subclass of the Rectangle class.') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` +The `Square` class should be a distinct class from the `Rectangle` class. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/shape_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import shape_calculator +from importlib import reload +reload(shape_calculator) +class UnitTests(unittest.TestCase): + maxDiff = None + + def test_distinct_classes(self): + actual = shape_calculator.Square is not shape_calculator.Rectangle + expected = True + self.assertEqual(actual, expected, 'Expected Square class to be a distinct class from the Rectangle class.') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +A square object should be an instance of the `Square` class and the `Rectangle` class. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/shape_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import shape_calculator +from importlib import reload +reload(shape_calculator) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.sq = shape_calculator.Square(5) + + def test_square_is_square_and_rectangle(self): + actual = isinstance(self.sq, shape_calculator.Square) and isinstance(self.sq, shape_calculator.Rectangle) + expected = True + self.assertEqual(actual, expected, 'Expected square object to be an instance of the Square class and the Rectangle class.') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +The string representation of `Rectangle(3, 6)` should be `Rectangle(width=3, height=6)`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/shape_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import shape_calculator +from importlib import reload +reload(shape_calculator) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.rect = shape_calculator.Rectangle(3, 6) + + def test_rectangle_string(self): + actual = str(self.rect) + expected = "Rectangle(width=3, height=6)" + self.assertEqual(actual, expected, 'Expected string representation of rectangle to be "Rectangle(width=3, height=6)"') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +The string representation of `Square(5)` should be `Square(side=5)`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/shape_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import shape_calculator +from importlib import reload +reload(shape_calculator) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.sq = shape_calculator.Square(5) + + def test_square_string(self): + actual = str(self.sq) + expected = "Square(side=5)" + self.assertEqual(actual, expected, 'Expected string representation of square to be "Square(side=5)"') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +`Rectangle(3, 6).get_area()` should return `18`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/shape_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import shape_calculator +from importlib import reload +reload(shape_calculator) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.rect = shape_calculator.Rectangle(3, 6) + + def test_area(self): + actual = self.rect.get_area() + expected = 18 + self.assertEqual(actual, expected, 'Expected area of rectangle to be 18') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +`Square(5).get_area()` should return `25`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/shape_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import shape_calculator +from importlib import reload +reload(shape_calculator) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.sq = shape_calculator.Square(5) + + def test_area(self): + actual = self.sq.get_area() + expected = 25 + self.assertEqual(actual, expected, 'Expected area of square to be 25') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +`Rectangle(3, 6).get_perimeter()` should return `18`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/shape_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import shape_calculator +from importlib import reload +reload(shape_calculator) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.rect = shape_calculator.Rectangle(3, 6) + + def test_perimeter(self): + actual = self.rect.get_perimeter() + expected = 18 + self.assertEqual(actual, expected, 'Expected perimeter of rectangle to be 18') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +`Square(5).get_perimeter()` should return `20`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/shape_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import shape_calculator +from importlib import reload +reload(shape_calculator) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.sq = shape_calculator.Square(5) + + def test_perimeter(self): + actual = self.sq.get_perimeter() + expected = 20 + self.assertEqual(actual, expected, 'Expected perimeter of square to be 20') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +`Rectangle(3, 6).get_diagonal()` should return `6.708203932499369`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/shape_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import shape_calculator +from importlib import reload +reload(shape_calculator) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.rect = shape_calculator.Rectangle(3, 6) + + def test_diagonal(self): + actual = self.rect.get_diagonal() + expected = 6.708203932499369 + self.assertEqual(actual, expected, 'Expected diagonal of rectangle to be 6.708203932499369') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +`Square(5).get_diagonal()` should return `7.0710678118654755`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/shape_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import shape_calculator +from importlib import reload +reload(shape_calculator) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.sq = shape_calculator.Square(5) + + def test_diagonal(self): + actual = self.sq.get_diagonal() + expected = 7.0710678118654755 + self.assertEqual(actual, expected, 'Expected diagonal of square to be 7.0710678118654755') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +An instance of the `Rectangle` class should have a different string representation after setting new values. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/shape_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import shape_calculator +from importlib import reload +reload(shape_calculator) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.rect = shape_calculator.Rectangle(3, 6) + + def test_set_attributes(self): + self.rect.set_width(7) + self.rect.set_height(8) + actual = str(self.rect) + expected = "Rectangle(width=7, height=8)" + self.assertEqual(actual, expected, 'Expected string representation of rectangle after setting new values to be "Rectangle(width=7, height=8)"') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +An instance of the `Square` class should have a different string representation after setting new values by using `.set_side()`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/shape_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import shape_calculator +from importlib import reload +reload(shape_calculator) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.sq = shape_calculator.Square(5) + + def test_set_attributes(self): + self.sq.set_side(2) + actual = str(self.sq) + expected = "Square(side=2)" + self.assertEqual(actual, expected, 'Expected string representation of square after setting new values to be "Square(side=2)"') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +An instance of the `Square` class should have a different string representation after setting new values by using `.set_width()` or `set_height()`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/shape_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import shape_calculator +from importlib import reload +reload(shape_calculator) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.sq = shape_calculator.Square(5) + + def test_set_attributes(self): + self.sq.set_height(2) + actual = str(self.sq) + expected = "Square(side=2)" + self.assertEqual(actual, expected, 'Expected string representation of square after setting new values to be "Square(side=2)"') + self.sq.set_width(4) + actual = str(self.sq) + expected = "Square(side=4)" + self.assertEqual(actual, expected, 'Expected string representation of square after setting width to be "Square(side=4)"') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +The `.get_picture()` method should return a different string representation of a `Rectangle` instance. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/shape_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import shape_calculator +from importlib import reload +reload(shape_calculator) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.rect = shape_calculator.Rectangle(3, 6) + + def test_rectangle_picture(self): + self.rect.set_width(7) + self.rect.set_height(3) + actual = self.rect.get_picture() + expected = "*******\\n*******\\n*******\\n" + self.assertEqual(actual, expected, 'Expected rectangle picture to be different.') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +The `.get_picture()` method should return a different string representation of a `Square` instance. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/shape_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import shape_calculator +from importlib import reload +reload(shape_calculator) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.sq = shape_calculator.Square(5) + + def test_square_picture(self): + self.sq.set_side(2) + actual = self.sq.get_picture() + expected = "**\\n**\\n" + self.assertEqual(actual, expected, 'Expected square picture to be different.') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +The `.get_picture()` method should return the string `Too big for picture.` if the `width` or `height` attributes are larger than `50`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/shape_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import shape_calculator +from importlib import reload +reload(shape_calculator) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.rect = shape_calculator.Rectangle(3, 6) + + def test_big_picture(self): + self.rect.set_width(51) + self.rect.set_height(3) + actual = self.rect.get_picture() + expected = "Too big for picture." + self.assertEqual(actual, expected, 'Expected message: "Too big for picture."') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +`Rectangle(15,10).get_amount_inside(Square(5))` should return `6`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/shape_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import shape_calculator +from importlib import reload +reload(shape_calculator) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.rect = shape_calculator.Rectangle(3, 6) + self.sq = shape_calculator.Square(5) + + def test_get_amount_inside(self): + self.rect.set_height(10) + self.rect.set_width(15) + actual = self.rect.get_amount_inside(self.sq) + expected = 6 + self.assertEqual(actual, expected, 'Expected "get_amount_inside" to return 6.') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +`Rectangle(4,8).get_amount_inside(Rectangle(3, 6))` should return `1`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/shape_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import shape_calculator +from importlib import reload +reload(shape_calculator) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.rect = shape_calculator.Rectangle(3, 6) + + def test_get_amount_inside_two_rectangles(self): + rect2 = shape_calculator.Rectangle(4, 8) + actual = rect2.get_amount_inside(self.rect) + expected = 1 + self.assertEqual(actual, expected, 'Expected "get_amount_inside" to return 1.') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +`Rectangle(2,3).get_amount_inside(Rectangle(3, 6))` should return `0`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/shape_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py',` +import unittest +import shape_calculator +from importlib import reload +reload(shape_calculator) +class UnitTests(unittest.TestCase): + maxDiff = None + def setUp(self): + self.rect = shape_calculator.Rectangle(3, 6) + + def test_get_amount_inside_none(self): + rect2 = shape_calculator.Rectangle(2, 3) + actual = rect2.get_amount_inside(self.rect) + expected = 0 + self.assertEqual(actual, expected, 'Expected "get_amount_inside" to return 0.') +`); + const testCode = ` +from unittest import main +from importlib import reload +import test_module +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +class Rectangle: + pass + +class Square: + pass ``` # --solutions-- -```js -/** - Backend challenges don't need solutions, - because they would need to be tested against a full working project. - Please check our contributing guidelines to learn more. -*/ +```py +class Rectangle: + def __init__(self, width, height): + self.width = width + self.height = height + + def __str__(self): + return f'Rectangle(width={self.width}, height={self.height})' + + def set_width(self, width): + self.width = width + + def set_height(self, height): + self.height = height + + def get_area(self): + area = self.width * self.height + return area + + def get_perimeter(self): + perimeter = self.width * 2 + self.height * 2 + return perimeter + + def get_diagonal(self): + diagonal = (self.width ** 2 + self.height ** 2) ** 0.5 + return diagonal + + def get_picture(self): + if self.width < 50 and self.height < 50: + picture = f'{"*"*self.width}\n'*self.height + return picture + else: + return 'Too big for picture.' + + def get_amount_inside(self, polygon): + h_number = self.height // polygon.height + w_number = self.width // polygon.width + repetition = h_number * w_number + return repetition + + +class Square(Rectangle): + def __init__(self, side): + self.width = side + self.height = side + + def __str__(self): + return f'Square(side={self.width})' + + def set_width(self, side): + self.width = side + self.height = side + + def set_height(self, side): + self.height = side + self.width = side + + def set_side(self,side): + self.width = side + self.height = side + ``` diff --git a/curriculum/challenges/english/07-scientific-computing-with-python/scientific-computing-with-python-projects/probability-calculator.md b/curriculum/challenges/english/07-scientific-computing-with-python/scientific-computing-with-python-projects/probability-calculator.md index 33bb3dd35b9..02d627018f3 100644 --- a/curriculum/challenges/english/07-scientific-computing-with-python/scientific-computing-with-python-projects/probability-calculator.md +++ b/curriculum/challenges/english/07-scientific-computing-with-python/scientific-computing-with-python-projects/probability-calculator.md @@ -1,27 +1,18 @@ --- id: 5e44414f903586ffb414c950 title: Probability Calculator -challengeType: 10 +challengeType: 23 forumTopicId: 462364 dashedName: probability-calculator --- # --description-- -You will be working on this project with our Replit starter code. - -- Start by importing the project on Replit. -- Next, you will see a `.replit` window. -- Select `Use run command` and click the `Done` button. - - -# --instructions-- - Suppose there is a hat containing 5 blue balls, 4 red balls, and 2 green balls. What is the probability that a random draw of 4 balls will contain at least 1 red ball and 2 green balls? While it would be possible to calculate the probability using advanced mathematics, an easier way is to write a program to perform a large number of experiments to estimate an approximate probability. For this project, you will write a program to determine the approximate probability of drawing certain balls randomly from a hat. -First, create a `Hat` class in `prob_calculator.py`. The class should take a variable number of arguments that specify the number of balls of each color that are in the hat. For example, a class object could be created in any of these ways: +First, create a `Hat` class in `main.py`. The class should take a variable number of arguments that specify the number of balls of each color that are in the hat. For example, a class object could be created in any of these ways: ```py hat1 = Hat(yellow=3, blue=2, green=6) @@ -33,7 +24,7 @@ A hat will always be created with at least one ball. The arguments passed into t The `Hat` class should have a `draw` method that accepts an argument indicating the number of balls to draw from the hat. This method should remove balls at random from `contents` and return those balls as a list of strings. The balls should not go back into the hat during the draw, similar to an urn experiment without replacement. If the number of balls to draw exceeds the available quantity, return all the balls. -Next, create an `experiment` function in `prob_calculator.py` (not inside the `Hat` class). This function should accept the following arguments: +Next, create an `experiment` function in `main.py` (not inside the `Hat` class). This function should accept the following arguments: - `hat`: A hat object containing balls that should be copied inside the function. - `expected_balls`: An object indicating the exact group of balls to attempt to draw from the hat for the experiment. For example, to determine the probability of drawing 2 blue balls and 1 red ball from the hat, set `expected_balls` to `{"blue":2, "red":1}`. @@ -54,38 +45,215 @@ probability = experiment(hat=hat, num_experiments=2000) ``` +The output would be something like this: + +```bash +>>> 0.356 +``` + Since this is based on random draws, the probability will be slightly different each time the code is run. -*Hint: Consider using the modules that are already imported at the top of `prob_calculator.py`. Do not initialize random seed within `prob_calculator.py`.* +_Hint: Consider using the modules that are already imported at the top. Do not initialize random seed within the file._ -## Development - -Write your code in `prob_calculator.py`. For development, you can use `main.py` to test your code. Click the "run" button and `main.py` will run. - -The boilerplate includes `import` statements for the `copy` and `random` modules. Consider using those in your project. - -## Testing - -The unit tests for this project are in `test_module.py`. We imported the tests from `test_module.py` to `main.py` for your convenience. The tests will run automatically whenever you hit the "run" button. - -## Submitting - -Copy your project's URL and submit it to freeCodeCamp. # --hints-- -It should correctly calculate probabilities and pass all tests. +Creation of `hat` object should add correct contents. ```js +({ + test: () => { + pyodide.FS.writeFile("/home/pyodide/probability_calculator.py", code); + pyodide.FS.writeFile( + "/home/pyodide/test_module.py", + ` +import unittest +import probability_calculator +from importlib import reload +reload(probability_calculator) + +probability_calculator.random.seed(95) +class UnitTests(unittest.TestCase): + maxDiff = None + def test_hat_class_contents(self): + hat = probability_calculator.Hat(red=3,blue=2) + actual = hat.contents + expected = ["red","red","red","blue","blue"] + self.assertEqual(actual, expected, 'Expected creation of hat object to add correct contents.') + ` + ); + const testCode = ` +from unittest import main +import test_module +from importlib import reload + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + }, +}); +``` + +The `draw` method in `hat` class should reduce number of items in contents. + + +```js +({ + test: () => { + pyodide.FS.writeFile("/home/pyodide/probability_calculator.py", code); + pyodide.FS.writeFile( + "/home/pyodide/test_module.py", + ` +import unittest +import probability_calculator +from importlib import reload + +reload(probability_calculator) + +probability_calculator.random.seed(95) +def test_hat_draw(self): + hat = probability_calculator.Hat(red=5,blue=2) + actual = hat.draw(2) + expected = ['blue', 'red'] + self.assertEqual(actual, expected, 'Expected hat draw to return two random items from hat contents.') + actual = len(hat.contents) + expected = 5 + self.assertEqual(actual, expected, 'Expected hat draw to reduce number of items in contents.') + ` + ); + const testCode = ` +from unittest import main +import test_module +from importlib import reload + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + }, +}); +``` + +The `experiment` method should return a different probability. + + +```js +({ + test: () => { + pyodide.FS.writeFile("/home/pyodide/probability_calculator.py", code); + pyodide.FS.writeFile( + "/home/pyodide/test_module.py", + ` +import unittest +import probability_calculator +from importlib import reload + +reload(probability_calculator) + +probability_calculator.random.seed(95) +class UnitTests(unittest.TestCase): + maxDiff = None + def test_prob_experiment(self): + hat = probability_calculator.Hat(blue=3,red=2,green=6) + probability = probability_calculator.experiment(hat=hat, expected_balls={"blue":2,"green":1}, num_balls_drawn=4, num_experiments=1000) + actual = probability + expected = 0.272 + self.assertAlmostEqual(actual, expected, delta = 0.01, msg = 'Expected experiment method to return a different probability.') + hat = probability_calculator.Hat(yellow=5,red=1,green=3,blue=9,test=1) + probability = probability_calculator.experiment(hat=hat, expected_balls={"yellow":2,"blue":3,"test":1}, num_balls_drawn=20, num_experiments=100) + actual = probability + expected = 1.0 + self.assertAlmostEqual(actual, expected, delta = 0.01, msg = 'Expected experiment method to return a different probability.') + ` + ); + const testCode = ` +from unittest import main +import test_module +from importlib import reload + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + }, +}); +``` + +# --seed-- + +## --seed-contents-- + +```py +import copy +import random + +class Hat: + pass + +def experiment(hat, expected_balls, num_balls_drawn, num_experiments): + pass ``` # --solutions-- -```js -/** - Backend challenges don't need solutions, - because they would need to be tested against a full working project. - Please check our contributing guidelines to learn more. -*/ +```py +import copy +import random + +class Hat: + def __init__(self, **hat): + self.hat = hat + contents = [] + for i in hat: + for j in range(hat[i]): + contents.append(i) + self.contents = contents + + + def draw(self, number): + drawn = [] + if number >= len(self.contents): + return self.contents + else: + for i in range(number): + drawn.append( + self.contents.pop(random.randrange(len(self.contents))) + ) + return drawn + +def experiment(hat, expected_balls, num_balls_drawn, num_experiments): + + expected_balls_list = [] + drawn_list = [] + success = 0 + for i in expected_balls: + for j in range(expected_balls[i]): + expected_balls_list.append(i) + for j in range(num_experiments): + hat_copy = copy.deepcopy(hat) + drawn_list.append(hat_copy.draw(num_balls_drawn)) + exp_ball_list_copy = expected_balls_list[:] + for k in range(len(drawn_list[j])): + try: + ind = exp_ball_list_copy.index(drawn_list[j][k]) + exp_ball_list_copy.pop(ind) + except: + continue + if len(exp_ball_list_copy) == 0: + success += 1 + + + + + probability = success/num_experiments + + return probability ``` diff --git a/curriculum/challenges/english/07-scientific-computing-with-python/scientific-computing-with-python-projects/time-calculator.md b/curriculum/challenges/english/07-scientific-computing-with-python/scientific-computing-with-python-projects/time-calculator.md index 015b9e343f4..b72fc2b462e 100644 --- a/curriculum/challenges/english/07-scientific-computing-with-python/scientific-computing-with-python-projects/time-calculator.md +++ b/curriculum/challenges/english/07-scientific-computing-with-python/scientific-computing-with-python-projects/time-calculator.md @@ -1,21 +1,13 @@ --- id: 5e444136903586ffb414c94d title: Time Calculator -challengeType: 10 +challengeType: 23 forumTopicId: 462360 dashedName: time-calculator --- # --description-- -You will be working on this project with our Replit starter code. - -- Start by importing the project on Replit. -- Next, you will see a `.replit` window. -- Select `Use run command` and click the `Done` button. - -# --instructions-- - Write a function named `add_time` that takes in two required parameters and one optional parameter: - a start time in the 12-hour clock format (ending in AM or PM) @@ -52,32 +44,539 @@ add_time("6:30 PM", "205:12") Do not import any Python libraries. Assume that the start times are valid times. The minutes in the duration time will be a whole number less than 60, but the hour can be any whole number. -## Development - -Write your code in `time_calculator.py`. For development, you can use `main.py` to test your `time_calculator()` function. Click the "run" button and `main.py` will run. - -## Testing - -The unit tests for this project are in `test_module.py`. We imported the tests from `test_module.py` to `main.py` for your convenience. The tests will run automatically whenever you hit the "run" button. - -## Submitting - -Copy your project's URL and submit it to freeCodeCamp. - # --hints-- - -It should correctly add times and pass all tests. +Calling `add_time("3:30 PM", "2:12")` should return `5:42 PM`. ```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/time_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py', ` +import unittest +import time_calculator +from importlib import reload +reload(time_calculator) + + +class UnitTests(unittest.TestCase): + maxDiff = None + def test_same_period(self): + actual = time_calculator.add_time("3:30 PM", "2:12") + expected = "5:42 PM" + self.assertEqual(actual, expected, 'Expected calling "add_time()" with "3:30 PM", "2:12" to return "5:42 PM"') + `); + const testCode = ` +from unittest import main +import test_module +from importlib import reload + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +Calling `add_time("11:55 AM", "3:12")` should return `3:07 PM`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/time_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py', ` +import unittest +time_calculator +from importlib import reload + +reload(time_calculator) + + +class UnitTests(unittest.TestCase): + maxDiff = None + def test_different_period(self): + actual = time_calculator.add_time("11:55 AM", "3:12") + expected = "3:07 PM" + self.assertEqual(actual, expected, 'Expected calling "add_time()" with "11:55 AM", "3:12" to return "3:07 PM"') + `); + const testCode = ` +from unittest import main +import test_module +from importlib import reload + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +Expected time to end with `"(next day)"` when it is the next day. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/time_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py', ` +import unittest +time_calculator +from importlib import reload + +reload(time_calculator) + + +class UnitTests(unittest.TestCase): + maxDiff = None + def test_next_day(self): + actual = time_calculator.add_time("9:15 PM", "5:30") + expected = "2:45 AM (next day)" + self.assertEqual(actual, expected, 'Expected time to end with "(next day)" when it is the next day.') + `); + const testCode = ` +from unittest import main +import test_module +from importlib import reload + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +Expected period to change from `AM` to `PM` at `12:00`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/time_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py', ` +import unittest +time_calculator +from importlib import reload + +reload(time_calculator) + + +class UnitTests(unittest.TestCase): + maxDiff = None + def test_period_change_at_twelve(self): + actual = time_calculator.add_time("11:40 AM", "0:25") + expected = "12:05 PM" + self.assertEqual(actual, expected, 'Expected period to change from AM to PM at 12:00') + `); + const testCode = ` +from unittest import main +import test_module +from importlib import reload + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + + +Calling `add_time("2:59 AM", "24:00")` should return `2:59 AM`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/time_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py', ` +import unittest +time_calculator +from importlib import reload + +reload(time_calculator) + + +class UnitTests(unittest.TestCase): + maxDiff = None + def test_twenty_four(self): + actual = time_calculator.add_time("2:59 AM", "24:00") + expected = "2:59 AM (next day)" + self.assertEqual(actual, expected, 'Expected calling "add_time()" with "2:59 AM", "24:00" to return "2:59 AM"') + `); + const testCode = ` +from unittest import main +import test_module +from importlib import reload + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +Calling `add_time("11:59 PM", "24:05")` should return `12:04 AM (2 days later)`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/time_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py', ` +import unittest +time_calculator +from importlib import reload + +reload(time_calculator) + + +class UnitTests(unittest.TestCase): + maxDiff = None + def test_two_days_later(self): + actual = time_calculator.add_time("11:59 PM", "24:05") + expected = "12:04 AM (2 days later)" + self.assertEqual(actual, expected, 'Expected calling "add_time()" with "11:59 PM", "24:05" to return "12:04 AM (2 days later)"') + `); + const testCode = ` +from unittest import main +import test_module +from importlib import reload + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +Calling `add_time("8:16 PM", "466:02")` should return `6:18 AM (20 days later)`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/time_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py', ` +import unittest +time_calculator +from importlib import reload + +reload(time_calculator) + + +class UnitTests(unittest.TestCase): + maxDiff = None + def test_high_duration(self): + actual = time_calculator.add_time("8:16 PM", "466:02") + expected = "6:18 AM (20 days later)" + self.assertEqual(actual, expected, 'Expected calling "add_time()" with "8:16 PM", "466:02" to return "6:18 AM (20 days later)"') + `); + const testCode = ` +from unittest import main +import test_module +from importlib import reload + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +Expected adding `0:00` to return the initial time. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/time_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py', ` +import unittest +time_calculator +from importlib import reload + +reload(time_calculator) + + +class UnitTests(unittest.TestCase): + maxDiff = None + def test_no_change(self): + actual = time_calculator.add_time("5:01 AM", "0:00") + expected = "5:01 AM" + self.assertEqual(actual, expected, 'Expected adding 0:00 to return initial time.') + `); + const testCode = ` +from unittest import main +import test_module +from importlib import reload + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + + +Calling `add_time("3:30 PM", "2:12", "Monday")`should return `5:42 PM, Monday`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/time_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py', ` +import unittest +time_calculator +from importlib import reload + +reload(time_calculator) + + +class UnitTests(unittest.TestCase): + maxDiff = None + def test_same_period_with_day(self): + actual = time_calculator.add_time("3:30 PM", "2:12", "Monday") + expected = "5:42 PM, Monday" + self.assertEqual(actual, expected, 'Expected calling "add_time()" with "3:30 PM", "2:12", "Monday" to return "5:42 PM, Monday"') + `); + const testCode = ` +from unittest import main +import test_module +from importlib import reload + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + + +Calling `add_time("2:59 AM", "24:00", "saturDay")` should return `2:59 AM, Sunday (next day)`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/time_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py', ` +import unittest +time_calculator +from importlib import reload + +reload(time_calculator) + + +class UnitTests(unittest.TestCase): + maxDiff = None + def test_twenty_four_with_day(self): + actual = time_calculator.add_time("2:59 AM", "24:00", "saturDay") + expected = "2:59 AM, Sunday (next day)" + self.assertEqual(actual, expected, 'Expected calling "add_time()" with "2:59 AM", "24:00", "saturDay" to return "2:59 AM, Sunday (next day)"') + `); + const testCode = ` +from unittest import main +import test_module +from importlib import reload + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +Calling `add_time("11:59 PM", "24:05", "Wednesday")` should return `"12:04 AM, Friday (2 days later)"`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/time_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py', ` +import unittest +time_calculator +from importlib import reload + +reload(time_calculator) + + +class UnitTests(unittest.TestCase): + maxDiff = None + def test_two_days_later_with_day(self): + actual = time_calculator.add_time("11:59 PM", "24:05", "Wednesday") + expected = "12:04 AM, Friday (2 days later)" + self.assertEqual(actual, expected, 'Expected calling "add_time()" with "11:59 PM", "24:05", "Wednesday" to return "12:04 AM, Friday (2 days later)"') + `); + const testCode = ` +from unittest import main +import test_module +from importlib import reload + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +Calling `add_time("8:16 PM", "466:02", "tuesday") `should return `6:18 AM, Monday (20 days later)`. + +```js +({ + test: () => { + pyodide.FS.writeFile('/home/pyodide/time_calculator.py', code); + pyodide.FS.writeFile('/home/pyodide/test_module.py', ` +import unittest +time_calculator +from importlib import reload + +reload(time_calculator) + + +class UnitTests(unittest.TestCase): + maxDiff = None + def test_high_duration_with_day(self): + actual = time_calculator.add_time("8:16 PM", "466:02", "tuesday") + expected = "6:18 AM, Monday (20 days later)" + self.assertEqual(actual, expected, 'Expected calling "add_time()" with "8:16 PM", "466:02", "tuesday" to return "6:18 AM, Monday (20 days later)"') + `); + const testCode = ` +from unittest import main +import test_module +from importlib import reload + +reload(test_module) +t = main(module='test_module', exit=False) +t.result.wasSuccessful() +`; + const out = __pyodide.runPython(testCode); + assert(out); + } +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +def add_time(start, duration): + + + + + + return new_time ``` # --solutions-- -```js -/** - Backend challenges don't need solutions, - because they would need to be tested against a full working project. - Please check our contributing guidelines to learn more. -*/ +```py +def add_time(start, duration, day=''): + start_arr = start[0:-3].split(':') + dur_arr = duration.split(':') + tail = '' + # converting to 24h format + if 'AM' in start: + if start_arr[0] == '12': + start_arr[0] = '00' + elif 'PM' in start: + if start_arr[0] == '12': + start_arr[0] = '12' + else: + start_arr[0] = f'{int(start_arr[0]) + 12}' + + # adding minutes + sum_m = int(start_arr[1]) + int(dur_arr[1]) + if sum_m > 59: + + if (sum_m - 60 * (sum_m//60)) < 10: + mins = f'0{(sum_m - 60 * (sum_m//60))}' + dur_arr[0] = int(dur_arr[0]) + sum_m//60 + + else: + mins = sum_m - 60 * (sum_m//60) + dur_arr[0] = int(dur_arr[0]) + sum_m//60 + else: + if sum_m < 10: + mins = f'0{sum_m}' + else: + mins = sum_m + + #adding hours + sum_h = int(start_arr[0]) + int(dur_arr[0]) + if sum_h < 24: + hours= sum_h + #time_24 = f'{hours}:{mins}' + else: + days_after = sum_h//24 + if days_after == 1: + hours = sum_h - 24 + tail = ' (next day)' + else: + hours = sum_h - 24 * days_after + tail = f' ({days_after} days later)' + + #converting back to AM/PM + if hours == 0: + hours = 12 + time = f'{hours}:{mins} AM' + final_time = f'{time}{tail}' + elif hours < 12: + time = f'{hours}:{mins} AM' + final_time = f'{time}{tail}' + else: + if hours > 12: + hours = hours - 12 + time = f'{hours}:{mins} PM' + final_time = f'{time}{tail}' + + #days of the week + week = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] + if day: + day = day.capitalize() + if not tail: + final_time = f'{time}, {day}' + elif tail == ' (next day)': + index = week.index(day) + 1 + if index == 7: + index = 0 + week_day = f', {week[index]}' + final_time = f'{time}{week_day}{tail}' + elif tail: + index = (week.index(day) + days_after) % 7 + week_day = f', {week[index]}' + final_time = f'{time}{week_day}{tail}' + + + print('\n') + print(final_time) + print('\n') + return final_time + ``` diff --git a/curriculum/schema/__snapshots__/challenge-schema.test.js.snap b/curriculum/schema/__snapshots__/challenge-schema.test.js.snap index 8cd7250ace4..c9afa05dcca 100644 --- a/curriculum/schema/__snapshots__/challenge-schema.test.js.snap +++ b/curriculum/schema/__snapshots__/challenge-schema.test.js.snap @@ -92,7 +92,7 @@ const schema = Joi.object() challengeOrder: Joi.number(), removeComments: Joi.bool().required(), certification: Joi.string().regex(slugWithSlashRE), - challengeType: Joi.number().min(0).max(22).required(), + challengeType: Joi.number().min(0).max(23).required(), checksum: Joi.number(), // TODO: require this only for normal challenges, not certs dashedName: Joi.string().regex(slugRE), diff --git a/curriculum/schema/challenge-schema.js b/curriculum/schema/challenge-schema.js index 775ab66ec88..332f2218d04 100644 --- a/curriculum/schema/challenge-schema.js +++ b/curriculum/schema/challenge-schema.js @@ -89,7 +89,7 @@ const schema = Joi.object() challengeOrder: Joi.number(), removeComments: Joi.bool().required(), certification: Joi.string().regex(slugWithSlashRE), - challengeType: Joi.number().min(0).max(22).required(), + challengeType: Joi.number().min(0).max(23).required(), checksum: Joi.number(), // TODO: require this only for normal challenges, not certs dashedName: Joi.string().regex(slugRE), diff --git a/curriculum/test/test-challenges.js b/curriculum/test/test-challenges.js index 7c35484080d..5f70bbec434 100644 --- a/curriculum/test/test-challenges.js +++ b/curriculum/test/test-challenges.js @@ -408,7 +408,9 @@ function populateTestsForLang({ lang, challenges, meta, superBlocks }) { { [challengeTypes.js]: buildJSChallenge, [challengeTypes.jsProject]: buildJSChallenge, - [challengeTypes.python]: buildPythonChallenge + [challengeTypes.python]: buildPythonChallenge, + [challengeTypes.multifilePythonCertProject]: + buildPythonChallenge }[challengeType] ?? buildDOMChallenge; // The python tests are (currently) slow, so we give them more time. diff --git a/shared/config/challenge-types.ts b/shared/config/challenge-types.ts index 392d41b5a38..8d2c7977d99 100644 --- a/shared/config/challenge-types.ts +++ b/shared/config/challenge-types.ts @@ -22,6 +22,7 @@ const multipleChoice = 19; const python = 20; const dialogue = 21; const fillInTheBlank = 22; +const multifilePythonCertProject = 23; export const challengeTypes = { html, @@ -47,7 +48,8 @@ export const challengeTypes = { multipleChoice, python, dialogue, - fillInTheBlank + fillInTheBlank, + multifilePythonCertProject }; export const hasNoTests = (challengeType: number): boolean => @@ -105,7 +107,8 @@ export const viewTypes = { [multipleChoice]: 'odin', [python]: 'modern', [dialogue]: 'dialogue', - [fillInTheBlank]: 'fillInTheBlank' + [fillInTheBlank]: 'fillInTheBlank', + [multifilePythonCertProject]: 'classic' }; // determine the type of submit function to use for the challenge on completion @@ -135,5 +138,6 @@ export const submitTypes = { [multipleChoice]: 'tests', [python]: 'tests', [dialogue]: 'tests', - [fillInTheBlank]: 'tests' + [fillInTheBlank]: 'tests', + [multifilePythonCertProject]: 'tests' }; diff --git a/tools/client-plugins/browser-scripts/python-worker.ts b/tools/client-plugins/browser-scripts/python-worker.ts index c1b8ba90e32..818209e4ebd 100644 --- a/tools/client-plugins/browser-scripts/python-worker.ts +++ b/tools/client-plugins/browser-scripts/python-worker.ts @@ -134,6 +134,7 @@ function initRunPython() { else: return "" `); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call const getResetId = globals.get('__get_reset_id') as PyProxy & (() => string); return { runPython, getResetId, globals };