aladdin 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +16 -10
- data/assets/favicon.ico +0 -0
- data/assets/images/graphic.png +0 -0
- data/assets/images/no_gravatar.gif +0 -0
- data/assets/javascripts/app.js +76 -0
- data/bin/aladdin +2 -17
- data/lib/aladdin.rb +58 -13
- data/lib/aladdin/app.rb +27 -7
- data/lib/aladdin/cli.rb +29 -0
- data/lib/aladdin/commands/new.rb +16 -0
- data/lib/aladdin/commands/server.rb +10 -0
- data/lib/aladdin/mixin/logger.rb +26 -0
- data/lib/aladdin/mixin/weak_comparator.rb +60 -0
- data/lib/aladdin/render/error.rb +20 -0
- data/lib/aladdin/render/image.rb +54 -0
- data/lib/aladdin/render/markdown.rb +111 -8
- data/lib/aladdin/render/multi.rb +40 -0
- data/lib/aladdin/render/navigation.rb +34 -0
- data/lib/aladdin/render/problem.rb +114 -0
- data/lib/aladdin/render/sanitize.rb +3 -1
- data/lib/aladdin/render/short.rb +30 -0
- data/lib/aladdin/render/table.rb +109 -0
- data/lib/aladdin/render/template.rb +32 -0
- data/lib/aladdin/submission.rb +92 -0
- data/lib/aladdin/version.rb +1 -1
- data/skeleton/images/graphic.png +0 -0
- data/skeleton/index.md +3 -0
- data/views/haml/exe.haml +5 -0
- data/views/haml/img.haml +6 -0
- data/views/haml/layout.haml +44 -12
- data/views/haml/multi.haml +18 -0
- data/views/haml/nav.haml +5 -0
- data/views/haml/short.haml +14 -0
- data/views/haml/table.haml +30 -0
- data/views/scss/app.scss +50 -44
- data/views/scss/mathjax.scss +5 -0
- data/views/scss/pygment.scss +9 -6
- metadata +47 -18
- data/assets/images/foundation/orbit/bullets.jpg +0 -0
- data/assets/images/foundation/orbit/left-arrow-small.png +0 -0
- data/assets/images/foundation/orbit/left-arrow.png +0 -0
- data/assets/images/foundation/orbit/loading.gif +0 -0
- data/assets/images/foundation/orbit/mask-black.png +0 -0
- data/assets/images/foundation/orbit/pause-black.png +0 -0
- data/assets/images/foundation/orbit/right-arrow-small.png +0 -0
- data/assets/images/foundation/orbit/right-arrow.png +0 -0
- data/assets/images/foundation/orbit/rotator-black.png +0 -0
- data/assets/images/foundation/orbit/timer-black.png +0 -0
- data/views/haml/index.haml +0 -43
@@ -0,0 +1,109 @@
|
|
1
|
+
# ~*~ encoding: utf-8 ~*~
|
2
|
+
module Aladdin
|
3
|
+
|
4
|
+
module Render
|
5
|
+
|
6
|
+
# Renders table problems marked up in JSON as HTML.
|
7
|
+
#
|
8
|
+
# The grid should be given as a 2-dimensional array that represents the
|
9
|
+
# table to be filled in. +"?"+ is a special token used in the grid to
|
10
|
+
# indicate cells that require student input.
|
11
|
+
#
|
12
|
+
# The answer should also be given as a 2-dimensional array. However, a
|
13
|
+
# dummy token may be used in cells that do not require student input to
|
14
|
+
# cut redundancy. In the example below, the +"-"+ token is used.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# {
|
18
|
+
# "format": "table",
|
19
|
+
# "question": "fill me in",
|
20
|
+
# "grid": [[0, "?", 2], [3, "?", 5]],
|
21
|
+
# "answer": [["-", 1, "-"], ["-", 4, "-"]
|
22
|
+
# }
|
23
|
+
class Table < Problem
|
24
|
+
|
25
|
+
# Name of template file for rendering table problems.
|
26
|
+
TEMPLATE = 'table.haml'
|
27
|
+
|
28
|
+
# Optional headings key.
|
29
|
+
HEADINGS = 'headings'
|
30
|
+
|
31
|
+
# Required grid key.
|
32
|
+
GRID = 'grid'
|
33
|
+
|
34
|
+
# Special token indicating that the cell should be filled in.
|
35
|
+
FILL_ME_IN = '?'
|
36
|
+
|
37
|
+
accessor HEADINGS, GRID
|
38
|
+
|
39
|
+
# Ensures that the +headings+ key exists.
|
40
|
+
def initialize(json)
|
41
|
+
json[HEADINGS] ||= nil
|
42
|
+
super
|
43
|
+
end
|
44
|
+
|
45
|
+
# Checks if the given json contains a valid table.
|
46
|
+
# @return [Boolean] true iff the json contains a valid table.
|
47
|
+
def valid?
|
48
|
+
super and
|
49
|
+
valid_grid? and
|
50
|
+
valid_answer?
|
51
|
+
end
|
52
|
+
|
53
|
+
# Gets the expected answer, in www-form-urlencoded format.
|
54
|
+
# @return [Hash] answers, as expected from student's form submission
|
55
|
+
def answer
|
56
|
+
return @answer unless @answer.nil?
|
57
|
+
@answer = encode_answer
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Boolean] true iff the given cell is an input cell
|
61
|
+
def self.input?(cell)
|
62
|
+
cell == FILL_ME_IN
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# Iterates through each cell in the provided grid to look for answers
|
68
|
+
# cells that require input and takes the answer from the answers array.
|
69
|
+
# For example, if the answer for a cell at [0][1] is 6, the returned
|
70
|
+
# hash will contain
|
71
|
+
#
|
72
|
+
# {'0' => {'1' => 6}}
|
73
|
+
#
|
74
|
+
# @return [Hash] answers
|
75
|
+
def encode_answer
|
76
|
+
encoded, ans = {}, @json[ANSWER]
|
77
|
+
grid.each_with_index do |row, i|
|
78
|
+
row.each_with_index do |cell, j|
|
79
|
+
next unless Table.input? cell
|
80
|
+
encoded[i.to_s] ||= {}
|
81
|
+
encoded[i.to_s][j.to_s] = serialize ans[i][j]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
encoded
|
85
|
+
end
|
86
|
+
|
87
|
+
# @return [Boolean] true iff the json contains a valid grid.
|
88
|
+
def valid_grid?
|
89
|
+
@json.has_key? GRID and is_2d_array? grid
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [Boolean] true iff +answer+ is a valid 2D array and has the
|
93
|
+
# same number of rows as +grid+.
|
94
|
+
def valid_answer?
|
95
|
+
ans = @json[ANSWER]
|
96
|
+
is_2d_array? ans and ans.size == grid.size
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [Boolean] true iff +t+ is a 2-dimensional array.
|
100
|
+
def is_2d_array?(t)
|
101
|
+
t.is_a? Array and t.all? { |row| row.is_a? Array }
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# ~*~ encoding: utf-8 ~*~
|
2
|
+
module Aladdin
|
3
|
+
|
4
|
+
module Render
|
5
|
+
|
6
|
+
# Child classes should provide a +TEMPLATE+ string constant that contains
|
7
|
+
# the path to the HAML file.
|
8
|
+
class Template
|
9
|
+
|
10
|
+
# Renders the given problem using {#view}.
|
11
|
+
# @todo TODO should probably show some error message in the preview,
|
12
|
+
# so that the author doesn't have to read the logs.
|
13
|
+
def render(locals={})
|
14
|
+
view.render Object.new, locals
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
# Retrieves the +view+ singleton. If it is nil, initializes it from
|
20
|
+
# +self.class.TEMPLATE+.
|
21
|
+
# @return [Haml::Engine] haml engine
|
22
|
+
def view
|
23
|
+
return @view unless @view.nil?
|
24
|
+
file = File.join Aladdin::VIEWS[:haml], self.class::TEMPLATE
|
25
|
+
@view = Haml::Engine.new(File.read file)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# ~*~ encoding: utf-8 ~*~
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
module Aladdin
|
5
|
+
|
6
|
+
# Student submission.
|
7
|
+
#
|
8
|
+
# == DANGER DANGER DANGER ==
|
9
|
+
# The scratchspace code assumes that there is only one submission at a time,
|
10
|
+
# it's unsuitable for production use. It does not impose any security
|
11
|
+
# restrictions at all.
|
12
|
+
class Submission
|
13
|
+
include Aladdin::Mixin::Logger
|
14
|
+
include Aladdin::Mixin::WeakComparator
|
15
|
+
|
16
|
+
SCRATCHSPACE = '.__ss'
|
17
|
+
|
18
|
+
# Creates a new student submission.
|
19
|
+
# @param [String] id exercise ID
|
20
|
+
# @param [Type] type quiz or code
|
21
|
+
# @param [Hash] params form values
|
22
|
+
# @param [String] input student input
|
23
|
+
def initialize(id, type, params, input)
|
24
|
+
@id, @type, @params, @input = id, type, params, input
|
25
|
+
end
|
26
|
+
|
27
|
+
# Verifies the student's submission.
|
28
|
+
def verify
|
29
|
+
case @type
|
30
|
+
when Type::CODE then verify_code
|
31
|
+
when Type::QUIZ then verify_quiz
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Verifies quiz answers by comparing the submitted answer against the
|
38
|
+
# answer in the solution file.
|
39
|
+
# @return [String] (json-encoded) true iff the submitted answer is correct.
|
40
|
+
def verify_quiz
|
41
|
+
id = @id.gsub File::SEPARATOR, '' # protect against directory attacks
|
42
|
+
solution = File.expand_path id + Aladdin::DATA_EXT, Aladdin::DATA_DIR
|
43
|
+
File.open(solution, 'rb') { |f| same? @params['answer'], Marshal.restore(f) }.to_json
|
44
|
+
rescue => e
|
45
|
+
logger.warn e.message
|
46
|
+
false.to_json
|
47
|
+
end
|
48
|
+
|
49
|
+
# Executes the verification script and returns the JSON-encoded results.
|
50
|
+
# @example
|
51
|
+
# ./verify --id=0 --input=path/to/student/input
|
52
|
+
def verify_code
|
53
|
+
scratchspace do
|
54
|
+
# FIXME: catch errors
|
55
|
+
filename = SecureRandom.uuid
|
56
|
+
IO.write(filename, @input)
|
57
|
+
bin = File.join '..', Aladdin.config['verify']['bin']
|
58
|
+
`#{bin} --id=#{@id} --input=#{filename}`
|
59
|
+
IO.read 'genie-results.json'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def scratchspace
|
64
|
+
enter_scratchspace
|
65
|
+
results = yield
|
66
|
+
exit_scratchspace
|
67
|
+
return results
|
68
|
+
end
|
69
|
+
|
70
|
+
def enter_scratchspace
|
71
|
+
# TODO: handle errors
|
72
|
+
Dir.mkdir SCRATCHSPACE
|
73
|
+
Dir.chdir SCRATCHSPACE
|
74
|
+
end
|
75
|
+
|
76
|
+
def exit_scratchspace
|
77
|
+
# TODO: handle errors
|
78
|
+
Dir.chdir '..'
|
79
|
+
FileUtils.rm_rf SCRATCHSPACE
|
80
|
+
end
|
81
|
+
|
82
|
+
# Submission Type, for use as enum.
|
83
|
+
module Type
|
84
|
+
# Quiz Type
|
85
|
+
QUIZ = 'quiz'
|
86
|
+
# Code Type
|
87
|
+
CODE = 'code'
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
data/lib/aladdin/version.rb
CHANGED
Binary file
|
data/skeleton/index.md
ADDED
data/views/haml/exe.haml
ADDED
data/views/haml/img.haml
ADDED
data/views/haml/layout.haml
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
-# coding: UTF-8
|
1
2
|
!!! 5
|
2
3
|
|
3
4
|
-# paulirish.com/2008/conditional-stylesheets-vs-css-hacks-answer-neither/
|
@@ -10,23 +11,52 @@
|
|
10
11
|
%meta{charset: 'utf-8'}
|
11
12
|
%meta{name: 'viewport', content: 'width=device-width'}
|
12
13
|
|
13
|
-
%title
|
14
|
+
%title= title
|
14
15
|
%link{rel: 'stylesheet', href:'stylesheets/app.css'}
|
15
|
-
%link{rel: 'stylesheet', href:'stylesheets/pygment.css'}
|
16
|
-
%link{rel: 'stylesheet', href:'stylesheets/github.css'}
|
17
16
|
%script{src: 'javascripts/foundation/modernizr.foundation.js'}
|
18
17
|
|
19
18
|
%body
|
20
19
|
|
21
20
|
.row
|
22
|
-
.twelve.columns
|
23
|
-
%h1 Genie Tutorial
|
24
|
-
%hr/
|
25
21
|
|
26
|
-
|
27
|
-
|
28
|
-
|
22
|
+
%section{role: 'complementary'}
|
23
|
+
.row
|
24
|
+
.six.columns
|
25
|
+
%h1= title
|
26
|
+
%h4
|
27
|
+
<!-- TODO: user gravatar -->
|
28
|
+
%img{src: 'images/no_gravatar.gif', width: 20, height: 20, alt: 'John Doe'}
|
29
|
+
<!-- TOOD: user name -->
|
30
|
+
%small John Doe
|
31
|
+
%h4.subheader= description
|
32
|
+
%div.panel.meta
|
33
|
+
%h6.subheader Dec 18, 2012 12:00PM
|
34
|
+
%ul.inline-list
|
35
|
+
-categories.each do |category|
|
36
|
+
%li
|
37
|
+
%a{href: '#'}=category
|
38
|
+
.six.columns
|
39
|
+
%h1
|
40
|
+
%a.th.hero{href: '#'}
|
41
|
+
<!-- TODO: graphic from user's repo -->
|
42
|
+
%img{src: 'images/graphic.png'}
|
43
|
+
|
44
|
+
%hr/
|
45
|
+
%section{role: 'lesson'}
|
46
|
+
.row
|
47
|
+
.twelve.columns
|
48
|
+
!=yield # output is sanitized by the sanitize gem
|
49
|
+
|
50
|
+
%hr/
|
29
51
|
|
52
|
+
.row#footer
|
53
|
+
.ten.columns
|
54
|
+
.radius.progress.success.ten
|
55
|
+
%span.meter.eight 80%
|
56
|
+
.two.columns
|
57
|
+
%span <!-- TODO: score goes here -->
|
58
|
+
|
59
|
+
-# foundation
|
30
60
|
%script{src: 'javascripts/foundation/jquery.js'}
|
31
61
|
%script{src: 'javascripts/foundation/jquery.cookie.js'}
|
32
62
|
%script{src: 'javascripts/foundation/jquery.event.move.js'}
|
@@ -36,16 +66,18 @@
|
|
36
66
|
%script{src: 'javascripts/foundation/jquery.foundation.buttons.js'}
|
37
67
|
%script{src: 'javascripts/foundation/jquery.foundation.clearing.js'}
|
38
68
|
%script{src: 'javascripts/foundation/jquery.foundation.forms.js'}
|
39
|
-
%script{src: 'javascripts/foundation/jquery.foundation.joyride.js'}
|
40
69
|
%script{src: 'javascripts/foundation/jquery.foundation.magellan.js'}
|
41
70
|
%script{src: 'javascripts/foundation/jquery.foundation.mediaQueryToggle.js'}
|
42
71
|
%script{src: 'javascripts/foundation/jquery.foundation.navigation.js'}
|
43
|
-
%script{src: 'javascripts/foundation/jquery.foundation.orbit.js'}
|
44
|
-
%script{src: 'javascripts/foundation/jquery.foundation.reveal.js'}
|
45
72
|
%script{src: 'javascripts/foundation/jquery.foundation.tabs.js'}
|
46
73
|
%script{src: 'javascripts/foundation/jquery.foundation.tooltips.js'}
|
47
74
|
%script{src: 'javascripts/foundation/jquery.foundation.topbar.js'}
|
48
75
|
%script{src: 'javascripts/foundation/jquery.placeholder.js'}
|
76
|
+
%script{src: 'javascripts/foundation/app.js'}
|
77
|
+
|
78
|
+
-# mathjax
|
79
|
+
%script{src: 'https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'}
|
80
|
+
%script{src: 'javascripts/app.js'}
|
49
81
|
|
50
82
|
:plain
|
51
83
|
</html>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
-# coding: UTF-8
|
2
|
+
%form.question{id: "question_#{id}"}
|
3
|
+
%fieldset
|
4
|
+
|
5
|
+
%legend Problem #{index}
|
6
|
+
%input.q-id{type: 'hidden', value: id}
|
7
|
+
|
8
|
+
%p= question
|
9
|
+
|
10
|
+
.row.collapse
|
11
|
+
.ten.mobile-three.columns
|
12
|
+
-options.each do |value, label|
|
13
|
+
%label{for: 'answer'}
|
14
|
+
%input{name: 'answer', type: 'radio', value: value}
|
15
|
+
= label
|
16
|
+
.two.mobile-one.columns
|
17
|
+
%a.button.submit.small.expand.postfix.radius{href: '#'} Submit
|
18
|
+
|
data/views/haml/nav.haml
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
-# coding: UTF-8
|
2
|
+
%form.question{id: "question_#{id}"}
|
3
|
+
%fieldset
|
4
|
+
|
5
|
+
%legend Problem #{index}
|
6
|
+
%input.q-id{type: 'hidden', value: id}
|
7
|
+
|
8
|
+
%p= question
|
9
|
+
|
10
|
+
.row.collapse
|
11
|
+
.ten.mobile-three.columns
|
12
|
+
%input{name: 'answer', type: 'text'}
|
13
|
+
.two.mobile-one.columns
|
14
|
+
%a.button.small.submit.expand.postfix.radius{href: '#'} Submit
|
@@ -0,0 +1,30 @@
|
|
1
|
+
-# coding: UTF-8
|
2
|
+
%form.question{id: "question_#{id}"}
|
3
|
+
%fieldset
|
4
|
+
|
5
|
+
%legend Problem #{index}
|
6
|
+
%input.q-id{type: 'hidden', value: id}
|
7
|
+
|
8
|
+
%p= question
|
9
|
+
|
10
|
+
.row
|
11
|
+
.twelve.columns
|
12
|
+
%table
|
13
|
+
%thead
|
14
|
+
-if headings
|
15
|
+
%tr
|
16
|
+
-headings.each do |heading|
|
17
|
+
%th= heading
|
18
|
+
|
19
|
+
%tbody
|
20
|
+
-grid.each_with_index do |row, i|
|
21
|
+
%tr
|
22
|
+
-row.each_with_index do |cell, j|
|
23
|
+
%td
|
24
|
+
- if Aladdin::Render::Table.input? cell
|
25
|
+
%input{name: "answer[#{i}][#{j}]", type: 'text'}
|
26
|
+
- else
|
27
|
+
= cell
|
28
|
+
.row
|
29
|
+
.two.columns.mobile-one
|
30
|
+
%a.button.small.submit.radius{href: '#'} Submit
|
data/views/scss/app.scss
CHANGED
@@ -1,47 +1,53 @@
|
|
1
|
-
// You custom settings file to override Foundation defaults
|
2
1
|
@import "settings";
|
3
|
-
|
4
|
-
// Comment out this import if you are customizing you imports below
|
5
2
|
@import "foundation";
|
6
3
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
4
|
+
/* settings */
|
5
|
+
$borderWidth: 2px;
|
6
|
+
|
7
|
+
.highlight {
|
8
|
+
@import "pygment";
|
9
|
+
@import "github";
|
10
|
+
}
|
11
|
+
|
12
|
+
@import "mathjax";
|
13
|
+
|
14
|
+
body {
|
15
|
+
padding: 0 $columnGutter/2;
|
16
|
+
}
|
17
|
+
|
18
|
+
/* Meta panel */
|
19
|
+
div.meta.panel {
|
20
|
+
border-top: $borderWidth solid $darkEdge;
|
21
|
+
}
|
22
|
+
|
23
|
+
/* Lesson graphic */
|
24
|
+
a.hero img {
|
25
|
+
width: 100%;
|
26
|
+
}
|
27
|
+
|
28
|
+
/* Floating navigation bar */
|
29
|
+
#sections {
|
30
|
+
background: darken($white, 5%);
|
31
|
+
padding-top: 10px;
|
32
|
+
width: $rowWidth + 18;
|
33
|
+
border-bottom: $borderWidth solid $darkEdge;
|
34
|
+
z-index: 3;
|
35
|
+
}
|
36
|
+
|
37
|
+
/* Progress bar */
|
38
|
+
#footer .meter {
|
39
|
+
color: $white;
|
40
|
+
font-weight: 800;
|
41
|
+
text-align: right;
|
42
|
+
padding: 2px 2px 0 0;
|
43
|
+
}
|
44
|
+
|
45
|
+
/* Styling within lesson */
|
46
|
+
|
47
|
+
section[role='lesson'] .block-image {
|
48
|
+
text-align: center;
|
49
|
+
}
|
50
|
+
|
51
|
+
section[role='lesson'] ul {
|
52
|
+
margin-left: 17px;
|
53
|
+
}
|