Changed: Structure Overhal
This commit is contained in:
parent
c320b27664
commit
033a1fa94f
@ -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
|
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
|
Abdalaziz,Abunjaila,Male,DiKum,30%,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
|
Marleen,Adolphi,Female,MeWi6,30%,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
|
Sarina,Apel,Female,MeWi1,30%,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
|
Skofiare,Berisha,Female,DiKum,30%,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
|
Aurela,Brahimi,Female,MeWi2,30%,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
|
Cam Thu,Do,Female,MeWi3,30%,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
|
Nova,Eib,Female,MeWi4,30%,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
|
Lena,Fricke,Female,MeWi4,30%,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
|
Nele,Grundke,Female,MeWi6,30%,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
|
Anna,Grünewald,Female,MeWi3,30%,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
|
Yannik,Haupt,Male,NoGroup,30%,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
|
Janna,Heiny,Female,MeWi1,30%,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
|
Milena,Krieger,Female,MeWi1,30%,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
|
Julia,Limbach,Female,MeWi6,30%,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
|
Viktoria,Litza,Female,MeWi5,30%,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
|
Leonie,Manthey,Female,MeWi1,30%,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
|
Izabel,Mike,Female,MeWi2,30%,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
|
Lea,Noglik,Female,MeWi5,30%,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
|
Donika,Nuhiu,Female,MeWi5,30%,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
|
Julia,Renner,Female,MeWi4,30%,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
|
Fabian,Rothberger,Male,MeWi3,30%,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
|
Natascha,Rott,Female,MeWi1,30%,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
|
Isabel,Rudolf,Female,MeWi4,30%,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
|
Melina,Sablotny,Female,MeWi6,30%,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
|
Alea,Schleier,Female,DiKum,30%,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
|
Flemming,Schur,Male,MeWi3,30%,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
|
Marie,Seeger,Female,DiKum,30%,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
|
Lucy,Thiele,Female,MeWi6,30%,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
|
Lara,Troschke,Female,MeWi2,30%,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
|
Inga-Brit,Turschner,Female,MeWi2,30%,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
|
Alea,Unger,Female,MeWi5,30%,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
|
Marie,Wallbaum,Female,MeWi5,30%,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
|
Katharina,Walz,Female,MeWi4,30%,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
|
Xiaowei,Wang,Male,NoGroup,30%,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
|
Lilly-Lu,Warnken,Female,DiKum,30%,30,15,18,30,14,17,19,14,16,24
|
||||||
|
,,,,,,,,,,,,,,
|
||||||
,,,,,,,,,,,,,,
|
,,,,,,,,,,,,,,
|
||||||
|
|
||||||
|
|
Binary file not shown.
@ -1,8 +1,9 @@
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pprint
|
import pprint
|
||||||
import sys
|
import sys
|
||||||
sys.path.append('..')
|
sys.path.append('../grapher/dbmodel')
|
||||||
from model import *
|
from model import *
|
||||||
|
from utils import *
|
||||||
|
|
||||||
df = pd.read_csv("Student_list.csv")
|
df = pd.read_csv("Student_list.csv")
|
||||||
df = df.dropna()
|
df = df.dropna()
|
||||||
@ -31,10 +32,8 @@ groups = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
print(df)
|
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
|
# Create Class
|
||||||
clas = Class.create(name='WiSe 24/25')
|
clas = Class.create(name='WiSe 24/25')
|
||||||
@ -55,7 +54,8 @@ for index, row in df.iterrows():
|
|||||||
sex=row["Sex"],
|
sex=row["Sex"],
|
||||||
class_id=clas.id,
|
class_id=clas.id,
|
||||||
group_id=Group.select().where(Group.name == row["Group"]),
|
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:]:
|
for title, points in list(row.to_dict().items())[5:]:
|
||||||
|
@ -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()
|
|
15
grapher/dbmodel/__init__.py
Normal file
15
grapher/dbmodel/__init__.py
Normal 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)
|
61
grapher/dbmodel/model.dbml
Normal file
61
grapher/dbmodel/model.dbml
Normal 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
117
grapher/dbmodel/model.py
Normal 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
172
grapher/dbmodel/utils.py
Normal 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
122
grapher/dbmodel/view.py
Normal 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)
|
8
grapher/grader/__init__.py
Normal file
8
grapher/grader/__init__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from .valuation import (
|
||||||
|
get_gradings,
|
||||||
|
get_grader,
|
||||||
|
Std30PercentRule,
|
||||||
|
Std50PercentRule,
|
||||||
|
StdGermanGradingMiddleSchool,
|
||||||
|
StdGermanGradingHighSchool
|
||||||
|
)
|
@ -158,12 +158,12 @@ class StdGermanGrading(BaseGrading):
|
|||||||
Std30PercentRule = StdPercentRule({
|
Std30PercentRule = StdPercentRule({
|
||||||
"pAssed": 0.3,
|
"pAssed": 0.3,
|
||||||
"Failed": 0.0
|
"Failed": 0.0
|
||||||
}, "Std30PercentRule", "30 Percent")
|
}, "Std30PercentRule", "30%")
|
||||||
|
|
||||||
Std50PercentRule = StdPercentRule({
|
Std50PercentRule = StdPercentRule({
|
||||||
"Passed": 0.5,
|
"Passed": 0.5,
|
||||||
"Failed": 0.0
|
"Failed": 0.0
|
||||||
}, "Std50PercentRule", "50 Percent")
|
}, "Std50PercentRule", "50%")
|
||||||
|
|
||||||
StdGermanGradingMiddleSchool = StdGermanGrading({
|
StdGermanGradingMiddleSchool = StdGermanGrading({
|
||||||
1: 0.96,
|
1: 0.96,
|
||||||
@ -172,7 +172,7 @@ StdGermanGradingMiddleSchool = StdGermanGrading({
|
|||||||
4: 0.45,
|
4: 0.45,
|
||||||
5: 0.16,
|
5: 0.16,
|
||||||
6: 0.00
|
6: 0.00
|
||||||
}, "StdGermanGradingMiddleSchool", "Secondary School")
|
}, "StdGermanGradingMiddleSchool", "Mittelstufe")
|
||||||
|
|
||||||
StdGermanGradingHighSchool = StdGermanGrading({
|
StdGermanGradingHighSchool = StdGermanGrading({
|
||||||
15: 0.95,
|
15: 0.95,
|
4
grapher/gui/__init__.py
Normal file
4
grapher/gui/__init__.py
Normal 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
|
63
grapher/gui/analyzer/__init__.py
Normal file
63
grapher/gui/analyzer/__init__.py
Normal 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
|
@ -1,6 +1,6 @@
|
|||||||
# Custom
|
# Custom
|
||||||
from model import *
|
from dbmodel import *
|
||||||
from appstate import AppState
|
from gui import AppState
|
||||||
from grader.valuation import *
|
from grader.valuation import *
|
||||||
|
|
||||||
# External
|
# External
|
||||||
@ -140,8 +140,8 @@ def student_graph(app_state: AppState) -> None:
|
|||||||
statics.points = np.sum(statics.sub_points)
|
statics.points = np.sum(statics.sub_points)
|
||||||
if statics.points.is_integer():
|
if statics.points.is_integer():
|
||||||
statics.points = int(statics.points)
|
statics.points = int(statics.points)
|
||||||
statics.grader = get_grader("Oberstufe")
|
#statics.grader = get_grader("Oberstufe")
|
||||||
#statics.grader = get_grader(statics.student.grader)
|
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_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)]
|
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"):
|
if imgui.button("Change"):
|
||||||
statics.state = not statics.state
|
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
|
|
@ -1,6 +1,4 @@
|
|||||||
from imgui_bundle import hello_imgui
|
from dbmodel import *
|
||||||
from model import *
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
class AppState:
|
class AppState:
|
||||||
current_class_id: int
|
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)
|
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
|
self.current_submission_id = submissions[0].id if submissions else None
|
||||||
|
|
||||||
LOG_DEBUG(f"Updated App State {repr(self)}")
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'''
|
return f'''
|
||||||
Class ID: {self.current_class_id}
|
Class ID: {self.current_class_id}
|
||||||
@ -38,12 +34,5 @@ class AppState:
|
|||||||
Submission ID: {self.current_submission_id}
|
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)
|
|
||||||
|
|
80
grapher/gui/database/__init__.py
Normal file
80
grapher/gui/database/__init__.py
Normal 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
|
@ -7,8 +7,9 @@ to set up the database editing environment.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Custom
|
# Custom
|
||||||
from model import *
|
from dbmodel import *
|
||||||
from appstate import *
|
from gui import *
|
||||||
|
from grader import get_gradings
|
||||||
|
|
||||||
# External
|
# External
|
||||||
from imgui_bundle import (
|
from imgui_bundle import (
|
||||||
@ -20,11 +21,18 @@ from imgui_bundle import (
|
|||||||
hello_imgui
|
hello_imgui
|
||||||
)
|
)
|
||||||
|
|
||||||
|
import peewee
|
||||||
|
|
||||||
# Built In
|
# Built In
|
||||||
from typing import List
|
from typing import List
|
||||||
import shelve
|
import shelve
|
||||||
from pathlib import Path
|
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:
|
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
|
# Retrieve the last used database file from persistent storage
|
||||||
with shelve.open("state") as state:
|
with shelve.open("state") as state:
|
||||||
statics.current = Path(state["DB"])
|
statics.current = Path(state.get("DB") or "")
|
||||||
statics.inited = True
|
statics.inited = True
|
||||||
|
|
||||||
# Render UI title and display file information
|
# 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
|
# Handle JSON files by converting them to SQLite databases
|
||||||
if statics.res.extension() == '.json':
|
if statics.res.extension() == '.json':
|
||||||
file = filename.removesuffix('.json') + '.db' # Convert JSON filename to SQLite filename
|
file = filename.removesuffix('.json') + '.db' # Convert JSON filename to SQLite filename
|
||||||
db.init(file)
|
init_db(file)
|
||||||
db.connect(reuse_if_open=True)
|
|
||||||
db.create_tables([Class, Student, Lecture, Submission, Group])
|
|
||||||
load_from_json(str(info)) # Convert and load JSON data into the database
|
load_from_json(str(info)) # Convert and load JSON data into the database
|
||||||
LOG_INFO(f"Successfully created {file}")
|
LOG_INFO(f"Successfully created {file}")
|
||||||
|
|
||||||
# Handle SQLite database files directly
|
# Handle SQLite database files directly
|
||||||
if statics.res.extension() == '.db':
|
if statics.res.extension() == '.db':
|
||||||
file = str(statics.res.path())
|
file = str(statics.res.path())
|
||||||
db.init(file)
|
init_db(file)
|
||||||
db.connect(reuse_if_open=True)
|
|
||||||
db.create_tables([Class, Student, Lecture, Submission, Group])
|
|
||||||
LOG_INFO(f"Successfully loaded {filename}")
|
LOG_INFO(f"Successfully loaded {filename}")
|
||||||
|
|
||||||
# Save the selected database path to persistent storage
|
# Save the selected database path to persistent storage
|
||||||
@ -150,6 +154,11 @@ def select_file(app_state: AppState):
|
|||||||
@immapp.static(inited=False)
|
@immapp.static(inited=False)
|
||||||
def table(app_state: AppState) -> None:
|
def table(app_state: AppState) -> None:
|
||||||
statics = table
|
statics = table
|
||||||
|
|
||||||
|
if db.is_closed():
|
||||||
|
imgui.text("DB")
|
||||||
|
return
|
||||||
|
|
||||||
if not statics.inited:
|
if not statics.inited:
|
||||||
statics.table_flags = (
|
statics.table_flags = (
|
||||||
imgui.TableFlags_.row_bg.value
|
imgui.TableFlags_.row_bg.value
|
||||||
@ -215,7 +224,7 @@ def table(app_state: AppState) -> None:
|
|||||||
imgui.end_table()
|
imgui.end_table()
|
||||||
|
|
||||||
@immapp.static(inited=False)
|
@immapp.static(inited=False)
|
||||||
def class_editor() -> None:
|
def editor() -> None:
|
||||||
"""
|
"""
|
||||||
Class Editor UI Component.
|
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.
|
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()
|
statics.classes = Class.select()
|
||||||
if not statics.inited:
|
if not statics.inited:
|
||||||
statics.selected = 0
|
statics.selected = 0
|
||||||
@ -260,86 +272,91 @@ def class_editor() -> None:
|
|||||||
LOG_INFO(f"Deleted: {clas.name}")
|
LOG_INFO(f"Deleted: {clas.name}")
|
||||||
|
|
||||||
|
|
||||||
def database_editor(app_state: AppState) -> None:
|
def create_editor_popup(table, action: str, selectors: dict) -> None:
|
||||||
"""
|
table_flags = (
|
||||||
Database Editor UI Function.
|
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.
|
id = 0
|
||||||
"""
|
for k, v in table._meta.fields.items():
|
||||||
class_editor()
|
# Don't show Fields
|
||||||
|
match type(v):
|
||||||
|
case peewee.AutoField:
|
||||||
|
continue
|
||||||
|
case peewee.DateTimeField:
|
||||||
|
continue
|
||||||
|
|
||||||
def database_docking_splits() -> List[hello_imgui.DockingSplit]:
|
imgui.table_next_row()
|
||||||
"""
|
imgui.table_set_column_index(0)
|
||||||
Defines the docking layout for the database editor.
|
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.
|
if k == 'grader':
|
||||||
"""
|
graders = [g.alt_name for g in get_gradings()]
|
||||||
split_main_command = hello_imgui.DockingSplit()
|
_, selectors[id] = imgui.combo(f"##{id}", selectors[id], graders)
|
||||||
split_main_command.initial_dock = "MainDockSpace"
|
else:
|
||||||
split_main_command.new_dock = "CommandSpace"
|
_, selectors[id] = imgui.input_text(f"##{id}", selectors[id])
|
||||||
split_main_command.direction = imgui.Dir.down
|
case peewee.FloatField:
|
||||||
split_main_command.ratio = 0.3
|
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()
|
labels = list()
|
||||||
split_main_command2.initial_dock = "CommandSpace"
|
match k:
|
||||||
split_main_command2.new_dock = "CommandSpace2"
|
case 'class_id':
|
||||||
split_main_command2.direction = imgui.Dir.right
|
labels = [clas.name for clas in Class.select()]
|
||||||
split_main_command2.ratio = 0.3
|
case 'lecture_id':
|
||||||
|
labels = [lecture.title for lecture in Lecture.select()]
|
||||||
|
|
||||||
split_main_misc = hello_imgui.DockingSplit()
|
if not labels:
|
||||||
split_main_misc.initial_dock = "MainDockSpace"
|
imgui.text("No Element for this Attribute")
|
||||||
split_main_misc.new_dock = "MiscSpace"
|
else:
|
||||||
split_main_misc.direction = imgui.Dir.left
|
_, selectors[id] = imgui.combo(f"##{id}", selectors[id], labels)
|
||||||
split_main_misc.ratio = 0.2
|
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]:
|
if imgui.button(action):
|
||||||
"""
|
match action:
|
||||||
Defines the dockable windows for the database editor.
|
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,
|
# Clear & Close Popup
|
||||||
table viewer, and editor.
|
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
|
|
||||||
|
|
142
grapher/gui/database/editor.py
Normal file
142
grapher/gui/database/editor.py
Normal 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
30
grapher/gui/gui.py
Normal 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
24
grapher/gui/logger.py
Normal 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
BIN
grapher/gui/state
Normal file
Binary file not shown.
@ -1,54 +1,21 @@
|
|||||||
"""
|
import shelve
|
||||||
Student Analyzer Application
|
|
||||||
|
|
||||||
This script initializes and runs the Student Analyzer application, which provides an interface for
|
from imgui_bundle import (
|
||||||
managing student data, class records, and submissions. It uses the Hello ImGui framework for UI rendering
|
hello_imgui,
|
||||||
and integrates a database to store and manipulate student information.
|
immapp
|
||||||
|
)
|
||||||
|
|
||||||
Modules:
|
from gui.logger import LOG_ERROR
|
||||||
- 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 gui import (
|
||||||
from model import * # Importing database models like Class, Student, Lecture, and Submission
|
AppState,
|
||||||
from appstate import AppState, LOG_ERROR # Application state management
|
analyzer_layout,
|
||||||
|
database_editor_layout,
|
||||||
# Layouts
|
menu_bar,
|
||||||
from analyzer import analyzer_layout # Main layout for the analyzer
|
status_bar
|
||||||
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 dbmodel import init_db
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""Main function to initialize and run the application."""
|
"""Main function to initialize and run the application."""
|
||||||
@ -58,13 +25,12 @@ def main() -> None:
|
|||||||
try:
|
try:
|
||||||
with shelve.open("state") as state:
|
with shelve.open("state") as state:
|
||||||
v = state.get("DB") # Retrieve stored database connection info
|
v = state.get("DB") # Retrieve stored database connection info
|
||||||
if v:
|
print(v)
|
||||||
db.init(v)
|
init_db(v)
|
||||||
db.connect()
|
|
||||||
db.create_tables([Class, Student, Lecture, Submission, Group]) # Ensure tables exist
|
|
||||||
app_state.update()
|
app_state.update()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG_ERROR(f"Database Initialization {e}")
|
LOG_ERROR(f"Database Initialization {e}")
|
||||||
|
print(e)
|
||||||
|
|
||||||
# Set Window Parameters
|
# Set Window Parameters
|
||||||
runner_params = hello_imgui.RunnerParams()
|
runner_params = hello_imgui.RunnerParams()
|
||||||
@ -110,3 +76,4 @@ def main() -> None:
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
BIN
grapher/state
Normal file
BIN
grapher/state
Normal file
Binary file not shown.
21
main.py
21
main.py
@ -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
176
model.py
@ -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
44
pyproject.toml
Normal 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"
|
Loading…
Reference in New Issue
Block a user