moodleQuizDownloader 0.0.1 → 0.2.0

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-gemset +1 -0
  3. data/.ruby-version +1 -0
  4. data/README.md +33 -8
  5. data/lib/moodleQuizDownloader.rb +15 -2
  6. data/lib/moodleQuizDownloader/attempt_selector.rb +37 -0
  7. data/lib/moodleQuizDownloader/file_name_creator.rb +3 -3
  8. data/lib/moodleQuizDownloader/moodle_parser.rb +42 -5
  9. data/lib/moodleQuizDownloader/option_handler.rb +11 -4
  10. data/lib/moodleQuizDownloader/quiz_downloader.rb +87 -39
  11. data/lib/moodleQuizDownloader/version.rb +1 -1
  12. data/moodleQuizDownloader.gemspec +10 -6
  13. data/spec/anonymize.rb +26 -0
  14. data/spec/api/asciify_spec.rb +2 -2
  15. data/spec/api/faker_spec.rb +12 -0
  16. data/spec/api/pdfkit/issue_cmap_table_spec.rb +43 -0
  17. data/spec/api/pdfkit/testfiles/notworking.html +1364 -0
  18. data/spec/api/pdfkit/testfiles/notworkingimagesremoved.html +1364 -0
  19. data/spec/api/pdfkit/testfiles/working.html +1910 -0
  20. data/spec/api/pdfkit_spec.rb +2 -2
  21. data/spec/fileNameCreator_spec.rb +12 -3
  22. data/spec/moodleparsing/attempt_selector_spec.rb +52 -0
  23. data/spec/moodleparsing/config_spec.rb +10 -0
  24. data/spec/moodleparsing/irb_commands.md +13 -0
  25. data/spec/moodleparsing/moodleparsing_spec.rb +17 -28
  26. data/spec/moodleparsing/testfiles/attempts-17-shown.html +635 -0
  27. data/spec/moodleparsing/testfiles/attempts-17-shown.html.temp +635 -0
  28. data/spec/moodleparsing/testfiles/attempts-fewer-shown.html +467 -0
  29. data/spec/moodleparsing/{exam-overview-moodle-ss2013.html → testfiles/exam-overview-moodle-ss2013.html} +0 -0
  30. data/spec/moodleparsing/{exam-overview.html → testfiles/exam-overview.html} +0 -0
  31. data/spec/moodleparsing/testfiles/login_failed.html +159 -0
  32. data/spec/moodleparsing/testfiles/login_successful.html +307 -0
  33. data/spec/moodleparsing/testfiles/occ +1 -0
  34. data/spec/moodleparsing/{review-page-nutzerbild.html → testfiles/review-page-nutzerbild.html} +0 -0
  35. data/spec/moodleparsing/{review-page.html → testfiles/review-page.html} +0 -0
  36. data/spec/option_handler_spec.rb +9 -1
  37. data/spec/smoketest_connection_spec.rb +21 -0
  38. data/spec/spec_helper.rb +9 -0
  39. metadata +133 -36
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a8c17d2cdd565630203b2eaacaaf27fe47fe3110
4
- data.tar.gz: de31f285dd09c3cfd339c01a3761067169f08bbf
3
+ metadata.gz: a7e868ece8269a98c5a62ff40f9438412ed7f068
4
+ data.tar.gz: 5f9c904e12793af94eae0398fd24cbd3f8501e34
5
5
  SHA512:
6
- metadata.gz: 5d2fa647058a7534f13518b88ab538d1a195a57090f8293b4803b4044b134e256d20a396c2ac8a85d1d130fdea9c3ca88eb64dd3ca2424966062dfd72855470f
7
- data.tar.gz: b862718b20c6ae363765f1dae1d6ac1b97104375f87d20af8b471f46605a791f96f77fc6cae2c2f0565c5fdcfd76992820f84353fdde8cf49abe296bba71fb99
6
+ metadata.gz: 2c52ebce025f19243e3d224f2a3f1312f416585f6cf828b838626107f234f344e1e1a36ab584f90e58654f2ef4591a8aaf563acfb40cae731fdc5e6a0bdbaa10
7
+ data.tar.gz: 36d8382a844414c0ad59a5608d39f06c038d93882264173872b66bb85cd7eb8bc5bba6f1f728ab0ace98b9fe2f272306ec356eed9073ccffd1d82f20fdd6fce0
@@ -0,0 +1 @@
1
+ myrubystack
@@ -0,0 +1 @@
1
+ ruby-2.2.0
data/README.md CHANGED
@@ -1,20 +1,25 @@
1
1
  # MoodleQuizDownloader
