aladdin 0.0.1 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
}
|