Changed: Structure Overhal

This commit is contained in:
DerGrumpf 2025-02-14 13:34:31 +01:00
parent c320b27664
commit 033a1fa94f
27 changed files with 1059 additions and 528 deletions

4
Makefile Normal file
View File

@ -0,0 +1,4 @@
init:
pip install -r requirements.txt
.PHONY: init

View File

@ -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
,,,,,,,,,,,,,,
,,,,,,,,,,,,,,

1 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
2 Abdalaziz Abunjaila Male DiKum 30 Percent 30% 30.5 15 18 28 17 17 17 22 0 18
3 Marleen Adolphi Female MeWi6 30 Percent 30% 29.5 15 18 32 19 20 17 24 23 0
4 Sarina Apel Female MeWi1 30 Percent 30% 28.5 15 18 32 20 20 21 24 20 23
5 Skofiare Berisha Female DiKum 30 Percent 30% 29.5 13 18 34 20 17 20 26 16 0
6 Aurela Brahimi Female MeWi2 30 Percent 30% 17.5 15 15.5 26 16 17 19 16 0 0
7 Cam Thu Do Female MeWi3 30 Percent 30% 31 15 18 34 19 20 21.5 22 12 0
8 Nova Eib Female MeWi4 30 Percent 30% 31 15 15 34 20 20 21 27 19 21
9 Lena Fricke Female MeWi4 30 Percent 30% 0 0 0 0 0 0 0 0 0 0
10 Nele Grundke Female MeWi6 30 Percent 30% 23.5 13 16 28 20 17 21 18 22 11
11 Anna Grünewald Female MeWi3 30 Percent 30% 12 14 16 29 16 15 19 9 0 0
12 Yannik Haupt Male NoGroup 30 Percent 30% 18 6 14 21 13 2 9 0 0 0
13 Janna Heiny Female MeWi1 30 Percent 30% 30 15 18 33 18 20 22 25 24 30
14 Milena Krieger Female MeWi1 30 Percent 30% 30 15 18 33 20 20 21.5 26 20 22
15 Julia Limbach Female MeWi6 30 Percent 30% 27.5 12 18 29 11 19 17.5 26 24 28
16 Viktoria Litza Female MeWi5 30 Percent 30% 21.5 15 18 27 13 20 22 21 21 30
17 Leonie Manthey Female MeWi1 30 Percent 30% 28.5 14 18 29 20 10 18 23 16 28
18 Izabel Mike Female MeWi2 30 Percent 30% 29.5 15 15 35 11 15 19 21 21 27
19 Lea Noglik Female MeWi5 30 Percent 30% 22.5 15 17 34 13 10 20 21 19 6
20 Donika Nuhiu Female MeWi5 30 Percent 30% 31 13.5 18 35 14 10 17 18 19 8
21 Julia Renner Female MeWi4 30 Percent 30% 27.5 10 14 32 20 17 11 20 24 14
22 Fabian Rothberger Male MeWi3 30 Percent 30% 30.5 15 18 34 17 17 19 22 18 30
23 Natascha Rott Female MeWi1 30 Percent 30% 29.5 12 18 32 19 20 21 26 23 26
24 Isabel Rudolf Female MeWi4 30 Percent 30% 27.5 9 17 34 16 19 19 21 16 14
25 Melina Sablotny Female MeWi6 30 Percent 30% 31 15 18 33 20 20 21 19 11 28
26 Alea Schleier Female DiKum 30 Percent 30% 27 14 18 34 16 18 21.5 22 15 22
27 Flemming Schur Male MeWi3 30 Percent 30% 29.5 15 17 34 19 20 19 22 18 27
28 Marie Seeger Female DiKum 30 Percent 30% 27.5 15 18 32 14 9 17 22 9 25
29 Lucy Thiele Female MeWi6 30 Percent 30% 27.5 15 18 27 20 17 19 18 22 25
30 Lara Troschke Female MeWi2 30 Percent 30% 28.5 14 17 28 13 19 21 25 12 24
31 Inga-Brit Turschner Female MeWi2 30 Percent 30% 25.5 14 18 34 20 16 19 22 17 30
32 Alea Unger Female MeWi5 30 Percent 30% 30 12 18 31 20 20 21 22 15 21.5
33 Marie Wallbaum Female MeWi5 30 Percent 30% 28.5 14 18 34 17 20 19 24 12 22
34 Katharina Walz Female MeWi4 30 Percent 30% 31 15 18 31 19 19 17 24 17 14.5
35 Xiaowei Wang Male NoGroup 30 Percent 30% 30.5 14 18 26 19 17 0 0 0 0
36 Lilly-Lu Warnken Female DiKum 30 Percent 30% 30 15 18 30 14 17 19 14 16 24
37
38
39

Binary file not shown.

View File

@ -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:]:

View File

@ -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"<TestGrader: ({str(self.test_schema)})>")
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()

