From 033a1fa94fb05ebad2c2e8821ed701d61b7fea37 Mon Sep 17 00:00:00 2001
From: DerGrumpf
Date: Fri, 14 Feb 2025 13:34:31 +0100
Subject: [PATCH] Changed: Structure Overhal
---
Makefile | 4 +
assets/Student_list.csv | 71 +++----
assets/WiSe_24_25.db | Bin 65536 -> 90112 bytes
assets/convert.py | 10 +-
grader/tests/base_grader.py | 76 -------
grapher/dbmodel/__init__.py | 15 ++
grapher/dbmodel/model.dbml | 61 ++++++
grapher/dbmodel/model.py | 117 +++++++++++
grapher/dbmodel/utils.py | 172 ++++++++++++++++
grapher/dbmodel/view.py | 122 +++++++++++
grapher/grader/__init__.py | 8 +
{grader => grapher/grader}/valuation.py | 6 +-
grapher/gui/__init__.py | 4 +
grapher/gui/analyzer/__init__.py | 63 ++++++
.../gui/analyzer/analyzer.py | 64 +-----
appstate.py => grapher/gui/appstate.py | 13 +-
grapher/gui/database/__init__.py | 80 ++++++++
.../gui/database/database.py | 193 ++++++++++--------
grapher/gui/database/editor.py | 142 +++++++++++++
grapher/gui/gui.py | 30 +++
grapher/gui/logger.py | 24 +++
grapher/gui/state | Bin 0 -> 16384 bytes
gui.py => grapher/main.py | 71 ++-----
grapher/state | Bin 0 -> 16384 bytes
main.py | 21 --
model.py | 176 ----------------
pyproject.toml | 44 ++++
27 files changed, 1059 insertions(+), 528 deletions(-)
create mode 100644 Makefile
delete mode 100644 grader/tests/base_grader.py
create mode 100644 grapher/dbmodel/__init__.py
create mode 100644 grapher/dbmodel/model.dbml
create mode 100644 grapher/dbmodel/model.py
create mode 100644 grapher/dbmodel/utils.py
create mode 100644 grapher/dbmodel/view.py
create mode 100644 grapher/grader/__init__.py
rename {grader => grapher/grader}/valuation.py (97%)
create mode 100644 grapher/gui/__init__.py
create mode 100644 grapher/gui/analyzer/__init__.py
rename analyzer.py => grapher/gui/analyzer/analyzer.py (83%)
rename appstate.py => grapher/gui/appstate.py (70%)
create mode 100644 grapher/gui/database/__init__.py
rename database.py => grapher/gui/database/database.py (71%)
create mode 100644 grapher/gui/database/editor.py
create mode 100644 grapher/gui/gui.py
create mode 100644 grapher/gui/logger.py
create mode 100644 grapher/gui/state
rename gui.py => grapher/main.py (52%)
create mode 100644 grapher/state
delete mode 100644 main.py
delete mode 100644 model.py
create mode 100644 pyproject.toml
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..e9baacd
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,4 @@
+init:
+ pip install -r requirements.txt
+
+.PHONY: init
diff --git a/assets/Student_list.csv b/assets/Student_list.csv
index 5271b0d..53a3298 100644
--- a/assets/Student_list.csv
+++ b/assets/Student_list.csv
@@ -1,38 +1,39 @@
First Name,Last Name,Sex,Group,Grader,Tutorial 1,Tutorial 2,Extended Applications,Numpy & MatPlotLib,SciPy,Monte Carlo,Pandas & Seaborn,Folium,Statistical Test Methods,Data Analysis
-Abdalaziz,Abunjaila,Male,DiKum,30 Percent,30.5,15,18,28,17,17,17,22,0,18
-Marleen,Adolphi,Female,MeWi6,30 Percent,29.5,15,18,32,19,20,17,24,23,0
-Sarina,Apel,Female,MeWi1,30 Percent,28.5,15,18,32,20,20,21,24,20,23
-Skofiare,Berisha,Female,DiKum,30 Percent,29.5,13,18,34,20,17,20,26,16,0
-Aurela,Brahimi,Female,MeWi2,30 Percent,17.5,15,15.5,26,16,17,19,16,0,0
-Cam Thu,Do,Female,MeWi3,30 Percent,31,15,18,34,19,20,21.5,22,12,0
-Nova,Eib,Female,MeWi4,30 Percent,31,15,15,34,20,20,21,27,19,21
-Lena,Fricke,Female,MeWi4,30 Percent,0,0,0,0,0,0,0,0,0,0
-Nele,Grundke,Female,MeWi6,30 Percent,23.5,13,16,28,20,17,21,18,22,11
-Anna,Grünewald,Female,MeWi3,30 Percent,12,14,16,29,16,15,19,9,0,0
-Yannik,Haupt,Male,NoGroup,30 Percent,18,6,14,21,13,2,9,0,0,0
-Janna,Heiny,Female,MeWi1,30 Percent,30,15,18,33,18,20,22,25,24,30
-Milena,Krieger,Female,MeWi1,30 Percent,30,15,18,33,20,20,21.5,26,20,22
-Julia,Limbach,Female,MeWi6,30 Percent,27.5,12,18,29,11,19,17.5,26,24,28
-Viktoria,Litza,Female,MeWi5,30 Percent,21.5,15,18,27,13,20,22,21,21,30
-Leonie,Manthey,Female,MeWi1,30 Percent,28.5,14,18,29,20,10,18,23,16,28
-Izabel,Mike,Female,MeWi2,30 Percent,29.5,15,15,35,11,15,19,21,21,27
-Lea,Noglik,Female,MeWi5,30 Percent,22.5,15,17,34,13,10,20,21,19,6
-Donika,Nuhiu,Female,MeWi5,30 Percent,31,13.5,18,35,14,10,17,18,19,8
-Julia,Renner,Female,MeWi4,30 Percent,27.5,10,14,32,20,17,11,20,24,14
-Fabian,Rothberger,Male,MeWi3,30 Percent,30.5,15,18,34,17,17,19,22,18,30
-Natascha,Rott,Female,MeWi1,30 Percent,29.5,12,18,32,19,20,21,26,23,26
-Isabel,Rudolf,Female,MeWi4,30 Percent,27.5,9,17,34,16,19,19,21,16,14
-Melina,Sablotny,Female,MeWi6,30 Percent,31,15,18,33,20,20,21,19,11,28
-Alea,Schleier,Female,DiKum,30 Percent,27,14,18,34,16,18,21.5,22,15,22
-Flemming,Schur,Male,MeWi3,30 Percent,29.5,15,17,34,19,20,19,22,18,27
-Marie,Seeger,Female,DiKum,30 Percent,27.5,15,18,32,14,9,17,22,9,25
-Lucy,Thiele,Female,MeWi6,30 Percent,27.5,15,18,27,20,17,19,18,22,25
-Lara,Troschke,Female,MeWi2,30 Percent,28.5,14,17,28,13,19,21,25,12,24
-Inga-Brit,Turschner,Female,MeWi2,30 Percent,25.5,14,18,34,20,16,19,22,17,30
-Alea,Unger,Female,MeWi5,30 Percent,30,12,18,31,20,20,21,22,15,21.5
-Marie,Wallbaum,Female,MeWi5,30 Percent,28.5,14,18,34,17,20,19,24,12,22
-Katharina,Walz,Female,MeWi4,30 Percent,31,15,18,31,19,19,17,24,17,14.5
-Xiaowei,Wang,Male,NoGroup,30 Percent,30.5,14,18,26,19,17,0,0,0,0
-Lilly-Lu,Warnken,Female,DiKum,30 Percent,30,15,18,30,14,17,19,14,16,24
+Abdalaziz,Abunjaila,Male,DiKum,30%,30.5,15,18,28,17,17,17,22,0,18
+Marleen,Adolphi,Female,MeWi6,30%,29.5,15,18,32,19,20,17,24,23,0
+Sarina,Apel,Female,MeWi1,30%,28.5,15,18,32,20,20,21,24,20,23
+Skofiare,Berisha,Female,DiKum,30%,29.5,13,18,34,20,17,20,26,16,0
+Aurela,Brahimi,Female,MeWi2,30%,17.5,15,15.5,26,16,17,19,16,0,0
+Cam Thu,Do,Female,MeWi3,30%,31,15,18,34,19,20,21.5,22,12,0
+Nova,Eib,Female,MeWi4,30%,31,15,15,34,20,20,21,27,19,21
+Lena,Fricke,Female,MeWi4,30%,0,0,0,0,0,0,0,0,0,0
+Nele,Grundke,Female,MeWi6,30%,23.5,13,16,28,20,17,21,18,22,11
+Anna,Grünewald,Female,MeWi3,30%,12,14,16,29,16,15,19,9,0,0
+Yannik,Haupt,Male,NoGroup,30%,18,6,14,21,13,2,9,0,0,0
+Janna,Heiny,Female,MeWi1,30%,30,15,18,33,18,20,22,25,24,30
+Milena,Krieger,Female,MeWi1,30%,30,15,18,33,20,20,21.5,26,20,22
+Julia,Limbach,Female,MeWi6,30%,27.5,12,18,29,11,19,17.5,26,24,28
+Viktoria,Litza,Female,MeWi5,30%,21.5,15,18,27,13,20,22,21,21,30
+Leonie,Manthey,Female,MeWi1,30%,28.5,14,18,29,20,10,18,23,16,28
+Izabel,Mike,Female,MeWi2,30%,29.5,15,15,35,11,15,19,21,21,27
+Lea,Noglik,Female,MeWi5,30%,22.5,15,17,34,13,10,20,21,19,6
+Donika,Nuhiu,Female,MeWi5,30%,31,13.5,18,35,14,10,17,18,19,8
+Julia,Renner,Female,MeWi4,30%,27.5,10,14,32,20,17,11,20,24,14
+Fabian,Rothberger,Male,MeWi3,30%,30.5,15,18,34,17,17,19,22,18,30
+Natascha,Rott,Female,MeWi1,30%,29.5,12,18,32,19,20,21,26,23,26
+Isabel,Rudolf,Female,MeWi4,30%,27.5,9,17,34,16,19,19,21,16,14
+Melina,Sablotny,Female,MeWi6,30%,31,15,18,33,20,20,21,19,11,28
+Alea,Schleier,Female,DiKum,30%,27,14,18,34,16,18,21.5,22,15,22
+Flemming,Schur,Male,MeWi3,30%,29.5,15,17,34,19,20,19,22,18,27
+Marie,Seeger,Female,DiKum,30%,27.5,15,18,32,14,9,17,22,9,25
+Lucy,Thiele,Female,MeWi6,30%,27.5,15,18,27,20,17,19,18,22,25
+Lara,Troschke,Female,MeWi2,30%,28.5,14,17,28,13,19,21,25,12,24
+Inga-Brit,Turschner,Female,MeWi2,30%,25.5,14,18,34,20,16,19,22,17,30
+Alea,Unger,Female,MeWi5,30%,30,12,18,31,20,20,21,22,15,21.5
+Marie,Wallbaum,Female,MeWi5,30%,28.5,14,18,34,17,20,19,24,12,22
+Katharina,Walz,Female,MeWi4,30%,31,15,18,31,19,19,17,24,17,14.5
+Xiaowei,Wang,Male,NoGroup,30%,30.5,14,18,26,19,17,0,0,0,0
+Lilly-Lu,Warnken,Female,DiKum,30%,30,15,18,30,14,17,19,14,16,24
+,,,,,,,,,,,,,,
,,,,,,,,,,,,,,
diff --git a/assets/WiSe_24_25.db b/assets/WiSe_24_25.db
index 8e2e87587de8f5e50c15903ef23e6a37545dcda0..8b678017b104baa7322fa30f84fdfc07c36939a9 100644
GIT binary patch
literal 90112
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
literal 65536
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
MEKdU#J^U5_AN!y-+5i9m
diff --git a/assets/convert.py b/assets/convert.py
index 7c31ae1..c32af74 100644
--- a/assets/convert.py
+++ b/assets/convert.py
@@ -1,8 +1,9 @@
import pandas as pd
import pprint
import sys
-sys.path.append('..')
+sys.path.append('../grapher/dbmodel')
from model import *
+from utils import *
df = pd.read_csv("Student_list.csv")
df = df.dropna()
@@ -31,10 +32,8 @@ groups = {
}
print(df)
+init_db('WiSe_24_25.db')
-db.init("WiSe_24_25.db")
-db.connect()
-db.create_tables([Class, Student, Lecture, Submission, Group])
# Create Class
clas = Class.create(name='WiSe 24/25')
@@ -55,7 +54,8 @@ for index, row in df.iterrows():
sex=row["Sex"],
class_id=clas.id,
group_id=Group.select().where(Group.name == row["Group"]),
- grader=row["Grader"]
+ grader=row["Grader"],
+ residence_id=-1
)
for title, points in list(row.to_dict().items())[5:]:
diff --git a/grader/tests/base_grader.py b/grader/tests/base_grader.py
deleted file mode 100644
index 94e8d85..0000000
--- a/grader/tests/base_grader.py
+++ /dev/null
@@ -1,76 +0,0 @@
-import sys
-sys.path.append("..")
-
-from valuation import BaseGrading
-
-# Testing
-import unittest
-from unittest.mock import patch
-class TestBaseGrading(unittest.TestCase):
- test_schema = {"Grade1": 0.1, "Grade2": 0.3}
-
- @patch.multiple(BaseGrading, __abstractmethods__=set())
- def get_base_grader(self):
- return BaseGrading(self.test_schema, "TestGrader")
-
- def test_getter(self):
- grader = self.get_base_grader()
- self.assertEqual(grader.get("Grade1"), self.test_schema["Grade1"])
- self.assertEqual(grader.get("grade1"), self.test_schema["Grade1"])
-
- def test_len(self):
- grader = self.get_base_grader()
- self.assertEqual(len(grader), len(self.test_schema))
-
- def test_contains(self):
- grader = self.get_base_grader()
- self.assertTrue(0.1 in grader)
- self.assertTrue(0.9 in grader)
- self.assertFalse(100 in grader)
- self.assertFalse(None in grader)
- self.assertTrue("Grade1" in grader)
- self.assertTrue("gRADE2" in grader)
-
- def test_iter(self):
- grader = self.get_base_grader()
- for grade, test in zip(grader, self.test_schema):
- self.assertEqual(grade, test)
-
- def test_reversed(self):
- grader = self.get_base_grader()
- for grade, test in zip(reversed(grader), reversed(self.test_schema)):
- self.assertEqual(grade, test)
-
- def test_str(self):
- grader = self.get_base_grader()
- self.assertEqual(str(grader), "TestGrader")
-
- def test_repr(self):
- grader = self.get_base_grader()
- self.assertEqual(repr(grader), f"")
-
- def test_eq(self):
- grader = self.get_base_grader()
- self.assertTrue(grader == grader)
- self.assertTrue(grader != grader)
-
- def test_keys(self):
- grader = self.get_base_grader()
- for k1, t1 in zip(grader.keys(), self.test_schema.keys()):
- self.assertEqual(k1, t1)
-
- def test_items(self):
- grader = self.get_base_grader()
- for v1, t1 in zip(grader.values(), self.test_schema.values()):
- self.assertEqual(v1, t1)
-
- def test_items(self):
- grader = self.get_base_grader()
- for g1, t1 in zip(grader.items(), self.test_schema.items()):
- k, v = g1
- tk, tv = t1
- self.assertEqual(k, tk)
- self.assertEqual(v, tv)
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/grapher/dbmodel/__init__.py b/grapher/dbmodel/__init__.py
new file mode 100644
index 0000000..b7ff6ee
--- /dev/null
+++ b/grapher/dbmodel/__init__.py
@@ -0,0 +1,15 @@
+from .utils import (
+ tables,
+ table_labels,
+ init_db,
+ save_as_json,
+ create_from_json
+)
+
+from .model import db
+from .view import Cache
+
+# Export tables & corresponding View
+for table in tables:
+ globals()[table.__name__] = table
+ globals()[f"{table.__name__}Cache"] = Cache(table)
diff --git a/grapher/dbmodel/model.dbml b/grapher/dbmodel/model.dbml
new file mode 100644
index 0000000..f8a12d6
--- /dev/null
+++ b/grapher/dbmodel/model.dbml
@@ -0,0 +1,61 @@
+Table Class {
+ id integer [primary key]
+ name varchar
+ created_at timestamp
+}
+
+Table Student {
+ id integer [primary key]
+ name varchar
+ sex varchar
+ class_id integer [ref: < Class.id]
+ group_id integer [ref: < Group.id]
+ grader varchar
+ residence integer [ref: - Residence.id]
+ created_at timestamp
+}
+
+Table Lecture {
+ id integer [primary key]
+ title varchar
+ points integer
+ class_id integer [ref: < Class.id]
+ created_at timestamp
+}
+
+Table Submission {
+ id integer [primary key]
+ student_id integer [ref: < Student.id]
+ lecture_id integer [ref: < Lecture.id]
+ points float
+ created_at timestamp
+}
+
+Table Group {
+ id integer [primary key]
+ name varchar
+ project varchar
+ class_id integer [ref: < Class.id]
+ created_at timestamp
+}
+
+Table Town {
+ plz integer [primary key]
+ name varchar
+ created_at timestamp
+}
+
+Table Address {
+ id integer [primary key]
+ street varchar
+ number integer
+ created_at timestamp
+}
+
+Table Residence {
+ id integer [primary key]
+ town_plz integer [ref: > Town.plz]
+ address_id integer [ref: > Address.id]
+ created_at timestamp
+}
+
diff --git a/grapher/dbmodel/model.py b/grapher/dbmodel/model.py
new file mode 100644
index 0000000..a40a902
--- /dev/null
+++ b/grapher/dbmodel/model.py
@@ -0,0 +1,117 @@
+'''
+peewee ORM Database Model definition
+Documentation: https://docs.peewee-orm.com
+
+please look up model.dbml for Documentation
+Online Viewer: https://dbdiagram.io
+
+'''
+
+
+from peewee import *
+from datetime import datetime
+
+import json
+from typing import TextIO
+from pathlib import Path
+
+db = DatabaseProxy()
+
+# WIP: Add Switch Function
+if True:
+ database = SqliteDatabase(None, autoconnect=False)
+else:
+ database = PostgresqlDatabase(None, autoconnect=False)
+
+db.initialize(database)
+
+class BaseModel(Model):
+ '''
+ Base Model (needed for peewee) defines the Class Meta
+ and bounds Global db obj to every Table
+ '''
+ class Meta:
+ database = db
+
+
+class Town(BaseModel):
+ '''
+ Table for Storing Town Data
+ '''
+ plz = IntegerField(primary_key=True)
+ name = CharField()
+ created_at = DateTimeField(default=datetime.now)
+
+
+class Address(BaseModel):
+ '''
+ Table for Storing Address Data
+ '''
+ street = CharField()
+ number = IntegerField()
+ created_at = DateTimeField(default=datetime.now)
+
+
+class Residence(BaseModel):
+ '''
+ Table for Storing a unique Postal Adress
+ by combining Towntable Data with Addresstable Data
+ '''
+ town_plz = ForeignKeyField(Town, backref='plz')
+ address_id = ForeignKeyField(Address, backref='address')
+ created_at = DateTimeField(default=datetime.now)
+
+
+class Class(BaseModel):
+ '''
+ Baseline Order Base
+
+ Table for Storing a Class
+ '''
+ name = CharField()
+ created_at = DateTimeField(default=datetime.now)
+
+
+class Group(BaseModel):
+ '''
+ Table for Storing a project Group
+ '''
+ name = CharField()
+ project = CharField()
+ class_id = ForeignKeyField(Class, backref='class')
+ created_at = DateTimeField(default=datetime.now)
+
+
+class Student(BaseModel):
+ '''
+ Table for Storing a Student and linking him to appropiate Tables
+ '''
+ prename = CharField()
+ surname = CharField()
+ sex = CharField()
+ class_id = ForeignKeyField(Class, backref='class')
+ group_id = ForeignKeyField(Group, backref='group')
+ grader = CharField()
+ residence = ForeignKeyField(Residence, backref='residence', null=True)
+ created_at = DateTimeField(default=datetime.now)
+
+
+class Lecture(BaseModel):
+ '''
+ Table for defining a Lecture
+ '''
+ title = CharField()
+ points = IntegerField()
+ class_id = ForeignKeyField(Class, backref='class')
+ created_at = DateTimeField(default=datetime.now)
+
+
+class Submission(BaseModel):
+ '''
+ Table for defining a Submission from a Student for Lecture
+ '''
+ student_id = ForeignKeyField(Student, backref='student')
+ lecture_id = ForeignKeyField(Lecture, backref='lecture')
+ points = FloatField()
+ created_at = DateTimeField(default=datetime.now)
+
diff --git a/grapher/dbmodel/utils.py b/grapher/dbmodel/utils.py
new file mode 100644
index 0000000..04e9ad9
--- /dev/null
+++ b/grapher/dbmodel/utils.py
@@ -0,0 +1,172 @@
+'''
+Module provids Utilities to Interface with Database Model
+
+Includes:
+ - DateTime De-/Encoder for JSON loads/dumps
+ - Database Initialize Helper
+ - Module Class Summarizer
+'''
+
+
+import sys, inspect, json
+from datetime import datetime, date
+from pathlib import Path
+from playhouse.shortcuts import model_to_dict, dict_to_model
+from .model import *
+
+class DateTimeEncoder(json.JSONEncoder):
+ '''
+ Helper Class converting datetime.datetime -> isoformated String
+ '''
+
+
+ def default(self, obj):
+ if isinstance(obj, (datetime, date)):
+ return obj.isoformat()
+
+# Predefined used timestamp keys
+KEYNAMES = [
+ "date", "timestamp", "last_updatet", "last_created", "last_editet", "created_at"
+]
+
+def DateTimeDecoder(obj: dict) -> dict:
+ '''
+ Helper Function converting isoformated String -> datetime.datetime
+ '''
+
+
+ for key, value in obj.items():
+ if key not in KEYNAMES:
+ continue
+ try:
+ obj[key] = datetime.fromisoformat(value)
+ except ValueError:
+ pass
+ return obj
+
+
+def get_module_cls(module: str) -> tuple[tuple[str, object]]:
+ '''
+ Given a module name function returns a list of Classes defined only in that Module
+ '''
+
+
+ assert type(module) is str, "Provided Module isn't a String"
+ members = inspect.getmembers(sys.modules[module], inspect.isclass)
+ return tuple(member for member in members if member[1].__module__ == module)
+
+# precalculated from model.py
+tables = tuple(table[1] for table in get_module_cls('dbmodel.model') if not table[1] is BaseModel)
+table_labels = tuple(table[0] for table in get_module_cls('dbmodel.model') if not table[1] is BaseModel)
+
+def init_db(name: Path | str) -> None:
+ '''
+ (Creates,) Connects and Initializes the db descriptor from a given *.db file
+ '''
+
+
+ assert isinstance(name, (Path, str)), "Provided Name isn't of type Path | str"
+
+ # convert to String
+ if type(name) is Path:
+ name = str(name)
+
+ # Switch Database; Initialize if needed
+ if not db.is_closed():
+ db.close()
+ db.init(name)
+ db.connect()
+ db.create_tables(tables) # Ensure tables exist
+
+
+def save_as_json(filename: str, path: Path | str = Path('.')) -> Path:
+ '''
+ Saves the current loaded Database as .json to given
+
+ JSON Format:
+ {
+ : [
+ {: , ...}
+ ],
+ ...
+ }
+ '''
+
+
+ assert type(filename) is str, "Provided Filename isn't a String"
+ assert isinstance(path, Path | str), "Provided Path isn't of type Path | str"
+
+ # Convert given path to Path
+ if type(path) is str:
+ path = Path(path)
+
+ filename = Path(filename)
+
+ # Set Correct Suffix
+ if filename.suffix != '.json':
+ filename = filename.with_suffix('.json')
+
+ file = path.resolve().absolute() / filename
+
+ # dump db
+ database = {
+ table.__name__: list(table.select().dicts())
+ for table in tables
+ }
+
+ # db -> json
+ with open(file, "w") as fp:
+ json.dump(database, fp, cls=DateTimeEncoder)
+
+ return file
+
+def create_from_json(dbname: str, file: Path | str) -> Path:
+ '''
+ Creates a new Database .db in from given
+
+ Valid JSON Format:
+ {
+ : [
+ {: , ...}
+ ],
+ ...
+ }
+ '''
+
+ assert type(dbname) is str, "Provided Database name isn't a String"
+ assert isinstance(file, (Path | str)), "Provided file descriptor isn't of type Path | str"
+
+ # Convert file to Path
+ if type(file) is str:
+ file = Path(file)
+ assert file.suffix == '.json', "File isn't a JSON"
+
+ # Set correct Suffix and connect
+ dbname = Path(dbname).resolve().absolute()
+ if dbname.suffix != '.db':
+ dbname = dbname.with_suffix('.db')
+ init_db(dbname)
+
+ # load from json
+ with open(file, "r") as fp:
+ data = json.load(fp, object_hook=DateTimeDecoder)
+
+ assert all([keys == table.__name__ for keys, table in zip(data.keys(), tables)]), f"{file.name} can't be convert to Database"
+
+ # Insert Data
+ for k, v in data.items():
+ if not v:
+ continue
+
+ table = next((table for table in tables if table.__name__ == k))
+ table.insert_many(v).execute()
+
+ return dbname
+
+if __name__ == "__main__":
+ init_db('/home/phil/programming/grapher/assets/WiSe_24_25.db')
+ f = save_as_json("file")
+ print(f)
+ print(create_from_json("test", f))
+
+
diff --git a/grapher/dbmodel/view.py b/grapher/dbmodel/view.py
new file mode 100644
index 0000000..de97b1c
--- /dev/null
+++ b/grapher/dbmodel/view.py
@@ -0,0 +1,122 @@
+'''
+Module providing Database Views
+'''
+
+from collections.abc import Sequence
+from typing import Any
+
+from playhouse.shortcuts import model_to_dict
+
+from .model import *
+from .utils import tables
+
+class Cache(Sequence):
+ '''
+ Sequence Class caching Data from a BaseModel Table
+ '''
+ def __init__(self, table: BaseModel) -> None:
+ assert any(table.__name__ == t.__name__ for t in tables), f"Must provide a BaseModel Object; not {type(table)}"
+
+ self.table = table
+ self.data = None
+
+ def update(self, value: Any, update_param: str = 'id') -> None:
+ '''
+ Updates Cache by selecting data given by self.table. == value
+
+ '''
+ assert update_param in self.table._meta.sorted_field_names, f"Update Parameter must exist on {self.table.__name__}"
+
+ param = getattr(self.table, update_param)
+ data = self.table.select().where(param == value)
+ self.data = list(data) if data else None
+
+ def is_None(self) -> bool:
+ '''
+ Check if Cache is Empty
+ '''
+ return self.data is None
+
+ def __getitem__(self, id: int) -> BaseModel | None:
+ '''
+ Get item from Cache based on provided id
+ '''
+ assert type(id) is int, "ID must be an Integer"
+ for el in self.data:
+ if el.get_id() == id:
+ return el
+ return None
+
+ def __len__(self) -> int:
+ '''
+ returns Number of Elements Stored in Cache
+ '''
+ return len(self.data)
+
+ def __contains__(self, data: BaseModel | int) -> bool:
+ '''
+ Check if provided BaseModel object is stored in Cache
+ '''
+ assert isinstance(data, (self.table, int)), f"Check could only be made if Object of type {self.table.__name__} or integer id"
+
+ if isinstance(data, BaseModel):
+ return data in self.data
+ if isinstance(data, int):
+ for el in self.data:
+ if el.get_id() == data:
+ return True
+ return False
+
+ def __iter__(self) -> BaseModel:
+ '''
+ Generator returning BaseModel Objects stored in Cache
+ Note: Like SQL there is no real Order to the Data
+ '''
+ yield from self.data
+
+ def __reversed__(self) -> BaseModel:
+ '''
+ Same as __iter__ but reversed
+ '''
+ yield from reversed(self.data)
+
+ def index(self, value, start=0, stop=None):
+ '''
+ Not Implemented do to SQL Order Issue
+ '''
+ return NotImplemented
+
+ def count(self, value):
+ '''
+ Not Implemented do to assumed Uniqness of Data
+ '''
+ return NotImplemented
+
+ def to_dict(self, recurse=False) -> list[dict]:
+ '''
+ Returns a list containing the resolved BaseModel data as dict
+
+ If recurse == True Database trys to recurse all Relationships
+ Note: This Operation isn't Cache performed!
+
+ Format:
+ [
+ {: , ...},
+ ...
+ ]
+ '''
+ assert type(recurse) is bool, "recurse must be bool"
+ return [model_to_dict(data, recurse=recurse) for data in self.data]
+
+ def filter(self):
+ '''
+ WIP
+ '''
+ pass
+
+if __name__ == "__main__":
+ from utils import init_db
+ init_db('test.db')
+ from pprint import pprint
+
+ help(TownCache)
diff --git a/grapher/grader/__init__.py b/grapher/grader/__init__.py
new file mode 100644
index 0000000..44fbccd
--- /dev/null
+++ b/grapher/grader/__init__.py
@@ -0,0 +1,8 @@
+from .valuation import (
+ get_gradings,
+ get_grader,
+ Std30PercentRule,
+ Std50PercentRule,
+ StdGermanGradingMiddleSchool,
+ StdGermanGradingHighSchool
+)
diff --git a/grader/valuation.py b/grapher/grader/valuation.py
similarity index 97%
rename from grader/valuation.py
rename to grapher/grader/valuation.py
index ef42f74..d02c55f 100644
--- a/grader/valuation.py
+++ b/grapher/grader/valuation.py
@@ -158,12 +158,12 @@ class StdGermanGrading(BaseGrading):
Std30PercentRule = StdPercentRule({
"pAssed": 0.3,
"Failed": 0.0
-}, "Std30PercentRule", "30 Percent")
+}, "Std30PercentRule", "30%")
Std50PercentRule = StdPercentRule({
"Passed": 0.5,
"Failed": 0.0
-}, "Std50PercentRule", "50 Percent")
+}, "Std50PercentRule", "50%")
StdGermanGradingMiddleSchool = StdGermanGrading({
1: 0.96,
@@ -172,7 +172,7 @@ StdGermanGradingMiddleSchool = StdGermanGrading({
4: 0.45,
5: 0.16,
6: 0.00
-}, "StdGermanGradingMiddleSchool", "Secondary School")
+}, "StdGermanGradingMiddleSchool", "Mittelstufe")
StdGermanGradingHighSchool = StdGermanGrading({
15: 0.95,
diff --git a/grapher/gui/__init__.py b/grapher/gui/__init__.py
new file mode 100644
index 0000000..1dba19a
--- /dev/null
+++ b/grapher/gui/__init__.py
@@ -0,0 +1,4 @@
+from .appstate import AppState
+from .analyzer import analyzer_layout
+from .database import database_editor_layout
+from .gui import menu_bar, status_bar
diff --git a/grapher/gui/analyzer/__init__.py b/grapher/gui/analyzer/__init__.py
new file mode 100644
index 0000000..c68f99e
--- /dev/null
+++ b/grapher/gui/analyzer/__init__.py
@@ -0,0 +1,63 @@
+from imgui_bundle import hello_imgui, imgui
+#from app_state import AppState
+from typing import List
+
+from .analyzer import *
+
+def analyzer_docking_splits() -> List[hello_imgui.DockingSplit]:
+ split_main_misc = hello_imgui.DockingSplit()
+ split_main_misc.initial_dock = "MainDockSpace"
+ split_main_misc.new_dock = "MiscSpace"
+ split_main_misc.direction = imgui.Dir.down
+ split_main_misc.ratio = 0.25
+
+ # Then, add a space to the left which occupies a column whose width is 25% of the app width
+ split_main_command = hello_imgui.DockingSplit()
+ split_main_command.initial_dock = "MainDockSpace"
+ split_main_command.new_dock = "CommandSpace"
+ split_main_command.direction = imgui.Dir.left
+ split_main_command.ratio = 0.2
+
+ # Then, add CommandSpace2 below MainDockSpace
+ split_main_command2 = hello_imgui.DockingSplit()
+ split_main_command2.initial_dock = "MainDockSpace"
+ split_main_command2.new_dock = "CommandSpace2"
+ split_main_command2.direction = imgui.Dir.down
+ split_main_command2.ratio = 0.25
+
+ splits = [split_main_misc, split_main_command, split_main_command2]
+ return splits
+
+def set_analyzer_layout(app_state) -> List[hello_imgui.DockableWindow]:
+ student_selector = hello_imgui.DockableWindow()
+ student_selector.label = "Students"
+ student_selector.dock_space_name = "CommandSpace"
+ student_selector.gui_function = lambda: student_list(app_state)
+
+ student_info = hello_imgui.DockableWindow()
+ student_info.label = "Student Analyzer"
+ student_info.dock_space_name = "MainDockSpace"
+ student_info.gui_function = lambda: student_graph(app_state)
+
+ sex_info = hello_imgui.DockableWindow()
+ sex_info.label = "Analyze by Gender"
+ sex_info.dock_space_name = "MainDockSpace"
+ sex_info.gui_function = lambda: sex_graph(app_state)
+
+ student_ranking = hello_imgui.DockableWindow()
+ student_ranking.label = "Ranking"
+ student_ranking.dock_space_name = "MainDockSpace"
+ student_ranking.gui_function = lambda: ranking(app_state)
+
+ return [
+ student_selector,
+ student_info, sex_info,
+ student_ranking,
+ ]
+
+def analyzer_layout(app_state) -> hello_imgui.DockingParams:
+ docking_params = hello_imgui.DockingParams()
+ docking_params.layout_name = "Analyzer"
+ docking_params.docking_splits = analyzer_docking_splits()
+ docking_params.dockable_windows = set_analyzer_layout(app_state)
+ return docking_params
diff --git a/analyzer.py b/grapher/gui/analyzer/analyzer.py
similarity index 83%
rename from analyzer.py
rename to grapher/gui/analyzer/analyzer.py
index badbdc8..15f6a3e 100644
--- a/analyzer.py
+++ b/grapher/gui/analyzer/analyzer.py
@@ -1,6 +1,6 @@
# Custom
-from model import *
-from appstate import AppState
+from dbmodel import *
+from gui import AppState
from grader.valuation import *
# External
@@ -140,8 +140,8 @@ def student_graph(app_state: AppState) -> None:
statics.points = np.sum(statics.sub_points)
if statics.points.is_integer():
statics.points = int(statics.points)
- statics.grader = get_grader("Oberstufe")
- #statics.grader = get_grader(statics.student.grader)
+ #statics.grader = get_grader("Oberstufe")
+ statics.grader = get_grader(statics.student.grader)
statics.subs_data = np.array([p/mp.points for p, mp in zip(statics.sub_points, statics.lectures)], dtype=np.float32)*100
statics.subs_labels = [f"{l.title} {int(points) if points.is_integer() else points}/{l.points}" for l, points in zip(statics.lectures, statics.sub_points)]
@@ -316,60 +316,4 @@ def ranking(app_state: AppState) -> None:
if imgui.button("Change"):
statics.state = not statics.state
-def analyzer_docking_splits() -> List[hello_imgui.DockingSplit]:
- split_main_misc = hello_imgui.DockingSplit()
- split_main_misc.initial_dock = "MainDockSpace"
- split_main_misc.new_dock = "MiscSpace"
- split_main_misc.direction = imgui.Dir.down
- split_main_misc.ratio = 0.25
- # Then, add a space to the left which occupies a column whose width is 25% of the app width
- split_main_command = hello_imgui.DockingSplit()
- split_main_command.initial_dock = "MainDockSpace"
- split_main_command.new_dock = "CommandSpace"
- split_main_command.direction = imgui.Dir.left
- split_main_command.ratio = 0.2
-
- # Then, add CommandSpace2 below MainDockSpace
- split_main_command2 = hello_imgui.DockingSplit()
- split_main_command2.initial_dock = "MainDockSpace"
- split_main_command2.new_dock = "CommandSpace2"
- split_main_command2.direction = imgui.Dir.down
- split_main_command2.ratio = 0.25
-
- splits = [split_main_misc, split_main_command, split_main_command2]
- return splits
-
-def set_analyzer_layout(app_state: AppState) -> List[hello_imgui.DockableWindow]:
- student_selector = hello_imgui.DockableWindow()
- student_selector.label = "Students"
- student_selector.dock_space_name = "CommandSpace"
- student_selector.gui_function = lambda: student_list(app_state)
-
- student_info = hello_imgui.DockableWindow()
- student_info.label = "Student Analyzer"
- student_info.dock_space_name = "MainDockSpace"
- student_info.gui_function = lambda: student_graph(app_state)
-
- sex_info = hello_imgui.DockableWindow()
- sex_info.label = "Analyze by Gender"
- sex_info.dock_space_name = "MainDockSpace"
- sex_info.gui_function = lambda: sex_graph(app_state)
-
- student_ranking = hello_imgui.DockableWindow()
- student_ranking.label = "Ranking"
- student_ranking.dock_space_name = "MainDockSpace"
- student_ranking.gui_function = lambda: ranking(app_state)
-
- return [
- student_selector,
- student_info, sex_info,
- student_ranking,
- ]
-
-def analyzer_layout(app_state: AppState) -> hello_imgui.DockingParams:
- docking_params = hello_imgui.DockingParams()
- docking_params.layout_name = "Analyzer"
- docking_params.docking_splits = analyzer_docking_splits()
- docking_params.dockable_windows = set_analyzer_layout(app_state)
- return docking_params
diff --git a/appstate.py b/grapher/gui/appstate.py
similarity index 70%
rename from appstate.py
rename to grapher/gui/appstate.py
index 31979ca..134130d 100644
--- a/appstate.py
+++ b/grapher/gui/appstate.py
@@ -1,6 +1,4 @@
-from imgui_bundle import hello_imgui
-from model import *
-from datetime import datetime
+from dbmodel import *
class AppState:
current_class_id: int
@@ -28,8 +26,6 @@ class AppState:
submissions = Submission.select().where(Submission.lecture_id == self.current_lecture_id and Submission.student_id == self.current_student_id)
self.current_submission_id = submissions[0].id if submissions else None
- LOG_DEBUG(f"Updated App State {repr(self)}")
-
def __repr__(self):
return f'''
Class ID: {self.current_class_id}
@@ -38,12 +34,5 @@ class AppState:
Submission ID: {self.current_submission_id}
'''
-def log(log_level: hello_imgui.LogLevel, msg: str) -> None:
- time = datetime.now().strftime("%X")
- hello_imgui.log(log_level, f"[{time}] {msg}")
-LOG_DEBUG = lambda msg: log(hello_imgui.LogLevel.debug, msg)
-LOG_INFO = lambda msg: log(hello_imgui.LogLevel.info, msg)
-LOG_WARNING = lambda msg: log(hello_imgui.LogLevel.warning, msg)
-LOG_ERROR = lambda msg: log(hello_imgui.LogLevel.error, msg)
diff --git a/grapher/gui/database/__init__.py b/grapher/gui/database/__init__.py
new file mode 100644
index 0000000..b5b56cf
--- /dev/null
+++ b/grapher/gui/database/__init__.py
@@ -0,0 +1,80 @@
+from imgui_bundle import hello_imgui, imgui
+#from ..app_state import AppState
+from typing import List
+
+from .database import *
+
+from .editor import 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) -> 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)
+
+ eeditor = hello_imgui.DockableWindow()
+ eeditor.label = "Editor"
+ eeditor.dock_space_name = "CommandSpace"
+ eeditor.gui_function = lambda: editor(app_state)
+
+ return [file_dialog, log, table_view, eeditor]
+
+def database_editor_layout(app_state) -> 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.py b/grapher/gui/database/database.py
similarity index 71%
rename from database.py
rename to grapher/gui/database/database.py
index c5863b9..c49a27e 100644
--- a/database.py
+++ b/grapher/gui/database/database.py
@@ -7,8 +7,9 @@ to set up the database editing environment.
"""
# Custom
-from model import *
-from appstate import *
+from dbmodel import *
+from gui import *
+from grader import get_gradings
# External
from imgui_bundle import (
@@ -20,11 +21,18 @@ from imgui_bundle import (
hello_imgui
)
+import peewee
+
# Built In
from typing import List
import shelve
from pathlib import Path
-from datetime import datetime
+from datetime import datetime, timezone
+import pytz
+from tzlocal import get_localzone
+
+LOG_INFO = lambda x: x
+LOG_DEBUG = lambda x: x
def file_info(path: Path) -> None:
"""
@@ -86,7 +94,7 @@ def select_file(app_state: AppState):
# Retrieve the last used database file from persistent storage
with shelve.open("state") as state:
- statics.current = Path(state["DB"])
+ statics.current = Path(state.get("DB") or "")
statics.inited = True
# Render UI title and display file information
@@ -125,18 +133,14 @@ def select_file(app_state: AppState):
# 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])
+ init_db(file)
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])
+ init_db(file)
LOG_INFO(f"Successfully loaded {filename}")
# Save the selected database path to persistent storage
@@ -150,6 +154,11 @@ def select_file(app_state: AppState):
@immapp.static(inited=False)
def table(app_state: AppState) -> None:
statics = table
+
+ if db.is_closed():
+ imgui.text("DB")
+ return
+
if not statics.inited:
statics.table_flags = (
imgui.TableFlags_.row_bg.value
@@ -215,14 +224,17 @@ def table(app_state: AppState) -> None:
imgui.end_table()
@immapp.static(inited=False)
-def class_editor() -> None:
+def 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 = class_editor
+ if db.is_closed():
+ imgui.text("DB")
+ return
statics.classes = Class.select()
if not statics.inited:
statics.selected = 0
@@ -260,86 +272,91 @@ def class_editor() -> None:
LOG_INFO(f"Deleted: {clas.name}")
-def database_editor(app_state: AppState) -> None:
- """
- Database Editor UI Function.
+def create_editor_popup(table, action: str, selectors: dict) -> None:
+ table_flags = (
+ imgui.TableFlags_.row_bg.value
+ )
+ cols = 2
+ rows = len(table._meta.fields)
+
+ if imgui.begin_table("Editor Grid", cols, table_flags):
+ # Setup Header
+ for header in ["Attribute", "Value"]:
+ imgui.table_setup_column(header, imgui.TableColumnFlags_.width_stretch.value, 1/len(header))
+ imgui.table_headers_row()
- Calls the class editor function to render its UI component.
-
- :param app_state: The application state containing relevant database information.
- """
- class_editor()
+ id = 0
+ for k, v in table._meta.fields.items():
+ # Don't show Fields
+ match type(v):
+ case peewee.AutoField:
+ continue
+ case peewee.DateTimeField:
+ continue
-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
+ imgui.table_next_row()
+ imgui.table_set_column_index(0)
+ label = str(k).removesuffix('_id')
+ imgui.text(label.title())
+ imgui.table_set_column_index(1)
+
+ # Generate Input for Type
+ match type(v):
+ case peewee.IntegerField:
+ if id not in selectors:
+ selectors[id] = int()
+ _, selectors[id] = imgui.input_int(f"##{id}", selectors[id], 1)
+ case peewee.CharField:
+ if id not in selectors:
+ if k == 'grader':
+ selectors[id] = int()
+ else:
+ selectors[id] = str()
+
+ if k == 'grader':
+ graders = [g.alt_name for g in get_gradings()]
+ _, selectors[id] = imgui.combo(f"##{id}", selectors[id], graders)
+ else:
+ _, selectors[id] = imgui.input_text(f"##{id}", selectors[id])
+ case peewee.FloatField:
+ if id not in selectors:
+ selectors[id] = float()
+ _, selectors[id] = imgui.input_float(f"##{id}", selectors[id])
+ case peewee.ForeignKeyField:
+ if id not in selectors:
+ selectors[id] = int()
+
+ labels = list()
+ match k:
+ case 'class_id':
+ labels = [clas.name for clas in Class.select()]
+ case 'lecture_id':
+ labels = [lecture.title for lecture in Lecture.select()]
+
+ if not labels:
+ imgui.text("No Element for this Attribute")
+ else:
+ _, selectors[id] = imgui.combo(f"##{id}", selectors[id], labels)
+ case _:
+ imgui.text(f"Unknown Field {k}")
+
+ id += 1
+ imgui.end_table()
- 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
+ if imgui.button(action):
+ match action:
+ case "Create":
+ print("Create")
+ case "Update":
+ print("Update")
+ case "Delete":
+ print("Delete")
+ case _:
+ print("Unknown Case")
- 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
+ # Clear & Close Popup
+ selectors.clear()
+ imgui.close_current_popup()
- 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
diff --git a/grapher/gui/database/editor.py b/grapher/gui/database/editor.py
new file mode 100644
index 0000000..b8dac38
--- /dev/null
+++ b/grapher/gui/database/editor.py
@@ -0,0 +1,142 @@
+from imgui_bundle import (
+ imgui,
+ immapp,
+ imgui_md,
+ hello_imgui
+)
+
+from grader import get_gradings, get_grader
+
+from dbmodel import *
+
+@immapp.static(inited=False)
+def editor(app_state) -> 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.
+ """
+ if db.is_closed():
+ imgui.text("Please open a Database")
+ return
+
+ statics = editor
+ if not statics.inited:
+ statics.selected = 0
+ statics.actions = ["Create", "Update", "Delete"]
+ statics.action = str()
+ statics.inited = True
+ statics.selectors = dict()
+
+ imgui.text("Select what you want to Edit:")
+ changed, statics.selected = imgui.combo('##DBSelector', statics.selected, table_labels)
+ for action in statics.actions:
+ if imgui.button(action):
+ imgui.open_popup(table_labels[statics.selected])
+ statics.action = action
+ imgui.same_line()
+
+ if imgui.begin_popup_modal(table_labels[statics.selected])[0]:
+ table = tables[statics.selected]
+ imgui_md.render(f"# {statics.action} {table_labels[statics.selected]}")
+
+ if imgui.begin_table("Editor Grid", 2, imgui.TableFlags_.row_bg.value):
+ # Setup Header
+ for header in ["Attribute", "Value"]:
+ imgui.table_setup_column(header, imgui.TableColumnFlags_.width_stretch.value, 1/len(header))
+ imgui.table_headers_row()
+
+
+ student_editor(statics.action)
+
+ imgui.end_table()
+
+
+ imgui.end_popup()
+
+@immapp.static(inited=False)
+def student_editor(action: str) -> dict:
+ '''
+
+ '''
+ statics = student_editor
+ if not statics.inited:
+ statics.classes = tuple(Class.select())
+ statics.residences = tuple(Residence.select())
+ statics.classes_labels = tuple(clas.name for clas in statics.classes)
+ statics.graders = tuple(grader.alt_name for grader in get_gradings())
+ statics.buffer = {
+ 'prename': "",
+ 'surname': "",
+ 'sex': 0,
+ 'class_id': 0,
+ 'group_id': 0,
+ 'grader': 0,
+ 'residence': 0,
+
+ }
+ statics.inited = True
+
+ imgui.table_next_row()
+ imgui.table_set_column_index(0)
+ imgui.text("First Name")
+ imgui.table_set_column_index(1)
+ _, statics.buffer['prename'] = imgui.input_text("##prename", statics.buffer['prename'])
+
+ imgui.table_next_row()
+ imgui.table_set_column_index(0)
+ imgui.text("Last Name")
+ imgui.table_set_column_index(1)
+ _, statics.buffer['surname'] = imgui.input_text("##surname", statics.buffer['surname'])
+
+ imgui.table_next_row()
+ imgui.table_set_column_index(0)
+ imgui.text("Gender")
+ imgui.table_set_column_index(1)
+ _, statics.buffer['sex'] = imgui.combo("##sex", statics.buffer['sex'], ['Male', 'Female'])
+
+ imgui.table_next_row()
+ imgui.table_set_column_index(0)
+ imgui.text("Class")
+ imgui.table_set_column_index(1)
+ _, statics.buffer['class_id'] = imgui.combo("##class_id", statics.buffer['class_id'], statics.classes_labels)
+
+ imgui.table_next_row()
+ imgui.table_set_column_index(0)
+ imgui.text("Project Group")
+ imgui.table_set_column_index(1)
+ groups = tuple(Group.select().where(Group.class_id == statics.classes[statics.buffer['class_id']].id))
+ _, statics.buffer['group_id'] = imgui.combo("##group_id", statics.buffer['group_id'], tuple(group.name for group in groups))
+
+ imgui.table_next_row()
+ imgui.table_set_column_index(0)
+ imgui.text("Grader")
+ imgui.table_set_column_index(1)
+ _, statics.buffer['grader'] = imgui.combo("##grader", statics.buffer['grader'], statics.graders)
+
+ imgui.table_next_row()
+ imgui.table_set_column_index(0)
+ imgui.text("Residence")
+ imgui.table_set_column_index(1)
+ _, statics.buffer['residence'] = imgui.combo("##residence", statics.buffer['residence'], [str(residence.id) for residence in statics.residences])
+
+ if imgui.button(action):
+ match action:
+ case "Create":
+ Student.create(
+ prename = statics.buffer['prename'],
+ surname = statics.buffer['surname'],
+ sex = 'Female' if statics.buffer['sex'] else 'Male',
+ class_id = statics.classes[statics.buffer['class_id']].id,
+ group_id = groups[statics.buffer['group_id']].id,
+ residence = statics.residences[statics.buffer['residence']]
+ )
+ case "Update":
+ pass
+ case "Delete":
+ pass
+ statics.inited = False
+ imgui.close_current_popup()
+
diff --git a/grapher/gui/gui.py b/grapher/gui/gui.py
new file mode 100644
index 0000000..99a46af
--- /dev/null
+++ b/grapher/gui/gui.py
@@ -0,0 +1,30 @@
+"""
+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 dbmodel import * # Importing database models like Class, Student, Lecture, and Submission
+from .appstate import AppState
+
+# External
+from imgui_bundle import imgui, hello_imgui # ImGui-based UI framework
+
+def menu_bar(runner_params: hello_imgui.RunnerParams) -> None:
+ """Defines the application's menu bar."""
+ hello_imgui.show_app_menu(runner_params)
+ hello_imgui.show_view_menu(runner_params)
+
+def status_bar(app_state: AppState) -> None:
+ """Displays the status bar information."""
+ imgui.text("Student Analyzer by @DerGrumpf")
+
diff --git a/grapher/gui/logger.py b/grapher/gui/logger.py
new file mode 100644
index 0000000..1444e2f
--- /dev/null
+++ b/grapher/gui/logger.py
@@ -0,0 +1,24 @@
+'''
+WIP
+'''
+
+from datetime import datetime
+from imgui_bundle import hello_imgui
+import logging
+
+FORMAT = '[%(asctime)s] '
+
+def log(log_level: hello_imgui.LogLevel, msg: str) -> None:
+ time = datetime.now().strftime("%X")
+ hello_imgui.log(log_level, f"[{time}] {msg}")
+
+LOG_DEBUG = lambda msg: log(hello_imgui.LogLevel.debug, msg)
+LOG_INFO = lambda msg: log(hello_imgui.LogLevel.info, msg)
+LOG_WARNING = lambda msg: log(hello_imgui.LogLevel.warning, msg)
+LOG_ERROR = lambda msg: log(hello_imgui.LogLevel.error, msg)
+
+logging.basicConfig()
+logger = logging.getLogger('AppState')
+
+
+logger.info("Hello")
diff --git a/grapher/gui/state b/grapher/gui/state
new file mode 100644
index 0000000000000000000000000000000000000000..f03fe98e2b3029b3d1103880728d018993291465
GIT binary patch
literal 16384
zcmeI%!3l#v5Czc5(VLJ4EWr}&BE4Ba+OZ))-K^q3yyg&j!tTHxL>_$blTlfqS3_^nf1F
z1A0IY=m9-&vmSWVTYdij?~jAu%w>D7-t4@paNv;dhdjuGJY)fR$Qt@V9^@el$V1l9
h5Aq-nSwJ4XHJlJ2K!5-N0t5&UAV7cs0RndxSOLec*~S0>
literal 0
HcmV?d00001
diff --git a/gui.py b/grapher/main.py
similarity index 52%
rename from gui.py
rename to grapher/main.py
index 2b1ba3a..788294e 100644
--- a/gui.py
+++ b/grapher/main.py
@@ -1,54 +1,21 @@
-"""
-Student Analyzer Application
+import shelve
-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.
+from imgui_bundle import (
+ hello_imgui,
+ immapp
+)
-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.
-"""
+from gui.logger import LOG_ERROR
-# Custom
-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 # 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 # ImGui-based UI framework
-
-# Built-in
-import shelve # Persistent key-value storage
-from typing import List # Type hinting
-
-def menu_bar(runner_params: hello_imgui.RunnerParams) -> None:
- """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:
- """Displays the status bar information."""
- try:
- imgui.text("Student Analyzer by @DerGrumpf")
- except Exception as e:
- LOG_ERROR(f"status_bar {e}")
+from gui import (
+ AppState,
+ analyzer_layout,
+ database_editor_layout,
+ menu_bar,
+ status_bar
+)
+from dbmodel import init_db
def main() -> None:
"""Main function to initialize and run the application."""
@@ -58,13 +25,12 @@ def main() -> None:
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()
+ print(v)
+ init_db(v)
+ app_state.update()
except Exception as e:
LOG_ERROR(f"Database Initialization {e}")
+ print(e)
# Set Window Parameters
runner_params = hello_imgui.RunnerParams()
@@ -110,3 +76,4 @@ def main() -> None:
if __name__ == "__main__":
main()
+
diff --git a/grapher/state b/grapher/state
new file mode 100644
index 0000000000000000000000000000000000000000..7135d886abbae2699c650eb6a672550df1e53fc6
GIT binary patch
literal 16384
zcmeIyu?>Pi6vpw>*jQoW0#4u=)Yuxr1{Rb?BM}l20uJB+mfARgtqZt=3z_Jhcf}R9
zCSv|1@D2)i{POO*pQEnm2=P8u(Y0;sMc1jm2=Oq5D*^~0fB*srAbbHE(156A&>fE*wP$N_SI93Th&EC(89SBslx7U94?_(IX&daC2o
zt9fozaKORcA9b0BtJjFaKy_~Oqp6nqwl4kCrF0fp-l2DmPL|oWKrhpa*<`}b}iMSNa9j1sw&=9GD>Ih+#k*TaeuWu
d_TB$xjQ|1&Ab None:
- '''
- Rebuilding Database from a given json
- '''
- with open(fp, "r") as file:
- data = json.load(file)
-
- for c_k, c_v in data.items():
- Class.create(
- name=c_k,
- id=c_v["DB ID"],
- created_at=c_v["Date"]
- )
- #print(f"KLASSE = {c.id} {c.name} ({c.created_at})")
-
- for student in c_v["Students"]:
- Student.create(
- id=student["DB ID"],
- created_at=student["Date"],
- prename=student["First Name"],
- surname=student["Last Name"],
- sex=student["Sex"],
- class_id=c_v["DB ID"]
- )
- #print(f"STUDENT = {s.id}. {s.prename} {s.surname} {s.sex} ({s.created_at}) Klasse: {s.class_id}")
-
- for submission in student["Submissions"]:
- Submission.create(
- id=submission["DB ID"],
- created_at=submission["Date"],
- points=submission["Points"],
- lecture_id=submission["Lecture ID"],
- student_id=student["DB ID"]
- )
- #print(f"SUBMISSION = {sub.id}. {sub.points} Lecture: {sub.lecture_id} Student: {sub.student_id} ({sub.created_at})")
-
- for lecture in c_v["Lectures"]:
- Lecture.create(
- id=lecture["DB ID"],
- created_at=lecture["Date"],
- title=lecture["Title"],
- points=lecture["Points"],
- class_id=c_v["DB ID"]
- )
- #print(f"LECTURE = {l.id}. {l.title} {l.points} ({l.created_at}) Klasse: {l.class_id}")
-
-def dump_to_json(fp: Path, indent=None) -> None:
- '''
- Dump existing Database to Json
- '''
- classes = Class.select()
- d = {c.name: {
- "DB ID": int(c.id),
- "Date": c.created_at.isoformat(),
- "Students": [
- {
- "DB ID": s.id,
- "Date": s.created_at.isoformat(),
- "First Name": s.prename,
- "Last Name": s.surname,
- "Sex": s.sex,
- "Submissions": [
- {
- "DB ID": sub.id,
- "Date": sub.created_at.isoformat(),
- "Points": sub.points,
- "Lecture ID": sub.lecture_id.id
- }
- for sub in Submission.select().where(Submission.student_id == s.id)
- ]
- }
- for s in Student.select().where(Student.class_id == c.id)
- ],
- "Lectures": [
- {
- "DB ID": l.id,
- "Date": l.created_at.isoformat(),
- "Title": l.title,
- "Points": l.points
- }
- for l in Lecture.select().where(Lecture.class_id == c.id)
- ],
- }
- for c in classes
- }
-
- with open(fp, "w") as file:
- json.dump(d, file, indent=indent)
-
-def main():
- import random
- # Generate Test Data
- class1 = Class.create(name="WiSe 22/23")
- class2 = Class.create(name="WiSe 23/24")
- class3 = Class.create(name="WiSe 24/25")
-
- phil = Student.create(prename="Phil", surname="Keier", sex="Male", class_id=class1.id)
- calvin = Student.create(prename="Calvin", surname="Brandt", sex="Male", class_id=class2.id)
- nova = Student.create(prename="Nova", surname="Eib", sex="Female", class_id=class1.id)
- kathi = Student.create(prename="Katharina", surname="Walz", sex="Female", class_id=class3.id)
- victoria = Student.create(prename="Victoria", surname="Möller", sex="Female", class_id=class3.id)
-
- lec1 = Lecture.create(title="Tutorial 1", points=30, class_id=class1.id)
- lec2 = Lecture.create(title="Tutorial 1", points=30, class_id=class3.id)
- lec3 = Lecture.create(title="Tutorial 2", points=20, class_id=class1.id)
- lec4 = Lecture.create(title="Tutorial 2", points=20, class_id=class2.id)
- lec5 = Lecture.create(title="Extended Applications", points=44, class_id=class1.id)
-
- sub1_phil = Submission.create(student_id=phil.id, lecture_id=lec1.id, points=random.randint(0, lec1.points))
- sub2_phil = Submission.create(student_id=phil.id, lecture_id=lec3.id, points=random.randint(0, lec3.points))
- sub3_phil = Submission.create(student_id=phil.id, lecture_id=lec5.id, points=random.randint(0, lec5.points))
- sub1_nova = Submission.create(student_id=nova.id, lecture_id=lec1.id, points=random.randint(0, lec1.points))
- sub2_nova = Submission.create(student_id=nova.id, lecture_id=lec3.id, points=random.randint(0, lec3.points))
- sub1_kathi = Submission.create(student_id=kathi.id, lecture_id=lec3.id, points=random.randint(0, lec3.points))
- sub1_vici = Submission.create(student_id=victoria.id, lecture_id=lec2.id, points=random.randint(0, lec2.points))
-
- return
- fp = Path().cwd()/"Test.json"
- dump_to_json(fp)
- db.close()
- db.init("Test.db")
- db.connect()
- db.create_tables([Class, Student, Lecture, Submission])
- load_from_json(fp)
-
-
-if __name__ == "__main__":
- # main()
- db.init('wise_24_25.db')
- db.connect()
- dump_to_json(Path().cwd()/"TEST.json")
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..d6553f0
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,44 @@
+[tool.poetry]
+name = "grapher"
+version = "0.1.0"
+description = "A Quick & Dirty Student Analyzer written in Python & DearImGUI"
+authors = ["DerGrumpf (Phil Keier) "]
+readme = "README.md"
+license = "MIT"
+package-mode = false
+
+[project]
+dependencies = [
+ "annotated-types==0.7.0",
+ "glfw==2.8.0",
+ "imgui-bundle==1.6.2",
+ "munch==4.0.0",
+ "numpy==2.2.1",
+ "opencv-python==4.10.0.84",
+ "pandas==2.2.3",
+ "peewee==3.17.8",
+ "pillow==11.1.0",
+ "py-spy==0.4.0",
+ "pydantic==2.10.4",
+ "pydantic_core==2.27.2",
+ "PyGLM==2.7.3",
+ "PyOpenGL==3.1.7",
+ "python-dateutil==2.9.0.post0",
+ "pytz==2024.2",
+ "six==1.17.0",
+ "snakeviz==2.2.2",
+ "tornado==6.4.2",
+ "typing_extensions==4.12.2",
+ "tzdata==2024.2",
+]
+
+[project.urls]
+repository = "https://git.cyperpunk.de/DerGrumpf/grapher"
+
+[tool.poetry.dependencies]
+python = "^3.12"
+
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"