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 cd99f60..b86dd92 100644 Binary files a/assets/WiSe_24_25.db and b/assets/WiSe_24_25.db differ 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