View File

@ -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)

View File

@ -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
}

117
grapher/dbmodel/model.py Normal file
View File

@ -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)

172
grapher/dbmodel/utils.py Normal file
View File

@ -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 <filename>.json to given <path>
JSON Format:
{
<Tablename>: [
{<Tableparam>: <Value>, ...}
],
...
}
'''
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 <dbname>.db in from given <file>
Valid JSON Format:
{
<Tablename>: [
{<Tableparam>: <Value>, ...}
],
...
}
'''
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))

122
grapher/dbmodel/view.py Normal file
View File

@ -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.<update_param> == 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:
[
{<TableParam>: <Value>, ...},
...
]
'''
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)

View File

@ -0,0 +1,8 @@
from .valuation import (
get_gradings,
get_grader,
Std30PercentRule,
Std50PercentRule,
StdGermanGradingMiddleSchool,
StdGermanGradingHighSchool
)

View File

@ -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,

4
grapher/gui/__init__.py Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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,7 +224,7 @@ def table(app_state: AppState) -> None:
imgui.end_table()
@immapp.static(inited=False)
def class_editor() -> None:
def editor() -> None:
"""
Class Editor UI Component.
@ -223,6 +232,9 @@ def class_editor() -> None:
It maintains a static state to keep track of the selected class and fetches available classes.
"""
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)
Calls the class editor function to render its UI component.
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()
: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.
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)
Returns a list of docking splits that define the structure of the editor layout.
# 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()
: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
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()
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
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()]
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
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}")
return [split_main_misc, split_main_command, split_main_command2]
id += 1
imgui.end_table()
def set_database_editor_layout(app_state: AppState) -> List[hello_imgui.DockableWindow]:
"""
Defines the dockable windows for the database editor.
if imgui.button(action):
match action:
case "Create":
print("Create")
case "Update":
print("Update")
case "Delete":
print("Delete")
case _:
print("Unknown Case")
Creates and returns a list of dockable windows, including the database file selector, log window,
table viewer, and editor.
# Clear & Close Popup
selectors.clear()
imgui.close_current_popup()
: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

View File

@ -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()

30
grapher/gui/gui.py Normal file
View File

@ -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")

24
grapher/gui/logger.py Normal file
View File

@ -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")

BIN
grapher/gui/state Normal file

Binary file not shown.

View File

@ -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()

BIN
grapher/state Normal file

Binary file not shown.

21
main.py
View File

@ -1,21 +0,0 @@
import imgui
import numpy
phil = Student(
"Phil Keier", "772fb04b24caa68fd38a05ec2a22e62b", "Geomapping",
[Lecture("1. Tutorial 1", 28.5, 31), Lecture("2. Tutorial 2", 4.5, 15), Lecture("3. Extended Application", 18, 18)]
)
nova = Student(
"Nova Eib", "772fb04b24caa68fd38a05ec2a22e62b", "Mapping Maps",
[Lecture("1. Tutorial 1", 28.5, 31), Lecture("2. Tutorial 2", 4.5, 15), Lecture("3. Extended Application", 18, 18)]
)
kathi = Student(
"Katharina Walz", "772fb04b24caa68fd38a05ec2a22e62b", "Geomapping",
[Lecture("1. Tutorial 1", 28.5, 31), Lecture("2. Tutorial 2", 4.5, 15), Lecture("3. Extended Application", 18, 18), Lecture("4. Numpy & MatPlotLib", 3, 30)]
)
students = [phil, nova, kathi]
if __name__ == "__main__":
gui = GUI()

176
model.py
View File

@ -1,176 +0,0 @@
from peewee import *
from datetime import datetime
import json
from typing import TextIO
from pathlib import Path
db = SqliteDatabase(None, autoconnect=False)
class BaseModel(Model):
class Meta:
database = db
class Class(BaseModel):
name = CharField()
created_at = DateTimeField(default=datetime.now)
class Group(BaseModel):
name = CharField()
project = CharField()
class_id = ForeignKeyField(Class, backref='class')
created_at = DateTimeField(default=datetime.now)
class Student(BaseModel):
prename = CharField()
surname = CharField()
sex = CharField()
class_id = ForeignKeyField(Class, backref='class')
group_id = ForeignKeyField(Group, backref='group')
grader = CharField()
created_at = DateTimeField(default=datetime.now)
class Lecture(BaseModel):
title = CharField()
points = IntegerField()
class_id = ForeignKeyField(Class, backref='class')
created_at = DateTimeField(default=datetime.now)
class Submission(BaseModel):
student_id = ForeignKeyField(Student, backref='student')
lecture_id = ForeignKeyField(Lecture, backref='lecture')
points = FloatField()
created_at = DateTimeField(default=datetime.now)
def load_from_json(fp: Path) -> 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")

44
pyproject.toml Normal file
View File

@ -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) <p.keier@beyerstedt-it.de>"]
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"