Skip to content

Commit

Permalink
documents utils, tools, and last of pages
Browse files Browse the repository at this point in the history
  • Loading branch information
jhmullen committed Jul 11, 2018
1 parent 6a610ff commit 16d5df4
Show file tree
Hide file tree
Showing 8 changed files with 1,497 additions and 120 deletions.
1,429 changes: 1,311 additions & 118 deletions DOCS.md

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions app/components/CodeEditor/DrawerValidation.jsx
Expand Up @@ -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 <h1> 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) {
Expand Down Expand Up @@ -58,6 +65,11 @@ class DrawerValidation extends Component {
<tbody>
{ 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;
Expand Down
59 changes: 57 additions & 2 deletions app/pages/Slide.jsx
Expand Up @@ -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 {
Expand All @@ -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);
Expand All @@ -60,19 +68,31 @@ 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 => {
resp.status === 200 ? console.log("success") : console.log("error");
});
}

/**
* 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: [],
Expand All @@ -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});
}
Expand All @@ -117,6 +139,9 @@ class Slide extends Component {
}
}

/**
* On mount, hit the DB and add the keyboard listener
*/
componentDidMount() {
this.setState({mounted: true});

Expand All @@ -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;
Expand Down Expand Up @@ -163,19 +192,29 @@ 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;
browserHistory.push(`/island/${lid}/${mlid}`);
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});
Expand All @@ -185,6 +224,9 @@ class Slide extends Component {
}
}

/**
* Reveal or hide the discussion component.
*/
toggleDiscussion() {
if (!this.state.skipped) {
this.setState({confirmSkipOpen: true});
Expand All @@ -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) {
Expand All @@ -215,6 +261,7 @@ class Slide extends Component {

let SlideComponent = null;

// config for confetti
const config = {
angle: 270,
spread: 180,
Expand All @@ -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 (
Expand Down Expand Up @@ -276,7 +326,12 @@ class Slide extends Component {

<SlideComponent
island={currentIsland.theme}
// If this slide component is InputCode or Quiz, hook up the callback
// to unblock Slide.jsx when the user gets it right
unblock={this.unblock.bind(this)}
// This may be confusing, it is the only place in codelife where the data is
// DIRECTLY prop'd into the component as opposed to something like slideData=currentSlide.
// This means the properties are available directly in this.props inside each slide.
{...currentSlide} />

<div className="slide-footer">
Expand Down
4 changes: 4 additions & 0 deletions app/pages/Splash.jsx
Expand Up @@ -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() {
Expand Down
5 changes: 5 additions & 0 deletions app/pages/Survey.jsx
Expand Up @@ -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) {
Expand Down
9 changes: 9 additions & 0 deletions 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) {
Expand Down
49 changes: 49 additions & 0 deletions 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(`</${rule.outer}>`);
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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 <img />
* 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;
Expand All @@ -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 <html>)
* 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");
Expand All @@ -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 <p>) 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;
Expand All @@ -92,16 +134,23 @@ 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;
const property = rule.property;
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});
Expand Down

0 comments on commit 16d5df4

Please sign in to comment.