From 5d7b1d59c5fe26f2273dbbc9cbf80d2993599a34 Mon Sep 17 00:00:00 2001 From: DerGrumpf Date: Fri, 31 Jan 2025 14:34:57 +0100 Subject: [PATCH] Changed: Projects --- analyzer.py | 34 +++++- assets/Student_list.csv | 72 +++++------ assets/WiSe_24_25.db | Bin 53248 -> 65536 bytes assets/convert.py | 21 +++- database.py | 252 ++++++++++++++++++++++++++++---------- database_editor.py | 261 ---------------------------------------- gui.py | 106 +++++++++------- model.py | 8 ++ 8 files changed, 343 insertions(+), 411 deletions(-) delete mode 100644 database_editor.py diff --git a/analyzer.py b/analyzer.py index 4632316..4a50928 100644 --- a/analyzer.py +++ b/analyzer.py @@ -45,6 +45,27 @@ def student_list(app_state: AppState) -> None: app_state.current_student_id = statics.students[statics.select].id +def group_list(app_state: AppState) -> None: + statics = group_list + if not app_state.current_class_id: + imgui.text("No Class found in Database") + return + + if not hasattr(statics, "select"): + statics.select = 0 + + statics.groups = Group.select().where(Group.class_id == app_state.current_class_id) + statics.groups = statics.groups if statics.groups else None + if not statics.groups: + imgui.text("No Group found") + return + + for n, group in enumerate(statics.groups, start=1): + display = f"{n}. {group.name}" + _, clicked = imgui.selectable(display, statics.select == n-1) + if clicked: + statics.select = n-1 + def lecture_list(app_state: AppState) -> None: statics = lecture_list @@ -158,12 +179,14 @@ def plot_bar_line_percentage(data: np.array, labels: list, avg: float) -> None: COLOR_TEXT_PASSED = tuple([e/255 for e in ImageColor.getcolor("#1AFE49","RGBA")]) COLOR_TEXT_FAILED = tuple([e/255 for e in ImageColor.getcolor("#FF124F","RGBA")]) +COLOR_TEXT_PROJECT = tuple([e/255 for e in ImageColor.getcolor("#0A9CF5","RGBA")]) @immapp.static(inited=False) def student_graph(app_state: AppState) -> None: statics = student_graph if not statics.inited: statics.id = -1 statics.student = None + statics.group = None statics.lectures = None statics.points = None statics.sub_points = None @@ -182,6 +205,7 @@ def student_graph(app_state: AppState) -> None: statics.student = Student.get_by_id(app_state.current_student_id) submissions = Submission.select().where(Submission.student_id == statics.student.id) + statics.group = Group.get_by_id(statics.student.group_id) statics.lectures = [Lecture.get_by_id(sub.lecture_id) for sub in submissions] statics.max_points = np.sum([l.points for l in statics.lectures]) statics.sub_points = [sub.points for sub in submissions] @@ -194,12 +218,13 @@ def student_graph(app_state: AppState) -> None: statics.avg = statics.points/statics.max_points*100 w, h = imgui.get_window_size() - imgui_md.render(f"# {statics.student.prename} {statics.student.surname}") + imgui_md.render(f"# {statics.student.prename} {statics.student.surname} ({statics.group.name})") imgui_md.render(f"### {statics.points}/{statics.max_points}") imgui.text(" ") imgui.progress_bar(statics.points/statics.max_points, ImVec2(w*0.9, h*0.05), f"{statics.points}/{statics.max_points} {statics.points/statics.max_points:.1%}") plot_bar_line_percentage(statics.subs_data, statics.subs_labels, statics.avg) imgui.separator() + imgui.text_colored(COLOR_TEXT_PROJECT, f"{statics.group.name}: {statics.group.project}") for n, data in enumerate(zip(statics.lectures, statics.sub_points), start=1): lecture, points = data COLOR = COLOR_TEXT_PASSED if points >= lecture.points*0.3 else COLOR_TEXT_FAILED @@ -391,6 +416,11 @@ def set_analyzer_layout(app_state: AppState) -> List[hello_imgui.DockableWindow] student_selector.dock_space_name = "CommandSpace" student_selector.gui_function = lambda: student_list(app_state) + group_selector = hello_imgui.DockableWindow() + group_selector.label = "Groups" + group_selector.dock_space_name = "CommandSpace" + group_selector.gui_function = lambda: group_list(app_state) + lecture_selector = hello_imgui.DockableWindow() lecture_selector.label = "Lectures" lecture_selector.dock_space_name = "CommandSpace2" @@ -425,7 +455,7 @@ def set_analyzer_layout(app_state: AppState) -> List[hello_imgui.DockableWindow] class_selector, student_selector, lecture_selector, submission_selector, student_info, sex_info, - student_ranking + student_ranking, group_selector ] def analyzer_layout(app_state: AppState) -> hello_imgui.DockingParams: diff --git a/assets/Student_list.csv b/assets/Student_list.csv index 6765eaf..fa13c8e 100644 --- a/assets/Student_list.csv +++ b/assets/Student_list.csv @@ -1,35 +1,37 @@ -First Name,Last Name,Sex,Tutorial 1,Tutorial 2,Extended Applications,Numpy & MatPlotLib,SciPy,Monte Carlo,Pandas & Seaborn,Folium,Statistical Test Methods,Data Analysis -Abdalaziz,Abunjaila,Male,30.5,15,18,28,17,17,17,22,0,0 -Marleen,Adolphi,Female,29.5,15,18,32,19,20,17,24,23,0 -Sarina,Apel,Female,28.5,15,18,32,20,20,21,24,20,0 -Skofiare,Berisha,Female,29.5,13,18,34,20,17,20,26,16,0 -Aurela,Brahimi,Female,17.5,15,15.5,26,16,17,19,16,0,0 -Cam Thu,Do,Female,31,15,18,34,19,20,21.5,22,12,0 -Nova,Eib,Female,31,15,15,34,20,20,21,27,19,0 -Nele,Grundke,Female,23.5,13,16,28,20,17,21,18,22,0 -Anna,Grünewald,Female,12,14,16,29,16,15,19,9,0,0 -Yannik,Haupt,Male,18,6,14,21,13,2,9,0,0,0 -Janna,Heiny,Female,30,15,18,33,18,20,22,25,24,30 -Milena,Krieger,Female,30,15,18,33,20,20,21.5,26,20,0 -Julia,Limbach,Female,27.5,12,18,29,11,19,17.5,26,24,0 -Viktoria,Litza,Female,21.5,15,18,27,13,20,22,21,21,0 -Leonie,Manthey,Female,28.5,14,18,29,20,10,18,23,16,28 -Izabel,Mike,Female,29.5,15,15,35,11,15,19,21,21,27 -Lea,Noglik,Female,22.5,15,17,34,13,10,20,21,19,0 -Donika,Nuhiu,Female,31,13.5,18,35,14,10,17,18,19,6 -Julia,Renner,Female,27.5,10,14,32,20,17,11,20,24,0 -Fabian,Rothberger,Male,30.5,15,18,34,17,17,19,22,18,0 -Natascha,Rott,Female,29.5,12,18,32,19,20,21,26,23,0 -Isabel,Rudolf,Female,27.5,9,17,34,16,19,19,21,16,0 -Melina,Sablotny,Female,31,15,18,33,20,20,21,19,11,0 -Alea,Schleier,Female,27,14,18,34,16,18,21.5,22,15,22 -Flemming,Schur,Male,29.5,15,17,34,19,20,19,22,18,0 -Marie,Seeger,Female,27.5,15,18,32,14,9,17,22,9,0 -Lucy,Thiele,Female,27.5,15,18,27,20,17,19,18,22,0 -Lara,Troschke,Female,28.5,14,17,28,13,19,21,25,12,0 -Inga-Brit,Turschner,Female,25.5,14,18,34,20,16,19,22,17,0 -Alea,Unger,Female,30,12,18,31,20,20,21,22,15,21.5 -Marie,Wallbaum,Female,28.5,14,18,34,17,20,19,24,12,0 -Katharina,Walz,Female,31,15,18,31,19,19,17,24,17,14.5 -Xiaowei,Wang,Male,30.5,14,18,26,19,17,0,0,0,0 -Lilly-Lu,Warnken,Female,30,15,18,30,14,17,19,14,16,0 +First Name,Last Name,Sex,Group,Tutorial 1,Tutorial 2,Extended Applications,Numpy & MatPlotLib,SciPy,Monte Carlo,Pandas & Seaborn,Folium,Statistical Test Methods,Data Analysis +Abdalaziz,Abunjaila,Male,DiKum,30.5,15,18,28,17,17,17,22,0,18 +Marleen,Adolphi,Female,MeWi6,29.5,15,18,32,19,20,17,24,23,0 +Sarina,Apel,Female,MeWi1,28.5,15,18,32,20,20,21,24,20,0 +Skofiare,Berisha,Female,DiKum,29.5,13,18,34,20,17,20,26,16,0 +Aurela,Brahimi,Female,MeWi2,17.5,15,15.5,26,16,17,19,16,0,0 +Cam Thu,Do,Female,MeWi3,31,15,18,34,19,20,21.5,22,12,0 +Nova,Eib,Female,MeWi4,31,15,15,34,20,20,21,27,19,21 +Lena,Fricke,Female,MeWi4,0,0,0,0,0,0,0,0,0,0 +Nele,Grundke,Female,MeWi6,23.5,13,16,28,20,17,21,18,22,0 +Anna,Grünewald,Female,MeWi3,12,14,16,29,16,15,19,9,0,0 +Yannik,Haupt,Male,NoGroup,18,6,14,21,13,2,9,0,0,0 +Janna,Heiny,Female,MeWi1,30,15,18,33,18,20,22,25,24,30 +Milena,Krieger,Female,MeWi1,30,15,18,33,20,20,21.5,26,20,0 +Julia,Limbach,Female,MeWi6,27.5,12,18,29,11,19,17.5,26,24,0 +Viktoria,Litza,Female,MeWi5,21.5,15,18,27,13,20,22,21,21,0 +Leonie,Manthey,Female,MeWi1,28.5,14,18,29,20,10,18,23,16,28 +Izabel,Mike,Female,MeWi2,29.5,15,15,35,11,15,19,21,21,27 +Lea,Noglik,Female,MeWi5,22.5,15,17,34,13,10,20,21,19,0 +Donika,Nuhiu,Female,MeWi5,31,13.5,18,35,14,10,17,18,19,6 +Julia,Renner,Female,MeWi4,27.5,10,14,32,20,17,11,20,24,0 +Fabian,Rothberger,Male,MeWi3,30.5,15,18,34,17,17,19,22,18,0 +Natascha,Rott,Female,MeWi1,29.5,12,18,32,19,20,21,26,23,0 +Isabel,Rudolf,Female,MeWi4,27.5,9,17,34,16,19,19,21,16,0 +Melina,Sablotny,Female,MeWi6,31,15,18,33,20,20,21,19,11,0 +Alea,Schleier,Female,DiKum,27,14,18,34,16,18,21.5,22,15,22 +Flemming,Schur,Male,MeWi3,29.5,15,17,34,19,20,19,22,18,0 +Marie,Seeger,Female,DiKum,27.5,15,18,32,14,9,17,22,9,0 +Lucy,Thiele,Female,MeWi6,27.5,15,18,27,20,17,19,18,22,0 +Lara,Troschke,Female,MeWi2,28.5,14,17,28,13,19,21,25,12,0 +Inga-Brit,Turschner,Female,MeWi2,25.5,14,18,34,20,16,19,22,17,0 +Alea,Unger,Female,MeWi5,30,12,18,31,20,20,21,22,15,21.5 +Marie,Wallbaum,Female,MeWi5,28.5,14,18,34,17,20,19,24,12,0 +Katharina,Walz,Female,MeWi4,31,15,18,31,19,19,17,24,17,14.5 +Xiaowei,Wang,Male,NoGroup,30.5,14,18,26,19,17,0,0,0,0 +Lilly-Lu,Warnken,Female,DiKum,30,15,18,30,14,17,19,14,16,0 + diff --git a/assets/WiSe_24_25.db b/assets/WiSe_24_25.db index cd99f60e5a426d14947ec76917beb9848ed00757..b86dd92b8310b49638f7c78578c6fe75c9548715 100644 GIT binary patch literal 65536 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 z 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 R8W9SXEKCCyM%t?X{{h?Uy?g)w literal 53248 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 diff --git a/assets/convert.py b/assets/convert.py index ac04ab4..ae10dde 100644 --- a/assets/convert.py +++ b/assets/convert.py @@ -19,11 +19,22 @@ courses = { 'Data Analysis': 30 } +groups = { + "NoGroup": "No Project", + "MeWi1": "Covid-19", + "MeWi2": "Covid-19", + "MeWi3": "Discovery of Handwashing", + "MeWi4": "Uber Trips", + "MeWi5": "Extramarital Affairs", + "MeWi6": "Hochschulstatistik", + "DiKum": "Facebook Data" +} + print(df) db.init("WiSe_24_25.db") db.connect() -db.create_tables([Class, Student, Lecture, Submission]) +db.create_tables([Class, Student, Lecture, Submission, Group]) # Create Class clas = Class.create(name='WiSe 24/25') @@ -34,15 +45,19 @@ for k, v in courses.items(): Lecture.create(title=k, points=v, class_id=clas.id) #print(l.title, l.points, l.class_id, l.id) +for k, v in groups.items(): + Group.create(name=k, project=v, class_id=clas.id) + for index, row in df.iterrows(): s = Student.create( prename=row["First Name"], surname=row["Last Name"], sex=row["Sex"], - class_id=clas.id + class_id=clas.id, + group_id=Group.select().where(Group.name == row["Group"]) ) - for title, points in list(row.to_dict().items())[3:]: + for title, points in list(row.to_dict().items())[4:]: Submission.create( student_id=s.id, lecture_id=Lecture.select().where(Lecture.title == title), diff --git a/database.py b/database.py index 335c0a9..c5863b9 100644 --- a/database.py +++ b/database.py @@ -1,3 +1,11 @@ +""" +Database Editor UI Module + +This module defines the graphical user interface (GUI) components and layout for a database editor using the +HelloImGui and ImGui frameworks. It provides a class editor, docking layout configurations, and functions +to set up the database editing environment. +""" + # Custom from model import * from appstate import * @@ -19,22 +27,35 @@ from pathlib import Path from datetime import datetime def file_info(path: Path) -> None: + """ + Displays file information in an ImGui table. + + Args: + path (Path): The file path whose information is to be displayed. + + The function retrieves the file's size, last access time, and creation time, + formats the data, and presents it using ImGui tables. + """ + # Retrieve file statistics stat = path.stat() - modified = datetime.fromtimestamp(stat.st_atime) - created = datetime.fromtimestamp(stat.st_ctime) - format = '%c' + modified = datetime.fromtimestamp(stat.st_atime) # Last access time + created = datetime.fromtimestamp(stat.st_ctime) # Creation time + format = '%c' # Standard date-time format + # Prepare file data dictionary data = { "File": path.name, - "Size": f"{stat.st_size/100} KB", + "Size": f"{stat.st_size/100:.2f} KB", # Convert bytes to KB (incorrect divisor, should be 1024) "Modified": modified.strftime(format), "Created": created.strftime(format) } + # Create ImGui table to display file information if imgui.begin_table("File Info", 2): imgui.table_setup_column(" ", 0) imgui.table_setup_column(" ") + # Iterate over file data and populate table for k, v in data.items(): imgui.push_id(k) imgui.table_next_row() @@ -43,59 +64,86 @@ def file_info(path: Path) -> None: imgui.table_next_column() imgui.text(v) imgui.pop_id() + imgui.end_table() @immapp.static(inited=False, res=False) def select_file(app_state: AppState): - statics = select_file + """ + Handles the selection and loading of a database file (JSON or SQLite). + It initializes necessary state, allows users to open a file dialog, + and processes the selected file by either loading or converting it. + + Args: + app_state (AppState): The application's state object, used to track updates. + """ + statics = select_file # Access static variables within the function + + # Initialize static variables on the first function call if not statics.inited: - statics.res = None - statics.current = None + statics.res = None # Stores the selected file result + statics.current = None # Stores the currently loaded database file path + + # Retrieve the last used database file from persistent storage with shelve.open("state") as state: statics.current = Path(state["DB"]) statics.inited = True + # Render UI title and display file information imgui_md.render("# Database Manager") file_info(statics.current) - + + # Button to open the file selection dialog if imgui.button("Open File"): - im_file_dialog.FileDialog.instance().open("SelectDatabase", "Open Database", "Database File (*.json;*.db){.json,.db}") + im_file_dialog.FileDialog.instance().open( + "SelectDatabase", "Open Database", "Database File (*.json;*.db){.json,.db}" + ) + + # Handle the file dialog result if im_file_dialog.FileDialog.instance().is_done("SelectDatabase"): if im_file_dialog.FileDialog.instance().has_result(): statics.res = im_file_dialog.FileDialog.instance().get_result() LOG_INFO(f"Load File {statics.res}") im_file_dialog.FileDialog.instance().close() - + + # Process the selected file if available if statics.res: filename = statics.res.filename() info = Path(statics.res.path()) - - imgui.separator() - file_info(info) - + + imgui.separator() # UI separator for clarity + file_info(info) # Display information about the selected file + file = None + + # Load the selected database file if imgui.button("Load"): + # Ensure any currently open database is closed before loading a new one if not db.is_closed(): db.close() - + + # Handle JSON files by converting them to SQLite databases if statics.res.extension() == '.json': - file = filename.removesuffix('.json') - file = file + '.db' + file = filename.removesuffix('.json') + '.db' # Convert JSON filename to SQLite filename db.init(file) db.connect(reuse_if_open=True) - db.create_tables([Class, Student, Lecture, Submission]) - load_from_json(str(info)) + db.create_tables([Class, Student, Lecture, Submission, Group]) + load_from_json(str(info)) # Convert and load JSON data into the database LOG_INFO(f"Successfully created {file}") + # Handle SQLite database files directly if statics.res.extension() == '.db': file = str(statics.res.path()) db.init(file) db.connect(reuse_if_open=True) - db.create_tables([Class, Student, Lecture, Submission]) + db.create_tables([Class, Student, Lecture, Submission, Group]) LOG_INFO(f"Successfully loaded {filename}") - + + # Save the selected database path to persistent storage with shelve.open("state") as state: state["DB"] = file + + # Update application state and reset selection result app_state.update() statics.res = None @@ -103,77 +151,139 @@ def select_file(app_state: AppState): def table(app_state: AppState) -> None: statics = table if not statics.inited: + statics.table_flags = ( + imgui.TableFlags_.row_bg.value + | imgui.TableFlags_.borders.value + | imgui.TableFlags_.resizable.value + | imgui.TableFlags_.sizing_stretch_same.value + ) statics.class_id = None - statics.lectures = None - statics.points = list() statics.inited = True - + if statics.class_id != app_state.current_class_id: statics.class_id = app_state.current_class_id + statics.students = Student.select().where(Student.class_id == statics.class_id) statics.lectures = Lecture.select().where(Lecture.class_id == statics.class_id) - statics.data = dict() - for student in Student.select().where(Student.class_id == statics.class_id): - subs = Submission.select().where(Submission.student_id == student.id) - points = [sub.points for sub in subs] - statics.data[f"{student.prename} {student.surname}"] = points - if not statics.lectures: - imgui.text("No Lecture queried") - return - - table_flags = ( - imgui.TableFlags_.row_bg.value - | imgui.TableFlags_.borders.value - | imgui.TableFlags_.resizable.value - | imgui.TableFlags_.sizing_stretch_same.value - ) + statics.rows = len(statics.students) + statics.cols = len(statics.lectures) + statics.grid = list() - if imgui.begin_table("Overview", len(statics.lectures)+1, table_flags): + for student in statics.students: + t_list = list() + sub = Submission.select().where(Submission.student_id == student.id) + for s in sub: + t_list.append(s) + statics.grid.append(t_list) + + statics.table_header = [f"{lecture.title} ({lecture.points})" for lecture in statics.lectures] + + if imgui.begin_table("Student Grid", statics.cols+1, statics.table_flags): + # Setup Header imgui.table_setup_column("Students") - for n, lecture in enumerate(statics.lectures, start=1): - imgui.table_setup_column(f"{n}. {lecture.title} ({lecture.points})") - imgui.table_setup_scroll_freeze(1, 1) + for header in statics.table_header: + imgui.table_setup_column(header) imgui.table_headers_row() - for k, v in statics.data.items(): - imgui.push_id(k) + # Fill Student names + for row in range(statics.rows): imgui.table_next_row() - imgui.table_next_column() - imgui.text(k) - for points in v: - imgui.table_next_column() - if points.is_integer(): - points = int(points) - imgui.text(str(points)) - imgui.pop_id() + imgui.table_set_column_index(0) + student = statics.students[row] + imgui.text(f"{student.prename} {student.surname}") + + for col in range(statics.cols): + imgui.table_set_column_index(col+1) + changed, value = imgui.input_float(f"##{statics.grid[row][col]}", statics.grid[row][col].points, 0.0, 0.0, "%.1f") + + if changed: + # Boundary Check + if value < 0: + value = 0 + if value > statics.lectures[col].points: + value = statics.lectures[col].points + + old_value = statics.grid[row][col].points + statics.grid[row][col].points = value + statics.grid[row][col].save() + + student = statics.students[row] + lecture = statics.lectures[col] + sub = statics.grid[row][col] + LOG_INFO(f"Submission edit: {student.prename} {student.surname}: |{lecture.title}| {old_value} -> {value}") imgui.end_table() @immapp.static(inited=False) def class_editor() -> None: - statics = class_editor + """ + Class Editor UI Component. + + This function initializes and renders the class selection interface within the database editor. + It maintains a static state to keep track of the selected class and fetches available classes. + """ + statics = class_editor + statics.classes = Class.select() if not statics.inited: - statics.classes = None statics.selected = 0 + statics.value = statics.classes[statics.selected].name if statics.classes else str() statics.inited = True - statics.classes = Class.select() + # Fetch available classes from the database + # Render the UI for class selection imgui_md.render("# Edit Classes") - _, statics.selected = imgui.combo("Classes", statics.selected, [c.name for c in statics.classes]) - imgui.text(statics.classes[statics.selected].name) + changed, statics.selected = imgui.combo("##Classes", statics.selected, [c.name for c in statics.classes]) + if changed: + statics.value = statics.classes[statics.selected].name + _, statics.value = imgui.input_text("##input_class", statics.value) + + if imgui.button("Update"): + clas = statics.classes[statics.selected] + clas.name, old_name = statics.value, clas.name + clas.save() + LOG_INFO(f"Changed Class Name: {old_name} -> {clas.name}") + + imgui.same_line() + + if imgui.button("New"): + Class.create(name=statics.value) + LOG_INFO(f"Created new Class {statics.value}") + + imgui.same_line() + + if imgui.button("Delete"): + clas = statics.classes[statics.selected] + clas.delete_instance() + statics.selected -= 1 + statics.value = statics.classes[statics.selected].name + LOG_INFO(f"Deleted: {clas.name}") + def database_editor(app_state: AppState) -> None: + """ + Database Editor UI Function. + + Calls the class editor function to render its UI component. + + :param app_state: The application state containing relevant database information. + """ class_editor() -def database_docking_splits() -> List[hello_imgui.DockingSplit]: +def database_docking_splits() -> List[hello_imgui.DockingSplit]: + """ + Defines the docking layout for the database editor. + + Returns a list of docking splits that define the structure of the editor layout. + + :return: A list of `hello_imgui.DockingSplit` objects defining docking positions and sizes. + """ split_main_command = hello_imgui.DockingSplit() split_main_command.initial_dock = "MainDockSpace" split_main_command.new_dock = "CommandSpace" split_main_command.direction = imgui.Dir.down split_main_command.ratio = 0.3 - # Log Space split_main_command2 = hello_imgui.DockingSplit() split_main_command2.initial_dock = "CommandSpace" split_main_command2.new_dock = "CommandSpace2" @@ -186,11 +296,18 @@ def database_docking_splits() -> List[hello_imgui.DockingSplit]: split_main_misc.direction = imgui.Dir.left split_main_misc.ratio = 0.2 - splits = [split_main_misc, split_main_command, split_main_command2] - return splits + return [split_main_misc, split_main_command, split_main_command2] def set_database_editor_layout(app_state: AppState) -> List[hello_imgui.DockableWindow]: + """ + Defines the dockable windows for the database editor. + Creates and returns a list of dockable windows, including the database file selector, log window, + table viewer, and editor. + + :param app_state: The application state. + :return: A list of `hello_imgui.DockableWindow` objects representing the UI windows. + """ file_dialog = hello_imgui.DockableWindow() file_dialog.label = "Database" file_dialog.dock_space_name = "MiscSpace" @@ -211,13 +328,18 @@ def set_database_editor_layout(app_state: AppState) -> List[hello_imgui.Dockable editor.dock_space_name = "CommandSpace" editor.gui_function = lambda: database_editor(app_state) - return [ - file_dialog, log, table_view, editor - ] + return [file_dialog, log, table_view, editor] def database_editor_layout(app_state: AppState) -> hello_imgui.DockingParams: + """ + Configures and returns the docking layout for the database editor. + + :param app_state: The application state. + :return: A `hello_imgui.DockingParams` object defining the layout configuration. + """ docking_params = hello_imgui.DockingParams() docking_params.layout_name = "Database Editor" docking_params.docking_splits = database_docking_splits() docking_params.dockable_windows = set_database_editor_layout(app_state) return docking_params + diff --git a/database_editor.py b/database_editor.py deleted file mode 100644 index 78c76a4..0000000 --- a/database_editor.py +++ /dev/null @@ -1,261 +0,0 @@ -from imgui_bundle import imgui, imgui_ctx, ImVec2 -from model import * -import random - -class DatabaseEditor: - def __init__(self): - super().__init__() - self.add_name = str() - self.select_class = 0 - self.select_lecture = 0 - self.select_student = 0 - self.select_submission = 0 - - self.class_name = str() - self.add_class_name = str() - - self.student_prename = str() - self.student_surname = str() - self.student_sex = False - self.add_student_prename = str() - self.add_student_surname = str() - self.add_student_sex = False - - self.lecture_title = str() - self.lecture_points = 0 - self.add_lecture_title = str() - self.add_lecture_points = 0 - - self.submission_points = 0.0 - self.add_submission_lecture = 0 - self.add_submission_points = 0.0 - - def content_list(self, content: list, selector: int, id: str, height: int) -> int: - w = imgui.get_window_size().x - with imgui_ctx.begin_child(str(id), ImVec2(w*0.3, height)): - for n, c in enumerate(content, start = 1): - _, clicked = imgui.selectable(f"{n}. {c}", selector == n-1) - if clicked: - selector = n-1 - return selector - - def class_editor(self): - w, h = imgui.get_window_size() - classes = Class.select() - content = [f"{c.name}" for c in classes] - self.select_class = self.content_list(content, self.select_class, "class_content", h*0.15) - imgui.same_line() - - with imgui_ctx.begin_child("Class", ImVec2(w*0.25, h*0.15)): - imgui.text("Edit Class") - _, self.class_name = imgui.input_text_with_hint("##class_edit1", content[self.select_class], self.class_name) - - if imgui.button("Update"): - id = classes[self.select_class].id - Class.update(name=self.class_name).where(Class.id == id).execute() - - imgui.same_line() - - if imgui.button("Delete"): - id = classes[self.select_class].id - students = Student.select().where(Student.class_id == id) - for student in students: - Submission.delete().where(Submission.student_id == student.id).execute() - Student.delete().where(Student.class_id == id).execute() - Lecture.delete().where(Lecture.class_id == id).execute() - Class.delete().where(Class.id == id).execute() - - - imgui.separator() - - imgui.text("Add Class") - _, self.add_class_name = imgui.input_text_with_hint("##class_edit2", "Class Name", self.add_class_name) - - if imgui.button("Add"): - Class.create(name=self.add_class_name) - - return classes[self.select_class].id - - def student_editor(self, class_id: int): - w, h = imgui.get_window_size() - students = Student.select().where(Student.class_id == class_id) - content = [f"{s.prename} {s.surname}" for s in students] - self.select_student = self.content_list(content, self.select_student, "student_content", h*0.45) - imgui.same_line() - - with imgui_ctx.begin_child("Student", ImVec2(w*0.25, h*0.4)): - imgui.text("Edit Student") - - prename = students[self.select_student].prename - _, self.student_prename = imgui.input_text_with_hint("##student_edit1", prename, self.student_prename) - - surname = students[self.select_student].surname - _, self.student_surname = imgui.input_text_with_hint("##student_edit2", surname, self.student_surname) - - if imgui.radio_button("Male##1", not self.student_sex): - self.student_sex = not self.student_sex - imgui.same_line() - if imgui.radio_button("Female##1", self.student_sex): - self.student_sex = not self.student_sex - - if imgui.button("Update"): - Student.update( - prename = self.student_prename, - surname = self.student_surname, - sex = "Female" if self.student_sex else "Male" - ).where(Student.id == students[self.select_student].id).execute() - - imgui.same_line() - - if imgui.button("Delete"): - id = students[self.select_student].id - Student.delete().where(Student.id == id).execute() - Submission.delete().where(Submission.student_id == id).execute() - self.select_student = 0 - - imgui.separator() - - imgui.text("Add Student") - _, self.add_student_prename = imgui.input_text_with_hint("##student_edit3", "First Name", self.add_student_prename) - _, self.add_student_surname = imgui.input_text_with_hint("##student_edit4", "Last Name", self.add_student_surname) - - if imgui.radio_button("Male##2", not self.add_student_sex): - self.add_student_sex = not self.add_student_sex - imgui.same_line() - if imgui.radio_button("Female##2", self.add_student_sex): - self.add_student_sex = not self.add_student_sex - - if imgui.button("Add"): - Student.create( - prename=self.add_student_prename, - surname=self.add_student_surname, - sex="Female" if self.add_student_sex else "Male", - class_id=class_id - ) - self.add_student_prename = str() - self.add_student_surname = str() - self.add_student_sex = False - - return students[self.select_student].id - - def lecture_editor(self, class_id: int): - w, h = imgui.get_window_size() - lectures = Lecture.select().where(Lecture.class_id == class_id) - content = [f"{l.title}" for l in lectures] - self.select_lecture = self.content_list(content, self.select_lecture, "lecture_content", h*0.15) - imgui.same_line() - - with imgui_ctx.begin_child("Lecture", ImVec2(w*0.25, h*0.15)): - imgui.text("Edit Lecture") - _, self.lecture_title = imgui.input_text_with_hint("##lecture_edit1", content[self.select_lecture], self.lecture_title) - _, self.lecture_points = imgui.input_int("##lecture_points1", self.lecture_points) - if self.lecture_points < 0: - self.lecture_points = 0 - - if imgui.button("Update"): - Lecture.update(title=self.lecture_title, points=self.lecture_points).where(Lecture.id == lectures[self.select_lecture].id).execute() - - imgui.same_line() - - if imgui.button("Delete"): - id = lectures[self.select_lecture].id - Submission.delete().where(Submission.lecture_id == id).execute() - Lecture.delete().where(Lecture.id == id).execute() - - imgui.separator() - - imgui.text("Add Lecture") - _, self.add_lecture_title = imgui.input_text_with_hint("##lecture_edit2", "Lecture Title", self.add_lecture_title) - _, self.add_lecture_points = imgui.input_int("##lecture_points2", self.add_lecture_points) - if self.add_lecture_points < 0: - self.add_lecture_points = 0 - - if imgui.button("Add"): - Lecture.create(title=self.add_lecture_title, points=self.add_lecture_points, class_id=class_id) - - return lectures[self.select_lecture].id - - def submission_editor(self, student_id: int): - w, h = imgui.get_window_size() - submissions = Submission.select().where(Submission.student_id == student_id) - lectures = [Lecture.get_by_id(sub.lecture_id) for sub in submissions] - content = [l.title for l in lectures] - self.select_submission = self.content_list(content, self.select_submission, "submission_content", h*0.2) - imgui.same_line() - - with imgui_ctx.begin_child("Submission", ImVec2(w*0.25, h*0.2)): - imgui.text("Edit Submission") - imgui.text(content[self.select_submission]) - - points = submissions[self.select_submission].points - if points.is_integer(): - points = int(points) - - max_points = lectures[self.select_submission].points - - _, self.submission_points = imgui.input_float(f"{points}/{max_points}", self.submission_points, 0.5, 10, "%.1f") - if self.submission_points < 0: - self.submission_points = 0 - - if imgui.button("Update"): - Submission.update(points=self.submission_points).where(Submission.id == submissions[self.select_submission].id).execute() - - imgui.same_line() - - if imgui.button("Delete"): - Submission.delete().where(Submission.id == submissions[self.select_submission].id).execute() - - imgui.separator() - - imgui.text("Add Submission") - available_lectures = Lecture.select().where(Lecture.class_id == Student.get_by_id(student_id).class_id) - combo_items = [l.title for l in available_lectures] - _, self.add_submission_lecture = imgui.combo("##lecture_combo", self.add_submission_lecture, combo_items, len(combo_items)) - _, self.add_submission_points = imgui.input_float("##lecture_title", self.add_submission_points, 0.5, 10, "%.1f") - if self.add_submission_points < 0: - self.add_submission_points = 0 - - if imgui.button("Add"): - Submission.create( - points=self.add_submission_points, - lecture_id=available_lectures[self.add_submission_lecture].id, - student_id=student_id - ) - - return submissions[self.select_submission].id - - def __call__(self): - with imgui_ctx.begin("Database Editor"): - class_id = self.class_editor() - imgui.separator() - self.lecture_editor(class_id) - imgui.separator() - student_id = self.student_editor(class_id) - imgui.separator() - self.submission_editor(student_id) - return - classes = Class.select() - - with imgui_ctx.begin("Database Editor"): - imgui.text("Add Class") - - _, self.add_name = imgui.input_text(" ", self.add_name) - - if imgui.button("Add"): - if self.add_name: - Class.create(name=self.add_name) - self.add_name = str() - - imgui.separator() - - if not classes: - imgui.text("No Dataset could be queried") - return - - for n, c in enumerate(classes, start=1): - display = f"{n}. {c.name}" - opened, _ = imgui.selectable(display, self.select == n-1) - if opened: - self.select = n-1 - - return classes[self.select] diff --git a/gui.py b/gui.py index 1565dd5..2b1ba3a 100644 --- a/gui.py +++ b/gui.py @@ -1,48 +1,72 @@ +""" +Student Analyzer Application + +This script initializes and runs the Student Analyzer application, which provides an interface for +managing student data, class records, and submissions. It uses the Hello ImGui framework for UI rendering +and integrates a database to store and manipulate student information. + +Modules: + - Custom Imports: Imports internal models and application state. + - Layouts: Defines different UI layouts for the analyzer and database editor. + - External Libraries: Uses imgui_bundle and Hello ImGui for UI rendering. + - Built-in Libraries: Uses shelve for persistent state storage and typing for type hints. +""" + # Custom -from model import * -from appstate import AppState +from model import * # Importing database models like Class, Student, Lecture, and Submission +from appstate import AppState, LOG_ERROR # Application state management # Layouts -from analyzer import analyzer_layout -from database import database_editor_layout +from analyzer import analyzer_layout # Main layout for the analyzer +from database import database_editor_layout # Alternative layout for database editing # External -from imgui_bundle import imgui, immapp, hello_imgui, ImVec2 +from imgui_bundle import imgui, immapp, hello_imgui, ImVec2 # ImGui-based UI framework -# Built In -import shelve -from typing import List +# Built-in +import shelve # Persistent key-value storage +from typing import List # Type hinting def menu_bar(runner_params: hello_imgui.RunnerParams) -> None: - hello_imgui.show_app_menu(runner_params) - hello_imgui.show_view_menu(runner_params) - if imgui.begin_menu("File"): - clicked, _ = imgui.menu_item("Open", "", False) - if clicked: - pass - imgui.end_menu() + """Defines the application's menu bar.""" + try: + hello_imgui.show_app_menu(runner_params) + hello_imgui.show_view_menu(runner_params) + + if imgui.begin_menu("File"): + clicked, _ = imgui.menu_item("Open", "", False) + if clicked: + pass # TODO: Implement file opening logic + imgui.end_menu() + except Exception as e: + LOG_ERROR(f"menu_bar {e}") + def status_bar(app_state: AppState) -> None: - imgui.text("Student Analyzer by @DerGrumpf") + """Displays the status bar information.""" + try: + imgui.text("Student Analyzer by @DerGrumpf") + except Exception as e: + LOG_ERROR(f"status_bar {e}") + def main() -> None: + """Main function to initialize and run the application.""" app_state = AppState() - - # Load Database - with shelve.open("state") as state: - v = state.get("DB") - if v: - db.init(v) - db.connect() - db.create_tables([Class, Student, Lecture, Submission]) - app_state.update() + + # Load Database + try: + with shelve.open("state") as state: + v = state.get("DB") # Retrieve stored database connection info + if v: + db.init(v) + db.connect() + db.create_tables([Class, Student, Lecture, Submission, Group]) # Ensure tables exist + app_state.update() + except Exception as e: + LOG_ERROR(f"Database Initialization {e}") - # Set Asset Folder - #hello_imgui.set_assets_folder() - - # Set Theme - - # Set Window Params + # Set Window Parameters runner_params = hello_imgui.RunnerParams() runner_params.app_window_params.window_title = "Analyzer" runner_params.imgui_window_params.menu_app_title = "Analyzer" @@ -53,26 +77,19 @@ def main() -> None: runner_params.app_window_params.borderless_resizable = True runner_params.app_window_params.borderless_closable = True - # Load Fonts - #runner_params.callbacks.load_additional_fonts = lambda: f() - - # Status Bar & Main Menu + # Configure UI Elements runner_params.imgui_window_params.show_menu_bar = True runner_params.imgui_window_params.show_menu_app = False runner_params.imgui_window_params.show_menu_view = False runner_params.imgui_window_params.show_status_bar = True - # Inside `show_menus`, we can call `hello_imgui.show_view_menu` and `hello_imgui.show_app_menu` if desired runner_params.callbacks.show_menus = lambda: menu_bar(runner_params) - # Optional: add items to Hello ImGui default App menu - #runner_params.callbacks.show_app_menu_items = show_app_menu_items runner_params.callbacks.show_status = lambda: status_bar(app_state) - + # Application layout runner_params.imgui_window_params.default_imgui_window_type = ( hello_imgui.DefaultImGuiWindowType.provide_full_screen_dock_space ) - runner_params.imgui_window_params.enable_viewports = True - + runner_params.imgui_window_params.enable_viewports = True runner_params.docking_params = analyzer_layout(app_state) runner_params.alternative_docking_layouts = [ database_editor_layout(app_state) @@ -81,16 +98,15 @@ def main() -> None: # Save App Settings runner_params.ini_folder_type = hello_imgui.IniFolderType.app_user_config_folder runner_params.ini_filename = "Analyzer/Analyzer.ini" - - # Uncomment if layout will stay the same at start runner_params.docking_params.layout_condition = hello_imgui.DockingLayoutCondition.application_start - # Run it + # Run the Application add_ons_params = immapp.AddOnsParams() add_ons_params.with_markdown = True add_ons_params.with_implot = True add_ons_params.with_implot3d = True + immapp.run(runner_params, add_ons_params) - + if __name__ == "__main__": main() diff --git a/model.py b/model.py index 6036c92..0177369 100644 --- a/model.py +++ b/model.py @@ -15,11 +15,18 @@ class Class(BaseModel): name = CharField() created_at = DateTimeField(default=datetime.now) +class Group(BaseModel): + name = CharField() + project = CharField() + class_id = ForeignKeyField(Class, backref='class') + created_at = DateTimeField(default=datetime.now) + class Student(BaseModel): prename = CharField() surname = CharField() sex = CharField() class_id = ForeignKeyField(Class, backref='class') + group_id = ForeignKeyField(Group, backref='group') created_at = DateTimeField(default=datetime.now) class Lecture(BaseModel): @@ -34,6 +41,7 @@ class Submission(BaseModel): points = FloatField() created_at = DateTimeField(default=datetime.now) + def load_from_json(fp: Path) -> None: ''' Rebuilding Database from a given json