Skip to content

Instantly share code, notes, and snippets.

@JoshCheek
Created April 26, 2016 02:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JoshCheek/181663ae1324bd6a0e0024ccf3afb023 to your computer and use it in GitHub Desktop.
Save JoshCheek/181663ae1324bd6a0e0024ccf3afb023 to your computer and use it in GitHub Desktop.
FCC challenges
require 'nokogiri'
require 'rest-client'
require 'erb'
def get_html(url, filename)
one_hour = 1*60*60
stale_time = Time.now - one_hour
if File.exist?(filename) && stale_time < File.stat(filename).mtime
File.read filename
else
$stderr.puts "Fetching #{url}"
raw_html = RestClient.get url
File.write filename, raw_html
raw_html
end
end
def traverse(element, &callback)
element.children.each do |child|
callback.call(child)
traverse child, &callback
end
end
def get_programs(doc)
programs = {}
current_program = nil
challenge_set = nil
traverse doc.at_css('.map-accordion') do |element|
case element.name
when 'h2'
text = element.text.gsub(/\W+/, ' ').strip
current_program = {}
programs[text] = current_program
when 'h3'
text = element.text.gsub(/\W+/, ' ').strip
challenge_set = []
current_program[text] = challenge_set
when 'p'
classes = element['class'].split
if classes.include?('challenge-title') && !classes.include?('disabled')
# there is a: element['name']
# but it doesn't seem to be used
text = element.at_css('a').children[0].text
challenge_set << text
end
end
end
programs
end
def get_completions(doc, corrections)
completions = []
traverse doc.at_css('body') do |element|
next unless element.name == 'tr'
# <tr>
# <td class="col-xs-5 hidden-xs">Reverse a String</td>
# <td class="col-xs-2 hidden-xs">Apr 08, 2016</td>
# <td class="col-xs-2 hidden-xs">Apr 08, 2016</td>
# <td class="col-xs-2 hidden-xs">
# <a href="/challenges/Reverse a String?solution=function%20reverseString(str)%20%7B%0A%20%20var%20s%20%3D%20%22%22%3B%0A%20%20for(var%20i%3Dstr.length-1%3B%20i%20%3E%3D%200%3B%20--i)%0A%20%20%20%20s%20%2B%3D%20str%5Bi%5D%3B%0A%20%20%0A%20%20return%20s%3B%0A%7D%0A%0AreverseString(%22hello%22)%3B%0A" target="_blank">View solution</a>
# </td>
# <td class="col-xs-12 visible-xs">
# <a href="/challenges/Reverse a String?solution=function%20reverseString(str)%20%7B%0A%20%20var%20s%20%3D%20%22%22%3B%0A%20%20for(var%20i%3Dstr.length-1%3B%20i%20%3E%3D%200%3B%20--i)%0A%20%20%20%20s%20%2B%3D%20str%5Bi%5D%3B%0A%20%20%0A%20%20return%20s%3B%0A%7D%0A%0AreverseString(%22hello%22)%3B%0A" target="_blank">Reverse a String</a>
# </td>
# </tr>
a = element.at_css('a')
next if !a # the row is a header
name = element.at_css('td').text
name = corrections.fetch(name, name)
complete = (a.text == 'View solution')
link = File.join("https://www.freecodecamp.com", a['href'])
completions << {name: name, link: link, complete: complete}
end
completions
end
def summary(programs, students, removed)
all_solved = students.flat_map { |_, s| s[:solutions] }.map { |s| s[:name] }.uniq
all_solutions = programs.values.map(&:values).flatten
unaccounted_for = all_solved - all_solutions - removed
html = <<-HTML
<!doctype html>
<html>
<head>
<title>Student FCC Summary</title>
<style>
body {
padding: 2em;
}
table {
border: 0.3em solid #888;
background-color: #DDD;
padding: 1em;
}
.program-name {
text-align: center;
background-color: #555;
color: #FFF;
font-weight: bold;
font-size: 1.2em;
}
.table-header {
background-color: #888;
color: #EEE;
}
.challenge-name {
text-align: center;
background-color: #AAA;
color: #FFF;
}
</style>
</head>
<body>
<% if unaccounted_for.any? %>
<h2>Unaccounted for</h2>
<ul class="unaccounted-for">
<% unaccounted_for.each do |challenge| %>
<li><%= challenge %></li>
<% end %>
</ul>
<% end %>
<table>
<% programs.each do |program_name, challenge_sets| %>
<tr>
<td class="program-name" colspan="<%=h students.length+1 %>"><%=h program_name %></td>
</tr>
<% challenge_sets.each do |challenge_name, challenges| %>
<tr class="table-header">
<th>Challenge</td>
<% students.each do |name, student| %>
<th><%=h name.capitalize %></td>
<% end %>
</tr>
<tr>
<td class="challenge-name" colspan="<%=h students.length+1 %>"><%=h challenge_name %></td>
</tr>
<% challenges.each do |challenge| %>
<tr>
<td><%=h challenge %></td>
<% students.each do |student_name, student_data| %>
<td>
<% solution = student_data[:solutions].find { |solution| solution[:name] == challenge } %>
<% if solution && solution[:complete] %>
<a href="<%=h solution[:link] %>">Complete</a>
<% elsif solution %>
<a href="<%=h solution[:link] %>">Incomplete</a>
<% else %>
Unattempted
<% end %>
</td>
<% end %>
</tr>
<% end %>
<% end %>
<% end %>
</table>
</body>
</html>
HTML
ERB.new(html).result(binding)
end
# the cache dir
Dir.mkdir 'cache' unless Dir.exist?('cache')
# get the challenges
doc = Nokogiri::HTML(get_html('https://www.freecodecamp.com/map', 'cache/fcc_map.html'))
programs = get_programs(doc)
# get each student's progress
students = {
javi: {
username: 'Javi-Rev',
solutions: [],
},
sean: {
username: 'SeanGallen',
solutions: [],
},
matthew: {
username: 'MatthewSwan',
solutions: [],
},
dongmin: {
username: 'medi86',
solutions: [],
},
james: {
username: 'JamesMarkWilton',
solutions: [],
},
megan: {
username: 'darkstarre',
solutions: [],
},
greg: {
username: '101glover',
solutions: [],
},
vince: {
username: 'Vince331',
solutions: [],
},
derreck: {
username: 'DerreckM',
solutions: [],
},
josh: {
username: 'JoshCheek',
solutions: [],
},
}
corrections = {
'Use Bracket Notation to Find the NthtoLast Character in a String' => 'Use Bracket Notation to Find the Nth-to-Last Character in a String',
'Access MultiDimensional Arrays With Indexes' => 'Access Multi-Dimensional Arrays With Indexes',
'Manipulate Arrays With push' => 'Manipulate Arrays With push()',
'Manipulate Arrays With pop' => 'Manipulate Arrays With pop()',
'Manipulate Arrays With shift' => 'Manipulate Arrays With shift()',
'Manipulate Arrays With unshift' => 'Manipulate Arrays With unshift()',
'Global vs Local Scope in Functions' => 'Global vs. Local Scope in Functions',
'Introducing JavaScript Object Notation JSON' => 'Introducing JavaScript Object Notation (JSON)',
'Iterate over Arrays with map' => 'Iterate over Arrays with .map',
'Condense arrays with reduce' => 'Condense arrays with .reduce',
'Filter Arrays with filter' => 'Filter Arrays with .filter',
'Sort Arrays with sort' => 'Sort Arrays with .sort',
'Concatenate Arrays with concat' => 'Reverse Arrays with .reverse',
'Split Strings with split' => 'Concatenate Arrays with .concat',
'Join Strings with join' => 'Split Strings with .split',
'Reverse Arrays with reverse' => 'Join Strings with .join',
'Concatenate Strings with concat' => 'Concatenate Arrays with .concat',
}
removed = [
'Use RGB to Color Elements Gray',
'Browse Camper News',
'Reference our Wiki',
'Perform Arithmetic Operations on Decimals with JavaScript',
'Create a JavaScript Slot Machine',
'Add your JavaScript Slot Machine Slots',
'Bring your JavaScript Slot Machine to Life',
'Give your JavaScript Slot Machine some Stylish Images',
'Watch us Code Live on Twitchtv',
'Meet Bonfire',
]
students.each do |name, student|
raw_html = get_html("https://www.freecodecamp.com/#{student[:username]}", "cache/#{name}.html")
doc = Nokogiri::HTML(raw_html)
student[:solutions].concat(get_completions(doc, corrections))
end
def h(text)
ERB::Util.html_escape(text)
end
filename = 'result.html'
File.write filename, summary(programs, students, removed)
$stderr.puts "Result saved in #{filename} ($ open #{filename})"
@BrantDFaulkner
Copy link

-DerreckM
+fcc809bd3b7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment