346 lines
12 KiB
Python
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
|
|
|