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,620 @@
1
+ #gui.rb
2
+
3
+ #Include Dir for OCRA
4
+ $:.unshift File.dirname($0)
5
+
6
+ # includes
7
+ require 'os'
8
+
9
+ if OS.windows?
10
+ require 'rubygems'
11
+ require 'bundler/setup'
12
+
13
+ require 'tk'
14
+ require 'thread'
15
+
16
+ require 'ruby_proctor/constants.rb'
17
+ require 'ruby_proctor/exam.rb'
18
+ require 'ruby_proctor/question.rb'
19
+ require 'ruby_proctor/processor.rb'
20
+ require 'ruby_proctor/proctor.rb'
21
+ require 'ruby_proctor/string_ext.rb'
22
+
23
+ include Constants
24
+
25
+ def ruby_proctor_gui
26
+ proctoring = TkVariable.new # Variable Used to Let Screen Behave Differently
27
+ proctoring.value = false
28
+
29
+ set_filepath = TkVariable.new
30
+ set_filepath.value = ''
31
+
32
+ set_time_limit = TkVariable.new
33
+ set_time_limit.value = ''
34
+
35
+ set_number_questions = TkVariable.new
36
+ set_number_questions.value = TkVariable.new
37
+ # Main Menu
38
+ root = TkRoot.new { title "Ruby Proctor - Main Menu" }
39
+
40
+ logo = TkPhotoImage.new()
41
+ logo.file = __dir__ + "/logo.gif"
42
+
43
+ logo_label = TkLabel.new(root) do
44
+ image logo
45
+ grid('row'=>0, 'column'=>0, 'padx'=>25, 'pady'=>5, 'columnspan'=>1, 'sticky'=>'WE')
46
+ end
47
+
48
+ config_quiz = TkButton.new(root) do
49
+ text "Set Options"
50
+ grid('row'=>1, 'column'=>0, 'padx'=>25, 'pady'=>5, 'columnspan'=>1, 'sticky'=>'WE')
51
+ end
52
+ config_quiz.comman = Proc.new {
53
+ configuration(root, set_filepath, set_number_questions, set_time_limit)
54
+ }
55
+
56
+ # Apply Quiz Config Button
57
+ load_button = TkButton.new(root) do
58
+ text "Run Quiz"
59
+ grid('row'=>2, 'column'=>0, 'padx'=>25, 'pady'=>5, 'columnspan'=>1, 'sticky'=>'WE')
60
+ #pack("side" => "bottom", "padx"=> "50", "pady"=> "50")
61
+ end
62
+
63
+ load_button.comman = Proc.new {
64
+ processing_window(root, root, set_filepath, set_number_questions, set_time_limit)
65
+ }
66
+
67
+ view_log = TkButton.new(root) do
68
+ text "View Scores"
69
+ grid('row'=>3, 'column'=>0, 'padx'=>25, 'pady'=>5, 'columnspan'=>1, 'sticky'=>'WE')
70
+ end
71
+ view_log.comman = Proc.new {
72
+ view_log(root)
73
+ }
74
+
75
+ exit = TkButton.new(root) do
76
+ text "Exit"
77
+ grid('row'=>4, 'column'=>0, 'padx'=>25, 'pady'=>5, 'columnspan'=>1, 'sticky'=>'WE')
78
+ end
79
+ exit.comman = Proc.new {
80
+ root.destroy()
81
+ }
82
+
83
+ root.update()
84
+ root['geometry'] = calc_center_geometry(root, root.winfo_width(), root.winfo_height)
85
+
86
+ Tk.mainloop
87
+ end
88
+
89
+ def configuration(root, set_filepath, set_number_questions, set_time_limit)
90
+
91
+ new_filepath = TkVariable.new
92
+ new_filepath.value = set_filepath.value
93
+ new_time_limit = TkVariable.new
94
+ new_time_limit.value = set_time_limit.value
95
+ new_number_questions = TkVariable.new
96
+ new_number_questions.value = set_number_questions.value
97
+
98
+ configuration_top = TkToplevel.new { title "Ruby Proctor - Quiz Configuration" }
99
+ configuration_top.grab_set()
100
+
101
+ # configuration_top.protocol("WM_DELETE_WINDOW", Proc.new {
102
+ # put("test!")
103
+ # })
104
+
105
+ #Tk.root.protocol “WM_DELETE_WINDOW”, proc {puts “foo”}
106
+
107
+ file_entry = TkEntry.new(configuration_top) do
108
+ grid('row'=>0, 'column'=>0, 'padx'=>5, 'pady'=>5, 'columnspan'=>4, 'sticky'=>'WE')
109
+ end
110
+
111
+ button = TkButton.new(configuration_top) do
112
+ text "Open"
113
+ grid('row'=>0, 'column'=>4, 'padx'=>5, 'pady'=>5)
114
+ end
115
+
116
+ button.comman = Proc.new {
117
+ l_value = Tk.getOpenFile
118
+ if !l_value.empty?
119
+ new_filepath.value = l_value
120
+ end
121
+ }
122
+
123
+ file_entry.textvariable = new_filepath
124
+
125
+ # Number of Questions
126
+ lb1 = TkLabel.new(configuration_top) do
127
+ text 'Number of Questions: '
128
+ #background "yellow"
129
+ #foreground "blue"
130
+ grid('row'=>1, 'column'=>0)
131
+ end
132
+
133
+ number_questions_entry = TkEntry.new(configuration_top) do
134
+ grid('row'=>1, 'column'=>1, 'padx'=>5, 'pady'=>5)
135
+ #pack("side" => "left", "padx"=> "50", "pady"=> "50")
136
+ end
137
+
138
+ number_questions_entry.textvariable = new_number_questions
139
+
140
+ # Time Limit
141
+ lb2 = TkLabel.new(configuration_top) do
142
+ text 'Time Limit (Minutes): '
143
+ #background "yellow"
144
+ # foreground "blue"
145
+ grid('row'=>1, 'column'=>2)
146
+ end
147
+ time_limit_entry = TkEntry.new(configuration_top) do
148
+ grid('row'=>1, 'column'=>3, 'padx'=>5, 'pady'=>5)
149
+ #pack("side" => "left", "padx"=> "50", "pady"=> "50")
150
+ end
151
+
152
+ time_limit_entry.textvariable = new_time_limit
153
+
154
+ # Apply Quiz Config Button
155
+ load_button = TkButton.new(configuration_top) do
156
+ text "Apply"
157
+ grid('row'=>1, 'column'=>4, 'padx'=>5, 'pady'=>5)
158
+ #pack("side" => "bottom", "padx"=> "50", "pady"=> "50")
159
+ end
160
+
161
+ load_button.comman = Proc.new {
162
+
163
+ if validate(new_filepath, new_number_questions, new_time_limit)
164
+ configuration_top.destroy();
165
+ set_filepath.value = new_filepath.value
166
+ set_number_questions.value = new_number_questions.value
167
+ set_time_limit.value = new_time_limit.value
168
+ end
169
+
170
+ #processing_window(root, configuration_top, filepath, number_questions, time_limit)
171
+ }
172
+ configuration_top.update()
173
+ configuration_top['geometry'] = calc_center_geometry(configuration_top, configuration_top.winfo_width(), configuration_top.winfo_height)
174
+ #file_entry.textvariable = new_filepath
175
+
176
+ end
177
+
178
+
179
+ def validate(filepath, number_questions, time_limit)
180
+
181
+ if number_questions
182
+ if number_questions.to_s.is_integer?
183
+ if number_questions.to_i <= 0 || number_questions.to_i > Constants::QUIZ_MAX_QUESTIONS
184
+ Tk.messageBox(
185
+ 'type' => 'ok',
186
+ 'icon' => 'info',
187
+ 'title' => 'Number of Questions outside of Range',
188
+ 'message' => 'Number of questions must be greater than 0 but no more than 10,000. Specifying more questions than the provided answer key file has will just use all questions available, no repeats'
189
+ )
190
+ return false
191
+ return false
192
+ end
193
+ elsif (number_questions != '')
194
+ Tk.messageBox(
195
+ 'type' => 'ok',
196
+ 'icon' => 'info',
197
+ 'title' => 'Invalid Number of Questions',
198
+ 'message' => 'Make sure # of Questions is a valid integer!'
199
+ )
200
+ return false
201
+ end
202
+ end
203
+
204
+ if time_limit
205
+ if time_limit.to_s.is_integer?
206
+ if time_limit.to_i <= 0
207
+ Tk.messageBox(
208
+ 'type' => 'ok',
209
+ 'icon' => 'info',
210
+ 'title' => 'Time Limit was Less than or equal to 0',
211
+ 'message' => 'Time Limit must be a positive number (If you want Unlimited Time, leave input blank)'
212
+ )
213
+ return false
214
+ end
215
+ elsif (time_limit != '')
216
+ Tk.messageBox(
217
+ 'type' => 'ok',
218
+ 'icon' => 'info',
219
+ 'title' => 'Time Limit must be a number greater than 0',
220
+ 'message' => 'Time Limit must be a number greater than 0 *Leave Minutes Blank if you want unlimited time)'
221
+ )
222
+ return false
223
+ end
224
+ end
225
+
226
+ if(!File.exist?(filepath))
227
+ Tk.messageBox(
228
+ 'type' => 'ok',
229
+ 'icon' => 'info',
230
+ 'title' => 'File Not Found',
231
+ 'message' => 'File was not found, please make sure your filepath is valid!'
232
+ )
233
+ return false
234
+ end
235
+ return true
236
+ end
237
+
238
+ def calc_center_geometry(win, window_width, window_height)
239
+ screen_width = win.winfo_screenwidth()
240
+ screen_height = win.winfo_screenheight()
241
+
242
+ x = ((screen_width/2) - (window_width/2)).to_i
243
+ y = ((screen_height/2) - (window_height/2)).to_i
244
+
245
+
246
+ "%sx%s+%i+%i" % [window_width, window_height, x, y]
247
+ end
248
+
249
+ def processing_window(root, configuration_top, filepath, number_questions, time_limit)
250
+
251
+ if validate(filepath, number_questions, time_limit)
252
+ processing_top = TkToplevel.new {
253
+ title "Ruby Proctor - Processing"
254
+ resizable false, false
255
+ overrideredirect 1
256
+ }
257
+
258
+ processing_top['geometry'] = calc_center_geometry(processing_top, 100, 30)
259
+
260
+ loading_label = TkLabel.new(processing_top) do
261
+ text 'Loading ...'
262
+ background "blue"
263
+ foreground "white"
264
+ grid('row'=>1, 'column'=>2, 'ipadx'=>25, 'ipady'=>5)
265
+ end
266
+
267
+ # Loading Indicator
268
+ # loading_top = TkToplevel.new { title "Processing Quiz ..." }
269
+ #progress_bar = Tk::ProgressBar.new(loading_top)
270
+ # progress_bar.pack("side" => 'bottom')
271
+ #progress_bar.mode = indeterminate
272
+
273
+ # Ruby is really weird with Local & Instance Variables
274
+ if (number_questions.value.empty?)
275
+ processor = Processor.new(filepath, -1)
276
+ else
277
+ processor = Processor.new(filepath, number_questions)
278
+ end
279
+ # Try to Process, and cleanly display any exceptions
280
+ Thread.new {
281
+ begin
282
+ exam = processor.process()
283
+
284
+ if (time_limit.value.empty?)
285
+ exam(exam, -1, root)
286
+ else
287
+ exam(exam, time_limit.to_i, root)
288
+ end
289
+ rescue => e
290
+ Tk.messageBox(
291
+ 'type' => 'ok',
292
+ 'icon' => 'error',
293
+ 'title' => 'Processing Exception Occurred',
294
+ 'message' => 'Error Processing Exam File: ' + e.message
295
+ )
296
+
297
+ puts e.backtrace
298
+ ensure
299
+ processing_top.destroy()
300
+ end
301
+ }
302
+ end
303
+ end
304
+
305
+ def exam(exam, time_limit, root)
306
+ # # Start Officiating Exam
307
+ # proctor = Proctor.new(exam, time)
308
+ # proctor.officiate_exam
309
+
310
+ time_left = TkVariable.new
311
+ start_time = Time.now
312
+
313
+ question_window = TkToplevel.new { }
314
+ question_window.grab_set()
315
+
316
+ thread = false
317
+ if (time_limit && time_limit > 0)
318
+ thread = Thread.new {
319
+ time_limit.downto(0) do |i|
320
+ time_left.value = i
321
+
322
+ if (i > 0)
323
+ sleep 60
324
+ end
325
+ end
326
+
327
+ proctor = Proctor.new(exam, time_limit)
328
+ proctor.grade_exam(start_time)
329
+
330
+ question_window.destroy()
331
+
332
+ logger = Logger.new
333
+ logger.write_to_log(exam)
334
+
335
+ display_results(exam.results)
336
+
337
+ }
338
+ end
339
+
340
+ display_question(exam, 0, question_window, true, start_time, time_left, thread, time_limit)
341
+ end
342
+
343
+ def display_question(exam, question_num, question_window, initialize, start_time, time_left, timer_thread, time_limit)
344
+
345
+ human_question_num = question_num + 1
346
+
347
+ question_window['title'] = "Ruby Proctor - Quiz Question #" + human_question_num.to_s
348
+
349
+ if !initialize
350
+ question_window.winfo_children().each { |widgets|
351
+ widgets.destroy()
352
+ }
353
+ end
354
+
355
+ # Timer
356
+ if (timer_thread)
357
+ time_left_num = TkLabel.new(question_window) {
358
+ text "Time Left (Minutes): "
359
+ pack('side' => 'top', 'padx'=>5, 'pady'=>5)
360
+ }
361
+
362
+ time_left_text = TkEntry.new(question_window) {
363
+ textvariable time_left
364
+ pack('side' => 'top', 'padx'=>5, 'pady'=>5)
365
+ state 'disabled'
366
+ justify 'center'
367
+ }
368
+ end
369
+
370
+ TkSeparator.new(question_window) do
371
+ pack('fill' => 'x')
372
+ end
373
+
374
+ #Question
375
+ question_label = TkLabel.new(question_window) do
376
+ text human_question_num.to_s + ". " + exam.questions[question_num].question
377
+ #background "yellow"
378
+ #foreground "blue"
379
+ anchor 'w'
380
+ pack('side' => 'top', 'fill' => 'x', 'padx'=>5, 'pady'=>5)
381
+ end
382
+
383
+ answer = TkVariable.new
384
+ answer.value = exam.questions[question_num].selected_answer
385
+ answer_num = 1
386
+
387
+ exam.questions[question_num].answers.each do |choice|
388
+ TkRadioButton.new(question_window) {
389
+ text answer_num.to_s + ". " + choice
390
+ variable answer
391
+ value answer_num
392
+ anchor 'w'
393
+ pack('side' => 'top', 'fill' => 'x')
394
+ }
395
+ answer_num += 1
396
+ end
397
+
398
+ TkSeparator.new(question_window) do
399
+ pack('fill' => 'x')
400
+ end
401
+
402
+ previous_button = TkButton.new(question_window) {
403
+ text 'Back'
404
+ pack('side' => 'left', 'fill' => 'x', 'padx'=>5, 'pady'=>5)
405
+
406
+ }
407
+
408
+ previous_button.comman = Proc.new {
409
+ exam.questions[question_num].selected_answer = answer.value
410
+ display_question(exam, question_num - 1, question_window, false, start_time, time_left, timer_thread, time_limit)
411
+ }
412
+
413
+ next_button = TkButton.new(question_window) {
414
+ text 'Next'
415
+ pack('side' => 'left', 'fill' => 'x', 'padx'=>5, 'pady'=>5)
416
+ }
417
+
418
+ next_button.comman = Proc.new {
419
+ exam.questions[question_num].selected_answer = answer.value
420
+ display_question(exam, question_num + 1, question_window, false, start_time, time_left, timer_thread, time_limit)
421
+ }
422
+
423
+ if (1 == human_question_num)
424
+ previous_button['state'] = 'disabled'
425
+ end
426
+
427
+ if (exam.questions.length == human_question_num)
428
+ next_button['state'] = 'disabled'
429
+ end
430
+
431
+ submit_button = TkButton.new(question_window) {
432
+ text 'Submit'
433
+ pack('side' => 'right', 'fill' => 'x', 'padx'=>5, 'pady'=>5)
434
+ }
435
+
436
+ submit_button.comman = Proc.new {
437
+ #display_question(exam, question_num + 1)
438
+ answer = Tk.messageBox(
439
+ 'type' => 'yesno',
440
+ 'icon' => 'question',
441
+ 'title' => 'Ready to Submit?',
442
+ 'message' => 'Are You Wanting to Submit this Quiz?'
443
+ )
444
+
445
+ if (answer)
446
+ if (timer_thread)
447
+ timer_thread.kill()
448
+ end
449
+
450
+ proctor = Proctor.new(exam, time_limit)
451
+ proctor.grade_exam(start_time)
452
+
453
+ question_window.destroy()
454
+
455
+ logger = Logger.new
456
+ logger.write_to_log(exam)
457
+
458
+ display_results(exam.results)
459
+
460
+ end
461
+ }
462
+
463
+ #if question_num == 0
464
+ question_window['geometry'] = ""
465
+ question_window.update()
466
+ question_window['geometry'] = calc_center_geometry(question_window, question_window.winfo_width(), question_window.winfo_height)
467
+
468
+ #end
469
+ end
470
+
471
+ def display_results(results)
472
+ results_win = TkToplevel.new { title "Ruby Proctor - Quiz Results" }
473
+ results_win.grab_set()
474
+
475
+ display_result_attribute(results_win, 0, "Number of Correct Questions", results.num_correct.to_s + ' / ' + results.total_questions.to_s)
476
+ display_result_attribute(results_win, 1, "Grade %", results.grade.to_s)
477
+ display_result_attribute(results_win, 2, "Letter Grade", results.letter_grade)
478
+ display_result_attribute(results_win, 3, "Time Started", results.time_started)
479
+ display_result_attribute(results_win, 4, "Time Completed", results.time_completed)
480
+ display_result_attribute(results_win, 5, "Time Elapsed", results.time_elapsed)
481
+
482
+ if (results.time_left)
483
+ display_result_attribute(results_win, 6, "Time Left", results.time_left)
484
+ end
485
+
486
+ TkSeparator.new(results_win) do
487
+ grid('row'=>7, 'column'=>0, 'columnspan'=>2, 'sticky'=>'WE')
488
+ end
489
+
490
+ ok_button = TkButton.new(results_win) {
491
+ text 'OK'
492
+ grid('row'=>8, 'column'=>0, 'padx'=>10, 'pady'=>10, 'columnspan'=>2, 'sticky'=>'WE')
493
+ }
494
+
495
+ ok_button.comman = Proc.new {
496
+ results_win.destroy()
497
+ }
498
+
499
+ results_win.update()
500
+ results_win['geometry'] = calc_center_geometry(results_win, results_win.winfo_width(), results_win.winfo_height)
501
+ end
502
+
503
+ def view_log(root)
504
+
505
+ begin
506
+ logger = Logger.new
507
+
508
+ if (File.exist?(logger.file_path))
509
+
510
+ quiz_attempts = logger.read_from_log
511
+
512
+ log_top = TkToplevel.new { title "Ruby Proctor - Quiz Log" }
513
+ log_top.grab_set()
514
+
515
+ # Create Frame
516
+ list_frame = TkFrame.new(log_top) do
517
+ padx 10
518
+ pady 10
519
+ pack('fill' => 'x')
520
+ end
521
+
522
+ list = TkListbox.new(list_frame) do
523
+ width 0
524
+ height 10
525
+ setgrid 1
526
+ selectmode 'browse'
527
+ pack('side' => 'left','fill' => 'x', 'expand'=>1)
528
+ end
529
+
530
+ list.bind("Double-Button-1", Proc.new {
531
+ if (list.curselection().length > 0)
532
+ display_results(quiz_attempts[list.curselection()[0]])
533
+ else
534
+ Tk.messageBox(
535
+ 'type' => 'ok',
536
+ 'icon' => 'error',
537
+ 'title' => 'No Quiz File Selected!',
538
+ 'message' => 'Cannot Open, No Quiz File Selected'
539
+ )
540
+ end
541
+ })
542
+
543
+ scrollbar = TkScrollbar.new(list_frame) {
544
+ command Proc.new {|*args|
545
+ list.yview(*args)
546
+ }
547
+ pack('side'=>'left', 'fill'=>'y')
548
+ }
549
+
550
+ list['yscrollcommand'] = Proc.new {|*args|
551
+ scrollbar.set(*args)
552
+ }
553
+
554
+ quiz_num = 1
555
+ for attempt in quiz_attempts do
556
+ list.insert(quiz_num - 1, quiz_num.to_s + ". - " + attempt.quiz_name)
557
+ quiz_num += 1
558
+ end
559
+
560
+ open_button = TkButton.new(log_top) {
561
+ text 'Open Results'
562
+ pack('side' => 'right', 'padx'=>5, 'pady'=>5)
563
+ }
564
+
565
+ open_button.comman = Proc.new {
566
+ if (list.curselection().length > 0)
567
+ display_results(quiz_attempts[list.curselection()[0]])
568
+ else
569
+ Tk.messageBox(
570
+ 'type' => 'ok',
571
+ 'icon' => 'error',
572
+ 'title' => 'No Quiz File Selected!',
573
+ 'message' => 'Cannot Open, No Quiz File Selected'
574
+ )
575
+ end
576
+ }
577
+
578
+ log_top.update()
579
+ log_top['geometry'] = calc_center_geometry(log_top, 10, 10)
580
+ else
581
+ Tk.messageBox(
582
+ 'type' => 'ok',
583
+ 'icon' => 'error',
584
+ 'title' => 'Unable to Read Log File',
585
+ 'message' => "Unable to Read Log File, File Doesn't Exist!"
586
+ )
587
+ end
588
+ rescue => e
589
+ Tk.messageBox(
590
+ 'type' => 'ok',
591
+ 'icon' => 'error',
592
+ 'title' => 'Logging Exception Occurred',
593
+ 'message' => 'Error Reading Log File: ' + e.message
594
+ )
595
+ end
596
+ end
597
+
598
+ def display_result_attribute(win, row, name, value)
599
+
600
+ tk_value = TkVariable.new
601
+ tk_value.value = value
602
+
603
+ # Number of Questions
604
+ TkLabel.new(win) do
605
+ text name + ":"
606
+ #background "yellow"
607
+ #foreground "blue"
608
+ grid('row'=>row, 'column'=>0, 'padx'=>5, 'pady'=>5)
609
+ end
610
+
611
+ entry = TkEntry.new(win) do
612
+ grid('row'=>row, 'column'=>1, 'padx'=>5, 'pady'=>5)
613
+ #pack("side" => "left", "padx"=> "50", "pady"=> "50")
614
+ textvariable tk_value
615
+ state 'disabled'
616
+ end
617
+
618
+ # entry.state('disabled')
619
+ end
620
+ end