From 7f99df67cb42c83ce190d4962a0141d5307efd33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20Wiesner?= Date: Tue, 6 Jan 2026 06:50:45 +0100 Subject: [PATCH] init --- main.py | 582 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 6 + 2 files changed, 588 insertions(+) create mode 100644 main.py create mode 100644 requirements.txt diff --git a/main.py b/main.py new file mode 100644 index 0000000..0b63a6d --- /dev/null +++ b/main.py @@ -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("", 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("", 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) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5a4550e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +ezodf +lxml +pillow +pip +tk +ttkthemes