outproc/main.py
András Wiesner 7f99df67cb init
2026-01-06 06:50:45 +01:00

582 lines
19 KiB
Python

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)