2
2
 
3
- TODO: Write a gem description
3
+ Automatically downloads all attempts of a Moodle Quiz and stores them as PDF or HTML.
4
4
 
5
5
  ## Installation
6
6
 
7
- Add this line to your application's Gemfile:
7
+ $ gem install moodleQuizDownloader
8
8
 
9
- gem 'moodleQuizDownloader'
9
+ ## What it does
10
10
 
11
- And then execute:
11
+ This script downloads all attempts of a moodle quiz and saves them in pdfs named with the student names.
12
+ For this it logs into moodle via the http/html web site, goes to the quiz and scrapes all information like student names etc. from the html it finds.
12
13
 
13
- $ bundle
14
+ To get started, you first need the moodle id of the quiz you want to download. E.g. if the url shown in the browser was
14
15
 
15
- Or install it yourself as:
16
+ https://moodle.htw-berlin.de/mod/quiz/report.php?id=4711&mode=overview
16
17
 
17
- $ gem install moodleQuizDownloader
18
+ If you view the quiz's attempts in the browser,
19
+
20
+ 4711
21
+
22
+ is the id.
18
23
 
19
24
  ## Usage
20
25
 
@@ -25,7 +30,7 @@ for example, run
25
30
  to see a list of all attempts in exam 4711 on the specified server,
26
31
  and
27
32
 
28
- $ moodleQuizDownloader.rb -e 4711 -u drblinken -s http://moodle2.htw-berlin.de/ -p geheim --verbose download
33
+ $ moodleQuizDownloader.rb -e 4711 -u drblinken -s http://moodle.someschool.de/ -p geheim --verbose download
29
34
 
30
35
  to download them.
31
36
 
@@ -33,6 +38,26 @@ to see all options:
33
38
 
34
39
  $ moodleQuizDownloader.rb help
35
40
 
41
+ ## try it out in irb
42
+
43
+ e.g.:
44
+
45
+ irb
46
+ load 'lib/moodleQuizDownloader.rb'
47
+
48
+ arguments = "-o /Users/kleinen/Documents/ss2013/exam- ckup/info3/pz1/exam_a/raw -e 23843".split(" ")
49
+ options = OptionHandler.new(arguments).parse
50
+ q = QuizDownloader.new
51
+ agent = Mechanize.new
52
+ q.smoketest(options.moodle_server)
53
+ q.attemptlist(options,agent)
54
+ page = q.smoketest(options.moodle_server)
55
+ username_input = page.search('#username')
56
+
57
+ ## Todos / New Features
58
+
59
+ * Add an option to be prompted for the password ( or just do it automatically if it is not provided)
60
+
36
61
  ## Contributing
37
62
 
38
63
  1. Fork it
@@ -1,15 +1,28 @@
1
1
  require_relative "moodleQuizDownloader/version.rb"
2
2
  require_relative "moodleQuizDownloader/option_handler.rb"
3
3
  require_relative "moodleQuizDownloader/quiz_downloader.rb"
4
+ require_relative "moodleQuizDownloader/attempt_selector.rb"
4
5
 
5
6
  module MoodleQuizDownloader
7
+
6
8
  def run_script(arguments)
7
- options = OptionHandler.new(arguments).parse
9
+ option_handler = OptionHandler.new(arguments)
10
+ options = option_handler.options
8
11
  if options.usage
9
12
  puts options.usage
10
13
  exit
11
14
  end
12
- QuizDownloader.new.run(options)
15
+ options.moodle_password ||= prompt_for_password
16
+ begin
17
+ QuizDownloader.new(options).run
18
+ rescue PageException => e
19
+ puts "#{e.message}"
20
+ end
21
+ end
22
+
23
+ def prompt_for_password(prompt="Enter Password")
24
+ require 'highline/import'
25
+ ask(prompt) {|q| q.echo = false}
13
26
  end
14
27
  end
15
28
 
