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)