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"