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.
@@ -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,4 @@
1
+ class Configuration
2
+
3
+ attr_accessor :filepath, :num_questions, :time_limit
4
+
@@ -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
@@ -0,0 +1,5 @@
1
+ class String
2
+ def is_integer?
3
+ self.to_i.to_s == self
4
+ end
5
+ end
@@ -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