grapher/database.py

346 lines
12 KiB
Python
Raw Normal View History

2025-01-31 14:34:57 +01:00
"""
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.
"""
2025-01-24 12:33:03 +01:00
# Custom
from model import *
from appstate import *
# External
from imgui_bundle import (
imgui,
imgui_ctx,
immapp,
imgui_md,
im_file_dialog,
hello_imgui
)
# Built In
from typing import List
import shelve
from pathlib import Path
from datetime import datetime
def file_info(path: Path) -> None:
2025-01-31 14:34:57 +01:00
"""
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
2025-01-24 12:33:03 +01:00
stat = path.stat()
2025-01-31 14:34:57 +01:00
modified = datetime.fromtimestamp(stat.st_atime) # Last access time
created = datetime.fromtimestamp(stat.st_ctime) # Creation time
format = '%c' # Standard date-time format
2025-01-24 12:33:03 +01:00
2025-01-31 14:34:57 +01:00
# Prepare file data dictionary
2025-01-24 12:33:03 +01:00
data = {
"File": path.name,
2025-01-31 14:34:57 +01:00
"Size": f"{stat.st_size/100:.2f} KB", # Convert bytes to KB (incorrect divisor, should be 1024)
2025-01-24 12:33:03 +01:00
"Modified": modified.strftime(format),
"Created": created.strftime(format)
}
2025-01-31 14:34:57 +01:00
# Create ImGui table to display file information
2025-01-24 12:33:03 +01:00
if imgui.begin_table("File Info", 2):
imgui.table_setup_column(" ", 0)
imgui.table_setup_column(" ")
2025-01-31 14:34:57 +01:00
# Iterate over file data and populate table
2025-01-24 12:33:03 +01:00
for k, v in data.items():
imgui.push_id(k)
imgui.table_next_row()
imgui.table_next_column()
imgui.text(k)
imgui.table_next_column()
imgui.text(v)
imgui.pop_id()
2025-01-31 14:34:57 +01:00
2025-01-24 12:33:03 +01:00
imgui.end_table()
@immapp.static(inited=False, res=False)
def select_file(app_state: AppState):
2025-01-31 14:34:57 +01:00
"""
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
2025-01-24 12:33:03 +01:00
if not statics.inited:
2025-01-31 14:34:57 +01:00
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
2025-01-24 12:33:03 +01:00
with shelve.open("state") as state:
statics.current = Path(state["DB"])
statics.inited = True
2025-01-31 14:34:57 +01:00
# Render UI title and display file information
2025-01-24 12:33:03 +01:00
imgui_md.render("# Database Manager")
file_info(statics.current)
2025-01-31 14:34:57 +01:00
# Button to open the file selection dialog
2025-01-24 12:33:03 +01:00
if imgui.button("Open File"):
2025-01-31 14:34:57 +01:00
im_file_dialog.FileDialog.instance().open(
"SelectDatabase", "Open Database", "Database File (*.json;*.db){.json,.db}"
)
# Handle the file dialog result
2025-01-24 12:33:03 +01:00
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()
2025-01-31 14:34:57 +01:00
# Process the selected file if available
2025-01-24 12:33:03 +01:00
if statics.res:
filename = statics.res.filename()
info = Path(statics.res.path())
2025-01-31 14:34:57 +01:00
imgui.separator() # UI separator for clarity
file_info(info) # Display information about the selected file
2025-01-24 12:33:03 +01:00
file = None
2025-01-31 14:34:57 +01:00
# Load the selected database file
2025-01-24 12:33:03 +01:00
if imgui.button("Load"):
2025-01-31 14:34:57 +01:00
# Ensure any currently open database is closed before loading a new one
2025-01-24 12:33:03 +01:00
if not db.is_closed():
db.close()
2025-01-31 14:34:57 +01:00
# Handle JSON files by converting them to SQLite databases
2025-01-24 12:33:03 +01:00
if statics.res.extension() == '.json':
2025-01-31 14:34:57 +01:00
file = filename.removesuffix('.json') + '.db' # Convert JSON filename to SQLite filename
2025-01-24 12:33:03 +01:00
db.init(file)
db.connect(reuse_if_open=True)
2025-01-31 14:34:57 +01:00
db.create_tables([Class, Student, Lecture, Submission, Group])
load_from_json(str(info)) # Convert and load JSON data into the database
2025-01-24 12:33:03 +01:00
LOG_INFO(f"Successfully created {file}")
2025-01-31 14:34:57 +01:00
# Handle SQLite database files directly
2025-01-24 12:33:03 +01:00
if statics.res.extension() == '.db':
file = str(statics.res.path())
db.init(file)
db.connect(reuse_if_open=True)
2025-01-31 14:34:57 +01:00
db.create_tables([Class, Student, Lecture, Submission, Group])
2025-01-24 12:33:03 +01:00
LOG_INFO(f"Successfully loaded {filename}")
2025-01-31 14:34:57 +01:00
# Save the selected database path to persistent storage
2025-01-24 12:33:03 +01:00
with shelve.open("state") as state:
state["DB"] = file
2025-01-31 14:34:57 +01:00
# Update application state and reset selection result
2025-01-24 12:33:03 +01:00
app_state.update()
statics.res = None
@immapp.static(inited=False)
def table(app_state: AppState) -> None:
statics = table
if not statics.inited:
2025-01-31 14:34:57 +01:00
statics.table_flags = (
imgui.TableFlags_.row_bg.value
| imgui.TableFlags_.borders.value
| imgui.TableFlags_.resizable.value
| imgui.TableFlags_.sizing_stretch_same.value
)
2025-01-24 12:33:03 +01:00
statics.class_id = None
statics.inited = True
2025-01-31 14:34:57 +01:00
2025-01-24 12:33:03 +01:00
if statics.class_id != app_state.current_class_id:
statics.class_id = app_state.current_class_id
2025-01-31 14:34:57 +01:00
statics.students = Student.select().where(Student.class_id == statics.class_id)
2025-01-24 12:33:03 +01:00
statics.lectures = Lecture.select().where(Lecture.class_id == statics.class_id)
2025-01-31 14:34:57 +01:00
statics.rows = len(statics.students)
statics.cols = len(statics.lectures)
statics.grid = list()
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
2025-01-24 12:33:03 +01:00
imgui.table_setup_column("Students")
2025-01-31 14:34:57 +01:00
for header in statics.table_header:
imgui.table_setup_column(header)
2025-01-24 12:33:03 +01:00
imgui.table_headers_row()
2025-01-31 14:34:57 +01:00
# Fill Student names
for row in range(statics.rows):
2025-01-24 12:33:03 +01:00
imgui.table_next_row()
2025-01-31 14:34:57 +01:00
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}")
2025-01-24 12:33:03 +01:00
imgui.end_table()
@immapp.static(inited=False)
def class_editor() -> None:
2025-01-31 14:34:57 +01:00
"""
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()
2025-01-24 12:33:03 +01:00
if not statics.inited:
statics.selected = 0
2025-01-31 14:34:57 +01:00
statics.value = statics.classes[statics.selected].name if statics.classes else str()
2025-01-24 12:33:03 +01:00
statics.inited = True
2025-01-31 14:34:57 +01:00
# Fetch available classes from the database
2025-01-24 12:33:03 +01:00
2025-01-31 14:34:57 +01:00
# Render the UI for class selection
2025-01-24 12:33:03 +01:00
imgui_md.render("# Edit Classes")
2025-01-31 14:34:57 +01:00
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}")
2025-01-24 12:33:03 +01:00
def database_editor(app_state: AppState) -> None:
2025-01-31 14:34:57 +01:00
"""
Database Editor UI Function.
Calls the class editor function to render its UI component.
:param app_state: The application state containing relevant database information.
"""
2025-01-24 12:33:03 +01:00
class_editor()
2025-01-31 14:34:57 +01:00
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.
"""
2025-01-24 12:33:03 +01:00
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
split_main_command2 = hello_imgui.DockingSplit()
split_main_command2.initial_dock = "CommandSpace"
split_main_command2.new_dock = "CommandSpace2"
split_main_command2.direction = imgui.Dir.right
split_main_command2.ratio = 0.3
split_main_misc = hello_imgui.DockingSplit()
split_main_misc.initial_dock = "MainDockSpace"
split_main_misc.new_dock = "MiscSpace"
split_main_misc.direction = imgui.Dir.left
split_main_misc.ratio = 0.2
2025-01-31 14:34:57 +01:00
return [split_main_misc, split_main_command, split_main_command2]
2025-01-24 12:33:03 +01:00
def set_database_editor_layout(app_state: AppState) -> List[hello_imgui.DockableWindow]:
2025-01-31 14:34:57 +01:00
"""
Defines the dockable windows for the database editor.
2025-01-24 12:33:03 +01:00
2025-01-31 14:34:57 +01:00
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.
"""
2025-01-24 12:33:03 +01:00
file_dialog = hello_imgui.DockableWindow()
file_dialog.label = "Database"
file_dialog.dock_space_name = "MiscSpace"
file_dialog.gui_function = lambda: select_file(app_state)
log = hello_imgui.DockableWindow()
log.label = "Logs"
log.dock_space_name = "CommandSpace2"
log.gui_function = hello_imgui.log_gui
table_view = hello_imgui.DockableWindow()
table_view.label = "Table"
table_view.dock_space_name = "MainDockSpace"
table_view.gui_function = lambda: table(app_state)
editor = hello_imgui.DockableWindow()
editor.label = "Editor"
editor.dock_space_name = "CommandSpace"
editor.gui_function = lambda: database_editor(app_state)
2025-01-31 14:34:57 +01:00
return [file_dialog, log, table_view, editor]
2025-01-24 12:33:03 +01:00
def database_editor_layout(app_state: AppState) -> hello_imgui.DockingParams:
2025-01-31 14:34:57 +01:00
"""
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.
"""
2025-01-24 12:33:03 +01:00
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
2025-01-31 14:34:57 +01:00