ruql-olx 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Gemfile.lock +3 -1
- data/README.md +19 -1
- data/lib/ruql/olx.rb +1 -0
- data/lib/ruql/olx/exported_edx_course.rb +136 -0
- data/lib/ruql/olx/olx.rb +50 -41
- data/lib/ruql/olx/version.rb +1 -1
- data/ruql-olx.gemspec +3 -2
- metadata +19 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 76998a04ff820500209dd5edd8bf3fee5a1eb06425367b31ce02583f242c4ec4
|
4
|
+
data.tar.gz: a54822aa0ccf2cf6999af1bf9ecd14a044a740820eaac184b3a7d52ebde5277b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bd0bcd4bab32b7ede92f8120402085e30cf9ac03d46a6951cfaf18a0ba586ebda7638866c3e38e14ddc5a9d2a41bde931c42b76255dd06ada456cc67f75e220f
|
7
|
+
data.tar.gz: 706b36d71e9c5e5b8d2889ce736743e1e584eefecee42a4a61e9da0c1f13d3013965d418ba48bc10e22661a0d097d645a53a60afa640abaf995935ebf5f8441e
|
data/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
ruql-olx (0.0.
|
4
|
+
ruql-olx (0.0.3)
|
5
5
|
builder (~> 3.0)
|
6
6
|
ruql (~> 1.0, >= 1.0.2)
|
7
7
|
|
@@ -9,6 +9,7 @@ GEM
|
|
9
9
|
remote: https://rubygems.org/
|
10
10
|
specs:
|
11
11
|
builder (3.2.4)
|
12
|
+
byebug (11.1.3)
|
12
13
|
diff-lcs (1.4.4)
|
13
14
|
rake (10.5.0)
|
14
15
|
rspec (3.9.0)
|
@@ -31,6 +32,7 @@ PLATFORMS
|
|
31
32
|
|
32
33
|
DEPENDENCIES
|
33
34
|
bundler (~> 1.17)
|
35
|
+
byebug
|
34
36
|
rake (~> 10.0)
|
35
37
|
rspec (~> 3.0)
|
36
38
|
ruql-olx!
|
data/README.md
CHANGED
@@ -6,9 +6,10 @@ developed and used by [edX](https://edx.org) for rendering course assets.
|
|
6
6
|
|
7
7
|
## Installation
|
8
8
|
|
9
|
-
Add
|
9
|
+
Add to your application's Gemfile:
|
10
10
|
|
11
11
|
```ruby
|
12
|
+
gem 'ruql'
|
12
13
|
gem 'ruql-olx'
|
13
14
|
```
|
14
15
|
|
@@ -22,6 +23,23 @@ Or install it yourself as:
|
|
22
23
|
|
23
24
|
## Usage
|
24
25
|
|
26
|
+
The simplest usage is to change to the root directory of the course
|
27
|
+
export (the directory containing subdirectories `chapters`, `course`,
|
28
|
+
etc.) and say `ruql olx --chapter 'Chapter Display Name' quizfile.rb`
|
29
|
+
|
30
|
+
|
31
|
+
- for each problem, create drafts/problem/<uuid>.xml containng <problem>...</problem>
|
32
|
+
- create a sequential using existing logic; let its uid be UU.xml
|
33
|
+
- for each quiz, create drafts/vertical/<uuid>.xml containing:
|
34
|
+
<vertical display_name="Quiz name" index_in_children_list="0" parent_url="i4x://BerkeleyX/CSTest101/sequential/UU">
|
35
|
+
<problem url_name="5d7a8b06b3b5440aa9c1f6059f2e0945"/>
|
36
|
+
<problem url_name="2ffcfc7a92894770896bd15e311ef77e"/>
|
37
|
+
</vertical>
|
38
|
+
|
39
|
+
- search for chapters/ .xml file having <chapter display_name="Chapter
|
40
|
+
Display Name"
|
41
|
+
- in that file, add <sequential url_name="UU"/> as the last child of <chapter>
|
42
|
+
|
25
43
|
The simplest usage is `ruql olx --sequential seq.xml quizfile.rb > output.olx`.
|
26
44
|
|
27
45
|
The OLX representation of all the questions in `quizfile.rb` will be
|
data/lib/ruql/olx.rb
CHANGED
@@ -0,0 +1,136 @@
|
|
1
|
+
module Ruql
|
2
|
+
class Olx
|
3
|
+
class ExportedEdxCourse
|
4
|
+
|
5
|
+
require 'securerandom' # for UUID generation
|
6
|
+
require 'builder'
|
7
|
+
|
8
|
+
def initialize(quiz, chapter_name, course_root, course_options, dryrun: false)
|
9
|
+
@dryrun = dryrun
|
10
|
+
@course_options = course_options
|
11
|
+
@quiz = quiz
|
12
|
+
@root = course_root
|
13
|
+
verify_writable_dir(@problem_dir = File.join(@root, 'drafts', 'problem'))
|
14
|
+
verify_writable_dir(@vertical_dir = File.join(@root, 'drafts', 'vertical'))
|
15
|
+
verify_writable_dir(@sequential_dir = File.join(@root, 'sequential'))
|
16
|
+
@chapter_file = find_chapter_file(chapter_name)
|
17
|
+
@course_name = find_course_name
|
18
|
+
@created_files = [] # to delete in case of error, or list created
|
19
|
+
@modified_files = []
|
20
|
+
@quiz_filehandle = nil
|
21
|
+
@problem_ids = []
|
22
|
+
@sequential_id = uuid()
|
23
|
+
@vertical_id = uuid()
|
24
|
+
end
|
25
|
+
|
26
|
+
# Create a file containing a single problem and remember its UUID.
|
27
|
+
def add_problem(xml_text)
|
28
|
+
url = uuid()
|
29
|
+
problem_file = File.join(@problem_dir, "#{url}.xml")
|
30
|
+
begin
|
31
|
+
File.open(problem_file, 'w') { |f| f.puts xml_text }
|
32
|
+
@problem_ids << url
|
33
|
+
@created_files << problem_file
|
34
|
+
rescue StandardError => e
|
35
|
+
cleanup
|
36
|
+
raise IOError.new(:message => e.message)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_quiz
|
41
|
+
quiz_vertical = uuid()
|
42
|
+
begin
|
43
|
+
create_sequential
|
44
|
+
create_vertical
|
45
|
+
append_quiz_to_chapter
|
46
|
+
rescue StandardError => e
|
47
|
+
cleanup
|
48
|
+
raise IOError.new(:message => e.message)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def report
|
53
|
+
report = ["Files created:"]
|
54
|
+
@created_files.each { |f| report << " #{f}" }
|
55
|
+
report << "Files modified:"
|
56
|
+
@modified_files.each { |f| report << " #{f}" }
|
57
|
+
cleanup if @dryrun
|
58
|
+
report.join("\n")
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def uuid
|
64
|
+
SecureRandom.hex(16)
|
65
|
+
end
|
66
|
+
|
67
|
+
def create_sequential
|
68
|
+
file = File.join(@sequential_dir, "#{@sequential_id}.xml")
|
69
|
+
File.open(file, 'w') do |fh|
|
70
|
+
quiz_header = Builder::XmlMarkup.new(:target => fh, :indent => 2)
|
71
|
+
quiz_header.sequential(@course_options)
|
72
|
+
end
|
73
|
+
@created_files << file
|
74
|
+
end
|
75
|
+
|
76
|
+
def create_vertical
|
77
|
+
file = File.join(@vertical_dir, "#{@vertical_id}.xml")
|
78
|
+
File.open(file, 'w') do |fh|
|
79
|
+
vert = Builder::XmlMarkup.new(:target => fh, :indent => 2)
|
80
|
+
vert.vertical(
|
81
|
+
display_name: @quiz.title,
|
82
|
+
index_in_children_list: 0,
|
83
|
+
parent_url: "i4x://#{@course_name}/sequential/#{@sequential_id}") do
|
84
|
+
@problem_ids.each do |prob|
|
85
|
+
vert.problem(url_name: prob)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
@created_files << file
|
90
|
+
end
|
91
|
+
|
92
|
+
# Verify that the given directory exists and is writable
|
93
|
+
def verify_writable_dir(dir)
|
94
|
+
unless (File.directory?(dir) && File.writable?(dir)) ||
|
95
|
+
FileUtils.mkdir_p(dir)
|
96
|
+
raise IOError.new("Directory #{dir} must exist and be writable")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Find the chapter .xml file for the chapter whose name matches 'name'.
|
101
|
+
# If multiple chapters match, no guarantee on which gets picked.
|
102
|
+
def find_chapter_file(name)
|
103
|
+
regex = /<chapter display_name="([^"]+)">/i
|
104
|
+
Dir.glob(File.join @root, 'chapter', '*.xml').each do |filename|
|
105
|
+
first_line = File.open(filename, &:gets)
|
106
|
+
return filename if (first_line =~ regex) && ($1 == name)
|
107
|
+
end
|
108
|
+
raise Ruql::OptionsError.new("Chapter '#{name}' not found in #{@root}/chapter/")
|
109
|
+
end
|
110
|
+
|
111
|
+
def append_quiz_to_chapter
|
112
|
+
# insert just before last line
|
113
|
+
chapter_markup = File.readlines(@chapter_file).insert(-2,
|
114
|
+
%Q{ <sequential url_name="#{@sequential_id}"/>})
|
115
|
+
File.open(@chapter_file, 'w') { |f| f.puts chapter_markup }
|
116
|
+
@modified_files << @chapter_file
|
117
|
+
end
|
118
|
+
|
119
|
+
# Find the course's name ("#{org}/#{course}") from course.xml file
|
120
|
+
def find_course_name
|
121
|
+
course_markup = File.readlines(File.join(@root, 'course.xml')).join('')
|
122
|
+
org = name = nil
|
123
|
+
org = $1 if course_markup =~ /<course.*org="([^"]+)"/
|
124
|
+
name = $1 if course_markup =~ /<course.*course="([^"]+)"/
|
125
|
+
raise Ruql::OptionsError.new("Cannot get course org and name from #{@root}/course.xml") unless org && name
|
126
|
+
"#{org}/#{name}"
|
127
|
+
end
|
128
|
+
|
129
|
+
def cleanup
|
130
|
+
# delete any created files; ignore errors during deletion
|
131
|
+
@created_files.each { |f| File.delete(f) rescue nil }
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
data/lib/ruql/olx/olx.rb
CHANGED
@@ -6,24 +6,50 @@ module Ruql
|
|
6
6
|
attr_reader :output
|
7
7
|
|
8
8
|
def initialize(quiz,options={})
|
9
|
-
@
|
9
|
+
@quiz = quiz
|
10
|
+
@dryrun = !! options.delete('--dry-run')
|
11
|
+
if (chapter = options.delete('--chapter'))
|
12
|
+
root = options.delete('--root') || Dir.getwd
|
13
|
+
@edx = Ruql::Olx::ExportedEdxCourse.new(quiz, chapter, root,
|
14
|
+
# quiz options for edX
|
15
|
+
{
|
16
|
+
is_time_limited: "true",
|
17
|
+
default_time_limit_minutes: self.time_limit,
|
18
|
+
display_name: quiz.title,
|
19
|
+
exam_review_rules: "",
|
20
|
+
is_onboarding_exam: "false",
|
21
|
+
is_practice_exam: "false",
|
22
|
+
is_proctored_enabled: "false"
|
23
|
+
},
|
24
|
+
dryrun: @dryrun)
|
25
|
+
end
|
10
26
|
@groupselect = (options.delete('--group-select') || 1_000_000).to_i
|
11
27
|
@output = ''
|
12
|
-
@
|
13
|
-
@h = Builder::XmlMarkup.new(:target => @output, :indent => 2)
|
28
|
+
@h = nil # XML Builder object
|
14
29
|
end
|
15
30
|
|
16
31
|
def self.allowed_options
|
17
32
|
opts = [
|
18
|
-
['--
|
19
|
-
['--
|
33
|
+
['--chapter', GetoptLong::REQUIRED_ARGUMENT],
|
34
|
+
['--root', GetoptLong::REQUIRED_ARGUMENT],
|
35
|
+
['--group-select', GetoptLong::REQUIRED_ARGUMENT],
|
36
|
+
['--dry-run', GetoptLong::NO_ARGUMENT]
|
20
37
|
]
|
21
38
|
help = <<eos
|
22
|
-
The OLX renderer
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
39
|
+
The OLX renderer modifies an exported edX course tree in place to incorporate the quiz.
|
40
|
+
It supports these options:
|
41
|
+
--chapter '<name>'
|
42
|
+
Insert the quiz as the last child (sequential) of the chapter whose display name
|
43
|
+
is <name>. Use quotes as needed to protect spaces/special characters in chapter name.
|
44
|
+
If multiple chapter names match, the quiz will end up in one of them.
|
45
|
+
If this option is omitted, the problem XML will instead be written to standard output,
|
46
|
+
and no files will be created or modified.
|
47
|
+
--root=<path>
|
48
|
+
Specify <path> as root directory of the edX course export. If omitted, default is '.'
|
49
|
+
--dry-run
|
50
|
+
Only valid with --chapter: report names of created files but then delete them from export.
|
51
|
+
The files are created and deleted, so not a true dry run, but leaves the export intact
|
52
|
+
while verifying that the changes are possible.
|
27
53
|
--group-select=<n>
|
28
54
|
If multiple RuQL questions share the same 'group' attribute, include at most n of them
|
29
55
|
in the output. If omitted, defaults to "all questions in group".
|
@@ -32,26 +58,15 @@ eos
|
|
32
58
|
end
|
33
59
|
|
34
60
|
def render_quiz
|
61
|
+
# caller expects to find "quiz content" in @output, but if we modified edX, then
|
62
|
+
# output is just the report of what we did.
|
35
63
|
@groups_seen = Hash.new { 0 }
|
36
64
|
@group_count = 0
|
37
65
|
render_questions
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
# write the quiz header
|
43
|
-
def write_quiz_header
|
44
|
-
fh = File.open(@sequential, 'w')
|
45
|
-
@quiz_header = Builder::XmlMarkup.new(:target => fh, :indent => 2)
|
46
|
-
@quiz_header.sequential(
|
47
|
-
is_time_limited: "true",
|
48
|
-
default_time_limit_minutes: self.time_limit,
|
49
|
-
display_name: @quiz.title,
|
50
|
-
exam_review_rules: "",
|
51
|
-
is_onboarding_exam: "false",
|
52
|
-
is_practice_exam: "false",
|
53
|
-
is_proctored_enabled: "false")
|
54
|
-
fh.close
|
66
|
+
if @edx
|
67
|
+
@edx.add_quiz
|
68
|
+
@output = @edx.report
|
69
|
+
end
|
55
70
|
end
|
56
71
|
|
57
72
|
def time_limit
|
@@ -66,6 +81,8 @@ eos
|
|
66
81
|
# this is what's called when the OLX template yields:
|
67
82
|
def render_questions
|
68
83
|
@quiz.questions.each do |q|
|
84
|
+
question_xml = ''
|
85
|
+
@h = Builder::XmlMarkup.new(:target => question_xml, :indent => 2)
|
69
86
|
# have we maxed out the number of questions per group for this group?
|
70
87
|
next unless more_in_group?(q)
|
71
88
|
case q
|
@@ -74,6 +91,12 @@ eos
|
|
74
91
|
else
|
75
92
|
raise Ruql::QuizContentError.new "Unknown question type: #{q}"
|
76
93
|
end
|
94
|
+
# question_xml now contains the XML of the given question...
|
95
|
+
if @edx
|
96
|
+
@edx.add_problem(question_xml)
|
97
|
+
else
|
98
|
+
@output << question_xml << "\n"
|
99
|
+
end
|
77
100
|
end
|
78
101
|
end
|
79
102
|
|
@@ -85,19 +108,6 @@ eos
|
|
85
108
|
return (@groups_seen[group] <= @groupselect)
|
86
109
|
end
|
87
110
|
|
88
|
-
# //This Question has Html formatting in the answer
|
89
|
-
# <problem display_name="Multiple Choice" markdown="null">
|
90
|
-
# <multiplechoiceresponse>
|
91
|
-
# <label>Which condition is necessary when using Ruby's collection methods such as map and reject?</label>
|
92
|
-
# <description> </description>
|
93
|
-
# <choicegroup type="MultipleChoice">
|
94
|
-
# <choice correct="false">The collection on which they operate must consist of objects of the same type.</choice>
|
95
|
-
# <choice correct="true">The collection must respond to <pre> <tt>each</tt> </pre></choice>
|
96
|
-
# <choice correct="false">Every element of the collection must respond to <pre> <tt>each</tt> </pre></choice>
|
97
|
-
# <choice correct="false">The collection itself must be one of Ruby's built-in collection types, such as <pre> <tt>Array</tt> or <tt>Set</tt> </pre></choice>
|
98
|
-
# </choicegroup>
|
99
|
-
# </multiplechoiceresponse>
|
100
|
-
# </problem>
|
101
111
|
def render_multiple_choice(q)
|
102
112
|
@h.problem(display_name: 'MultipleChoice', markdown: 'null') do
|
103
113
|
@h.multiplechoiceresponse do
|
@@ -107,7 +117,6 @@ eos
|
|
107
117
|
end
|
108
118
|
end
|
109
119
|
end
|
110
|
-
@output << "\n\n"
|
111
120
|
end
|
112
121
|
|
113
122
|
def render_select_multiple(q)
|
data/lib/ruql/olx/version.rb
CHANGED
data/ruql-olx.gemspec
CHANGED
@@ -6,8 +6,8 @@ require "ruql/olx/version"
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "ruql-olx"
|
8
8
|
spec.version = Ruql::Olx::VERSION
|
9
|
-
spec.summary = "OLX
|
10
|
-
spec.description = "OLX
|
9
|
+
spec.summary = "OLX renderer for RuQL"
|
10
|
+
spec.description = "OLX renderer for RuQL"
|
11
11
|
spec.authors = ["Armando Fox"]
|
12
12
|
spec.email = 'fox@berkeley.edu'
|
13
13
|
spec.homepage = 'https://github.com/saasbook/ruql-olx'
|
@@ -39,5 +39,6 @@ Gem::Specification.new do |spec|
|
|
39
39
|
spec.add_development_dependency "bundler", "~> 1.17"
|
40
40
|
spec.add_development_dependency "rake", "~> 10.0"
|
41
41
|
spec.add_development_dependency "rspec", "~> 3.0"
|
42
|
+
spec.add_development_dependency "byebug"
|
42
43
|
end
|
43
44
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruql-olx
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Armando Fox
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-08-
|
11
|
+
date: 2020-08-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ruql
|
@@ -86,7 +86,21 @@ dependencies:
|
|
86
86
|
- - "~>"
|
87
87
|
- !ruby/object:Gem::Version
|
88
88
|
version: '3.0'
|
89
|
-
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: byebug
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
description: OLX renderer for RuQL
|
90
104
|
email: fox@berkeley.edu
|
91
105
|
executables: []
|
92
106
|
extensions: []
|
@@ -101,6 +115,7 @@ files:
|
|
101
115
|
- bin/console
|
102
116
|
- bin/setup
|
103
117
|
- lib/ruql/olx.rb
|
118
|
+
- lib/ruql/olx/exported_edx_course.rb
|
104
119
|
- lib/ruql/olx/olx.rb
|
105
120
|
- lib/ruql/olx/version.rb
|
106
121
|
- ruql-olx.gemspec
|
@@ -128,5 +143,5 @@ requirements: []
|
|
128
143
|
rubygems_version: 3.0.8
|
129
144
|
signing_key:
|
130
145
|
specification_version: 4
|
131
|
-
summary: OLX
|
146
|
+
summary: OLX renderer for RuQL
|
132
147
|
test_files: []
|