Skip to content

Commit

Permalink
working version
Browse files Browse the repository at this point in the history
  • Loading branch information
armandofox committed May 21, 2015
1 parent 648a91b commit c1bcbb3
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 74 deletions.
3 changes: 3 additions & 0 deletions hw2edxml/Gemfile
@@ -0,0 +1,3 @@
source 'https://rubygems.org'

gem 'redcarpet'
10 changes: 10 additions & 0 deletions hw2edxml/Gemfile.lock
@@ -0,0 +1,10 @@
GEM
remote: https://rubygems.org/
specs:
redcarpet (3.2.3)

PLATFORMS
ruby

DEPENDENCIES
redcarpet
2 changes: 1 addition & 1 deletion hw2edxml/README.md
Expand Up @@ -22,7 +22,7 @@ Each toplevel heading (`<h1>`-equivalent) becomes a vertical.

Within a vertical (that is, between two `<h1>`s):

* an element with `<div class="ruql" data-display-name="Foobar">` is passed
* an element with `<pre class="ruql" data-display-name="Foobar">` is passed
to the RuQL edXML generator and the results are packaged as an edXML
`<problem>` element whose student-visible title will be the value of the
`data-display-name` attribute.
Expand Down
54 changes: 22 additions & 32 deletions hw2edxml/lib/hw2edxml.rb
@@ -1,5 +1,5 @@
require 'yaml'
require_relative 'hw2edxml/xml_writer'
require 'fileutils'
require_relative 'hw2edxml/chunk'
require_relative 'hw2edxml/chunker'

Expand All @@ -22,46 +22,36 @@ def run!
die_with "File not found: " + e.message +
"\nMake sure you're running from the root directory of a homework assignment."
end
check_for_missing_keys!
generate_xml_files!
end

def initialize
@chunks = []
begin
make_directories!
rescue RuntimeError => e
die_with "Unable to create directories: #{e.message}"
end
begin
generate_xml_files!
rescue RuntimeError => e
die_with "Error generating XML: #{e.message}"
end
end


private

def generate_xml_files!
write_toplevel_files!
chunks.each do |chunk|
problem_name = chunk.file_path
begin
File.open("#{problem_name}.xml", "w") do |f|
f.write chunk.to_edxml
end
rescue RuntimeError => e
die_with "Writing #{problem_name}.xml: " + e.message
end
def make_directories!
basename = 'studio'
begin
Dir.mkdir basename
rescue Errno::EEXIST
die_with "Directory '#{basename}' exists and would be overwritten, please remove it"
end
%w(sequential html vertical problem).each { |d| Dir.mkdir "#{basename}/#{d}" }
end

def write_toplevel_files!

end

def check_for_missing_keys!
missing_from_config = chunks.keys - config.keys
unless missing_from_config.empty?
die_with "Keys #{missing_from_config.join ','} referenced in README.md but not in config.yml"
end
missing_from_chunks = config.keys - chunks.keys
unless missing_from_chunks.empty?
die_with "Keys #{missing_from_chunks.join ','} referenced in config.yml but not in README.md"
def generate_xml_files!
chunks.each do |chunk|
chunk.write_self!
end
end

def read_config(file)
begin
YAML::load(IO.read file)
Expand Down
5 changes: 4 additions & 1 deletion hw2edxml/lib/hw2edxml/autograder_chunk.rb
Expand Up @@ -13,9 +13,11 @@ def initialize(elt)
@grader_payload = elt.attribute('data-grader-payload').to_s
@points = elt.attribute('data-points').to_s
@queue_name = Hw2Edxml.config[:queue_name]
super(elt.to_s, Hw2Edxml::Chunk.name_for(elt))
super(elt.elements.map(&:to_s).join(''), Hw2Edxml::Chunk.name_for(elt))
end

def type ; :problem ; end

def to_edxml
@xml.problem do
@xml.startouttext
Expand All @@ -26,6 +28,7 @@ def to_edxml
@output
end


def autograder_form
form = ''
xml = Builder::XmlMarkup.new(:target => form, :indent => 2)
Expand Down
29 changes: 21 additions & 8 deletions hw2edxml/lib/hw2edxml/chunk.rb
@@ -1,8 +1,10 @@
require 'rexml/document'
require_relative 'autograder_chunk'

class Hw2Edxml
class Chunk
require_relative 'autograder_chunk'
require_relative 'vertical_chunk'

