ruby_proctor 1.0.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +2 -0
- data/bin/ruby_proctor +11 -0
- data/bin/ruby_proctor.bat +3 -0
- data/bin/ruby_proctor.sh +3 -0
- data/lib/ruby_proctor/configuration.rb +10 -0
- data/lib/ruby_proctor/constants.rb +13 -0
- data/lib/ruby_proctor/exam.rb +20 -0
- data/lib/ruby_proctor/hashify.rb +26 -0
- data/lib/ruby_proctor/interfaces/gui.rb +620 -0
- data/lib/ruby_proctor/interfaces/logo.gif +0 -0
- data/lib/ruby_proctor/interfaces/terminal.rb +161 -0
- data/lib/ruby_proctor/logger.rb +109 -0
- data/lib/ruby_proctor/logging/quiz_log.rb +4 -0
- data/lib/ruby_proctor/processor.rb +126 -0
- data/lib/ruby_proctor/proctor.rb +206 -0
- data/lib/ruby_proctor/question.rb +17 -0
- data/lib/ruby_proctor/string_ext.rb +5 -0
- data/lib/ruby_proctor.rb +16 -0
- metadata +61 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# main.rb
|
|
2
|
+
#
|
|
3
|
+
|
|
4
|
+
# includes
|
|
5
|
+
require 'rubygems'
|
|
6
|
+
require 'bundler/setup'
|
|
7
|
+
|
|
8
|
+
require 'ostruct'
|
|
9
|
+
|
|
10
|
+
require 'ruby_proctor/constants.rb'
|
|
11
|
+
require 'ruby_proctor/exam.rb'
|
|
12
|
+
require 'ruby_proctor/question.rb'
|
|
13
|
+
require 'ruby_proctor/logger'
|
|
14
|
+
require 'ruby_proctor/processor.rb'
|
|
15
|
+
require 'ruby_proctor/proctor.rb'
|
|
16
|
+
require 'ruby_proctor/string_ext.rb'
|
|
17
|
+
|
|
18
|
+
include Constants
|
|
19
|
+
|
|
20
|
+
def ruby_proctor_terminal
|
|
21
|
+
|
|
22
|
+
# Print Pretty ASCII Logo
|
|
23
|
+
puts ''
|
|
24
|
+
puts '##################################################################'
|
|
25
|
+
puts '## ______ _ ______ _ ##'
|
|
26
|
+
puts '## | ___ \ | | | ___ \ | | ##'
|
|
27
|
+
puts '## | |_/ / _| |__ _ _| |_/ / __ ___ ___| |_ ___ _ __ ##'
|
|
28
|
+
puts "## | / | | | '_ \\| | | | __/ '__/ _ \\ / __| __/ _ \\| '__| ##"
|
|
29
|
+
puts '## | |\ \ |_| | |_) | |_| | | | | | (_) | (__| || (_) | | ##'
|
|
30
|
+
puts '## \_| \_\__,_|_.__/ \__, \_| |_| \___/ \___|\__\___/|_| ##'
|
|
31
|
+
puts '## __/ | ##'
|
|
32
|
+
puts '## |___/ ##'
|
|
33
|
+
puts '##################################################################'
|
|
34
|
+
puts ''
|
|
35
|
+
|
|
36
|
+
# Parse Options, POSIX Style
|
|
37
|
+
require 'optparse'
|
|
38
|
+
|
|
39
|
+
options = OpenStruct.new
|
|
40
|
+
|
|
41
|
+
begin
|
|
42
|
+
OptionParser.new do |opt|
|
|
43
|
+
opt.on('-f [FILE_PATH]', 'File Path to Quiz (Can be relative or absolute)') { |o| options.file_path = o }
|
|
44
|
+
opt.on('-q [NUM_QUESTIONS]', 'number of questions for quiz') { |o| options.num_questions = o }
|
|
45
|
+
opt.on('-t [TIME_LIMIT]', 'Sets a time limit by number of minutes') { |o| options.time_limit = o }
|
|
46
|
+
opt.on('-v', 'Provides a print out of all of your quiz attempts') { |o| options.view_quizzes = true }
|
|
47
|
+
end.parse!
|
|
48
|
+
rescue => e
|
|
49
|
+
puts "** Error Parsing Arguments: " + e.message + " **"
|
|
50
|
+
puts "Try Resolving the above error, and try running again\n\n"
|
|
51
|
+
puts "Exiting..."
|
|
52
|
+
exit # Exit Program, we can't use this Exam File
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
options_hash = options.to_h()
|
|
56
|
+
|
|
57
|
+
if options.view_quizzes && options_hash.length == 1
|
|
58
|
+
# Delegate Printing out All Quizzes for current PID
|
|
59
|
+
logger = Logger.new
|
|
60
|
+
|
|
61
|
+
begin
|
|
62
|
+
quiz_attempts = logger.read_from_log
|
|
63
|
+
logger.print_quiz_attempts(quiz_attempts)
|
|
64
|
+
puts "Exiting..."
|
|
65
|
+
exit
|
|
66
|
+
rescue => e
|
|
67
|
+
puts "** Error Reading Log File: " + e.message + " **"
|
|
68
|
+
puts "Try Resolving the above error, and try running again\n\n"
|
|
69
|
+
puts "Exiting..."
|
|
70
|
+
exit # Exit Program, we can't use this Exam File
|
|
71
|
+
end
|
|
72
|
+
elsif options.view_quizzes && options_hash.length > 1
|
|
73
|
+
puts "Invalid Arguments: No other Arguments can be used with -v for viewing the quizzes"
|
|
74
|
+
puts "Exiting..."
|
|
75
|
+
exit
|
|
76
|
+
elsif options_hash.length == 0
|
|
77
|
+
puts "Not enough arguments to perform any action, run 'ruby_proctor -h' to get a list of available arguments and their function"
|
|
78
|
+
puts "Exiting..."
|
|
79
|
+
exit
|
|
80
|
+
elsif !options.file_path
|
|
81
|
+
puts "No File name was specified with quiz configuration! Make sure you provide a filepath with the -f argument"
|
|
82
|
+
puts "Exiting..."
|
|
83
|
+
exit
|
|
84
|
+
end
|
|
85
|
+
# # Check that only a filename is passed
|
|
86
|
+
# if ARGV.length < 1
|
|
87
|
+
# puts "Too few arguments, arguments are as follows: ruby_proctor <file_path> <num_questions> <time_in_minutes> "
|
|
88
|
+
# puts "Exiting..."
|
|
89
|
+
# exit
|
|
90
|
+
# elsif ARGV.length > 3
|
|
91
|
+
# puts "Too many arguments, arguments are as follows: ruby_proctor <file_path> <num_questions> <time_in_minutes> "
|
|
92
|
+
# puts "Exiting..."
|
|
93
|
+
# exit
|
|
94
|
+
#end
|
|
95
|
+
options.file_path.untaint()
|
|
96
|
+
|
|
97
|
+
filepath = options.file_path
|
|
98
|
+
max_questions = -1
|
|
99
|
+
|
|
100
|
+
if options.num_questions
|
|
101
|
+
if options.num_questions.is_integer?
|
|
102
|
+
if options.num_questions.to_i <= 0 || options.num_questions.to_i > Constants::QUIZ_MAX_QUESTIONS
|
|
103
|
+
puts "Can't pick zero, a negative number, or above 10000 questions for an exam, please pick a number above 0, but less than 10000 (Max supported questions)"
|
|
104
|
+
puts "NOTE: anything above the number of total questions in the exam will return the entire question set and no more"
|
|
105
|
+
puts "Exiting..."
|
|
106
|
+
exit
|
|
107
|
+
end
|
|
108
|
+
max_questions = options.num_questions.to_i
|
|
109
|
+
else
|
|
110
|
+
puts "Number of Questions Specified must be a number greater than 0"
|
|
111
|
+
puts "NOTE: anything above the number of total questions in the exam will return the entire question set and no more"
|
|
112
|
+
puts "Exiting..."
|
|
113
|
+
exit
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
time = -1
|
|
118
|
+
if options.time_limit
|
|
119
|
+
if options.time_limit.is_integer?
|
|
120
|
+
if options.time_limit.to_i <= 0
|
|
121
|
+
puts "Error: Can't pick zero or negative number of minutes, please pick a number above 0"
|
|
122
|
+
puts "NOTE: Omit the -t flag if you wish for an unlimited time test"
|
|
123
|
+
puts "Exiting..."
|
|
124
|
+
exit
|
|
125
|
+
end
|
|
126
|
+
time = options.time_limit.to_i
|
|
127
|
+
else
|
|
128
|
+
puts "Number of minutes must be a valid number greater than 0"
|
|
129
|
+
puts "NOTE: Omit the -t flag if you wish for an unlimited time test"
|
|
130
|
+
puts "Exiting..."
|
|
131
|
+
exit
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# if(!File.exist?(filepath))
|
|
136
|
+
# puts 'Questions File not found, exiting..'
|
|
137
|
+
# exit
|
|
138
|
+
# end
|
|
139
|
+
|
|
140
|
+
#puts "Welcome to Ruby Proctor!\n"
|
|
141
|
+
|
|
142
|
+
puts "Beginning to Process Exam ...\n\n"
|
|
143
|
+
processor = Processor.new(filepath, max_questions)
|
|
144
|
+
|
|
145
|
+
# Try to Process, and cleanly display any exceptions
|
|
146
|
+
begin
|
|
147
|
+
exam = processor.process()
|
|
148
|
+
puts "Exam Processed Successfully"
|
|
149
|
+
rescue => e
|
|
150
|
+
puts "** Error Processing Exam File: " + e.message + " **"
|
|
151
|
+
puts "Try Resolving the above error, and try running again\n\n"
|
|
152
|
+
puts "Exiting..."
|
|
153
|
+
exit # Exit Program, we can't use this Exam File
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Start Officiating Exam
|
|
157
|
+
proctor = Proctor.new(exam, time)
|
|
158
|
+
proctor.officiate_exam
|
|
159
|
+
|
|
160
|
+
puts 'Exiting...'
|
|
161
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
|
|
2
|
+
require 'ruby_proctor/constants'
|
|
3
|
+
require 'ruby_proctor/question'
|
|
4
|
+
require 'ruby_proctor/exam'
|
|
5
|
+
require 'ruby_proctor/string_ext'
|
|
6
|
+
require 'ostruct'
|
|
7
|
+
|
|
8
|
+
require 'yaml'
|
|
9
|
+
|
|
10
|
+
require 'os'
|
|
11
|
+
|
|
12
|
+
include Process::UID
|
|
13
|
+
include Constants
|
|
14
|
+
|
|
15
|
+
class Logger
|
|
16
|
+
|
|
17
|
+
attr_accessor :file_path, :real_uid
|
|
18
|
+
|
|
19
|
+
#YAML.load_stream(File.read('test.yml'))
|
|
20
|
+
class LoggingError < StandardError
|
|
21
|
+
def initialize(msg="Error Occurred During Logging")
|
|
22
|
+
super
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize()
|
|
27
|
+
|
|
28
|
+
if OS.windows?
|
|
29
|
+
@file_path = ENV['ALLUSERSPROFILE'] + "/Ruby Proctor/quizlog.dat"
|
|
30
|
+
@real_uid = ENV['USERNAME']
|
|
31
|
+
else
|
|
32
|
+
@file_path = '/var/log/' + QUIZ_FILE_NAME
|
|
33
|
+
@real_uid = Process.uid
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
#@effective_uid = Process.euid
|
|
37
|
+
|
|
38
|
+
#if (Process::UID.sid_available?)
|
|
39
|
+
# @saved_uid = `ps -p #{Process.pid} -o svuid=`.strip
|
|
40
|
+
#else
|
|
41
|
+
# @saved_uid = ''
|
|
42
|
+
#end
|
|
43
|
+
|
|
44
|
+
#puts @saved_uid
|
|
45
|
+
#puts @real_uid
|
|
46
|
+
#puts @effective_uid
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def write_to_log(exam)
|
|
50
|
+
exam.results.uid = @real_uid
|
|
51
|
+
|
|
52
|
+
open(@file_path, 'a+') { |f|
|
|
53
|
+
f.puts exam.results.to_hash.to_yaml
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def read_from_log
|
|
58
|
+
quiz_attempts = Array.new
|
|
59
|
+
|
|
60
|
+
begin
|
|
61
|
+
YAML.load_stream(File.read(@file_path)) do |document|
|
|
62
|
+
#puts document
|
|
63
|
+
if document.key?("uid") && document["uid"] == @real_uid
|
|
64
|
+
quiz_attempts.push(OpenStruct.new(document))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
quiz_attempts
|
|
68
|
+
rescue => e
|
|
69
|
+
raise LoggingError.new "Error Opening File: File doesn't exist or permissions are incorrect!"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def print_quiz_attempts(quiz_attempts)
|
|
75
|
+
|
|
76
|
+
quiz_num = 1;
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if (quiz_attempts.length > 0)
|
|
80
|
+
|
|
81
|
+
for attempt in quiz_attempts do
|
|
82
|
+
|
|
83
|
+
puts "| Quiz # " + quiz_num.to_s + " |"
|
|
84
|
+
puts "---------------------------"
|
|
85
|
+
|
|
86
|
+
puts 'Number of Questions Answered Correctly: ' + attempt.num_correct.to_s + ' / ' + attempt.total_questions.to_s
|
|
87
|
+
puts 'Grade (Percentage): ' + attempt.grade.to_s
|
|
88
|
+
puts 'Letter Grade: ' + attempt.letter_grade
|
|
89
|
+
|
|
90
|
+
if (attempt.quiz_name)
|
|
91
|
+
puts 'Quiz Name: ' + attempt.quiz_name
|
|
92
|
+
end
|
|
93
|
+
puts 'Time Started: ' + attempt.time_started
|
|
94
|
+
puts 'Time Completed: ' + attempt.time_completed
|
|
95
|
+
puts 'Time Elapsed: ' + attempt.time_elapsed
|
|
96
|
+
|
|
97
|
+
if (attempt.time_elapsed && attempt.time_left)
|
|
98
|
+
puts 'Time Left: ' + attempt.time_left
|
|
99
|
+
puts ""
|
|
100
|
+
end
|
|
101
|
+
puts ""
|
|
102
|
+
#puts "---------------------------\n\n\n"
|
|
103
|
+
quiz_num += 1
|
|
104
|
+
end
|
|
105
|
+
else # Why is this useless?
|
|
106
|
+
puts "No Quiz Results to Display!"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
require 'ruby_proctor/constants'
|
|
2
|
+
require 'ruby_proctor/question'
|
|
3
|
+
require 'ruby_proctor/exam'
|
|
4
|
+
require 'ruby_proctor/string_ext'
|
|
5
|
+
|
|
6
|
+
include Constants
|
|
7
|
+
|
|
8
|
+
class Processor
|
|
9
|
+
|
|
10
|
+
@@_NOT_COMMENT_PATTERN = '^\s*[^\\' + Constants::COMMENT + ']'
|
|
11
|
+
@@_QUESTION_PATTERN = '^\s*' + Constants::Q_START
|
|
12
|
+
@@_ANSWERS_START_PATTERN = '^\s*' + Constants::A_START
|
|
13
|
+
@@_ANSWERS_END_PATTERN = '^\s*' + Constants::A_END
|
|
14
|
+
|
|
15
|
+
class ProcessingError < StandardError
|
|
16
|
+
def initialize(msg="Error Occurred During Processing")
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(file, max_questions)
|
|
22
|
+
@file = file
|
|
23
|
+
@max_questions = max_questions
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def process
|
|
27
|
+
questions = Array.new
|
|
28
|
+
|
|
29
|
+
last_symbol = ''
|
|
30
|
+
line_count = 0
|
|
31
|
+
last_question = Question.new(-1) # Dummy to define type? Weak Types are weird in OO
|
|
32
|
+
last_answer_set = nil
|
|
33
|
+
|
|
34
|
+
question_count = 0;
|
|
35
|
+
|
|
36
|
+
line_num = 0
|
|
37
|
+
|
|
38
|
+
begin
|
|
39
|
+
IO.foreach(@file) do |l|
|
|
40
|
+
line_num += 1
|
|
41
|
+
line = l.strip
|
|
42
|
+
|
|
43
|
+
if line.match(@@_NOT_COMMENT_PATTERN) && !line.empty? # Check first character isn't *, nor all whitespace/empty
|
|
44
|
+
if line.upcase.match(@@_QUESTION_PATTERN)
|
|
45
|
+
if last_symbol == Constants::A_START
|
|
46
|
+
raise ProcessingError, "Answer Key for previous question was not closed - Line #" + line_num.to_s
|
|
47
|
+
else
|
|
48
|
+
last_symbol = Constants::Q_START
|
|
49
|
+
last_question = Question.new(question_count + 1)
|
|
50
|
+
question_count += 1
|
|
51
|
+
end
|
|
52
|
+
elsif line.upcase.match(@@_ANSWERS_START_PATTERN)
|
|
53
|
+
if !last_question.question.empty?
|
|
54
|
+
if last_symbol == Constants::Q_START
|
|
55
|
+
last_symbol = Constants::A_START
|
|
56
|
+
elsif last_symbol == Constants::A_START
|
|
57
|
+
raise ProcessingError, "Duplicate answer key start symbol - Line #" + line_num.to_s
|
|
58
|
+
elsif last_symbol == Constants::A_END
|
|
59
|
+
raise ProcessingError, "Cannot Have multiple sets of answer keys for one Question - Line #" + line_num.to_s
|
|
60
|
+
else
|
|
61
|
+
raise ProcessingError, "Cannot attribute answer tag to question (orphaned answer key), check file syntax - Line #" + line_num.to_s
|
|
62
|
+
end
|
|
63
|
+
else
|
|
64
|
+
raise ProcessingError, "Question is empty - Line #" + line_num.to_s
|
|
65
|
+
end
|
|
66
|
+
elsif line.upcase.match(@@_ANSWERS_END_PATTERN)
|
|
67
|
+
if (last_question.answers.empty?)
|
|
68
|
+
raise ProcessingError, "Answer Key is empty - Line #" + line_num.to_s
|
|
69
|
+
else
|
|
70
|
+
last_symbol = Constants::A_END
|
|
71
|
+
questions.push(last_question)
|
|
72
|
+
line_count = 0
|
|
73
|
+
end
|
|
74
|
+
else
|
|
75
|
+
if last_symbol == Constants::Q_START # Start Processing Question Lines
|
|
76
|
+
last_question.question += l # Keep Formatting
|
|
77
|
+
elsif last_symbol == Constants::A_START # Get Correct Answer
|
|
78
|
+
if line_count == 0
|
|
79
|
+
if line.is_integer?
|
|
80
|
+
last_question.correct_answer = line.to_i
|
|
81
|
+
else
|
|
82
|
+
raise ProcessingError, "Correct answer not an integer - Line #" + line_num.to_s
|
|
83
|
+
end
|
|
84
|
+
else
|
|
85
|
+
last_question.answers.push(line)
|
|
86
|
+
end
|
|
87
|
+
line_count += 1
|
|
88
|
+
elsif last_symbol == Constants::A_END
|
|
89
|
+
raise ProcessingError, "Free text outside of Question or Answer Key, Add * to beginning if intended to be a comment - Line #" + line_num.to_s
|
|
90
|
+
else
|
|
91
|
+
raise ProcessingError, "Free text before first Question Symbol Add * to beginning if intended to be a comment - Line #" + line_num.to_s
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
# process the line of text here
|
|
96
|
+
end
|
|
97
|
+
rescue => e
|
|
98
|
+
raise ProcessingError, "Quiz File doesn't exist or cannot be read due to permissions"
|
|
99
|
+
end
|
|
100
|
+
create_exam(File.basename(@file), questions)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def create_exam(filename, questions)
|
|
104
|
+
exam_questions = Array.new
|
|
105
|
+
|
|
106
|
+
questions.shuffle! # Shuffles original array by Ruby's internal randomization algorithm
|
|
107
|
+
|
|
108
|
+
count = 0
|
|
109
|
+
questions.each do |question|
|
|
110
|
+
|
|
111
|
+
# If Max Questions is Specified, Check Limit
|
|
112
|
+
if (@max_questions != -1 && count >= @max_questions)
|
|
113
|
+
break
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
exam_questions.push(question)
|
|
117
|
+
count += 1 # Ruby doesnt have incrementation syntax!
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
if exam_questions.length == 0
|
|
121
|
+
raise ProcessingError.new "No Questions were processed from the exam file, probably an empty file"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
Exam.new(filename, exam_questions)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
require 'ruby_proctor/question'
|
|
2
|
+
require 'ruby_proctor/exam'
|
|
3
|
+
require 'ruby_proctor/string_ext'
|
|
4
|
+
require 'ruby_proctor/logger'
|
|
5
|
+
|
|
6
|
+
require 'timeout'
|
|
7
|
+
|
|
8
|
+
class Proctor
|
|
9
|
+
|
|
10
|
+
attr_accessor :exam
|
|
11
|
+
|
|
12
|
+
def initialize(exam, time)
|
|
13
|
+
@exam = exam
|
|
14
|
+
@time = time
|
|
15
|
+
@logger = Logger.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def officiate_exam
|
|
19
|
+
start_time = Time.now
|
|
20
|
+
|
|
21
|
+
puts "\n" + 'This is a ' + exam.questions.length.to_s + ' question exam'
|
|
22
|
+
|
|
23
|
+
if (@time <= 0)
|
|
24
|
+
puts "You have unlimited time to complete, good luck!"
|
|
25
|
+
elsif (@time == 1)
|
|
26
|
+
puts 'you have ' + @time.to_s + ' minute to complete, good luck!'
|
|
27
|
+
else
|
|
28
|
+
puts 'you have ' + @time.to_s + ' minutes to complete, good luck!'
|
|
29
|
+
end
|
|
30
|
+
puts 'Starting Exam....' + "\n\n"
|
|
31
|
+
|
|
32
|
+
question_number = 1
|
|
33
|
+
exam.questions.each do |question|
|
|
34
|
+
puts "Question #" + question_number.to_s + ": \n" + question.question
|
|
35
|
+
|
|
36
|
+
answer_num = 1
|
|
37
|
+
|
|
38
|
+
puts "\nChoices:"
|
|
39
|
+
question.answers.each do |answer|
|
|
40
|
+
puts answer_num.to_s + ' - ' + answer
|
|
41
|
+
answer_num += 1
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
begin
|
|
45
|
+
answer = get_answer(question.answers.length, start_time)
|
|
46
|
+
rescue => e
|
|
47
|
+
print e
|
|
48
|
+
print "Time is Up!\n\n"
|
|
49
|
+
break
|
|
50
|
+
end
|
|
51
|
+
puts ''
|
|
52
|
+
|
|
53
|
+
question.selected_answer = answer
|
|
54
|
+
question_number += 1
|
|
55
|
+
end
|
|
56
|
+
puts "Exam is Over!\n\n"
|
|
57
|
+
|
|
58
|
+
print_exam_results(start_time)
|
|
59
|
+
|
|
60
|
+
# Try to Process, and cleanly display any exceptions
|
|
61
|
+
begin
|
|
62
|
+
@logger.write_to_log(@exam)
|
|
63
|
+
rescue => e
|
|
64
|
+
puts "** Error Writing to Log File: " + e.message + " **"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def print_exam_results(start_time)
|
|
71
|
+
puts '################'
|
|
72
|
+
puts '# Quiz Results #'
|
|
73
|
+
puts '################'
|
|
74
|
+
puts ''
|
|
75
|
+
|
|
76
|
+
# Calculate Percentage
|
|
77
|
+
grade = grade_exam(start_time)
|
|
78
|
+
|
|
79
|
+
puts 'Number of Questions Answered Correctly: ' + @exam.results.num_correct.to_s + ' / ' + @exam.results.total_questions.to_s
|
|
80
|
+
puts 'Grade (Percentage): ' + @exam.results.grade.to_s
|
|
81
|
+
puts 'Letter Grade: ' + @exam.results.letter_grade
|
|
82
|
+
puts 'Time Started: ' + @exam.results.time_started
|
|
83
|
+
puts 'Time Completed: ' + @exam.results.time_completed
|
|
84
|
+
puts 'Time Elapsed: ' + @exam.results.time_elapsed
|
|
85
|
+
|
|
86
|
+
if (@time > 0)
|
|
87
|
+
puts 'Time Left: ' + @exam.results.time_left
|
|
88
|
+
puts ""
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def grade_exam(start_time)
|
|
93
|
+
num_correct = 0
|
|
94
|
+
grade = 0
|
|
95
|
+
exam.questions.each do |question|
|
|
96
|
+
if question.correct_answer.eql?(question.selected_answer)
|
|
97
|
+
num_correct += 1
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Calculate Percentage
|
|
102
|
+
grade = (num_correct.to_f / exam.questions.length.to_f) * 100
|
|
103
|
+
|
|
104
|
+
#puts 'Number of Questions Answered Correctly: ' + num_correct.to_s + ' / ' + exam.questions.length.to_s
|
|
105
|
+
@exam.results.num_correct = num_correct
|
|
106
|
+
@exam.results.total_questions = exam.questions.length
|
|
107
|
+
@exam.results.grade = grade.to_s + '%'
|
|
108
|
+
@exam.results.letter_grade = get_letter_grade(grade)
|
|
109
|
+
@exam.results.time_started = start_time.strftime("%m/%d/%Y %I:%M %p")
|
|
110
|
+
@exam.results.time_completed = Time.now.strftime("%m/%d/%Y %I:%M %p")
|
|
111
|
+
|
|
112
|
+
time_elapsed = calc_time_elapsed(start_time).round
|
|
113
|
+
|
|
114
|
+
time_elapsed_hours = time_elapsed / (60*60)
|
|
115
|
+
time_elapsed_minutes = (time_elapsed / 60) % 60
|
|
116
|
+
time_elapsed_seconds = time_elapsed % 60
|
|
117
|
+
|
|
118
|
+
@exam.results.time_elapsed = time_elapsed_hours.to_s + ':' + time_elapsed_minutes.to_s + ':' + time_elapsed_seconds.to_s
|
|
119
|
+
|
|
120
|
+
if (@time > 0)
|
|
121
|
+
# Calculate Time Left
|
|
122
|
+
time_left = calc_time_left(start_time).round
|
|
123
|
+
|
|
124
|
+
time_left_hours = time_left / (60*60)
|
|
125
|
+
time_left_minutes = (time_left / 60) % 60
|
|
126
|
+
time_left_seconds = time_left % 60
|
|
127
|
+
|
|
128
|
+
@exam.results.time_left = time_left_hours.to_s + ':' + time_left_minutes.to_s + ':' + time_left_seconds.to_s
|
|
129
|
+
puts ""
|
|
130
|
+
end
|
|
131
|
+
grade
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def get_letter_grade(grade)
|
|
135
|
+
letter_grade = 'F'
|
|
136
|
+
if grade >= 90
|
|
137
|
+
letter_grade = 'A'
|
|
138
|
+
elsif grade >= 80
|
|
139
|
+
letter_grade = 'B'
|
|
140
|
+
elsif grade >= 70
|
|
141
|
+
letter_grade = 'C'
|
|
142
|
+
elsif grade >= 60
|
|
143
|
+
letter_grade = 'D'
|
|
144
|
+
end
|
|
145
|
+
letter_grade
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def get_answer(num_questions, start_time)
|
|
149
|
+
answer = -1
|
|
150
|
+
|
|
151
|
+
# Timeout is in seconds, so divide by 1000 to get correct units in float point
|
|
152
|
+
if (@time > 0)
|
|
153
|
+
# Calculate Time Left for Timeout
|
|
154
|
+
time_left = calc_time_left(start_time)
|
|
155
|
+
|
|
156
|
+
Timeout::timeout(time_left) do
|
|
157
|
+
answer = answer_loop(num_questions)
|
|
158
|
+
end
|
|
159
|
+
else
|
|
160
|
+
answer = answer_loop(num_questions)
|
|
161
|
+
end
|
|
162
|
+
answer
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def calc_time_elapsed(start_time)
|
|
166
|
+
time_elapsed = (Time.now.to_f - start_time.to_f)
|
|
167
|
+
time_elapsed
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def calc_time_left(start_time)
|
|
171
|
+
time_left = (@time * 60).to_f - calc_time_elapsed(start_time)
|
|
172
|
+
|
|
173
|
+
# Deal with potential negative number, I never experienced it, but I could see heavy CPU usage or some other
|
|
174
|
+
# Waiting or thread starvation create a problem.
|
|
175
|
+
if time_left < 0
|
|
176
|
+
time_left = 0
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
time_left # Return
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def answer_loop(num_questions)
|
|
183
|
+
valid_answer = false
|
|
184
|
+
|
|
185
|
+
print 'Select an answer (1-' + num_questions.to_s + '): '
|
|
186
|
+
while !valid_answer do
|
|
187
|
+
answer_string = STDIN.gets.chomp
|
|
188
|
+
|
|
189
|
+
if answer_string.is_integer?
|
|
190
|
+
answer = answer_string.to_i
|
|
191
|
+
if answer < 1 || answer > num_questions
|
|
192
|
+
print "out of selection range, pick between 1-" + num_questions.to_s + ": "
|
|
193
|
+
else
|
|
194
|
+
valid_answer = true
|
|
195
|
+
end
|
|
196
|
+
else
|
|
197
|
+
print "not a valid number, pick between 1-" + num_questions.to_s + ": "
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
end
|
|
201
|
+
answer # Force this as last assignment for return, don't need to run reassignment interestingly
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class Question
|
|
2
|
+
|
|
3
|
+
attr_accessor :id, :question, :answers, :correct_answer, :selected_answer
|
|
4
|
+
|
|
5
|
+
def initialize(id)
|
|
6
|
+
@id = id
|
|
7
|
+
@question = ''
|
|
8
|
+
@answers = Array.new()
|
|
9
|
+
@correct_answer = nil
|
|
10
|
+
@selected_answer = -1
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def isCorrect()
|
|
14
|
+
@correct_answer == @selected_answer
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
end
|
data/lib/ruby_proctor.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#$LOAD_PATH.unshift File.dirname($0)
|
|
2
|
+
|
|
3
|
+
require 'rubygems'
|
|
4
|
+
require 'bundler/setup' # IS unable to find os-1.1.4 on OCRA windows, lots of chicken/egg problems.
|
|
5
|
+
|
|
6
|
+
require 'os'
|
|
7
|
+
|
|
8
|
+
def ruby_proctor()
|
|
9
|
+
if OS.posix?
|
|
10
|
+
require 'ruby_proctor/interfaces/terminal.rb'
|
|
11
|
+
ruby_proctor_terminal()
|
|
12
|
+
elsif OS.windows?
|
|
13
|
+
require 'ruby_proctor/interfaces/gui.rb'
|
|
14
|
+
ruby_proctor_gui()
|
|
15
|
+
end
|
|
16
|
+
end
|