moodleQuizDownloader 0.0.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|