This commit is contained in:
András Wiesner 2026-01-06 06:50:45 +01:00
commit 7f99df67cb
2 changed files with 588 additions and 0 deletions

582
main.py Normal file
View File

@ -0,0 +1,582 @@
import ezodf
import sys
import re
from ttkthemes import ThemedTk
import tkinter as tk
from tkinter import filedialog, simpledialog, ttk, font, scrolledtext
import json
import os
SKIP_ROWS = 2
TASK_NAME_ROW = 0 + SKIP_ROWS
MAX_PTS_ROW = 1 + SKIP_ROWS
FIRST_DATA_LINE = 3 + SKIP_ROWS
class AMCTable:
def load_table(self, fn) -> None:
# read ODS file
self.__tab = ezodf.opendoc(fn)
sh = self.__tab.sheets[0]
ncols = sh.ncols()
# find the first, task-associated column
first_task_col = -1
for i in range(0, ncols):
taskid_cell = sh[TASK_NAME_ROW, i]
val = taskid_cell.value
if (val == "max"):
first_task_col = i + 1
#print("Első, pontszámot tartalmazó oszlop: ", first_task_col)
# dissect task identifiers
task_tree = {}
taskre = re.compile("F([0-9])[-_]?([0-9])([a-f])?[-_]?([a-zA-Z0-9_-]+)")
for i in range(first_task_col, ncols):
taskid_cell = sh[TASK_NAME_ROW, i]
maxpts_cell = sh[MAX_PTS_ROW, i]
taskid = taskid_cell.value
m = taskre.match(taskid)
if m is None:
break
maxpts = int(maxpts_cell.value)
tasknum = m.group(1)
subtasknum = m.group(2)
subsubtaskletter = m.group(3)
name = m.group(4)
# if subsubtaskletter is None:
# printsubsubtaskletter = ""
# else:
# printsubsubtaskletter = " " + subsubtaskletter + ")"
# if name is None:
# printname = ""
# else:
# printname = ": " + name
#print("F ", tasknum, "/", subtasknum, printsubsubtaskletter, printname, " (", taskid, ")", sep='')
tasknum = int(tasknum)
subtasknum = int(subtasknum)
if tasknum not in task_tree:
task_tree[tasknum] = { "subtasks": {} }
if subtasknum not in task_tree[tasknum]["subtasks"]:
task_tree[tasknum]["subtasks"][subtasknum] = { "subsubtasks": {} }
if subsubtaskletter is None:
subsubtaskletter = "-"
if subsubtaskletter not in task_tree[tasknum]["subtasks"][subtasknum]["subsubtasks"]:
task_tree[tasknum]["subtasks"][subtasknum]["subsubtasks"][subsubtaskletter] = []
task_tree[tasknum]["subtasks"][subtasknum]["subsubtasks"][subsubtaskletter].append({"name": name, "fullid": taskid, "column": i, "maxpts": maxpts})
self.__task_tree = task_tree
def export_hierarchy_json(self, fn) -> None:
json_dump = json.dumps(self.__task_tree)
with open(fn, "w") as jsonf:
jsonf.write(json_dump)
def export_hierarchy_md(self, fn) -> None:
md = ""
for ti in self.__task_tree:
task = self.__task_tree[ti]
md += "- F" + str(ti) + ":\n"
for sti in task["subtasks"]:
md += " - /" + str(sti) + ":\n"
subtask = task["subtasks"][sti]
for ssti in subtask["subsubtasks"]:
md += " - " + str(ssti) + ")\n"
subsubtask = subtask["subsubtasks"][ssti]
for amcquestion in subsubtask:
md += " - " + amcquestion["name"] + "\n"
with open(fn, "w") as jsonf:
jsonf.write(md)
def get_task_tree(self) -> dict:
return self.__task_tree
def create_scoring_sheet(self, fn, scoring) -> None:
scs = ezodf.Sheet("Scoring")
self.__tab.sheets += scs
scs.append_columns(len(scoring) + 1)
scs.append_rows(2)
comment = "of:=\"\""
i = 0
for ssti in scoring:
expr = scoring[ssti]
scs[0,i].set_value(ssti)
scs[1,i].formula = "of:" + expr
comment += " & \"" + ssti + ": \" & " + "[.$" + index_to_address(i) + "2]"
i += 1
if i < len(scoring):
comment += " & \", \""
scs[0,i].set_value("Komment")
scs[1,i].formula = comment
self.__tab.saveas(fn)
def get_first_sheet_name(self) -> str:
return self.__tab.sheets[0].name
def __init__(self) -> None:
self.__task_tree = {}
def index_to_address(idx) -> str:
BASE = 26
out = ""
quot = idx
place = 0
while (quot > 0) or (place == 0):
(quot, rem) = divmod(quot, BASE)
if (quot == 0) and (place != 0):
val = rem - 1
else:
val = rem
out += chr(ord('A') + val)
place += 1
return out[::-1]
class SpreadSheetPosition:
def __init__(self, idx) -> None:
self.__index = idx
self.__address = index_to_address(idx)
def index(self) -> int:
return self.__index
def address(self) -> str:
return self.__address
def __str__(self) -> str:
return self.__address + " (" + str(self.__index) + ")"
def __repr__(self) -> str:
return self.__str__()
class MainWin:
def populate_task_treeview(self, task_tree: dict) -> None:
self.__task_tree = dict(task_tree)
self.__itemdump = {}
self.__subsubtaskdump = {}
self.__summing = {}
self.__columns = {}
self.__sel_subsubtask = {}
self.__expr_editor.config(state="disabled")
tree = self.__tree
items = tree.get_children()
if items != ():
tree.delete(*items)
for ti in self.__task_tree:
task_level = tree.insert("", tk.END, text="F" + str(ti), tags=("task"), open=True)
subtasks = self.__task_tree[ti]["subtasks"]
t_maxpts = 0
task_id = str(ti)
self.__itemdump[task_id] = { "item": task_level, "task": ti }
for sti in subtasks:
subtask_level = tree.insert(task_level, tk.END, text="/" + str(sti), tags=("subtask"), open=True)
subsubtasks = subtasks[sti]["subsubtasks"]
st_maxpts = 0
subtask_id = task_id + "/" + str(sti)
self.__itemdump[subtask_id] = { "item": subtask_level, "task": ti, "subtask": sti }
for ssti in subsubtasks:
sst_maxpts = 0
subsubtask_level = tree.insert(subtask_level, tk.END, text=str(ssti) + ")", tags=("subsubtask", "selectable"), open=True)
subsubtask_id = subtask_id + "/" + str(ssti)
self.__itemdump[subsubtask_id] = { "item": subsubtask_level, "task": ti, "subtask": sti, "subsubtask": ssti, "id": subsubtask_id }
self.__subsubtaskdump[subsubtask_level] = subsubtask_id
self.__summing[subsubtask_id] = { "expression": "" }
for amcq in subsubtasks[ssti]:
maxpts = amcq["maxpts"]
sst_maxpts += maxpts
column = SpreadSheetPosition(amcq["column"])
fullid = amcq["fullid"]
amcq_level = tree.insert(subsubtask_level, tk.END, text=amcq["name"], values=(maxpts, column, fullid), tags=("all_missing"))
amcq_id = subsubtask_id + "/" + str(amcq["name"])
self.__columns[amcq_id] = { "column": column, "item": amcq_level }
self.__tree.item(subsubtask_level, values=(sst_maxpts, "", ""))
st_maxpts += sst_maxpts
self.__tree.item(subtask_level, values=(st_maxpts, "", ""))
t_maxpts += st_maxpts
self.__tree.item(task_level, values=(t_maxpts, "", ""))
def colorize_entries(self) -> None:
summed_amcq_missing = 0
for ti in self.__task_tree:
task_id = str(ti)
subtasks = self.__task_tree[ti]["subtasks"]
missing_tasks = 0
for sti in subtasks:
subtask_id = task_id + "/" + str(sti)
subsubtasks = subtasks[sti]["subsubtasks"]
missing_subsubtasks = 0
for ssti in subsubtasks:
subsubtask_id = subtask_id + "/" + str(ssti)
missing_amcq = 0
included_amcq = 0
if subsubtask_id in self.__summing:
expr = self.__summing[subsubtask_id]["expression"]
else:
expr = ""
for amcq in subsubtasks[ssti]:
regex = r"#\b" + amcq["name"] + r"\b#"
if re.search(regex, expr):
tags = ["nothing_missing"]
included_amcq += 1
else:
tags = ["all_missing"]
missing_amcq += 1
amcq_id = subsubtask_id + "/" + str(amcq["name"])
amcq_level = self.__columns[amcq_id]["item"]
self.__tree.item(amcq_level, tags=tags)
summed_amcq_missing += missing_amcq
if missing_amcq == 0:
tags = [ "nothing_missing" ]
elif missing_amcq > 0 and included_amcq > 0:
tags = [ "some_missing" ]
else:
tags = [ "all_missing" ]
if missing_amcq > 0:
missing_subsubtasks += 1
tags.append("selectable")
tags.append("subsubtask")
self.__tree.item(self.__itemdump[subsubtask_id]["item"], tags=tags)
if missing_subsubtasks == 0:
tags = [ "nothing_missing" ]
else:
tags = [ "all_missing" ]
missing_tasks += 1
tags.append("subtask")
self.__tree.item(self.__itemdump[subtask_id]["item"], tags=tags)
if missing_tasks == 0:
tags = [ "nothing_missing" ]
else:
tags = [ "all_missing" ]
tags.append("task")
self.__tree.item(self.__itemdump[task_id]["item"], tags=tags)
self.__status.config(text=str(summed_amcq_missing) + " mező nem szerepel a pontszámításban")
def init_ods_tab(self) -> None:
menuFrame = ttk.Frame(self.__loadOdsTab)
menuFrame.pack(side="top", fill="x")
treeFrame = ttk.Frame(self.__loadOdsTab)
treeFrame.pack(side="top", expand=True, fill="both")
tree = ttk.Treeview(treeFrame, columns=("maxpts", "odscolumn", "fullid"), selectmode="none")
tree.pack(side="left", expand=True, fill="both")
tree.heading("#0", text="Feladat")
tree.heading("maxpts", text="Max. pont")
tree.heading("odscolumn", text="ODS oszlop")
tree.heading("fullid", text="Azonosító")
tree.tag_configure("all_missing", background="Salmon")
tree.tag_configure("some_missing", background="Gold")
tree.tag_configure("nothing_missing", background="LawnGreen")
tree.tag_configure("selectable")
tfont = font.Font(family="Ubuntu Mono", size=11, weight="bold", underline=True)
tree.tag_configure("task", font=tfont)
stfont = font.Font(family="Ubuntu Mono", size=11, weight="bold")
tree.tag_configure("subtask", font=stfont)
sstfont = font.Font(family="Ubuntu Mono", size=11, slant="italic")
tree.tag_configure("subsubtask", font=sstfont)
self.__tree = tree
tree_scroll = ttk.Scrollbar(treeFrame, orient="vertical", command=tree.yview)
tree_scroll.pack(side="right", fill="y")
tree.configure(yscrollcommand=tree_scroll.set)
def tree_click(event) -> None:
item = self.__tree.identify_row(event.y)
if item:
tags = self.__tree.item(item, "tags")
if "selectable" in tags:
self.__tree.selection_set(item)
self.__sel_subsubtask = self.__itemdump[self.__subsubtaskdump[item]]
id = self.__sel_subsubtask["id"]
expr = self.__summing[id]["expression"]
self.__expr_editor.config(state="normal")
self.__expr_editor.delete(1.0, tk.END)
self.__expr_editor.insert(tk.END, expr)
pass
tree.bind("<Button-1>", tree_click)
expr_editor = scrolledtext.ScrolledText(treeFrame, wrap=tk.WORD, width=30, font=("Ubuntu Mono", 10))
expr_editor.config(background="white", state="disabled")
expr_editor.pack(side="right", expand=True, fill="both")
def expr_edit(event) -> None:
if self.__expr_editor.cget("state") == "disabled":
return
expr = self.__expr_editor.get(1.0, tk.END).strip()
self.__summing[self.__sel_subsubtask["id"]]["expression"] = expr
self.colorize_entries()
# amcqs = self.__tree.get_children(self.__sel_subsubtask["item"])
# for amcq in amcqs:
# item = self.__tree.item(amcq)
# pattern = r"#\b" + item["text"] + r"\b#"
# if re.search(pattern, expr):
# tags = ["nothing_missing"]
# else:
# tags = ["all_missing"]
# self.__tree.item(amcq, tags=tags)
# pass
expr_editor.bind("<KeyRelease>", expr_edit)
self.__expr_editor = expr_editor
def loadOds() -> None:
filetypes = [
("ODS táblázatok", "*.ods")
]
fn = filedialog.askopenfilename(
title="Táblázat megnyitása",
initialdir=os.getcwd(),
filetypes=filetypes)
if fn == ():
return
self.__table.load_table(fn)
task_tree = self.__table.get_task_tree()
self.populate_task_treeview(task_tree)
self.colorize_entries()
loadOdsBtn = tk.Button(menuFrame, text="Táblázat betöltése", command=loadOds)
loadOdsBtn.pack(side="left")
def save_hierarchy() -> None:
filetypes = [
("JSON", "*.json"),
("Markdown", "*.md")
]
fn = filedialog.asksaveasfilename(
title="Táblázathierarchia mentése",
initialdir=os.getcwd(),
filetypes=filetypes
)
if fn == ():
return
if fn.lower().endswith(".md"):
self.__table.export_hierarchy_md(fn)
else:
if not fn.endswith(".json"):
fn += ".json"
self.__table.export_hierarchy_json(fn)
storeHierarchyBtn = tk.Button(menuFrame, text="Hierarchia mentése", command=save_hierarchy)
storeHierarchyBtn.pack(side="left")
def save_summing() -> None:
filetypes = [
("JSON", "*.json"),
]
fn = filedialog.asksaveasfilename(
title="Pontozás mentése",
initialdir=os.getcwd(),
filetypes=filetypes
)
if fn == ():
return
if not fn.endswith(".json"):
fn += ".json"
json_dump = json.dumps(self.__summing)
with open(fn, "w") as jsonf:
jsonf.write(json_dump)
storeSummingBtn = tk.Button(menuFrame, text="Pontozás mentése", command=save_summing)
storeSummingBtn.pack(side="left")
def load_summing() -> None:
filetypes = [
("JSON", "*.json")
]
fn = filedialog.askopenfilename(
title="Pontozás betöltése",
initialdir=os.getcwd(),
filetypes=filetypes)
if fn == ():
return
with open(fn, "r") as jsonf:
self.__summing = json.load(jsonf)
self.colorize_entries()
loadSummingBtn = tk.Button(menuFrame, text="Pontozás betöltése", command=load_summing)
loadSummingBtn.pack(side="left")
def write_summing_into_table() -> None:
filetypes = [
("ODS", "*.ods"),
]
fn = filedialog.asksaveasfilename(
title="Pontozás táblázatba mentése",
initialdir=os.getcwd(),
filetypes=filetypes
)
if fn == ():
return
if not fn.endswith(".ods"):
fn += ".ods"
fsname = self.__table.get_first_sheet_name()
compiled_summing = {}
task_summing = {}
task_first_col = 0
prev_task = "1"
i = 0
for ssti in self.__summing:
if ssti[0] != prev_task:
task_summing["F" + prev_task] = "=SUM([.$" + index_to_address(task_first_col) + "2]:[.$" + index_to_address(i - 1) + "2])"
prev_task = ssti[0]
task_first_col = i
expr = self.__summing[ssti]["expression"]
fields = re.findall(r"#\b(.+?)\b#", expr)
fdl = str(FIRST_DATA_LINE + 1)
for field in fields:
amcq_id = ssti + "/" + field
if amcq_id in self.__columns:
address = "[.$" + fsname + ".$" + self.__columns[amcq_id]["column"].address() + fdl + "]"
expr = expr.replace("#" + field + "#", address)
ssti = ssti.strip("/-")
expr = expr.replace("\n", "")
compiled_summing["F" + ssti] = expr
i += 1
task_summing["F" + prev_task] = "=SUM([.$" + index_to_address(task_first_col) + "2]:[.$" + index_to_address(i - 1) + "2])"
self.__table.create_scoring_sheet(fn, compiled_summing | task_summing)
writeSummingBtn = tk.Button(menuFrame, text="Pontozás táblázatba írása", command=write_summing_into_table)
writeSummingBtn.pack(side="left")
statusBar = tk.Label(self.__loadOdsTab, anchor="w")
statusBar.pack(side="bottom", expand=False, fill="x")
statusBar.config(text="Nincs táblázat betöltve")
self.__status = statusBar
def init_gui(self) -> None:
win = ThemedTk()
win.title("AMC táblázatösszegző")
self.__win = win
tabs = ttk.Notebook(win)
loadOdsTab = ttk.Frame(tabs)
programScoringTab = ttk.Frame(tabs)
tabs.add(loadOdsTab, text="Táblázat")
#tabs.add(programScoringTab, text="Pontozás")
self.__loadOdsTab = loadOdsTab
#self.__programScoringTab = programScoringTab
self.init_ods_tab()
tabs.pack(expand=True, fill="both")
def init_table(self) -> None:
self.__table = AMCTable()
def __init__(self) -> None:
self.init_table()
self.init_gui()
def mainloop(self) -> None:
self.__win.mainloop()
# -----
win = MainWin()
win.mainloop()
exit(0)

6
requirements.txt Normal file
View File

@ -0,0 +1,6 @@
ezodf
lxml
pillow
pip
tk
ttkthemes