@@ -0,0 +1,37 @@
1
+ #<select id="id_attempts" name="attempts">
2
+ #
3
+ # <option selected="selected" value="enrolled_with"><#/option>
4
+ # <option value="enrolled_without"></option>
5
+ # <option value="enrolled_any"></option>
6
+ # <option value="all_with"></option>
7
+ #
8
+ #</select>
9
+ #
10
+ #
11
+ # <input id="id_submitbutton" type="submit" #value="Bericht anzeigen" name="submitbutton"></input#>
12
+ #
13
+ #
14
+
15
+
16
+ # moodle selecty "students who take the quiz and are enrolled"
17
+ # as default -
18
+ # to archive quizzes later on, this Module selects
19
+ # all attempts.
20
+
21
+
22
+ # form
23
+ #mform1
24
+ module MoodleAttemptSelector
25
+
26
+ def select_all_attempts_done(page)
27
+ form = page.form_with(id: 'mform1')
28
+ selectlist = form.field_with(id: 'id_attempts')
29
+ selectlist.value="all_with"
30
+ page = form.submit
31
+ end
32
+
33
+
34
+
35
+ end
36
+
37
+
@@ -5,8 +5,8 @@ class FileNameCreator
5
5
  @@regexp = /(\w*)( (\w*))? (\w*)/
6
6
  @@map = Asciify::Mapping.new(:default)
7
7
 
8
- def self.fileNameFor(outputdir,name)
9
- File.join(outputdir,name.asciify(@@map).gsub(@@regexp,"\\1\\3\\4.pdf"))
8
+ def self.file_name_for(outputdir,name,extension = 'pdf')
9
+ name = name.gsub("-","")
10
+ File.join(outputdir,name.asciify(@@map).gsub(@@regexp,"\\1\\3\\4.#{extension}"))
10
11
  end
11
-
12
12
  end
@@ -1,17 +1,29 @@
1
+
2
+ # This file contains Moodle settings
3
+ # like urls and xml paths
4
+ # decided against putting them in a config file,
5
+ # as they can be adapted here.
6
+ require_relative 'attempt_selector.rb'
1
7
  module MoodleParser
8
+ include MoodleAttemptSelector
2
9
  @@user_view_regexp = Regexp.new('http://moodle2.htw-berlin.de/moodle/user/view.php')
3
10
 
4
- def moodle_login_page(server)
5
- "#{server}/moodle/login/index.php"
11
+ def moodle_login_page(server)
12
+ "#{server}"
13
+ # oder vielleicht besser:
14
+ #https://moodle.htw-berlin.de/login/index.php
15
+ # "#{server}/moodle/login/index.php"
6
16
  end
7
17
  def moodle_item(server)
8
18
  "#{server}/moodle/mod/quiz/view.php?id="
9
19
  end
10
20
  def moodle_quiz_report(server)
11
- "#{server}/moodle/mod/quiz/report.php?mode=overview&id="
21
+ # https://moodle.htw-berlin.de/mod/quiz/report.php?id=46982&mode=overview
22
+ "#{server}/mod/quiz/report.php?mode=overview&id="
12
23
  end
13
24
 
14
25
  def login(agent,moodle_login_page,moodle_username,moodle_password)
26
+ chatter "connecting to moodle"
15
27
  puts "++++#{moodle_login_page}"
16
28
  page = agent.get(moodle_login_page)
17
29
  form = page.forms[1]
@@ -22,11 +34,22 @@ module MoodleParser
22
34
 
23
35
  def selectReviewLinks(page)
24
36
  page.links.select do |ll|
25
- cls = ll.attributes.attributes['class']
26
- cls && cls.value == 'reviewlink'
37
+ css_class = ll.attributes.attributes['class']
38
+ css_class && css_class.value == 'reviewlink'
27
39
  end
28
40
  end
29
41
 
42
+ def extract_complete_attempt_list(page)
43
+ page = select_all_attempts_done(page)
44
+ attempt_list = extract_attempt_list(page)
45
+ if (announced = extract_attempt_count(page)) != attempt_list.size
46
+ puts "#### WARNING: There was a different number of "
47
+ puts "#### attempts announced on the page - #{announced}"
48
+ puts "#### but only #{attempt_list.size} could be downloaded"
49
+ end
50
+ attempt_list
51
+ end
52
+
30
53
  def extract_attempt_list(page)
31
54
  i = 1
32
55
  result_list = []
@@ -39,6 +62,20 @@ module MoodleParser
39
62
  result_list
40
63
  end
41
64
 
