Skip to content

Commit

Permalink
add support for n type input exams
Browse files Browse the repository at this point in the history
  • Loading branch information
talkingtoaj committed Oct 3, 2023
1 parent a66e19c commit 38546f6
Show file tree
Hide file tree
Showing 9 changed files with 36 additions and 30 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
First off, thank you for considering contributing to Exam-Creator. It’s people like you that make Exam-Creator such a great tool.
First off, thank you for considering contributing to Unique-Exams. It’s people like you that make Unique-Exams such a great tool.

# How to report a bug

Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Exam-Creator
# Unique-Exams

Exam-Creator is a simple program to create randomized exams so each student receives a unique set of questions.
Unique-Exams is a simple program to create randomized exams so each student receives a unique set of questions.

## Why?

Expand All @@ -24,9 +24,13 @@ Run `poetry install`

### Usage

Put your template files in the `input` folder, following the "Exam Layout guide"
Have a look at the sample template files provided in the `input` folder to see the expected format.

Run `python exam_creator/main.py`
You can Follow the TEMPLATE_DESIGN.md guide to design your own templates. Remove the sample files and add your own template files to the `input` folder.

Modify config.py to reflect the text you want to use for placeholders for the Alternate questions.

Run `python unique_exams/main.py`

Generated exams will be placed in the `output` folder

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tool.poetry]
name = "exam-creator"
name = "unique-exams"
version = "0.1.0"
description = ""
authors = ["AJ laptop <talkingtoaj@gmail.com>"]
Expand Down
File renamed without changes.
7 changes: 5 additions & 2 deletions exam_creator/config.py → unique_exams/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
### Text strings you will use in the template exams to indicate the structure of the exam

# To indicate that the following paragraphs decribe one of the alternative questions, the preceeding paragraph should include this text
ALTERNATIVE_TEXT= "Alternatif Soru"
ALTERNATIVE_TEXT= "Question Alternate"

# To indicate the block of alternative questions is now over
END_ALTERNATIVES= "Alternatif Sonu"
END_ALTERNATIVES= "END ALTERNATES"

# Used to obfuscate the tokens used for unique exams. Not that important, but if you want to modify it, each number should be less than 5.
SEED = [0,2,1,3,0,1,2,0,3]
Binary file added unique_exams/input/Sample Examiner Version.docx
Binary file not shown.
Binary file added unique_exams/input/Sample Student Version.docx
Binary file not shown.
41 changes: 20 additions & 21 deletions exam_creator/main.py → unique_exams/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ def load_in_template(filename)->dict:

mode = 'common'
for i, paragraph in enumerate(template_doc.paragraphs):
# if paragraph text contains `Alternatif Soru`...
print(f"{i}:{mode}: {paragraph.text[:45]}")
if ALTERNATIVE_TEXT.lower() in paragraph.text.lower():
if mode == 'common':
Expand All @@ -34,7 +33,7 @@ def load_in_template(filename)->dict:
else:
# we save the current alternative
alternatives.append(alternative_content)
print(f"Detected next Alternatif Soru")
print(f"Detected next alternative choice")
mode = 'choice'
alternative_content = [] # we are starting a new alternative
continue # we don't save the header
Expand Down Expand Up @@ -190,30 +189,30 @@ def add_paragraph(target_document, source_paragraph):
# new_exams = int(input("How many new exams do you want to generate? "))
new_exams = 3
print(os.getcwd())
if "exam_creator" in os.getcwd():
if "unique_exams" in os.getcwd():
INPUT_FOLDER = os.path.join(os.getcwd(), "input")
OUTPUT_FOLDER = os.path.join(os.getcwd(), "output")
else:
# add to path exam_creator
INPUT_FOLDER = os.path.join(os.getcwd(), "exam_creator", "input")
OUTPUT_FOLDER = os.path.join(os.getcwd(), "exam_creator", "output")
examiner_path = os.path.join(INPUT_FOLDER, "JRSPA - Examiner Version - simplified.docx")
student_path = os.path.join(INPUT_FOLDER, "JRSPA - Student Version.docx")
examiner_loaded_content = load_in_template(examiner_path)
student_loaded_content = load_in_template(student_path)

examiner_structure = [len(choice['alternatives']) for choice in examiner_loaded_content['choices']]
student_structure = [len(choice['alternatives']) for choice in student_loaded_content['choices']]
if examiner_structure != student_structure:
raise Exception("The structure of the examiner and student versions are not the same. Please check the files")
INPUT_FOLDER = os.path.join(os.getcwd(), "unique_exams", "input")
OUTPUT_FOLDER = os.path.join(os.getcwd(), "unique_exams", "output")

exam_names = [f for f in os.listdir(INPUT_FOLDER) if f.endswith(".docx")]
exam_paths = [os.path.join(INPUT_FOLDER, f) for f in exam_names]
# remove file endings
exam_names = [os.path.splitext(f)[0] for f in exam_names]
loaded_contents = [load_in_template(os.path.join(INPUT_FOLDER, f)) for f in exam_paths]
exam_structures = []
for loaded_content in loaded_contents:
exam_structures.append([len(choice['alternatives']) for choice in loaded_content['choices']])
for i in range(len(exam_structures)):
if exam_structures[i] != exam_structures[0]:
raise Exception("The structure of the exam versions are not the same. Please check the files")
for i in range(new_exams):
choices = make_choices(student_loaded_content)
choices = make_choices(loaded_contents[0])
hash = to_hash(choices)
reversed_from_hash = from_hash(hash)
print (f"Choices: {choices} -> {hash} -> {reversed_from_hash}")
# choices is an array like [3,0,2,5], we now convert this into a reversable hash
examiner_output_path = os.path.join(OUTPUT_FOLDER, f"Examiner version {hash}.docx")
student_output_path = os.path.join(OUTPUT_FOLDER, f"Student version {hash}.docx")
create_new_document(student_loaded_content, choices, student_output_path, original=student_path)
create_new_document(examiner_loaded_content, choices, examiner_output_path, original=examiner_path)
for j, loaded_content in enumerate(loaded_contents):
exam_name = exam_names[j]
output_path = os.path.join(OUTPUT_FOLDER, f"{exam_name} - version {hash}.docx")
create_new_document(loaded_content, choices, output_path, original=exam_paths[j])
2 changes: 1 addition & 1 deletion exam_creator/token_hash.py → unique_exams/token_hash.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import random
from config import SEED

HEX_ENCODING1 = "aeiouAEIOU"
HEX_ENCODING2 = "bcdghjklmnprstvwxyz"
HEX_ENCODING3 = "BCDGHJKLMNPRSTVWXYZ-#$%&*+./0123456789:<=>?@^_~!'(){}[]|\\öäüÖÄÜßÖÄÜĞğİıŞşÇç"
HEX_ENCODINGS = [HEX_ENCODING1, HEX_ENCODING2, HEX_ENCODING3]
SEED = [0,2,1,3,0,1,2,0,3]

def value_to_char(value:int)->str:
# each HEX_ENCODING substring is unique. We select an encoding string randomly, weighted towards the earlier strings.
Expand Down

0 comments on commit 38546f6

Please sign in to comment.