diff --git a/DOCS.md b/DOCS.md index e3d5aa2..9200bcc 100644 --- a/DOCS.md +++ b/DOCS.md @@ -31,6 +31,13 @@ cheat sheet, and db routes to save their progress when they pass the test

CodeEditor is a two-panel rendering component for student code. It uses AceEditor for the student panel (left), and a remote rendering iframe for the page preview (right).

+
DrawerValidation
+

The CodeEditor is responsible for handling violation of the rules set out for a given +programming challenge. A list of errors is passed in as a prop - this component is +responsible for creating a readable sentence form of what is wrong with the student's code. +For example, "Your HTML code is missing an

tag". The format of this language is stored in +the database as "Your {{p1}} is missing {{p2}}." (Note that this need be translated by lang)

+

CollabList

Provides a list of Collaborators for a project provided by props.

@@ -84,6 +91,26 @@ It is used on the Homepage as well as in User Profiles to list their projects

+
Search
+

The Search component is embedded in Nav and is used to search users and projects. It utilizes postgres trigrams serverside (see readme)

+
+
SelectGeo
+

Component for selecting a location from a database of geo codes in Brazil

+
+
SelectSchool
+

Component for choosing a school from a db of school ids and geos - meant to be used in conjunction with SelectGeo

+
+
ShareDirectLink
+

Popover Component for sharing a link to a students project/codeblock. Copies url to clipboard.

+
+
ShareFacebookLink
+

Component for sharing facebook links. Due to the fact that xvfb screenshots require a few seconds to render, +this component receives a "screenshotReady" prop from the embedding component, which waits a few seconds +to ensure that Facebook's FIRST capture of the page has the finished screenshot

+
+
SignupForm
+

Sister component to AuthForm, this component wraps the Canon "signup" action and does appropriate error checking

+
InputCode

InputCode is a slide type that requires the student to complete a coding test The CodeEditor component is embedded with a series of rules, and the slide will @@ -101,9 +128,48 @@ For showing code examples with explanations.

TextImage is text left, image right. Images are stored in /slide_images/{id}.jpg Images are uploaded through the CMS and a translated version is chosen here via locales

+
Thread
+

A thread is the top-level child of a Discussion. Discussions have many threads, threads have many comments.

+
+
UserCard
+

UserCard is a component used on a profile page to display other users that share the logged in user's school or location

+
+
About
+

About page - contains translated about text and a simple photoslide component from Datawheel's first visit to Minas

+
+
Contest
+

