Skip to content

Commit

Permalink
paragraph formatting added
Browse files Browse the repository at this point in the history
  • Loading branch information
talkingtoaj committed Sep 27, 2023
1 parent 9aaac8b commit a66e19c
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 43 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Exam Generator
# Exam-Creator

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

## Why?

Expand Down
8 changes: 8 additions & 0 deletions exam_creator/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

### 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"

# To indicate the block of alternative questions is now over
END_ALTERNATIVES= "Alternatif Sonu"
112 changes: 71 additions & 41 deletions exam_creator/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import random
import os
from typing import List
from config import ALTERNATIVE_TEXT, END_ALTERNATIVES
from token_hash import to_hash, from_hash

def load_in_template(filename)->dict:
""" loads in the content of the file to memory, takes note of the choices and where they begin and end, returns a dict with all the loaded content """
Expand All @@ -23,7 +25,7 @@ def load_in_template(filename)->dict:
for i, paragraph in enumerate(template_doc.paragraphs):
# if paragraph text contains `Alternatif Soru`...
print(f"{i}:{mode}: {paragraph.text[:45]}")
if 'alternatif soru' in paragraph.text.lower():
if ALTERNATIVE_TEXT.lower() in paragraph.text.lower():
if mode == 'common':
# If we encounter a choice header while in common mode
alternatives = [] # start a new alternatives list
Expand All @@ -37,7 +39,7 @@ def load_in_template(filename)->dict:
alternative_content = [] # we are starting a new alternative
continue # we don't save the header
if mode == 'choice':
if 'alternatif sonu' in paragraph.text.lower():
if END_ALTERNATIVES.lower() in paragraph.text.lower():
# save current alternative and exit choice mode
alternatives.append(alternative_content)
# exit choice mode
Expand Down Expand Up @@ -73,32 +75,16 @@ def create_new_document(content:dict, choices:List[int], filename:str, original:
original_document = Document(original)
document = Document()

# copy settings and styles across
for style in original_document.styles:
try:
document.styles.add_style(style.name, style.type)
except ValueError:
# style already exists
pass
# The following are currently disabled as they are presently not working
# document.settings = original_document.settings
# document.part = original_document.part
# document.core_properties = original_document.core_properties
for table in original_document.tables:
row_len = len(table.rows)
col_len = len(table.columns)
new_table = document.add_table(rows=row_len, cols=col_len, style=table.style)
for r in range(row_len):
for c in range(col_len):
try:
original_cell_paragraph = table._rows[r].cells[c].paragraphs[0]
except IndexError:
# cell is empty
continue
new_table.rows[r].cells[c].add_paragraph(text=original_cell_paragraph.text, style=original_cell_paragraph.style)
for shaoe in original_document.inline_shapes:
document.add_picture(shaoe._inline.graphic.graphicData.pic.nvPicPr.cNvPr.get('name'), width=shaoe.width, height=shaoe.height)
# add_tables(document, original_document) # Currently disabled, as it seems to add the tables to the top of document, not in their correct locations
# for section in original_document.sections:
# document.add_section(section.start_type, section.continuous_section_break, section.orientation)
for shaoe in original_document.inline_shapes:
document.add_picture(shaoe._inline.graphic.graphicData.pic.nvPicPr.cNvPr.get('name'), width=shaoe.width, height=shaoe.height)

document.save(file_path)

# now we add the new content
Expand Down Expand Up @@ -127,13 +113,62 @@ def create_new_document(content:dict, choices:List[int], filename:str, original:
document.save(file_path)
print(f"Saved to {file_path}")

def add_paragraph(target_document, source_paragraph):
# method A
# document.add_paragraph(choice_paragraph.text, style=choice_paragraph.style)
def add_tables(target_document, original_document):
""" Adds tables from the original document to the target document"""

for table in original_document.tables:
row_len = len(table.rows)
col_len = len(table.columns)
new_table = target_document.add_table(rows=row_len, cols=col_len, style=table.style)
for r in range(row_len):
for c in range(col_len):
try:
original_cell_paragraph = table._rows[r].cells[c].paragraphs[0]
except IndexError:
# cell is empty
continue
new_table.rows[r].cells[c].add_paragraph(text=original_cell_paragraph.text, style=original_cell_paragraph.style)

# method B
def add_style(target_document, target_paragraph, source_style):
# TODO: This doesn't seem to be working ...
try:
target_style = target_document.styles[source_style.name]
except KeyError:
# style does not exist, create it
new_style = target_document.styles.add_style(source_style.name, source_style.type)
# add attributes
new_style.font.name = style.font.name
new_style.font.size = style.font.size
new_style.font.bold = style.font.bold
new_style.font.italic = style.font.italic
new_style.font.underline = style.font.underline
new_style.font.color.rgb = style.font.color.rgb
new_style.paragraph_format.alignment = style.paragraph_format.alignment
new_style.paragraph_format.first_line_indent = style.paragraph_format.first_line_indent
new_style.paragraph_format.line_spacing = style.paragraph_format.line_spacing
new_style.paragraph_format.line_spacing_rule = style.paragraph_format.line_spacing_rule
new_style.paragraph_format.left_indent = style.paragraph_format.left_indent
new_style.paragraph_format.right_indent = style.paragraph_format.right_indent
new_style.paragraph_format.space_after = style.paragraph_format.space_after
new_style.paragraph_format.space_before = style.paragraph_format.space_before
new_style.paragraph_format.widow_control = style.paragraph_format.widow_control
new_style.base_style = style.base_style
new_style.hidden = style.hidden
new_style.locked = style.locked
new_style.quick_style = style.quick_style
new_style.semi_hidden = style.semi_hidden
new_style.style_id = style.style_id
new_style.style_type = style.style_type
new_style.next_style = style.next_style
new_style.parent_style = style.parent_style
target_paragraph.style = target_document.styles[source_style.name]

def add_paragraph(target_document, source_paragraph):
""" Attempt to copy all formatting over from the original paragraph to a new paragraph appended at the end of the target document """
print(f"Adding: {source_paragraph.text:40}")
new_paragraph = target_document.add_paragraph()
new_paragraph.style = source_paragraph.style
add_style(target_document, new_paragraph, source_paragraph.style)
# copy runs across (text and formatting as it varies throughout the paragraph)
for run in source_paragraph.runs:
new_run = new_paragraph.add_run(run.text)
new_run.bold = run.bold
Expand All @@ -142,20 +177,15 @@ def add_paragraph(target_document, source_paragraph):
new_run.font.name = run.font.name
new_run.font.size = run.font.size
new_run.font.color.rgb = run.font.color.rgb
# copy paragraph formatting across
format = new_paragraph.paragraph_format
source_f = source_paragraph.paragraph_format
for attribute in ["first_line_indent", "keep_together", "keep_with_next", "left_indent",
"line_spacing", "line_spacing_rule", "page_break_before", "right_indent",
"space_after", "space_before", "widow_control"]:
setattr(format, attribute, getattr(source_f, attribute))
return new_paragraph

import string
HEX_ENCODING = "tpbacdomuginesAEIOUMDR"

def to_hash(choices):
hex_choices = [HEX_ENCODING[num] for num in choices]
return "".join(hex_choices)


def from_hash(hash):
return [HEX_ENCODING.index(char) for char in hash]


if __name__ == '__main__':
# new_exams = int(input("How many new exams do you want to generate? "))
new_exams = 3
Expand All @@ -176,7 +206,7 @@ def from_hash(hash):
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")

for i in range(new_exams):
choices = make_choices(student_loaded_content)
hash = to_hash(choices)
Expand Down
53 changes: 53 additions & 0 deletions exam_creator/token_hash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import random

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.
# If the value is larger than the encoding string, we try the next encoding string
encoding_len = 0
for i, encoding in enumerate(HEX_ENCODINGS):
if i == 0 and random.random() < 0.5:
# occasionally skip vowels for consonants
continue
encoding_len = len(encoding)
if value < encoding_len:
return encoding[value]
else:
raise Exception("Value is too large, no encoding found for value {value}, max length is {encoding_len}")

def to_hash(choices):
"""
Hash codes help us to identify unique exams,
allowing a possible future feature of markers
to reverse the hash to retrieve an exact replica
of the exam
"""
output = ""
# each choice has the corresponding seed index added to it, if the choice list is longer than the seed, the seed is repeated
seeded_choices = [choice + SEED[i] for i, choice in enumerate(choices)]
for value in seeded_choices:
output += value_to_char(value)
return output

def char_to_value(character:str)->int:
"""
reverses the value_to_char function
"""
for i, encoding in enumerate(HEX_ENCODINGS):
if character in encoding:
return encoding.index(character)
else:
raise Exception(f"Character {character} not found in any encoding")

def from_hash(hash:str):
"""
reverses the to_hash function
"""
value_list = [char_to_value(character) for character in hash]
# remove the seed
return [value - SEED[i] for i, value in enumerate(value_list)]

0 comments on commit a66e19c

Please sign in to comment.