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.
- checksums.yaml +4 -4
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/README.md +33 -8
- data/lib/moodleQuizDownloader.rb +15 -2
- data/lib/moodleQuizDownloader/attempt_selector.rb +37 -0
- data/lib/moodleQuizDownloader/file_name_creator.rb +3 -3
- data/lib/moodleQuizDownloader/moodle_parser.rb +42 -5
- data/lib/moodleQuizDownloader/option_handler.rb +11 -4
- data/lib/moodleQuizDownloader/quiz_downloader.rb +87 -39
- data/lib/moodleQuizDownloader/version.rb +1 -1
- data/moodleQuizDownloader.gemspec +10 -6
- data/spec/anonymize.rb +26 -0
- data/spec/api/asciify_spec.rb +2 -2
- data/spec/api/faker_spec.rb +12 -0
- data/spec/api/pdfkit/issue_cmap_table_spec.rb +43 -0
- data/spec/api/pdfkit/testfiles/notworking.html +1364 -0
- data/spec/api/pdfkit/testfiles/notworkingimagesremoved.html +1364 -0
- data/spec/api/pdfkit/testfiles/working.html +1910 -0
- data/spec/api/pdfkit_spec.rb +2 -2
- data/spec/fileNameCreator_spec.rb +12 -3
- data/spec/moodleparsing/attempt_selector_spec.rb +52 -0
- data/spec/moodleparsing/config_spec.rb +10 -0
- data/spec/moodleparsing/irb_commands.md +13 -0
- data/spec/moodleparsing/moodleparsing_spec.rb +17 -28
- data/spec/moodleparsing/testfiles/attempts-17-shown.html +635 -0
- data/spec/moodleparsing/testfiles/attempts-17-shown.html.temp +635 -0
- data/spec/moodleparsing/testfiles/attempts-fewer-shown.html +467 -0
- data/spec/moodleparsing/{exam-overview-moodle-ss2013.html → testfiles/exam-overview-moodle-ss2013.html} +0 -0
- data/spec/moodleparsing/{exam-overview.html → testfiles/exam-overview.html} +0 -0
- data/spec/moodleparsing/testfiles/login_failed.html +159 -0
- data/spec/moodleparsing/testfiles/login_successful.html +307 -0
- data/spec/moodleparsing/testfiles/occ +1 -0
- data/spec/moodleparsing/{review-page-nutzerbild.html → testfiles/review-page-nutzerbild.html} +0 -0
- data/spec/moodleparsing/{review-page.html → testfiles/review-page.html} +0 -0
- data/spec/option_handler_spec.rb +9 -1
- data/spec/smoketest_connection_spec.rb +21 -0
- data/spec/spec_helper.rb +9 -0
- metadata +133 -36
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a7e868ece8269a98c5a62ff40f9438412ed7f068
|
4
|
+
data.tar.gz: 5f9c904e12793af94eae0398fd24cbd3f8501e34
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2c52ebce025f19243e3d224f2a3f1312f416585f6cf828b838626107f234f344e1e1a36ab584f90e58654f2ef4591a8aaf563acfb40cae731fdc5e6a0bdbaa10
|
7
|
+
data.tar.gz: 36d8382a844414c0ad59a5608d39f06c038d93882264173872b66bb85cd7eb8bc5bba6f1f728ab0ace98b9fe2f272306ec356eed9073ccffd1d82f20fdd6fce0
|
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
myrubystack
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.2.0
|
data/README.md
CHANGED
@@ -1,20 +1,25 @@
|
|
1
1
|
# MoodleQuizDownloader
|
2
2
|
|
3
|
-
|
3
|
+
Automatically downloads all attempts of a Moodle Quiz and stores them as PDF or HTML.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
7
|
-
|
7
|
+
$ gem install moodleQuizDownloader
|
8
8
|
|
9
|
-
|
9
|
+
## What it does
|
10
10
|
|
11
|
-
|
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
|
-
|
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
|
-
|
16
|
+
https://moodle.htw-berlin.de/mod/quiz/report.php?id=4711&mode=overview
|
16
17
|
|
17
|
-
|
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://
|
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
|
data/lib/moodleQuizDownloader.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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.
|
9
|
-
|
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
|
-
|
5
|
-
"#{server}
|
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
|
-
|
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
|
-
|
26
|
-
|
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
|
-
|
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),
|
19
|
-
|
20
|
-
puts "
|
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 "
|
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
|
-
|
30
|
-
|
31
|
-
page
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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,
|
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
|
-
|
48
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
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
|
|