# Encapsulates a chunk of content that will become part of a Vertical.

# +type+ may be one of +:autograder+, +:ruql+, or +:html+
Expand All @@ -17,6 +19,9 @@ class Chunk
# The display name, usually from +data-display-name+ attribute
attr_accessor :display_name

# One or more child chunks (recursive data structure)
attr_accessor :chunks

# A randomly-generated ID string used as edX object identifier
def id
@id ||= Chunk.random_id
Expand All @@ -28,7 +33,7 @@ def initialize(raw_data='', display_name='')
@raw_data = raw_data
@display_name = display_name
@output = ''
@xml = Builder::XmlMarkup.new(:target => @output)
@xml = Builder::XmlMarkup.new(:target => @output, :indent => 2)
end

def type
Expand All @@ -37,7 +42,7 @@ def type

# Return file path relative to current directory
def file_path

"studio/#{type}/#{id}.xml"
end

protected
Expand All @@ -55,13 +60,14 @@ def self.random_id
Array.new(4) { sprintf("%08x", rand(2**32)) }.join('')
end

end
public

# Chunk type that starts a new page
class VerticalChunk < Chunk
def initialize(elt)
super(elt.to_s, elt.text)
def write_self!
File.open(file_path, "w") do |f|
f.write self.to_edxml
end
end

end

class HtmlChunk < Chunk
Expand All @@ -71,12 +77,19 @@ def initialize
def append_content(elt)
@raw_data << elt.to_s
end
def to_edxml
@raw_data
end
end

class RuqlChunk < Chunk
def initialize(elt)
super(elt.texts.join(''), Chunk.name_for(elt))
end
def type ; :problem ; end
def to_edxml
"(RuQL support pending)"
end
end

end
52 changes: 29 additions & 23 deletions hw2edxml/lib/hw2edxml/chunker.rb
@@ -1,13 +1,17 @@
require 'github/markup'
require 'redcarpet'
require 'rexml/document'
require 'debugger'

class Hw2Edxml
class Chunker
attr_reader :chunks
def initialize(filename)
html = GitHub::Markup.render(filename)
@doc = REXML::Document.new(html)
markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML, :fenced_code_blocks => true, :tables => true)
orig_md = File.read filename
html = markdown.render(orig_md)
wrapped_html = '<!DOCTYPE html><html><body>' << html << '</body></html>'
File.open("/tmp/tmp.html", "w") { |f| f.puts wrapped_html }
@doc = REXML::Document.new(wrapped_html)
@elements = []
validate_html
@chunks = extract_chunks
Expand All @@ -19,34 +23,36 @@ def validate_html
raise(ArgumentError, 'root node must be <html>') unless @doc.root.name == 'html'
raise(ArgumentError, 'no <body> found') unless @doc.root.elements['/html/body']
@elements = @doc.get_elements('/html/body/*')
first_elt = @elements.first
raise(ArgumentError, 'first element must be <h1>, <script language="ruql">, or <div class="autograder">') unless
ruql?(first_elt) || autograder?(first_elt) || vertical?(first_elt)
# check divs have unique names
divs = @doc.get_elements("/html/body/div[@class='autograder']")
if divs.any? { |elt| elt.attribute('data-display-name').to_s.empty? }
raise(ArgumentError, "all autograder <div>s must have a 'data-display-name' attribute")
end
names = divs.map { |elt| elt.attribute('data-display-name').to_s }
if names.length != names.uniq.length
raise(ArgumentError, 'autograder names must be unique')
end
raise(ArgumentError, 'first element must be <h1>') unless vertical?(@elements.first)
end

def ruql?(elt) ; elt.name =~ /^script$/i && elt.attribute('language').to_s =~ /ruql/i ; end
#def ruql?(elt) ; elt.name =~ /^script$/i && elt.attribute('language').to_s =~ /ruql/i ; end
def ruql?(elt) ; elt.name =~ /^pre$/i && elt.attribute('class').to_s =~ /ruql/i ; end
def autograder?(elt) ; elt.name =~ /^div$/i && elt.attribute('class').to_s =~ /autograder/i ; end
def vertical?(elt) ; elt.name =~ /^h1$/i ; end

