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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58a477614eb1b0baea402a669b3ae78984bad1f5956f41feb236831e185f91e9
4
- data.tar.gz: 5c3d28aac985353cf40ff49361c9c773074c3acb0be03915677997895c69d43b
3
+ metadata.gz: 76998a04ff820500209dd5edd8bf3fee5a1eb06425367b31ce02583f242c4ec4
4
+ data.tar.gz: a54822aa0ccf2cf6999af1bf9ecd14a044a740820eaac184b3a7d52ebde5277b
5
5
  SHA512:
6
- metadata.gz: c7320c20e7f0de379dcf0e1173b67dbb280dcea2e67c1dad747c196cb79b7387f030b59ab80a24e0494f86ce27e86316dddb77238a7fcb900ad478cf0abea8bf
7
- data.tar.gz: e5fa89ae5ed156ae184292fc0eea9d806d6a924161d21ee0d5d9d2dfacf4b44a0357bd1efbcf5654a44f088a9703be0b3666999a3c0b7f6e93b7bc83f5fdc26b
6
+ metadata.gz: bd0bcd4bab32b7ede92f8120402085e30cf9ac03d46a6951cfaf18a0ba586ebda7638866c3e38e14ddc5a9d2a41bde931c42b76255dd06ada456cc67f75e220f
7
+ data.tar.gz: 706b36d71e9c5e5b8d2889ce736743e1e584eefecee42a4a61e9da0c1f13d3013965d418ba48bc10e22661a0d097d645a53a60afa640abaf995935ebf5f8441e
data/.gitignore CHANGED
@@ -1,2 +1,3 @@
1
1
  TAGS
2
2
  pkg
3
+ .byebug_history
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ruql-olx (0.0.2)
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 this line to your application's Gemfile:
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
@@ -1,3 +1,4 @@
1
1
  require "ruql/olx/version"
2
2
  require "ruql/olx/olx"
3
+ require "ruql/olx/exported_edx_course"
3
4
 
@@ -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
@@ -6,24 +6,50 @@ module Ruql
6
6
  attr_reader :output
7
7
 
8
8
  def initialize(quiz,options={})
9
- @sequential = options.delete('--sequential')
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
- @quiz = quiz
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
- ['--sequential', GetoptLong::REQUIRED_ARGUMENT],
19
- ['--group-select', GetoptLong::REQUIRED_ARGUMENT]
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 supports these options:
23
- --sequential=<filename>.xml
24
- Write the OLX quiz header (includes quiz name, time limit, etc to <filename>.xml.
25
- This information can be copy-pasted into the appropriate <sequential> OLX element
26
- in a course export. If omitted, no quiz header .xml file is created.
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
- write_quiz_header if @sequential
39
- @output
40
- end
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> &lt;tt&gt;each&lt;/tt&gt; </pre></choice>
96
- # <choice correct="false">Every element of the collection must respond to <pre> &lt;tt&gt;each&lt;/tt&gt; </pre></choice>
97
- # <choice correct="false">The collection itself must be one of Ruby's built-in collection types, such as <pre> &lt;tt&gt;Array&lt;/tt&gt; or &lt;tt&gt;Set&lt;/tt&gt; </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)
@@ -1,5 +1,5 @@
1
1
  module Ruql
2
2
  class Olx
3
- VERSION = "0.0.2"
3
+ VERSION = "0.0.3"
4
4
  end
5
5
  end
@@ -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 5 renderer for RuQL"
10
- spec.description = "OLX 5 renderer for RuQL"
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.2
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-03 00:00:00.000000000 Z
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
- description: OLX 5 renderer for RuQL
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 5 renderer for RuQL
146
+ summary: OLX renderer for RuQL
132
147
  test_files: []