Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
f2025dc
Testing packages for scraping FreeCodeCamp
magoun Oct 16, 2018
8cac83c
Fix unfolding of FCC curriculum map before scraping html
magoun Oct 16, 2018
bc26431
Flesh out scraping logic for mapping FreeCodeCamp curriculum
magoun Oct 17, 2018
8ca5df6
Clean intro items from each subsection
magoun Oct 17, 2018
a579022
Refactor curriculum mapper into function call
magoun Oct 17, 2018
87344ab
Move curriculum mapping into lesson_map.js and refactor
magoun Oct 17, 2018
4d2d00e
Add usage note in header
magoun Oct 17, 2018
05a5cbb
Rename playground file to wip (work in progress)
magoun Oct 17, 2018
92013dd
Begin rewriting instructions for new scraper
magoun Oct 17, 2018
61dec32
Add check for React components loading, just in case
magoun Oct 17, 2018
2135837
Refactor student completion parsing
magoun Oct 17, 2018
855e7f0
Move logic for student progress shaping from wip.js to fcc_progress.js
magoun Oct 17, 2018
c969cf0
Scraped curriculum from 17 Oct 2018
magoun Oct 17, 2018
413cb98
Rename output file for clarity
magoun Oct 17, 2018
c8778cc
Rename output file and store scraped curriculum
magoun Oct 17, 2018
546b0bd
Add a file to accept a list of fcc profiles for checking
magoun Oct 18, 2018
9c1eab1
Begin speccing out logic for multiple profile scrapes with a single call
magoun Oct 18, 2018
977d2b0
Do not track progress reports pulled from fcc_progress.js
magoun Oct 18, 2018
cc03be2
Add comment to split call
magoun Oct 18, 2018
ff49443
Add JSON for new Greenville codes Curriculum
magoun Oct 18, 2018
bf0842a
Update progress checker to use Greenville Codes curriculum
magoun Oct 18, 2018
3235953
Update master curriculum to pull from gvl_codes_path.json
magoun Oct 19, 2018
b834a47
Finish working logic for bulk student progress scraping
magoun Oct 19, 2018
7af0e53
Change format to csv and rename for consistency
magoun Oct 19, 2018
9f63750
Remove line to clear write file, no longer needed
magoun Oct 19, 2018
63ac050
No longer needed for testing
magoun Oct 19, 2018
9f48bec
Move input/output declarations to script head for ease of use
magoun Oct 19, 2018
48ab862
Rename to match similar files
magoun Oct 19, 2018
9fcdff0
Update with current usage
magoun Oct 19, 2018
b794e25
Rename for clarity of use
magoun Oct 19, 2018
4254946
Update file references
magoun Oct 19, 2018
4c6d81e
Correct misnamed bulk/non-bulk files
magoun Oct 19, 2018
806e9f4
Remove unnessary dependency
magoun Oct 28, 2018
482bf2d
Add section on troubleshooting
magoun Oct 28, 2018
9fb655d
Add more to troubleshooting section
magoun Nov 6, 2018
1d235ae
Add debugging information
magoun Nov 6, 2018
582b0ce
Make debugging text standard and implement URL validation
magoun Nov 7, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fcc_progress.json
36 changes: 14 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,27 @@
# Greenville Codes Student Progress Tracker

## Overview

This script takes a newline-delimited list of FreeCodeCamp profile URLs and returns a report on their progress as a newline-delimited list of tab-delimited percentages.
This script takes a newline-delimited list of FreeCodeCamp profile URLs and returns a report on their progress as a newline-delimited list of comma-separated percentages. These can be easily pasted into a Google sheet used for tracking a cohort's progress through the Greenville Codes program.

## Installation

1. [Install Tampermonkey.](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo)
2. Navigate to the Utilities tab in Tampermonkey options.
3. Paste the following address into the input marked "URL" and click "Import" <https://raw.githubusercontent.com/dacohenii/fcc-progress/master/bulk-fcc-progress.js>
4. Confirm the installation.

![Installation](assets/install.gif)
Requires Node v7.6 or higher (testing done with v10.1.0).
Clone the repo and cd into it.
Install the dependencies with `npm install`.

## Usage

_Note: This script sends a request for all these URLs essentially at the same time, so and hasn't been tested for more than ~30 at a time. Too many more and their serers might not appreciate it._

1. Copy a spreadsheet column (or newline-separated list) of URLs to be scraped to the clipboard.
- empty cells / lines are okay and will be preserved on paste.
2. Invoke the script via the Tampermonkey menu.
3. A prompt should come up. Paste the addresses into the prompt and submit it.
4. At this point, it will start retrieving results.
5. A notification should appear to let you know the process is complete. When this happens select the top-left destination cell and paste the result.

## Misc. Notes
Create the FreeCodeCamp curriculum map with `node map_fcc_path.js`.
It will create fcc_path.json or overwrite the existing file.

The main functionality is in `bulk-fcc-progress.js`.
Use `node get_fcc_progress.js` to get a detailed progress report for a single FCC profile. You can specify to track the overall FCC curriculum or the Greenville Codes specific path in the file head.

`fcc-progress.js` scrapes a single profile and must be run directly from that page.
Use `node get_bulk_fcc_progress.js` to get a csv report of a full list of student profiles. By default, these are pulled from fcc_profiles.txt, but this can be modified in the file head.

This was developed and tested using Tampermonkey and Chrome 60. YMMV.
## Troubleshooting
Running `node get_bulk_fcc_progress.js` with an invalid url will return an error. This includes newline characters at the end of the file! I'll work on fixing this.

The lessons are currently hard-coded in the files. This can be pulled dynamically in the future, but they don't seem to change too often.
I ran into a few problems getting this to work on another computer. My errors were related to dependencies for Chrome. I resolved them by following the advice [here](https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md).

Also, it's a web scraper, which is likely to break at some point when they change their website, but that's okay, becuase the next version will probably use an API,
`npm install` loads the dependencies locally, so `ldd chrome | grep not` won't work in the base directory. You'll have to navigate all the way down to the node_modules directory for puppeteer, through the .local-chromium directory until you find the chrome binary, then run the command. This should give you the missing dependencies that need to be installed.
Binary file removed assets/install.gif
Binary file not shown.
464 changes: 0 additions & 464 deletions bulk-fcc-progress.js

This file was deleted.

404 changes: 0 additions & 404 deletions fcc-progress.js

This file was deleted.

1,507 changes: 1,507 additions & 0 deletions fcc_path.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions fcc_profiles.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
https://www.freecodecamp.org/magoun
https://www.freecodecamp.org/thirtythreeb
https://www.freecodecamp.org/jaaron0606
131 changes: 131 additions & 0 deletions get_bulk_fcc_progress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* Scrapes multiple user profile from fcc_profiles.txt and
* compares completed challenges against the Greenville Codes curriculum.
*
* Track Greenville Codes progress with gvl_codes_path.json.
*
* Usage: node bulk_fcc_progress.js
*
*/

const outputFile = 'student_progress.csv',
urlFile = 'fcc_profiles.txt',
trackFile = 'gvl_codes_path.json';

const fs = require('fs');
const puppeteer = require('puppeteer');
const URL = require('url');

// Function for filtering bad inputs from urlFile
let validateURL = (str) => {
const url = URL.parse(str);

if (url.hostname) {
return true;
} else {
console.log('Skipping ' + str + ' (invalid URL)');
return false;
}
};

// Read list of profile urls
// Returns an array of profile URLs
let readURLs = (file) => {
let profiles = fs.readFileSync(file, 'utf8');
console.log('Reading profile urls from ' + urlFile);

// Splitting the return on new line yields an array
return profiles.split('\n').filter(validateURL);
};

// Fetch html using puppeteer
let getHTML = async (url) => {
console.log('Getting HTML from ' + url);

const browser = await puppeteer.launch();
const page = await browser.newPage();

// Wait for React components to load
await page.goto(url, { waitUntil: 'networkidle0'});

const html = await page.content();
await browser.close();

return html;
};

// Parse the HTML for completed challenges
// Returns an array of challenge names
let parseHTML = (html) => {
const $ = require('cheerio').load(html);
let challenges = [];

$('tr', 'tbody').each(function (index, element) {
let challenge = $(this).find('a').text();
challenges.push(challenge);
});

return challenges;
};