def extract_chunks
# start from linear list of all the children of <body>.
# start from linear list of all the children of <body>. Toplevel elements of resulting list
# will all be Verticals, so once we start a new Vertical, subsequent chunks are consumed into it
# until the next Vertical.
# Note that #validate_html guarantees first element in overall list is a new vertical (h1).
@chunks = []
current_chunk = nil
@elements.each do |elt|
if ruql?(elt) then @chunks << Hw2Edxml::RuqlChunk.new(elt)
elsif autograder?(elt) then @chunks << Hw2Edxml::AutograderChunk.new(elt)
elsif vertical?(elt) then @chunks << Hw2Edxml::VerticalChunk.new(elt)
else # append to current HTML chunk
@chunks << Hw2Edxml::HtmlChunk.new unless @chunks.last.type == :html
@chunks.last.append_content(elt)
if vertical?(elt)
current_chunk = Hw2Edxml::VerticalChunk.new(elt)
@chunks << current_chunk
elsif ruql?(elt)
current_chunk.chunks << Hw2Edxml::RuqlChunk.new(elt)
elsif autograder?(elt)
current_chunk.chunks << Hw2Edxml::AutograderChunk.new(elt)
else # append to current HTML chunk, or start new HTML chunk if one is not active
active_html_chunk = current_chunk.chunks.last
unless active_html_chunk && (active_html_chunk.type == :html)
active_html_chunk = Hw2Edxml::HtmlChunk.new
current_chunk.chunks << active_html_chunk
end
active_html_chunk.append_content(elt)
end
end
@chunks
Expand Down
27 changes: 27 additions & 0 deletions hw2edxml/lib/hw2edxml/vertical_chunk.rb
@@ -0,0 +1,27 @@
class Hw2Edxml::VerticalChunk < Hw2Edxml::Chunk

# This is the only kind of chunk that can contain other chunks, so we override #to_edxml
# and #write_self! to recursively output them.

attr_accessor :chunks

def initialize(elt)
@chunks = []
super(elt.to_s, elt.text)
end

def to_edxml
@xml.vertical(:display_name => display_name) do
chunks.each do |chunk|
@xml.__send__(chunk.type, :url_name => chunk.id)
end
end
@output
end

def write_self!
super # first, write the vertical/999.xml file
chunks.each { |chunk| chunk.write_self! } # the nested chunks
end

end
14 changes: 5 additions & 9 deletions hw2edxml/spec/chunker_spec.rb
Expand Up @@ -6,16 +6,12 @@
before(:each) do
@chunker = Hw2Edxml::Chunker.new('spec/fixtures/valid_5_chunk.html')
end
it 'should have 8 chunks' do
expect(@chunker.chunks.length).to eq(8)
it 'has 3 verticals' do
expect(@chunker.chunks.length).to eq(3)
end
it 'should have correct chunk types' do
@chunker.chunks.map(&:type).should ==
[:vertical, :html, :autograder,
:vertical, :html,
:vertical, :html, :ruql]
it 'has correct chunk types' do
expect(@chunker.chunks.map(&:type)).to eq([:vertical, :vertical, :vertical])
end
it 'should extract raw HTML'
end
it 'should accept valid file' do
expect { Hw2Edxml::Chunker.new('spec/fixtures/valid_5_chunk.html') }.not_to raise_error
Expand All @@ -24,7 +20,7 @@
tests = {
'invalid_root_not_html' => 'root node must be <html>',
'invalid_no_body' => 'no <body> found',
'invalid_content_outside_divs' => 'first element must be <h1>, <script language="ruql">, or <div class="autograder">',
'invalid_content_outside_divs' => 'first element must be <h1>',
}
tests.each_pair do |fixture, error|
it "when #{fixture} should report #{error}" do
Expand Down
18 changes: 18 additions & 0 deletions hw2edxml/spec/vertical_chunk_spec.rb
@@ -0,0 +1,18 @@
require 'spec_helper'
describe Hw2Edxml::VerticalChunk do

describe 'outputs nested XML' do
before :each do
@chunks = [
double('chunk1', :type => 'problem', :id => 'p1'),
double('chunk2', :type => 'html', :id => 'h1'),
double('chunk3', :type => 'problem', :id => 'r1')
]
@subj = Hw2Edxml::VerticalChunk.new(REXML::Document.new('<h1>Foo</h1>'))
@subj.chunks = @chunks
end
subject { @subj.to_edxml }
it { should have_xml_element('vertical').with_attribute(:display_name, 'Foo') }
it { should have_xml_element('vertical/problem').with_attribute(:url_name, 'p1') }
end
end

0 comments on commit c1bcbb3

Please sign in to comment.