Skip to content

Commit

Permalink
Merge release v0.4.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Pixel48 committed May 27, 2020
2 parents 2c0719f + 024461b commit 79297db
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 40 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# SerialExaminer
keys/
test/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down Expand Up @@ -39,7 +43,6 @@ pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
test/
*.bat
*.cmd
.tox/
Expand Down
49 changes: 36 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SerialExaminer
SerialExaminer is a small tool to speed up assessing tests and detecting potential fraudsters. It's easy to use, fast to use and will be long developed and improved
SerialExaminer is a small tool to speed up test evaluation and detection of potential fraudsters. It's easy to use and works quickly with lots of files

Ask your students to send you test responses in .txt files named after their full names, journal number, any ID numbers or anything else, that can identify them. Aks them to write answers in these files in format `<question number>.<question answer>`, line by line. The order of questions and letter size don't matter
Ask your students to send you test responses in .txt files named after their full names, journal number, any ID numbers or anything else, that can identify them. Ask them to write answers in these files in format `<question number>.<question answer>`, line by line. The order of questions and letter size don't matter

### Table of Contents
- [Installation](https://github.com/Pixel48/SerialExaminer#installation)
Expand All @@ -10,43 +10,66 @@
- [Importing an exam key](https://github.com/Pixel48/SerialExaminer#importing-an-exam-key)
- [Checking tests](https://github.com/Pixel48/SerialExaminer#checking-tests)
- [Searching for cheaters](https://github.com/Pixel48/SerialExaminer#searching-for-cheaters)
- [Limits](https://github.com/Pixel48/SerialExaminer#limits)
- [Answers in exam key](https://github.com/Pixel48/SerialExaminer#answers-in-exam-key)
- [Records in result display](https://github.com/Pixel48/SerialExaminer#records-in-result-display)

## Installation
Download the latest [SerialExaminer installer](https://github.com/Pixel48/SerialExaminer/releases/latest)
> For now, downloading the installer will launch [UAC](https://en.wikipedia.org/wiki/User_Account_Control) *Unknown publisher* alert because it is not digitally signed - I'm just a student developing a small project and I can't afford digital certification

`SerialExaminerSetup.exe` is 32-bit. For 64-bit systems, download `SerialExaminerSetup-x64.exe`
> For now, downloading the installer will launch [UAC](https://en.wikipedia.org/wiki/User_Account_Control) *Unknown publisher* alert because it is not digitally signed - For now I can't afford digital certification
## Usage
How to use SerialExaminer interface

![Main window dummy](./docs/img/main_window.png)

### Generating an exam key
1. Provide the exact number of questions and answers in the exam and hit `Create key!` button
1. Provide the exact number of questions and answers in the exam and Press `Create key!` button

![Key parameters](./docs/img/key_parameters.png)

2. Provide answers to questions about the given number. If you make a mistake you can go back using the `<` button. Window will disappear automatically after entering the last answer

![Key data](./docs/img/key_ans.png)
> Now the key creator supports the number of answers in range from 4 to 12, future updates will gradually expand this range until they finally remove this restriction

3. After key answer window vanish, hit `Done` button and provide where to save exam key for potential future use. (You dont need to import key after creating it, it's imported immediately after save).
3. After key answer window vanish, press `Done` button and provide where to save exam key for potential future use. (You dont need to import key after creating it, it's imported immediately after save).

### Checking tests
1. Create or import correct exam key
2. Hit `Browse` button and provide folder with files written by your students
3. Hit `Check!` button to calculate results
4. Hit `Display` button to show test results
2. Press `Browse` button and provide folder with files written by your students
3. Press `Check` button to calculate results
4. Press `Display` button to show test results
5. Press `Export` button, if you want to generate report file with result table in selected format

![Result window](./docs/img/results.png)
> Future updates will add export options and `predicted grade` column in results
### Searching for cheaters
1. Hit `Import` button
1. Press `Import` button
2. In new dialog change file extension to ***Plain text (\*.txt)***
3. Select the source file to which you want to check the similarity in other tests
4. Hit `Browse` button and provide folder with other tests
5. Hit `Check!` button to calculate results
6. Hit `Display` button to show similarity of other tests to source test
4. Press `Browse` button and provide folder with other tests
5. Press `Check` button to calculate results
6. Press `Display` button to show similarity of other tests to source test

## Limits
SerialExaminer limitation list for [last release](https://github.com/Pixel48/SerialExaminer/releases/latest).

*These limits will be removed by future updates.*

### Answers in exam key
Key creator supports answers quantity in range from 4 to 12

![Minimum answers quantity](./docs/img/limit_min_answers_quantity.png)
![Maximum answers quantity](./docs/img/limit_max_answers_quantity.png)

### Records in result display
Test result display window supports up to 270.

Export feature will work with all records, limit applies to display only

![Maximum result display records](./docs/img/limit_results_display.png)

###### Copyright (c) 2020 [Pixel48](https://github.com/Pixel48/) All Rights Reserved
139 changes: 114 additions & 25 deletions SerialExaminer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
# SerialExaminer.py
# by Pixel
# Repository: https://github.com/Pixel48/SerialExaminer.git
# Documentation: https://github.com/Pixel48/SerialExaminer (WIP)
from tkinter import *
from tkinter import filedialog
from functools import partial
import tkinter.font as tkFont
import os, pickle
import openpyxl

versionTag = 'v0.3.0'
versionTag = '0.4.0'

# SOME GLOBALS
R = 0
Expand All @@ -22,16 +22,18 @@
questionCount = 0
answersCount = 0

def newRow(arg = 1):
def newRow(col = 0, row = 1):
global C, R
R += arg
R += row
C = 0
def newCol(arg = 1):
C += col
def newCol(col = 1):
global C
C += arg
def zeroCol():
C += col
def zeroCol(col = 0):
global C, R
C = R = 0
C = col
def splitLine(line):
if line.split('.')[0].isdigit():
line = line.split('.')
Expand Down Expand Up @@ -142,13 +144,14 @@ def importKey(self):
global KEY_FILE
self.keyButtonImport['state'] = DISABLED
self.keyButtonCreate['state'] = DISABLED
KEY_FILE = os.path.normpath(filedialog.askopenfilename(
KEY_FILE = filedialog.askopenfilename(
title = "Select exam key file",
initialdir = './keys',
filetypes =(("Exam key file", "*.exkey"),
filetypes =(
("Exam key file", "*.exkey"),
("Plain text", "*.txt"),
)
))
)
self.keyButtonImport['state'] = NORMAL
self.keyButtonCreate['state'] = NORMAL
if '.exkey' in KEY_FILE or '.txt' in KEY_FILE:
Expand All @@ -161,14 +164,16 @@ def importKey(self):
for line in keyf:
line = splitLine(line)
KEY_DICT[line[0]] = line[1]
if 0 in KEY_DICT.keys():
KEY_DICT.pop(0)
questionCount = len(KEY_DICT.keys())
self.inputButton['state'] = NORMAL
def browseExams(self):
global INPUT_FILES
testDir = os.path.normpath(filedialog.askdirectory(
title = "Examination txt files location",
initialdir = '.',
))
testDir = filedialog.askdirectory(
title = "Examination txt files location",
initialdir = '.',
)
testFiles = os.listdir(testDir)
buffer = []
for file in testFiles:
Expand All @@ -194,13 +199,64 @@ def examinate(self):
resultName = os.path.basename(testFile).split('.')[0]
RESULT_DICT[resultName] = [str(points) + '/' + str(questionCount), str(round(points*100/questionCount, 2)) + '%']
# NOTE: Result format: {<Filename>: ['<points>/<maxPoints', '<goodAnswersIn%>%']}
# self.outputButtonExport['state'] = NORMAL # NOTE: export feature
self.outputButtonExport['state'] = NORMAL
self.outputButtonDsiplay['state'] = NORMAL
def resultDisplay(self):
self.masterResultDisplayWindow = Toplevel(self.master)
self.appResultDisplayWindow = ResultDisplayWindow(self.masterResultDisplayWindow, self)
def resultExport(self):
pass
# NOTE: RESULT_DICT format: {<Filename>: ['<points>/<maxPoints', '<goodAnswersIn%>%']}
global RESULT_DICT
EXPORT_FILE = filedialog.asksaveasfilename(
title = "Save test result",
initialdir = '.',
initialfile = 'test',
defaultextension = '.xlsx',
filetypes =(
("Excel Spreadsheet ", '*.xlsx'),
("CSV file", "*.csv"),
("Plain text", "*.txt"),
)
)
if EXPORT_FILE[-4:] == '.csv':
with open(EXPORT_FILE, 'w') as export:
export.write(';FILENAME;'+'POINTS (max '+len(RESULT_DICT.keys())+');RESULT IN %\n')
i = 1
for key in RESULT_DICT:
export.write(str(i)+';'+key+';'+RESULT_DICT[key][0].split('/')[0]+';'+RESULT_DICT[key][1][:-1].replace('.',',')+'\n')
i += 1
export.close()
elif EXPORT_FILE[-4:] == '.txt':
with open(EXPORT_FILE, 'w') as export:
i = 1
space = ' '
for key in RESULT_DICT:
export.write(str(i)+'.'+space+key+' --- '+RESULT_DICT[key][0]+' --- '+RESULT_DICT[key][1]+'\n')
i += 1
if i > 9:
space = ' '
export.close()
elif EXPORT_FILE[-5:] == '.xlsx':
wb = openpyxl.Workbook()
sh = wb.create_sheet(index=0)
sh['B1'] = "FILENAME"
sh['C1'] = "POINTS"
sh['D1'] = "RESULT (in %)"
sh['E1'] = "MAX POINTS"
sh['E2'] = len(KEY_DICT.keys())
sh.column_dimensions['B'].width = 25
sh.column_dimensions['C'].width = 8
sh.column_dimensions['D'].width = 13
sh.column_dimensions['E'].width = 12
row = 2
col = 'ABCD'
for key in RESULT_DICT:
sh[str(col[0])+str(row)] = str(row-1)
sh[str(col[1])+str(row)] = key
sh[str(col[2])+str(row)] = int(RESULT_DICT[key][0].split('/')[0])
sh[str(col[3])+str(row)] = '=ROUND('+str(col[2])+str(row)+'*100/E2, 2)'
row += 1
wb.save(EXPORT_FILE)

class KeyCreatorWindow(object):
"""Creator for CreateKey Window"""
Expand Down Expand Up @@ -384,14 +440,16 @@ def exportKeyFile(self, file):

def die(self):
global KEY_FILE
KEY_FILE = os.path.normpath(filedialog.asksaveasfilename(
KEY_FILE = filedialog.asksaveasfilename(
title = "Select exam key file",
initialdir = './keys',
filetypes =(("Exam Key File", "*.exkey"),)
))
if '.exkey' not in KEY_FILE:
KEY_FILE += '.exkey'
if KEY_FILE != '..exkey': # if no filename provided, don't proceed
initialfile = 'key',
defaultextension = '.exkey',
filetypes =(
("Exam Key File", "*.exkey"),
)
)
if KEY_FILE != '': # if no filename provided, don't proceed
self.exportKeyFile(KEY_FILE)
self.above.inputButton['state'] = NORMAL
self.master.destroy()
Expand Down Expand Up @@ -536,8 +594,11 @@ def __init__(self, master, above):
self.frame.grid()
def build(self, frame):
global RESULT_DICT
x = 0
zeroCol()
# legend #

newCol()
Label(frame,
text = "Name",
fg = 'blue',
Expand All @@ -552,16 +613,44 @@ def build(self, frame):
text = "Result",
fg = 'red').grid(row = R, column = C)
# results #
for filename in RESULT_DICT:
newRow()
limit = 45
endLimit = 270
x = 0
for filename in list(RESULT_DICT.keys())[:endLimit]:
newRow(x//limit*4)
x += 1
xx = str(x)+'.'
if x > limit:
xx = '\t' + xx
Label(frame,
text = filename).grid(row = R, column = C)
text = xx).grid(row = R, column = C)
newCol()
Label(frame,
text = filename).grid(row = R, column = C, sticky = 'w')
newCol()
Label(frame,
text = RESULT_DICT[filename][0]).grid(row = R, column = C)
newCol()
Label(frame,
text = RESULT_DICT[filename][1]).grid(row = R, column = C)
if x % limit == 0 and x < endLimit:
zeroCol((x//limit)*4)
# legend #

newCol()
Label(frame,
text = "Name",
fg = 'blue',
width = 15).grid(row = R, column = C)
newCol()
Label(frame,
text = "Points",
fg = 'blue',
width = 10).grid(row = R, column = C)
newCol()
Label(frame,
text = "Result",
fg = 'red').grid(row = R, column = C)

def main():
root = Tk()
Expand Down
Binary file added docs/img/limit_max_answers_quantity.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/limit_min_answers_quantity.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/limit_results_display.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/img/results.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion installer.nsi
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
;Atributes
Name "SerialExaminer"
OutFile "SerialExaminerSetup.exe"
InstallDir $PROGRAMFILES\SerialExaminer
InstallDir "$PROGRAMFILES\SerialExaminer"
RequestExecutionLevel admin
;Unicode True

Expand Down

0 comments on commit 79297db

Please sign in to comment.