ruql-olx 0.0.2 → 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.
- 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: []
|