// Compare progress to overall curriculum
// Returns a comma delimited string
let getProgress = (completedChallenges) => {
const rawJSON = fs.readFileSync(trackFile);
let challengeMap = JSON.parse(rawJSON);

// Inconsistent capitalization, thus map to lower case
completedChallenges = completedChallenges.map(x => x.toLowerCase());

let progress = '';

// Loop through curriculum JSON
for (let section of Object.keys(challengeMap)) {
for (let subsection of Object.keys(challengeMap[section])) {
let numChallenges = challengeMap[section][subsection].length;

// Inconsistent capitalization, thus map to lower case
let subsectionArray = challengeMap[section][subsection].map(x => x.toLowerCase());

let numCompleted = subsectionArray.reduce((acc, val) => {
return completedChallenges.indexOf(val) != -1 ? ++acc : acc;
}, 0);

progress += `${Math.round(100 * numCompleted / numChallenges)}%` + ',';
}
}

return progress;
};

// Writes the url and progress strings to a file
let writeProgress = (writeArray, file) => {
const outputString = writeArray.join('\n');

fs.writeFile(file, outputString, (err) => {
// Report success / failure
const successMessage = 'Greenville Codes progress was written to ' + file;

err ? console.log(err) : console.log(successMessage);
});
};

// Runs the program
let run = async () => {
// Get urls from urlFile
const urlArray = readURLs(urlFile);
let writeArray = [];

// Assemble student progress into array
for (let url of urlArray) {
let progress = await getHTML(url)
.then(parseHTML)
.then(getProgress);

writeArray[urlArray.indexOf(url)] = url + ',' + progress;
}

writeProgress(writeArray, outputFile);
};

run();
95 changes: 95 additions & 0 deletions get_fcc_progress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* Scrapes a user profile from https://www.freecodecamp.org/{username} and
* compares completed challenges against the FCC curriculum. The results are
* output in JSON format to outputFile.
*
* Track Greenville Codes progress with gvl_codes_path.json.
* Track overall FreeCodeCamp progress with fcc_path.json.
*
* Usage: node fcc_progress.js
*
*/

const outputFile = 'fcc_progress.json',
url = 'https://www.freecodecamp.org/magoun',
trackFile = 'gvl_codes_path.json';

const fs = require('fs');
const puppeteer = require('puppeteer');

// Fetch html using puppeteer
let getHTML = async (url) => {
const browser = await puppeteer.launch();
const page = await browser.newPage();

// Wait for React components to load
await page.goto(url, { waitUntil: 'networkidle0'});

const html = await page.content();
await browser.close();
return html;
};

// Parse the HTML for completed challenges
// Returns an array of challenge names
let parseHTML = (html) => {
const $ = require('cheerio').load(html);
let challenges = [];

$('tr', 'tbody').each(function (index, element) {
let challenge = $(this).find('a').text();
challenges.push(challenge);
});

return challenges;
};

// Compare progress to overall curriculum
// Returns a JSON object
let getProgress = (completedChallenges) => {
const rawJSON = fs.readFileSync(trackFile);
let challengeMap = JSON.parse(rawJSON);

// Inconsistent capitalization, thus map to lower case
completedChallenges = completedChallenges.map(x => x.toLowerCase());

// Loop through curriculum JSON
for (let section of Object.keys(challengeMap)) {
for (let subsection of Object.keys(challengeMap[section])) {
let numChallenges = challengeMap[section][subsection].length;

// Inconsistent capitalization, thus map to lower case
let subsectionArray = challengeMap[section][subsection].map(x => x.toLowerCase());

let numCompleted = subsectionArray.reduce((acc, val) => {
return completedChallenges.indexOf(val) != -1 ? ++acc : acc;
}, 0);

challengeMap[section][subsection] = `${Math.round(100 * numCompleted / numChallenges)}%`;
}
}

return challengeMap;
};

// Writes the JSON object to a file
let writeJSON = (json) => {
// Format the json for readability
const jsonString = JSON.stringify(json, null, 2);

fs.writeFile(outputFile, jsonString, (err) => {
// Report success / failure
const successMessage = 'Greenville Codes progress was written to ' + outputFile;
err ? console.log(err) : console.log(successMessage);
});
};

// Runs the program
let run = () => {
getHTML(url)
.then(parseHTML)
.then(getProgress)
.then(writeJSON);
};

run();
Loading