65
+ def extract_attempt_count(page)
66
+ ##class: quizattemptcounts
67
+ quizattemptcounts_div = page.at('.quizattemptcounts')
68
+ m = quizattemptcounts_div.content.match(/Attempts: (\d+)/)
69
+ m[1].to_i
70
+ end
71
+
72
+ def all_attempts_shown(page,number_of_attempts_shown = nil)
73
+ unless number_of_attempts_shown
74
+ number_of_attempts_shown = extract_attempt_list(page).size
75
+ end
76
+ number_of_attempts_shown == extract_attempt_count(page)
77
+ end
78
+
42
79
  def extractUserName(page)
43
80
 
44
81
  l = page.links.select {|x| @@user_view_regexp.match(x.href)}
@@ -3,7 +3,7 @@ require 'ostruct'
3
3
 
4
4
  class OptionHandler
5
5
  def valid_commands
6
- [:list, :download, :connect]
6
+ [:list, :download, :connect, :options]
7
7
  end
8
8
  attr_reader :options, :optionparser
9
9
  def initialize(arguments = [])
@@ -20,7 +20,7 @@ class OptionHandler
20
20
  def valid?(options)
21
21
  options.valid &&
22
22
  options.moodle_username &&
23
- options.moodle_password &&
23
+ #options.moodle_password &&
24
24
  options.moodle_server &&
25
25
  options.exam_id != 0
26
26
  end
@@ -38,6 +38,7 @@ class OptionHandler
38
38
  options.exam_id = 0
39
39
  options.command = :list
40
40
  options.usage = nil
41
+ options.html = false
41
42
 
42
43
 
43
44
  opt_parser = OptionParser.new do |opts|
@@ -57,18 +58,23 @@ DELIM
57
58
  "Output Dir (defaults to . )") do | dir |
58
59
  options.outputdir = dir
59
60
  end
61
+ opts.on("-h", "--html",
62
+ "Save Exams in HTML instead as PDFs") do | html |
63
+ options.html = html
64
+ end
65
+
60
66
  opts.on("-u", "--user USERNAME",
61
67
  "Your Moodle User Name",
62
68
  "(can also be set via MOODLE_USERNAME environment variable)") do |username|
63
69
  options.moodle_username = username
64
70
  end