Contest Component handles the (currently postponed) contest, including all the steps and checks to ensure eligibility. +Page is public-facing (doesn't require login) to gain attention - the first step is creating an account

+
+
Glossary
+

The Glossary component retrieves words +It is worth noting that the storage of glossary words is somewhat split-brained. Here in Glossary.jsx, canon's "need" functionality +is used to ensure that words are rendered server-side and therefore indexable by search engines. However, glossary words used to live +in the Redux Store (and in QuillWrapper, this is still how they are loaded). Both places use the same API endpoint - but one +uses canon needs, the other puts the data into redux in App.jsx's mount method.

+
+
Home
+

Homepage component - mostly a wrapper for other smaller components (cards, features, etc)

+
+
Level
+

Main Level-viewing component (e.g., Jungle Island). It shows a list of the levels available as well as the ending codeblock test. +Codeblocks by other users are listed underneath the island

+
Island

Displays all available islands

+
Leaderboard
+

Leaderboard is a sortable table that lists users by their in-game progress.

+
+
LearnMore
+

Simple partners page to link to other online coding projects

+
+
LessonPlan
+

A public-facing listing of all slides and quizzes, to be used by teachers as a lesson plan, +students as a reference guide, and to aid in directing users to the site via search engines. +Lesson plan uses Canon's "Needs" to render the page serverside, so that the page is indexable +by SEO bots

+
Profile

Class component for a user profile. This is a public page and meant to be shared. @@ -125,6 +191,36 @@ This is shown on the public profile for a user and requires sending This is shown on the public profile for a user and requires sending 1 prop: a ref to the user

+
Projects
+

Projects is one of the largest Pages in codelife - It is responsible for all CRUD +operations of projects, processing screenshots, and listing user codeblocks for inspiration. +Longer term, this should be refactored into smaller components.

+
+
ResetPw
+

Very small wrapper class for PasswordReset Component

+
+
Share
+

The Share Page is a top-level page that does not require login, enabling users to share +their projects or codeblocks on facebook or with others. It looks up the content via name/user +and renders a fullscreen codeeditor for display, essentially acting as a hosting page for +the students' work. Show a Report bar on the bottom for logged in users to report inappropriate content

+
+
Slide
+

The slide component is the wrapper for all the various slidetypes in Codelife. However, +it interacts a great deal with the db and greater site, as reaching the last slide +updates user progress, and each slide has a Discussion board beneath it. It's important +to note that currently a Level must be beaten all at once - the "latestSlideCompleted" +variable in state is not persisted anywhere, and leaving the lesson does not restart the +user halfway through a level. Longer term, more granular tracking of user location would +be a nice enhancement.

+
+
Splash
+

Simple splash page that lists about text for the Codelife Project

+
+
Survey
+

Completed/Deprecated Survey Module from a 2017 Survey that followed a beta test in +Minas Gerais. Consists of Radio buttons and a DB post.

+
## Constants @@ -141,6 +237,44 @@ is the more correct one. Threads have entity_ids and types. Currently the only two types are comments and threads, however the intention was that discussions could expand to encompass projects/codeblocks or more

+
cvMatch
+

Given a rule and a block of code, check the Javascript and perform an exact match +check on the regex. Used for things like "code must contain getElementById"

+
+
cvNests
+

Given a rule and a block of code, check that a given tag is nested inside another +tag. Used for things like "html nests body." Note that this does not currently +account for subsequent occurences (only checks for first occurences)

+
+
cvUses
+

Given a rule and a block of code, use a hard-coded regex to check for a SPECIFIC +pattern. Example include a for block "for (;;) {}", ifelse "if () {} else {}" +or a generic invocation of a function "functionName(){}"

+
+
attrCount
+

Given a needle (like h1), an attribute (like color), a value (like red), and a JSON +representation of the code as prepared by himalaya (HTML parser), recursively climb +down the nested json tree, testing at each node for the presence of the needle, +and if provided, whether that node has an attribute, and, if provided, whether that +attribute's value exactly matches the provided value.

+
+
cvContainsSelfClosingTag
+

Given a rule and a block of code, search for a self closing tag such as +Optionally run attrCount to check for extra rules (such as requiring "src")

+
+
cvContainsOne
+

Given a rule and a block of code, ensure that the given needle (such as ) +occurs once and only once in the code (useful for tags like body, head, html)

+
+
cvContainsTag
+

Given a rule and a block of code, check if a given tag (such as

) is included in the +code. Optionally, use attrCount to match any provided attributes or values in the rule.

+
+
cvContainsStyle
+

Given a rule and a block of code, using the "css" module to turn the css into a crawlable +object. Fold over that generated parsed object and drill down to check if the rule's property +matches the property and value of the css entered by the student.

+
## Functions @@ -1013,6 +1147,19 @@ toggle fullscreen state * * * + + +## DrawerValidation +The CodeEditor is responsible for handling violation of the rules set out for a given +programming challenge. A list of errors is passed in as a prop - this component is +responsible for creating a readable sentence form of what is wrong with the student's code. +For example, "Your HTML code is missing an

tag". The format of this language is stored in +the database as "Your {{p1}} is missing {{p2}}." (Note that this need be translated by lang) + +**Kind**: global class + +* * * + ## CollabList @@ -1426,6 +1573,165 @@ with a "type" column that designates which table the report_id refers to. * * * + + +## Search +The Search component is embedded in Nav and is used to search users and projects. It utilizes postgres trigrams serverside (see readme) + +**Kind**: global class + +* [Search](#Search) + * [.handleChange()](#Search+handleChange) + * [.componentDidUpdate()](#Search+componentDidUpdate) + * [.onKeyDown()](#Search+onKeyDown) + * [.search()](#Search+search) + + +* * * + + + +### search.handleChange() +On keystroke, hit the search API endpoint + +**Kind**: instance method of [Search](#Search) + +* * * + + + +### search.componentDidUpdate() +Search is not a top-level component in routes.jsx, so it doesn't have access to the Router object that would indicate +what page the user is on. This makes it difficult to collapse the search results if the user clicks a link outside the +results. To get around this, a linkObj is passed down from App to Nav to here, and search is collapsed on URL change. + +**Kind**: instance method of [Search](#Search) + +* * * + + + +### search.onKeyDown() +onKeyDown is meant to capture inputs - but the arrow up/down aren't currently functional (user can use tabs and +Enter key to go through links however). This UX could be improved. + +**Kind**: instance method of [Search](#Search) + +* * * + + + +### search.search() +Send search query to API endpoint + +**Kind**: instance method of [Search](#Search) + +* * * + + + +## SelectGeo +Component for selecting a location from a database of geo codes in Brazil + +**Kind**: global class + +* [SelectGeo](#SelectGeo) + * [.componentDidMount()](#SelectGeo+componentDidMount) + * [.changeState()](#SelectGeo+changeState) + + +* * * + + + +### selectGeo.componentDidMount() +On Mount, get the gid (embedded in props via userprofile) and populate the search bar accordingly + +**Kind**: instance method of [SelectGeo](#SelectGeo) + +* * * + + + +### selectGeo.changeState() +Callback for the select box. Contains some code from what was going to be an "Unspecified" option, but was +changed to a higher up "Rather not say" option in EditProfile.jsx which cancels out the dialog entirely +and writes a hard-coded "-1" to the user's geo id. + +**Kind**: instance method of [SelectGeo](#SelectGeo) + +* * * + + + +## SelectSchool +Component for choosing a school from a db of school ids and geos - meant to be used in conjunction with SelectGeo + +**Kind**: global class + +* [SelectSchool](#SelectSchool) + * [.componentDidMount()](#SelectSchool+componentDidMount) + * [.updateSchoolList()](#SelectSchool+updateSchoolList) + + +* * * + + + +### selectSchool.componentDidMount() +On Mount, get the sid from props (originally retrieved from userprofile) and populate the dropdown accordingly + +**Kind**: instance method of [SelectSchool](#SelectSchool) + +* * * + + + +### selectSchool.updateSchoolList() +Callback for dual dropdown - Given a selected Geo, hit the API to retrieve all the schools for that Geo and populate the dropdown. + +**Kind**: instance method of [SelectSchool](#SelectSchool) + +* * * + + + +## ShareDirectLink +Popover Component for sharing a link to a students project/codeblock. Copies url to clipboard. + +**Kind**: global class + +* * * + + + +## ShareFacebookLink +Component for sharing facebook links. Due to the fact that xvfb screenshots require a few seconds to render, +this component receives a "screenshotReady" prop from the embedding component, which waits a few seconds +to ensure that Facebook's FIRST capture of the page has the finished screenshot + +**Kind**: global class + +* * * + + + +## SignupForm +Sister component to AuthForm, this component wraps the Canon "signup" action and does appropriate error checking + +**Kind**: global class + +* * * + + + +### signupForm.onSubmit() +When the user clicks submit, verify some info before calling datawheel-canon's `signup` action + +**Kind**: instance method of [SignupForm](#SignupForm) + +* * * + ## InputCode @@ -1603,223 +1909,1024 @@ Images are uploaded through the CMS and a translated version is chosen here via * * * - + -## Island -Displays all available islands +## Thread +A thread is the top-level child of a Discussion. Discussions have many threads, threads have many comments. **Kind**: global class -* [Island](#Island) - * [.componentDidMount()](#Island+componentDidMount) - * [.hasUserCompleted(milestone)](#Island+hasUserCompleted) ⇒ Boolean +* [Thread](#Thread) + * [.componentDidMount()](#Thread+componentDidMount) + * [.newComment()](#Thread+newComment) + * [.handleReport()](#Thread+handleReport) * * * - + -### island.componentDidMount() -On mount, fetch the progress for the currently logged in user. +### thread.componentDidMount() +On Mount, retrieve the thread from props -**Kind**: instance method of [Island](#Island) +**Kind**: instance method of [Thread](#Thread) * * * - + -### island.hasUserCompleted(milestone) ⇒ Boolean -On mount, fetch the progress for the currently logged in user. +### thread.newComment() +A thread can only have one text window open at a time for a new comment to be added. Keep the details of +this comment in state, and post it to the endpoint on submit -**Kind**: instance method of [Island](#Island) -**Returns**: Boolean - Returns a boolean whether or not the user has completed the provided island ID. +**Kind**: instance method of [Thread](#Thread) -| Param | Type | Description | -| --- | --- | --- | -| milestone | String | An island ID. | +* * * + + +### thread.handleReport() +A report, handled by the sub-component ReportBox, uses this callback to tell Thread that the user has submitted a report + +**Kind**: instance method of [Thread](#Thread) * * * - + -## Profile -Class component for a user profile. -This is a public page and meant to be shared. -If a user is logged in AND this is their profile, show an -edit button allowing them to edit it. +## UserCard +UserCard is a component used on a profile page to display other users that share the logged in user's school or location **Kind**: global class -* [Profile](#Profile) - * [new Profile(loading, error, profileUser)](#new_Profile_new) - * [.componentDidMount()](#Profile+componentDidMount) - * [.render()](#Profile+render) +* * * + + +## About +About page - contains translated about text and a simple photoslide component from Datawheel's first visit to Minas + +**Kind**: global class * * * - + -### new Profile(loading, error, profileUser) -Creates the Profile component with its initial state. +## Contest +Contest Component handles the (currently postponed) contest, including all the steps and checks to ensure eligibility. +Page is public-facing (doesn't require login) to gain attention - the first step is creating an account +**Kind**: global class -| Param | Type | Description | -| --- | --- | --- | -| loading | boolean | true by defaults gets flipped post AJAX. | -| error | string | Gets set if no username matches username URL param. | -| profileUser | object | Gets set to full user object from DB. | +* [Contest](#Contest) + * [.componentDidMount()](#Contest+componentDidMount) + * [.determineStep()](#Contest+determineStep) * * * - + -### profile.componentDidMount() -Grabs username from URL param, makes AJAX call to server and sets error -state (if no user is found) or profileUser (if one is). +### contest.componentDidMount() +On mount, load the user, their progress, and their projects. Use this data to populate state and fill in the +appropriate steps on the page -**Kind**: instance method of [Profile](#Profile) +**Kind**: instance method of [Contest](#Contest) * * * - + -### profile.render() -3 render states: -case (loading) - - show loading -case (error) - - show error msg from server -case (user found) - - user info +### contest.determineStep() +Signing up for the contest is a multi-step progress - use the state to determine where the user is +so the boxes on the page can be checked accordingly. -**Kind**: instance method of [Profile](#Profile) +**Kind**: instance method of [Contest](#Contest) * * * - + -## UserCodeBlocks -Class component for displaying lists of user's snippets. -This is shown on the public profile for a user and requires sending -1 prop: a ref to the user +## Glossary +The Glossary component retrieves words +It is worth noting that the storage of glossary words is somewhat split-brained. Here in Glossary.jsx, canon's "need" functionality +is used to ensure that words are rendered server-side and therefore indexable by search engines. However, glossary words used to live +in the Redux Store (and in QuillWrapper, this is still how they are loaded). Both places use the same API endpoint - but one +uses canon needs, the other puts the data into redux in App.jsx's mount method. **Kind**: global class -* [UserCodeBlocks](#UserCodeBlocks) - * [new UserCodeBlocks(loading, snippets)](#new_UserCodeBlocks_new) - * [.componentDidMount()](#UserCodeBlocks+componentDidMount) - - * * * - - -### new UserCodeBlocks(loading, snippets) -Creates the UserSnippets component with initial state. - + -| Param | Type | Description | -| --- | --- | --- | -| loading | boolean | true by defaults gets flipped post AJAX. | -| snippets | array | Gets set by AJAX call from DB call. | +## Home +Homepage component - mostly a wrapper for other smaller components (cards, features, etc) +**Kind**: global class * * * - + -### userCodeBlocks.componentDidMount() -Grabs user id from user prop, makes AJAX call to server and returns -the list of snippets. +### home.componentDidMount() +On mount, fetch the users progress so that a "continue your adventure" placard can be shown. +Whether the user is logged or not, fetch the featured cb/projects -**Kind**: instance method of [UserCodeBlocks](#UserCodeBlocks) +**Kind**: instance method of [Home](#Home) * * * - + -## UserProjects -Class component for displaying lists of user's projects. -This is shown on the public profile for a user and requires sending -1 prop: a ref to the user +## Level +Main Level-viewing component (e.g., Jungle Island). It shows a list of the levels available as well as the ending codeblock test. +Codeblocks by other users are listed underneath the island **Kind**: global class -* [UserProjects](#UserProjects) - * [new UserProjects(loading, projects)](#new_UserProjects_new) - * [.componentDidMount()](#UserProjects+componentDidMount) +* [Level](#Level) + * [.loadFromDB()](#Level+loadFromDB) + * [.maybeTriggerCodeblock()](#Level+maybeTriggerCodeblock) + * [.componentDidUpdate()](#Level+componentDidUpdate) + * [.componentDidMount()](#Level+componentDidMount) + * [.componentWillUnmount()](#Level+componentWillUnmount) + * [.toggleTest()](#Level+toggleTest) + * [.handleSave()](#Level+handleSave) + * [.onFirstCompletion()](#Level+onFirstCompletion) + * [.closeOverlay()](#Level+closeOverlay) + * [.hasUserCompleted()](#Level+hasUserCompleted) + * [.reportLike()](#Level+reportLike) + * [.allLevelsBeaten()](#Level+allLevelsBeaten) + * [.promptFinalTest()](#Level+promptFinalTest) + * [.saveCheckpoint()](#Level+saveCheckpoint) + * [.skipCheckpoint()](#Level+skipCheckpoint) + * [.buildCheckpointPopover()](#Level+buildCheckpointPopover) + * [.buildWinPopover()](#Level+buildWinPopover) + * [.buildTestPopover()](#Level+buildTestPopover) * * * - + -### new UserProjects(loading, projects) -Creates the UserProjects component with initial state. +### level.loadFromDB() +On Mount, or Update (meaning the user switched islands) Load the necessary progress/codeblock data from the db. +**Kind**: instance method of [Level](#Level) -| Param | Type | Description | -| --- | --- | --- | -| loading | boolean | true by defaults gets flipped post AJAX. | -| projects | array | Gets set by AJAX call from DB call. | +* * * + + +### level.maybeTriggerCodeblock() +The presence of `/show` in the URL is a permalink to open the codeblock. Was originally intended so that codeblockcards could directly link +to a user's own codeblock and automatically open it, but this feature was postponed. + +**Kind**: instance method of [Level](#Level) * * * - + -### userProjects.componentDidMount() -Grabs user id from user prop, makes AJAX call to server and returns -the list of projects. +### level.componentDidUpdate() +When the user changes pages, flush the state and reload from the database -**Kind**: instance method of [UserProjects](#UserProjects) +**Kind**: instance method of [Level](#Level) * * * - + -## UsersList -Class component for displaying lists of user's snippets. -This is shown on the public profile for a user and requires sending -1 prop: a ref to the user +### level.componentDidMount() +The code to load from DB already exists in ComponentDidUpdate, this dedupes that logic by just manually calling update on mount. -**Kind**: global class +**Kind**: instance method of [Level](#Level) -* [UsersList](#UsersList) - * [new UsersList(loading, snippets)](#new_UsersList_new) - * [.componentDidMount()](#UsersList+componentDidMount) +* * * + -* * * +### level.componentWillUnmount() +A timeout is registered on Codeblock completion to process the screenshot, ensuring that it is complete before allowing fb sharing. +Clear this timeout if the user leaves the page - +**Kind**: instance method of [Level](#Level) -### new UsersList(loading, snippets) -Creates the UserSnippets component with initial state. +* * * + -| Param | Type | Description | -| --- | --- | --- | -| loading | boolean | true by defaults gets flipped post AJAX. | -| snippets | array | Gets set by AJAX call from DB call. | +### level.toggleTest() +Hide or Show the codeblock test popover. Adjust the URL accordingly +**Kind**: instance method of [Level](#Level) * * * - + -### usersList.componentDidMount() -Grabs user id from user prop, makes AJAX call to server and returns -the list of snippets. +### level.handleSave() +Callback for CodeBlockEditor on save. The CodeBlockEditor passes its codeblock back out to Level so that its +Codeblock can be set. -**Kind**: instance method of [UsersList](#UsersList) +**Kind**: instance method of [Level](#Level) * * * - + + +### level.onFirstCompletion() +Called when the user finishes an island for the first time. Calls a refresh on the data +to unlock codeblocks, shows the victory message, and invites the user to the next island. + +**Kind**: instance method of [Level](#Level) + +* * * + + + +### level.closeOverlay() +Upon Closing the winning pop-up, send the player to the next island. + +**Kind**: instance method of [Level](#Level) + +* * * + + + +### level.hasUserCompleted() +Levels and Islands are mixed together in a single array - so this can be used to test if +a user has beaten a level (e.g. hello-world) or an entire island (e.g. island-1). + +**Kind**: instance method of [Level](#Level) + +* * * + + + +### level.reportLike() +The codeblocks underneath the island need to be informed via a callback when they are +liked or unliked, as this affects the sorting. + +**Kind**: instance method of [Level](#Level) + +* * * + + + +### level.allLevelsBeaten() +Used to determine if the final test should be shown. + +**Kind**: instance method of [Level](#Level) + +* * * + + + +### level.promptFinalTest() +If a user has beaten all the levels on this island, but has NOT created a codeblock yet, +they are in the state were the codeblock need be prompted + +**Kind**: instance method of [Level](#Level) + +* * * + + + +### level.saveCheckpoint() +Checkpoint is a pop-up that appears after level 1, asking the user to share their +school. This is the api callback to update their profile + +**Kind**: instance method of [Level](#Level) + +* * * + + + +### level.skipCheckpoint() +If the user elects not to provide their school, write a hard-coded -1 to their sid. +This saves the "prefer not to answer" choice and prevents future popups. + +**Kind**: instance method of [Level](#Level) + +* * * + + + +### level.buildCheckpointPopover() +This was written early in the project, before the Component nesting of React was +fully put to use. This method encapsulates the checkpoint popover - but this should +obviously be moved to a component, not a method. + +**Kind**: instance method of [Level](#Level) + +* * * + + + +### level.buildWinPopover() +This was written early in the project, before the Component nesting of React was +fully put to use. This method encapsulates the "You Win" popover - but this should +obviously be moved to a component, not a method. + +**Kind**: instance method of [Level](#Level) + +* * * + + + +### level.buildTestPopover() +This was written early in the project, before the Component nesting of React was +fully put to use. This method encapsulates the test popover - but this should +obviously be moved to a component, not a method. + +**Kind**: instance method of [Level](#Level) + +* * * + + + +## Island +Displays all available islands + +**Kind**: global class + +* [Island](#Island) + * [.componentDidMount()](#Island+componentDidMount) + * [.hasUserCompleted(milestone)](#Island+hasUserCompleted) ⇒ Boolean + + +* * * + + + +### island.componentDidMount() +On mount, fetch the progress for the currently logged in user. + +**Kind**: instance method of [Island](#Island) + +* * * + + + +### island.hasUserCompleted(milestone) ⇒ Boolean +On mount, fetch the progress for the currently logged in user. + +**Kind**: instance method of [Island](#Island) +**Returns**: Boolean - Returns a boolean whether or not the user has completed the provided island ID. + +| Param | Type | Description | +| --- | --- | --- | +| milestone | String | An island ID. | + + +* * * + + + +## Leaderboard +Leaderboard is a sortable table that lists users by their in-game progress. + +**Kind**: global class + +* [Leaderboard](#Leaderboard) + * [.componentDidMount()](#Leaderboard+componentDidMount) + * [.handleHeaderClick()](#Leaderboard+handleHeaderClick) + + +* * * + + + +### leaderboard.componentDidMount() +On Mount, retrieve stats from the API, calculate what percentage of completion this refers +to, given the state of currently released islands, and update state. + +**Kind**: instance method of [Leaderboard](#Leaderboard) + +* * * + + + +### leaderboard.handleHeaderClick() +On selection of a sorting property, sort the users accordingly + +**Kind**: instance method of [Leaderboard](#Leaderboard) + +* * * + + + +## LearnMore +Simple partners page to link to other online coding projects + +**Kind**: global class + +* * * + + + +## LessonPlan +A public-facing listing of all slides and quizzes, to be used by teachers as a lesson plan, +students as a reference guide, and to aid in directing users to the site via search engines. +Lesson plan uses Canon's "Needs" to render the page serverside, so that the page is indexable +by SEO bots + +**Kind**: global class + +* * * + + + +## Profile +Class component for a user profile. +This is a public page and meant to be shared. +If a user is logged in AND this is their profile, show an +edit button allowing them to edit it. + +**Kind**: global class + +* [Profile](#Profile) + * [new Profile(loading, error, profileUser)](#new_Profile_new) + * [.componentDidMount()](#Profile+componentDidMount) + * [.render()](#Profile+render) + + +* * * + + + +### new Profile(loading, error, profileUser) +Creates the Profile component with its initial state. + + +| Param | Type | Description | +| --- | --- | --- | +| loading | boolean | true by defaults gets flipped post AJAX. | +| error | string | Gets set if no username matches username URL param. | +| profileUser | object | Gets set to full user object from DB. | + + +* * * + + + +### profile.componentDidMount() +Grabs username from URL param, makes AJAX call to server and sets error +state (if no user is found) or profileUser (if one is). + +**Kind**: instance method of [Profile](#Profile) + +* * * + + + +### profile.render() +3 render states: +case (loading) + - show loading +case (error) + - show error msg from server +case (user found) + - user info + +**Kind**: instance method of [Profile](#Profile) + +* * * + + + +## UserCodeBlocks +Class component for displaying lists of user's snippets. +This is shown on the public profile for a user and requires sending +1 prop: a ref to the user + +**Kind**: global class + +* [UserCodeBlocks](#UserCodeBlocks) + * [new UserCodeBlocks(loading, snippets)](#new_UserCodeBlocks_new) + * [.componentDidMount()](#UserCodeBlocks+componentDidMount) + + +* * * + + + +### new UserCodeBlocks(loading, snippets) +Creates the UserSnippets component with initial state. + + +| Param | Type | Description | +| --- | --- | --- | +| loading | boolean | true by defaults gets flipped post AJAX. | +| snippets | array | Gets set by AJAX call from DB call. | + + +* * * + + + +### userCodeBlocks.componentDidMount() +Grabs user id from user prop, makes AJAX call to server and returns +the list of snippets. + +**Kind**: instance method of [UserCodeBlocks](#UserCodeBlocks) + +* * * + + + +## UserProjects +Class component for displaying lists of user's projects. +This is shown on the public profile for a user and requires sending +1 prop: a ref to the user + +**Kind**: global class + +* [UserProjects](#UserProjects) + * [new UserProjects(loading, projects)](#new_UserProjects_new) + * [.componentDidMount()](#UserProjects+componentDidMount) + + +* * * + + + +### new UserProjects(loading, projects) +Creates the UserProjects component with initial state. + + +| Param | Type | Description | +| --- | --- | --- | +| loading | boolean | true by defaults gets flipped post AJAX. | +| projects | array | Gets set by AJAX call from DB call. | + + +* * * + + + +### userProjects.componentDidMount() +Grabs user id from user prop, makes AJAX call to server and returns +the list of projects. + +**Kind**: instance method of [UserProjects](#UserProjects) + +* * * + + + +## UsersList +Class component for displaying lists of user's snippets. +This is shown on the public profile for a user and requires sending +1 prop: a ref to the user + +**Kind**: global class + +* [UsersList](#UsersList) + * [new UsersList(loading, snippets)](#new_UsersList_new) + * [.componentDidMount()](#UsersList+componentDidMount) + + +* * * + + + +### new UsersList(loading, snippets) +Creates the UserSnippets component with initial state. + + +| Param | Type | Description | +| --- | --- | --- | +| loading | boolean | true by defaults gets flipped post AJAX. | +| snippets | array | Gets set by AJAX call from DB call. | + + +* * * + + + +### usersList.componentDidMount() +Grabs user id from user prop, makes AJAX call to server and returns +the list of snippets. + +**Kind**: instance method of [UsersList](#UsersList) + +* * * + + + +## Projects +Projects is one of the largest Pages in codelife - It is responsible for all CRUD +operations of projects, processing screenshots, and listing user codeblocks for inspiration. +Longer term, this should be refactored into smaller components. + +**Kind**: global class + +* [Projects](#Projects) + * [.componentDidMount()](#Projects+componentDidMount) + * [.openProject()](#Projects+openProject) + * [.setExecState()](#Projects+setExecState) + * [.createNewProject()](#Projects+createNewProject) + * [.clickNewProject()](#Projects+clickNewProject) + * [.shareProject()](#Projects+shareProject) + * [.showLeaveAlert()](#Projects+showLeaveAlert) + * [.leaveCollab()](#Projects+leaveCollab) + * [.handleCheckbox()](#Projects+handleCheckbox) + * [.deleteProject()](#Projects+deleteProject) + * [.onClickProject()](#Projects+onClickProject) + * [.saveCodeToDB()](#Projects+saveCodeToDB) + * [.closeFirstTimeShare()](#Projects+closeFirstTimeShare) + * [.executeCode()](#Projects+executeCode) + * [.handleFork()](#Projects+handleFork) + * [.changeProjectName()](#Projects+changeProjectName) + * [.handleKey()](#Projects+handleKey) + + +* * * + + + +### projects.componentDidMount() +On Mount, retrieve all projects by the logged in user, as well as the projects by OTHER users +with whom the logged in user is listed as a collaborator, and put these in state. + +**Kind**: instance method of [Projects](#Projects) + +* * * + + + +### projects.openProject() +Given a project id, open the project itself by fetching it from the database and loading it +into state. Set the URL so it continues to match the open project permalink + +**Kind**: instance method of [Projects](#Projects) + +* * * + + + +### projects.setExecState() +The embedded CodeEditor is the only component that knows if the user has used javascript +in their project. When this changes in CodeEditor, it bubbles that up via this callback +so that Projects can dynamically show and hide an "Execute Code" button + +**Kind**: instance method of [Projects](#Projects) + +* * * + + + +### projects.createNewProject() +Callback for the create new project button. Trims the name of URL-breakers and whitespace, +posts an empty project to the API endpoint, and refreshes the project list from that API +payload so the Project List accurately reflects the new project collection. Update the URL +when the project is finished opening. + +**Kind**: instance method of [Projects](#Projects) + +* * * + + + +### projects.clickNewProject() +Opens the popover to name the project (which eventually calles createNewProject) + +**Kind**: instance method of [Projects](#Projects) + +* * * + + + +### projects.shareProject() +Deprecated / unused function + +**Kind**: instance method of [Projects](#Projects) + +* * * + + + +### projects.showLeaveAlert() +The alerts in this component have two states, false, or "truthy," that is, leaveAlert=false +means that the window closed, and setting leaveAlert to *what you want the alert to say* +makes it truthy, and therefore open. This click callback is the "are you sure" dialogue +for leaving a collaboration + +**Kind**: instance method of [Projects](#Projects) + +* * * + + + +### projects.leaveCollab() +Upon confirming that this user wants to leave a collab, remove that user from +the collabs tabel. Additionally, filter it out in state. Either way, close the +leaveAlert + +**Kind**: instance method of [Projects](#Projects) + +* * * + + + +### projects.handleCheckbox() +Though users are normally invited to share new projects on facebook, they may elect to +opt out and "never show this again" which needs to write to their userprofile + +**Kind**: instance method of [Projects](#Projects) + +* * * + + + +### projects.deleteProject() +Delete a given project. The argument here is confusing - originally clicking delete would +delete the project immediately. The addition of a deleteAlert (similar to leaveAlert) stores +the project to be deleted in the deleteAlert. + +**Kind**: instance method of [Projects](#Projects) + +* * * + + + +### projects.onClickProject() +When a user clicks a project, attempt to open it. Reach into the CodeEditor and check +if changes have been made, and if so, block the opening attempt until they save. + +**Kind**: instance method of [Projects](#Projects) + +* * * + + + +### projects.saveCodeToDB() +Prepare a payload containing the filename, id, content, etc to be sent to the update api. +If this is the first time the user is saving a project, offer to share it on Facebook. +Note that this is one of the many places in Codelife where a 5 second timer is used to allow +the screenshot time to process + +**Kind**: instance method of [Projects](#Projects) + +* * * + + + +### projects.closeFirstTimeShare() +Callback for closing the share window, save the users preference if they asked +not to be asked again. + +**Kind**: instance method of [Projects](#Projects) + +* * * + + + +### projects.executeCode() +Reach into the CodeEditor and call the executeCode function. This requires manual +execution - otherwise if the user was writing something like "alert()" then it would +render every single keystroke + +**Kind**: instance method of [Projects](#Projects) + +* * * + + + +### projects.handleFork() +On most parts of the site, forking a codeblock is as easy as creating the project +and navigating the user to that page. However, if a user forks a codeblock from HERE +on the project page, a slightly different behavior is required + +**Kind**: instance method of [Projects](#Projects) + +* * * + + + +### projects.changeProjectName() +Rename a project. Set title editability to false temporarily and prune URL-breakers +and leading/trailing whitespace from the new name. Write the project to the db and update state + +**Kind**: instance method of [Projects](#Projects) + +* * * + + + +### projects.handleKey() +Keyboard callbacks + +**Kind**: instance method of [Projects](#Projects) + +* * * + + + +## ResetPw +Very small wrapper class for PasswordReset Component + +**Kind**: global class + +* * * + + + +## Share +The Share Page is a top-level page that does not require login, enabling users to share +their projects or codeblocks on facebook or with others. It looks up the content via name/user +and renders a fullscreen codeeditor for display, essentially acting as a hosting page for +the students' work. Show a Report bar on the bottom for logged in users to report inappropriate content + +**Kind**: global class + +* [Share](#Share) + * [.componentDidMount()](#Share+componentDidMount) + * [.handleReport()](#Share+handleReport) + + +* * * + + + +### share.componentDidMount() +In order to color the ReportBox Button appropriate, it needs to be known if the logged +in user has reported this content. Fetch the reports to check. + +**Kind**: instance method of [Share](#Share) + +* * * + + + +### share.handleReport() +The ReportBox component needs a callback to tell this outer component that a report +has been processed. + +**Kind**: instance method of [Share](#Share) + +* * * + + + +## Slide +The slide component is the wrapper for all the various slidetypes in Codelife. However, +it interacts a great deal with the db and greater site, as reaching the last slide +updates user progress, and each slide has a Discussion board beneath it. It's important +to note that currently a Level must be beaten all at once - the "latestSlideCompleted" +variable in state is not persisted anywhere, and leaving the lesson does not restart the +user halfway through a level. Longer term, more granular tracking of user location would +be a nice enhancement. + +**Kind**: global class + +* [Slide](#Slide) + * [.unblock()](#Slide+unblock) + * [.saveProgress()](#Slide+saveProgress) + * [.componentDidUpdate()](#Slide+componentDidUpdate) + * [.componentDidMount()](#Slide+componentDidMount) + * [.hitDB()](#Slide+hitDB) + * [.editSlide()](#Slide+editSlide) + * [.advanceLevel()](#Slide+advanceLevel) + * [.toggleSkip()](#Slide+toggleSkip) + * [.toggleDiscussion()](#Slide+toggleDiscussion) + * [.onNewThread()](#Slide+onNewThread) + + +* * * + + + +### slide.unblock() +InputCode and Quiz slides are "blockers" in that they do not allow progress until a correct +answer is provided. This function is called when the user beats a slide. + +**Kind**: instance method of [Slide](#Slide) + +* * * + + + +### slide.saveProgress() +When the user reaches the final slide, write the level to the userprogress table. +If the user looks at the discussion board, this level is marked as "skipped", which +ultimately does not count towards overall completion%. Completing the level without +help marks the level as completed. + +**Kind**: instance method of [Slide](#Slide) + +* * * + + + +### slide.componentDidUpdate() +Slide.jsx handles all the transitions from slide to slide, so a lot of work need be done +when the user changes slides. The simplest case is beating a single slide, but they also +may have beaten this lesson (db write), reached a blocking slide, or be changing levels entirely + +**Kind**: instance method of [Slide](#Slide) + +* * * + + + +### slide.componentDidMount() +On mount, hit the DB and add the keyboard listener + +**Kind**: instance method of [Slide](#Slide) + +* * * + + + +### slide.hitDB() +Given the island / level / slide (lid, mlid, sid) from the URL params +Fetch the slides and userprogress from the db and start from the first slides + +**Kind**: instance method of [Slide](#Slide) + +* * * + + + +### slide.editSlide() +Admin-only direct link to the CMS to edit a slide's content + +**Kind**: instance method of [Slide](#Slide) + +* * * + + + +### slide.advanceLevel() +When the user goes to the next level, push the new URL and hard-reload. This should +be refactored to a more React-y state reset. + +**Kind**: instance method of [Slide](#Slide) + +* * * + + + +### slide.toggleSkip() +Show or hide the "show discussion" confirm/deny menu + +**Kind**: instance method of [Slide](#Slide) + +* * * + + + +### slide.toggleDiscussion() +Reveal or hide the discussion component. + +**Kind**: instance method of [Slide](#Slide) + +* * * + + + +### slide.onNewThread() +When a Discussion board posts a new thread, Slide needs to know this has happened. +This callback pushes the new thread onto the list so the "thread count" updates + +**Kind**: instance method of [Slide](#Slide) + +* * * + + + +## Splash +Simple splash page that lists about text for the Codelife Project + +**Kind**: global class + +* * * + + + +## Survey +Completed/Deprecated Survey Module from a 2017 Survey that followed a beta test in +Minas Gerais. Consists of Radio buttons and a DB post. + +**Kind**: global class + +* * * + + + +### survey.componentWillMount() +Grabs username from URL param, makes AJAX call to server and sets error +state (if no user is found) or overrides state (if one is). + +**Kind**: instance method of [Survey](#Survey) + +* * * + + ## threadInclude threadsRoute is used for retrieving threads and their associated comments. @@ -1836,6 +2943,92 @@ however the intention was that discussions could expand to encompass projects/co * * * + + +## cvMatch +Given a rule and a block of code, check the Javascript and perform an exact match +check on the regex. Used for things like "code must contain getElementById" + +**Kind**: global constant + +* * * + + + +## cvNests +Given a rule and a block of code, check that a given tag is nested inside another +tag. Used for things like "html nests body." Note that this does not currently +account for subsequent occurences (only checks for first occurences) + +**Kind**: global constant + +* * * + + + +## cvUses +Given a rule and a block of code, use a hard-coded regex to check for a SPECIFIC +pattern. Example include a for block "for (;;) {}", ifelse "if () {} else {}" +or a generic invocation of a function "functionName(){}" + +**Kind**: global constant + +* * * + + + +## attrCount +Given a needle (like h1), an attribute (like color), a value (like red), and a JSON +representation of the code as prepared by himalaya (HTML parser), recursively climb +down the nested json tree, testing at each node for the presence of the needle, +and if provided, whether that node has an attribute, and, if provided, whether that +attribute's value exactly matches the provided value. + +**Kind**: global constant + +* * * + + + +## cvContainsSelfClosingTag +Given a rule and a block of code, search for a self closing tag such as +Optionally run attrCount to check for extra rules (such as requiring "src") + +**Kind**: global constant + +* * * + + + +## cvContainsOne +Given a rule and a block of code, ensure that the given needle (such as ) +occurs once and only once in the code (useful for tags like body, head, html) + +**Kind**: global constant + +* * * + + + +## cvContainsTag +Given a rule and a block of code, check if a given tag (such as

) is included in the +code. Optionally, use attrCount to match any provided attributes or values in the rule. + +**Kind**: global constant + +* * * + + + +## cvContainsStyle +Given a rule and a block of code, using the "css" module to turn the css into a crawlable +object. Fold over that generated parsed object and drill down to check if the rule's property +matches the property and value of the css entered by the student. + +**Kind**: global constant + +* * * + ## flattenCodeBlock(user, cb) ⇒ Object diff --git a/app/components/CodeEditor/DrawerValidation.jsx b/app/components/CodeEditor/DrawerValidation.jsx index fa63d93..d0cecce 100644 --- a/app/components/CodeEditor/DrawerValidation.jsx +++ b/app/components/CodeEditor/DrawerValidation.jsx @@ -3,6 +3,13 @@ import {translate} from "react-i18next"; import "./DrawerValidation.css"; +/** + * The CodeEditor is responsible for handling violation of the rules set out for a given + * programming challenge. A list of errors is passed in as a prop - this component is + * responsible for creating a readable sentence form of what is wrong with the student's code. + * For example, "Your HTML code is missing an

