grapher/database.py
2025-01-31 14:34:57 +01:00

346 lines
12 KiB
Python

"""
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 *
# 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:
"""
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) # 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:.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()
imgui.table_next_column()
imgui.text(k)
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):
"""
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 # 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}"
)
# 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() # 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') + '.db' # Convert JSON filename to SQLite filename
db.init(file)
db.connect(reuse_if_open=True)
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, 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
@immapp.static(inited=False)
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.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.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
imgui.table_setup_column("Students")
for header in statics.table_header:
imgui.table_setup_column(header)
imgui.table_headers_row()
# Fill Student names
for row in range(statics.rows):
imgui.table_next_row()
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:
"""
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.selected = 0
statics.value = statics.classes[statics.selected].name if statics.classes else str()
statics.inited = True
# Fetch available classes from the database
# Render the UI for class selection
imgui_md.render("# Edit Classes")
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]:
"""
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
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
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"
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)
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