65
- opts.on("-p", "--password PASSWORD]",
71
+ opts.on("-p", "--password PASSWORD",
66
72
  "Your Moodle Password",
67
73
  "(can also be set via MOODLE_PASSWORD environment variable)") do |password|
68
74
  options.moodle_password = password
69
75
  end
70
76
 
71
- opts.on("-s", "--server SERVER]",
77
+ opts.on("-s", "--server SERVER",
72
78
  "Your Moodle Server",
73
79
  "(can also be set via MOODLE_SERVER environment variable)") do |server|
74
80
  options.moodle_server = server
@@ -101,4 +107,5 @@ DELIM
101
107
  validate(options,opt_parser)
102
108
  options
103
109
  end # parse()
110
+
104
111
  end
@@ -3,76 +3,124 @@ require 'pdfkit'
3
3
  require 'asciify'
4
4
  require "bundler/setup"
5
5
  require 'fileutils'
6
- require_relative 'file_name_creator'
7
6
 
7
+ require_relative 'file_name_creator'
8
8
  require_relative 'moodle_parser'
9
9
 
10
-
10
+ class PageException < Exception
11
+ end
11
12
 
12
13
  class QuizDownloader
14
+
13
15
  include MoodleParser
14
16
 
15
- def smoketest(server)
17
+ attr_reader :options
18
+ def initialize(options)
19
+ @options = options
20
+ end
21
+
22
+ def run
23
+ agent = Mechanize.new
24
+ case options.command
25
+ when :connect
26
+ smoketest(options.moodle_server, options.moodle_username, options.moodle_password)
27
+ when :list
28
+ attempt_list = attemptlist(agent)
29
+ puts "found #{attempt_list.length} attempts:"
30
+ puts attempt_list.map{|a,b| a}
31
+ when :download
32
+ chatter "options #{options.inspect}"
33
+ attempt_list = attemptlist(agent)
34
+ download(attempt_list,agent)
35
+ when :options
36
+ puts "#{options.inspect}"
37
+
38
+ else
39
+ puts "command #{options.command} not recognized"
40
+ end
41
+ end
42
+
43
+ def chatter(message)
44
+ puts "==== #{message}" if options.verbose
45
+ end
46
+
47
+ def smoketest(server, username = "xxx", password ="xxx")
16
48
  begin
17
49
  agent = Mechanize.new
18
- page = login(agent,moodle_login_page(server),"xxx","xxx")
19
- if page.nil?
20
- puts "could not connect to server #{server}"
50
+ page = login(agent,moodle_login_page(server),username,password)
51
+ unless server_reachable?(page)
52
+ puts "Could not connect to server #{server}"
21
53
  else
22
- puts "connection to server seems to be working"
54
+ puts "Connection to server seems to be working..."
55
+ if login_successful?(page)
56
+ puts "and login has been successful."
57
+ else
58
+ puts "but could not login with the given credentials."
59
+ end
23
60
  end
24
61
  rescue Exception => e
25
62
  puts "could not connect to server #{server}"
26
63
  #puts e.message
27
64
  end
65
+ page
28
66
  end
29
- def attemptlist(options,agent)
30
- puts "connecting to moodle" if options.verbose
31
- page = login(agent,moodle_login_page(options.moodle_server),options.moodle_username,options.moodle_password)
67
+
68
+ def login_successful?(page)
69
+ page.search('#username').empty?
70
+ end
71
+ def server_reachable?(page)
72
+ !page.nil?
73
+ end
74
+
75
+ def attemptlist(agent)
76
+
77
+ login(agent,moodle_login_page(options.moodle_server),options.moodle_username,options.moodle_password)
78
+
32
79
  moodle_overview_url = moodle_quiz_report(options.moodle_server)+options.exam_id.to_s
33
- puts "==== downloading overview" if options.verbose
34
- puts moodle_overview_url if options.verbose
35
- page = agent.get(moodle_overview_url)
36
- #attempts = selectReviewLinks(page)
37
- attempt_list = extract_attempt_list(page)
80
+ chatter "downloading overview"
81
+ chatter "url:#{moodle_overview_url}"
82
+ begin
83
+ page = agent.get(moodle_overview_url)
84
+ attempt_list = extract_attempt_list(page)
85
+ rescue Mechanize::ResponseCodeError => e
86
+ raise e unless (e.response_code == "404")
87
+ raise PageException, "Exam not found - maybe the wrong id: #{options.exam_id} ?\n(while trying to access: #{moodle_overview_url})"
88
+ end
89
+
38
90
  end
39
91
 
40
- def download(attempt_list,options,agent)
92
+ def download(attempt_list,agent)
41
93
  FileUtils.mkdir_p(options.outputdir)
94
+ count = 0
42
95
  attempt_list.each do | attempt|
43
96
  student_name, attempt_url = attempt
44
97
 
45
98
  attempt_url = attempt_url+"&showall=1"
46
99
  page = agent.get(attempt_url)
47
- puts "==== downloading attempt" if options.verbose
48
- puts attempt_url if options.verbose
100
+ chatter "downloading attempt"
101
+ chatter "url #{attempt_url}"
102
+ chatter attempt_url
49
103
  #student = extractUserName(page)
50
104
  student = student_name
51
- outputfile = FileNameCreator.fileNameFor(options.outputdir,student)
52
- puts "Loading: #{student}"
53
- kit = PDFKit.new(page.body)
54
- kit.to_file(outputfile)
55
- end
56
- end
57
105
 
106
+ puts "Loading: #{student}"
58
107
 
59
- def run(options)
60
- agent = Mechanize.new
61
- case options.command
62
- when :connect
63
- smoketest(options.moodle_server)
64
- when :list
65
- attempt_list = attemptlist(options,agent)
66
- puts "found #{attempt_list.length} attempts:"
67
- puts attempt_list.map{|a,b| a}
68
- when :download
69
- attempt_list = attemptlist(options,agent)
70
- download(attempt_list,options,agent)
108
+ chatter "downloading overview"
109
+ chatter "outputfile:#{options.outputdir}"
110
+ kit = PDFKit.new(page.body)
111
+ if options.html
112
+ chatter "save html"
113
+ outputfile_html = FileNameCreator.file_name_for(options.outputdir,student,'html')
114
+ File.open(outputfile_html, 'w') { |file| file.write(page.body) }
71
115
  else
72
- puts "command #{options.command} not recognized"
116
+ chatter "kit to file"
117
+ outputfile = FileNameCreator.file_name_for(options.outputdir,student)
118
+ kit.to_file(outputfile)
119
+ chatter "x"
120
+ end
121
+ count += 1
73
122
  end
123
+ puts "downloaded #{count} attempts"
74
124
  end
75
-
76
-
77
125
  end
78
126