tag". The format of this language is stored in + * the database as "Your {{p1}} is missing {{p2}}." (Note that this need be translated by lang) + */ class DrawerValidation extends Component { getErrorForRule(rule) { @@ -58,6 +65,11 @@ class DrawerValidation extends Component { { sortedRules.map((rule, i) => { const first = i === rules.indexOf(rules.find(r => r.needle === rule.needle)); + // for a given "needle" in a "haystack", group the rules underneath ONE needle. + // This is so things can be built like: + // h1: not included + // not nested inside body + // needs styling const family = sortedRules.filter(r => r.needle === rule.needle); const passing = 0; // const passing = family.filter(rule => rule.passing).length; diff --git a/app/pages/Slide.jsx b/app/pages/Slide.jsx index 43809b1..b1b66db 100644 --- a/app/pages/Slide.jsx +++ b/app/pages/Slide.jsx @@ -28,7 +28,11 @@ const compLookup = {TextImage, ImageText, TextText, TextCode, InputCode, RenderC /** * The slide component is the wrapper for all the various slidetypes in Codelife. However, * it interacts a great deal with the db and greater site, as reaching the last slide - * updates user progress, and each slide has a Discussion board beneath it. + * updates user progress, and each slide has a Discussion board beneath it. It's important + * to note that currently a Level must be beaten all at once - the "latestSlideCompleted" + * variable in state is not persisted anywhere, and leaving the lesson does not restart the + * user halfway through a level. Longer term, more granular tracking of user location would + * be a nice enhancement. */ class Slide extends Component { @@ -52,6 +56,10 @@ class Slide extends Component { }; } + /** + * InputCode and Quiz slides are "blockers" in that they do not allow progress until a correct + * answer is provided. This function is called when the user beats a slide. + */ unblock() { const {slides, currentSlide, latestSlideCompleted} = this.state; const i = slides.indexOf(currentSlide); @@ -60,6 +68,12 @@ class Slide extends Component { if (this.state.mounted) this.setState({latestSlideCompleted: newlatest, blocked: false}); } + /** + * When the user reaches the final slide, write the level to the userprogress table. + * If the user looks at the discussion board, this level is marked as "skipped", which + * ultimately does not count towards overall completion%. Completing the level without + * help marks the level as completed. + */ saveProgress(level) { const status = this.state.skipped ? "skipped" : "completed"; axios.post("/api/userprogress/save", {level, status}).then(resp => { @@ -67,12 +81,18 @@ class Slide extends Component { }); } + /** + * Slide.jsx handles all the transitions from slide to slide, so a lot of work need be done + * when the user changes slides. The simplest case is beating a single slide, but they also + * may have beaten this lesson (db write), reached a blocking slide, or be changing levels entirely + */ componentDidUpdate() { + // The level id (mlid) and slide id (sid) come in via URL params const {mlid, sid} = this.props.params; const {user} = this.props.auth; const {currentSlide, currentLevel, slides, sentProgress, latestSlideCompleted} = this.state; - // going to new level + // going to new level, reset most elements of state if (currentLevel && currentLevel.id !== mlid) { this.setState({ slides: [], @@ -95,8 +115,10 @@ class Slide extends Component { if (currentSlide && currentSlide.id !== sid) { const cs = slides.find(slide => slide.id === sid); if (cs) { + // if the new slide is inputcode/quiz, block the student from advancing let blocked = ["InputCode", "Quiz"].indexOf(cs.type) !== -1; if (slides.indexOf(cs) <= latestSlideCompleted) blocked = false; + // ... unless they have beaten it in the past if (this.state.done) blocked = false; this.setState({currentSlide: cs, blocked, showDiscussion: false}); } @@ -117,6 +139,9 @@ class Slide extends Component { } } + /** + * On mount, hit the DB and add the keyboard listener + */ componentDidMount() { this.setState({mounted: true}); @@ -125,6 +150,10 @@ class Slide extends Component { document.addEventListener("keypress", this.handleKey.bind(this)); } + /** + * Given the island / level / slide (lid, mlid, sid) from the URL params + * Fetch the slides and userprogress from the db and start from the first slides + */ hitDB() { const {lid, mlid} = this.props.params; let {sid} = this.props.params; @@ -163,12 +192,19 @@ class Slide extends Component { e.keyCode === 96 && this.props.auth.user.role > 0 ? this.unblock(this) : null; } + /** + * Admin-only direct link to the CMS to edit a slide's content + */ editSlide() { const {lid, mlid, sid} = this.props.params; const {browserHistory} = this.context; browserHistory.push(`/admin/lesson-builder/${lid}/${mlid}/${sid}`); } + /** + * When the user goes to the next level, push the new URL and hard-reload. This should + * be refactored to a more React-y state reset. + */ advanceLevel(mlid) { const {lid} = this.props.params; const {browserHistory} = this.context; @@ -176,6 +212,9 @@ class Slide extends Component { if (window) window.location.reload(); } + /** + * Show or hide the "show discussion" confirm/deny menu + */ toggleSkip() { if (!this.state.skipped) { this.setState({confirmSkipOpen: !this.state.confirmSkipOpen, showDiscussion: true, skipped: true}); @@ -185,6 +224,9 @@ class Slide extends Component { } } + /** + * Reveal or hide the discussion component. + */ toggleDiscussion() { if (!this.state.skipped) { this.setState({confirmSkipOpen: true}); @@ -194,6 +236,10 @@ class Slide extends Component { } } + /** + * When a Discussion board posts a new thread, Slide needs to know this has happened. + * This callback pushes the new thread onto the list so the "thread count" updates + */ onNewThread(thread) { const {currentSlide} = this.state; if (currentSlide) { @@ -215,6 +261,7 @@ class Slide extends Component { let SlideComponent = null; + // config for confetti const config = { angle: 270, spread: 180, @@ -229,6 +276,9 @@ class Slide extends Component { const sType = currentSlide.type; + // As mentioned earlier, there is no way to dynamically instantiate a component via + // a string identifier. As such, we look up the reference in a lookup table and + // instantiate it later SlideComponent = compLookup[sType]; return ( @@ -276,7 +326,12 @@ class Slide extends Component {
diff --git a/app/pages/Splash.jsx b/app/pages/Splash.jsx index 55da91b..74072a7 100644 --- a/app/pages/Splash.jsx +++ b/app/pages/Splash.jsx @@ -7,6 +7,10 @@ import Clouds from "components/Clouds"; import "./Splash.css"; +/** + * Simple splash page that lists about text for the Codelife Project + */ + class Splash extends Component { constructor() { diff --git a/app/pages/Survey.jsx b/app/pages/Survey.jsx index 9812d1d..3954a86 100644 --- a/app/pages/Survey.jsx +++ b/app/pages/Survey.jsx @@ -4,6 +4,11 @@ import {translate} from "react-i18next"; import {RadioGroup, Radio, Intent, Position, Toaster} from "@blueprintjs/core"; import LoadingSpinner from "components/LoadingSpinner"; +/** + * Completed/Deprecated Survey Module from a 2017 Survey that followed a beta test in + * Minas Gerais. Consists of Radio buttons and a DB post. + */ + class Survey extends Component { constructor(props) { diff --git a/app/reducers/index.js b/app/reducers/index.js index 5e98a22..7679a5d 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -1,3 +1,12 @@ +/** + * Reducers for Redux. It was noticed that a great deal of pages in Codelife would start + * their lifecycle by getting islands/levels/glossary so that decisions could be made about + * previous/next islands, userprogress, island themes, etc. To address this, the content + * was moved here, to a single Reducer, which is loaded in App.jsx, and all descending + * components make use of these preloaded objects. Longer term, it may make sense to promote + * more (maybe even ALL?) site data to this one-time load. + */ + export default { islands: (state = [], action) => { switch (action.type) { diff --git a/app/utils/codeValidation.js b/app/utils/codeValidation.js index 2adc8b4..f244f50 100644 --- a/app/utils/codeValidation.js +++ b/app/utils/codeValidation.js @@ -1,12 +1,30 @@ import css from "css"; +/** + * In order to check a student's codebase, each InputCode or Codeblock has its own set + * of rules. These are things like "CONTAINS h1" (html contains h1) or "JS_EQUALS total 10" + * (a running of the js results in the variable total being set to 10). When checking the + * student's code in CodeEditor, it makes use of these helper functions to determine whether + * the student has successfully passed this rule. + */ + +/** + * Given a rule and a block of code, check the Javascript and perform an exact match + * check on the regex. Used for things like "code must contain getElementById" + */ export const cvMatch = (rule, payload) => { const haystack = payload.theJS; return haystack.search(new RegExp(rule.regex)) >= 0; }; +/** + * Given a rule and a block of code, check that a given tag is nested inside another + * tag. Used for things like "html nests body." Note that this does not currently + * account for subsequent occurences (only checks for first occurences) + */ export const cvNests = (rule, payload) => { const haystack = payload.theText; + // get positions of the outer and inner tags, and ensure that they are in order const reOuter = new RegExp(`<${rule.outer}[^>]*>`, "g"); const outerOpen = haystack.search(reOuter); const outerClose = haystack.indexOf(``); @@ -17,6 +35,11 @@ export const cvNests = (rule, payload) => { outerOpen < innerOpen && innerOpen < innerClose && innerClose < outerClose && outerOpen < outerClose; }; +/** + * Given a rule and a block of code, use a hard-coded regex to check for a SPECIFIC + * pattern. Example include a for block "for (;;) {}", ifelse "if () {} else {}" + * or a generic invocation of a function "functionName(){}" + */ export const cvUses = (rule, payload) => { const haystack = payload.theJS; let re; @@ -33,6 +56,13 @@ export const cvUses = (rule, payload) => { return haystack.search(re) >= 0; }; +/** + * Given a needle (like h1), an attribute (like color), a value (like red), and a JSON + * representation of the code as prepared by himalaya (HTML parser), recursively climb + * down the nested json tree, testing at each node for the presence of the needle, + * and if provided, whether that node has an attribute, and, if provided, whether that + * attribute's value exactly matches the provided value. + */ export const attrCount = (needle, attribute, value, json) => { let count = 0; if (json.length === 0) return 0; @@ -58,6 +88,10 @@ export const attrCount = (needle, attribute, value, json) => { return count; }; +/** + * Given a rule and a block of code, search for a self closing tag such as + * Optionally run attrCount to check for extra rules (such as requiring "src") + */ export const cvContainsSelfClosingTag = (rule, payload) => { const html = payload.theText; const json = payload.theJSON; @@ -70,6 +104,10 @@ export const cvContainsSelfClosingTag = (rule, payload) => { return open !== -1 && hasAttr; }; +/** + * Given a rule and a block of code, ensure that the given needle (such as ) + * occurs once and only once in the code (useful for tags like body, head, html) + */ export const cvContainsOne = (rule, payload) => { const html = payload.theText; const re = new RegExp(`<${rule.needle}[^>]*>`, "g"); @@ -78,6 +116,10 @@ export const cvContainsOne = (rule, payload) => { return count === 1; }; +/** + * Given a rule and a block of code, check if a given tag (such as

) is included in the + * code. Optionally, use attrCount to match any provided attributes or values in the rule. + */ export const cvContainsTag = (rule, payload) => { const html = payload.theText; const json = payload.theJSON; @@ -92,6 +134,11 @@ export const cvContainsTag = (rule, payload) => { return tagClosed && hasAttr; }; +/** + * Given a rule and a block of code, using the "css" module to turn the css into a crawlable + * object. Fold over that generated parsed object and drill down to check if the rule's property + * matches the property and value of the css entered by the student. + */ export const cvContainsStyle = (rule, payload) => { const haystack = payload.theJSON; const needle = rule.needle; @@ -99,9 +146,11 @@ export const cvContainsStyle = (rule, payload) => { const value = rule.value; let head, html, style = null; let styleContent = ""; + // First, crawl through the students html to find the style tag if (haystack) html = haystack.find(e => e.tagName === "html"); if (html) head = html.children.find(e => e.tagName === "head"); if (head) style = head.children.find(e => e.tagName === "style"); + // Grab the CSS out of the style tag and parse it if (style && style.children && style.children[0]) styleContent = style.children[0].content; if (!styleContent) styleContent = ""; const obj = css.parse(styleContent, {silent: true}); diff --git a/sandbox/sandbox.js b/sandbox/sandbox.js index 62be6f5..a9524c7 100644 --- a/sandbox/sandbox.js +++ b/sandbox/sandbox.js @@ -1,3 +1,18 @@ +/** + * Sandbox is the special landing page where student content is rendered. It is currently + * hosted on codelife.tech/[filename].html, where [filename] is the URL from which the + * request is coming, with dashes instead of dots (e.g., codelife.com -> codelife-com.html). + * The Long List of files you see in this directory are unique landing pages for each of the + * possible URL iterations of Codelife. The reason we need a landing page for each of these + * is because the remote rendering makes use of the HTML method postMessage(), which allows + * a payload to be sent across URLs. postMessage requires that the host be explicitly set - + * each landing page listens ONLY for requests from its host, and uses this file to render + * the code in an iframe. postMessage is also used to return status messages to the requester, + * so that the CodeEditor can show which rules are passing and which are failing based on the + * remote execution of the javascript code. + */ + +// loopProtect is an open source library that catches infinite loops in js code. loopProtect.alias = 'protect'; window.loopProtect = loopProtect; @@ -8,6 +23,8 @@ loopProtect.hit = function (line) { if (source) source.postMessage(["catch", "Potential Infinite Loop found on line " + line], host); } +// isNode, isElement, and isNodeList are all catches for when the user attempts to console.log +// an html element. postMessage cannot directly transmit html Nodes, so they must be stringified function isNode(o) { return ( typeof Node === "object" ? o instanceof Node : @@ -31,8 +48,28 @@ function isNodeList(nodes) { (nodes.length === 0 || (typeof nodes[0] === "object" && nodes[0].nodeType > 0)); } +// listen for postMessage() window.addEventListener("message", receiveMessage, false); +/** + * For a full understanding of what's happening here, check CodeEditor.jsx. Before the + * students code is sent via postMessage, it is edited. One of the edits made is that + * a series of "parent.myPost()" functions are injected to the end of the student's js. + * By the time the student's code arrives HERE and is actually executed, parent.myPost + * will refer to THIS function. The job of this function is to receive the arguments + * from that injected javascript, and use postMessage to send the results of that function + * BACK to the react side. + * As an example - say rule 3 is that the user's javascript sets the value of "total" + * to 10. Before we send the students code here to be rendered in an iframe, we edit the code: + * -------------------------- + * total = undefined; + * [ original student code goes here] + * parent.myPost(rule3, total === 10) + * -------------------------- + * This way, the ACTUAL value of total in the VM can be tested. When the injected student code calls + * parent.myPost, it invokes this function, which then sends the results of that BACK THROUGH + * postMessage to the original window - so that we can tell the student if they are correct or not. + */ window.myPost = function() { const payload = []; for (const a of arguments) { @@ -47,14 +84,26 @@ window.myPost = function() { payload.push(arg); } } + // remember - host is determined by which html file (e.g. codelife-com.html) is embedding sandbox.html source.postMessage(payload, host); } +/** + * Listen for messages from the React host. Most of these will be complete web pages made + * by students, so the default behavior is to just inject it directly into an iframe + */ function receiveMessage(event) { if (event.origin !== host) return; source = event.source; + /** + * It takes some time for the iframe to "wake up" and finish being embedded on the page. + * There was no way on the React side to detect when the iframe was ready for writing, + * so for the first second or two, React sends wakeup messages every ~50ms or so until it + * receives "awake" back from this page. Once that has happened, simply dump the code + * into an iframe every time. + */ if (event.data === "wakeup") { source.postMessage("awake", host); } @@ -66,6 +115,7 @@ function receiveMessage(event) { win.protect = loopProtect; + // if URL is requested with ?screenshot=true, hide scrollbars (smile for the camera!) var code = event.data; if (isScreenshot) { if (code.indexOf("= 0) {