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 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: []