From 5d7b1d59c5fe26f2273dbbc9cbf80d2993599a34 Mon Sep 17 00:00:00 2001
From: DerGrumpf
Date: Fri, 31 Jan 2025 14:34:57 +0100
Subject: [PATCH] Changed: Projects
---
analyzer.py | 34 +++++-
assets/Student_list.csv | 72 +++++------
assets/WiSe_24_25.db | Bin 53248 -> 65536 bytes
assets/convert.py | 21 +++-
database.py | 252 ++++++++++++++++++++++++++++----------
database_editor.py | 261 ----------------------------------------
gui.py | 106 +++++++++-------
model.py | 8 ++
8 files changed, 343 insertions(+), 411 deletions(-)
delete mode 100644 database_editor.py
diff --git a/analyzer.py b/analyzer.py
index 4632316..4a50928 100644
--- a/analyzer.py
+++ b/analyzer.py
@@ -45,6 +45,27 @@ def student_list(app_state: AppState) -> None:
app_state.current_student_id = statics.students[statics.select].id
+def group_list(app_state: AppState) -> None:
+ statics = group_list
+ if not app_state.current_class_id:
+ imgui.text("No Class found in Database")
+ return
+
+ if not hasattr(statics, "select"):
+ statics.select = 0
+
+ statics.groups = Group.select().where(Group.class_id == app_state.current_class_id)
+ statics.groups = statics.groups if statics.groups else None
+ if not statics.groups:
+ imgui.text("No Group found")
+ return
+
+ for n, group in enumerate(statics.groups, start=1):
+ display = f"{n}. {group.name}"
+ _, clicked = imgui.selectable(display, statics.select == n-1)
+ if clicked:
+ statics.select = n-1
+
def lecture_list(app_state: AppState) -> None:
statics = lecture_list
@@ -158,12 +179,14 @@ def plot_bar_line_percentage(data: np.array, labels: list, avg: float) -> None:
COLOR_TEXT_PASSED = tuple([e/255 for e in ImageColor.getcolor("#1AFE49","RGBA")])
COLOR_TEXT_FAILED = tuple([e/255 for e in ImageColor.getcolor("#FF124F","RGBA")])
+COLOR_TEXT_PROJECT = tuple([e/255 for e in ImageColor.getcolor("#0A9CF5","RGBA")])
@immapp.static(inited=False)
def student_graph(app_state: AppState) -> None:
statics = student_graph
if not statics.inited:
statics.id = -1
statics.student = None
+ statics.group = None
statics.lectures = None
statics.points = None
statics.sub_points = None
@@ -182,6 +205,7 @@ def student_graph(app_state: AppState) -> None:
statics.student = Student.get_by_id(app_state.current_student_id)
submissions = Submission.select().where(Submission.student_id == statics.student.id)
+ statics.group = Group.get_by_id(statics.student.group_id)
statics.lectures = [Lecture.get_by_id(sub.lecture_id) for sub in submissions]
statics.max_points = np.sum([l.points for l in statics.lectures])
statics.sub_points = [sub.points for sub in submissions]
@@ -194,12 +218,13 @@ def student_graph(app_state: AppState) -> None:
statics.avg = statics.points/statics.max_points*100
w, h = imgui.get_window_size()
- imgui_md.render(f"# {statics.student.prename} {statics.student.surname}")
+ imgui_md.render(f"# {statics.student.prename} {statics.student.surname} ({statics.group.name})")
imgui_md.render(f"### {statics.points}/{statics.max_points}")
imgui.text(" ")
imgui.progress_bar(statics.points/statics.max_points, ImVec2(w*0.9, h*0.05), f"{statics.points}/{statics.max_points} {statics.points/statics.max_points:.1%}")
plot_bar_line_percentage(statics.subs_data, statics.subs_labels, statics.avg)
imgui.separator()
+ imgui.text_colored(COLOR_TEXT_PROJECT, f"{statics.group.name}: {statics.group.project}")
for n, data in enumerate(zip(statics.lectures, statics.sub_points), start=1):
lecture, points = data
COLOR = COLOR_TEXT_PASSED if points >= lecture.points*0.3 else COLOR_TEXT_FAILED
@@ -391,6 +416,11 @@ def set_analyzer_layout(app_state: AppState) -> List[hello_imgui.DockableWindow]
student_selector.dock_space_name = "CommandSpace"
student_selector.gui_function = lambda: student_list(app_state)
+ group_selector = hello_imgui.DockableWindow()
+ group_selector.label = "Groups"
+ group_selector.dock_space_name = "CommandSpace"
+ group_selector.gui_function = lambda: group_list(app_state)
+
lecture_selector = hello_imgui.DockableWindow()
lecture_selector.label = "Lectures"
lecture_selector.dock_space_name = "CommandSpace2"
@@ -425,7 +455,7 @@ def set_analyzer_layout(app_state: AppState) -> List[hello_imgui.DockableWindow]
class_selector, student_selector,
lecture_selector, submission_selector,
student_info, sex_info,
- student_ranking
+ student_ranking, group_selector
]
def analyzer_layout(app_state: AppState) -> hello_imgui.DockingParams:
diff --git a/assets/Student_list.csv b/assets/Student_list.csv
index 6765eaf..fa13c8e 100644
--- a/assets/Student_list.csv
+++ b/assets/Student_list.csv
@@ -1,35 +1,37 @@
-First Name,Last Name,Sex,Tutorial 1,Tutorial 2,Extended Applications,Numpy & MatPlotLib,SciPy,Monte Carlo,Pandas & Seaborn,Folium,Statistical Test Methods,Data Analysis
-Abdalaziz,Abunjaila,Male,30.5,15,18,28,17,17,17,22,0,0
-Marleen,Adolphi,Female,29.5,15,18,32,19,20,17,24,23,0
-Sarina,Apel,Female,28.5,15,18,32,20,20,21,24,20,0
-Skofiare,Berisha,Female,29.5,13,18,34,20,17,20,26,16,0
-Aurela,Brahimi,Female,17.5,15,15.5,26,16,17,19,16,0,0
-Cam Thu,Do,Female,31,15,18,34,19,20,21.5,22,12,0
-Nova,Eib,Female,31,15,15,34,20,20,21,27,19,0
-Nele,Grundke,Female,23.5,13,16,28,20,17,21,18,22,0
-Anna,Grünewald,Female,12,14,16,29,16,15,19,9,0,0
-Yannik,Haupt,Male,18,6,14,21,13,2,9,0,0,0
-Janna,Heiny,Female,30,15,18,33,18,20,22,25,24,30
-Milena,Krieger,Female,30,15,18,33,20,20,21.5,26,20,0
-Julia,Limbach,Female,27.5,12,18,29,11,19,17.5,26,24,0
-Viktoria,Litza,Female,21.5,15,18,27,13,20,22,21,21,0
-Leonie,Manthey,Female,28.5,14,18,29,20,10,18,23,16,28
-Izabel,Mike,Female,29.5,15,15,35,11,15,19,21,21,27
-Lea,Noglik,Female,22.5,15,17,34,13,10,20,21,19,0
-Donika,Nuhiu,Female,31,13.5,18,35,14,10,17,18,19,6
-Julia,Renner,Female,27.5,10,14,32,20,17,11,20,24,0
-Fabian,Rothberger,Male,30.5,15,18,34,17,17,19,22,18,0
-Natascha,Rott,Female,29.5,12,18,32,19,20,21,26,23,0
-Isabel,Rudolf,Female,27.5,9,17,34,16,19,19,21,16,0
-Melina,Sablotny,Female,31,15,18,33,20,20,21,19,11,0
-Alea,Schleier,Female,27,14,18,34,16,18,21.5,22,15,22
-Flemming,Schur,Male,29.5,15,17,34,19,20,19,22,18,0
-Marie,Seeger,Female,27.5,15,18,32,14,9,17,22,9,0
-Lucy,Thiele,Female,27.5,15,18,27,20,17,19,18,22,0
-Lara,Troschke,Female,28.5,14,17,28,13,19,21,25,12,0
-Inga-Brit,Turschner,Female,25.5,14,18,34,20,16,19,22,17,0
-Alea,Unger,Female,30,12,18,31,20,20,21,22,15,21.5
-Marie,Wallbaum,Female,28.5,14,18,34,17,20,19,24,12,0
-Katharina,Walz,Female,31,15,18,31,19,19,17,24,17,14.5
-Xiaowei,Wang,Male,30.5,14,18,26,19,17,0,0,0,0
-Lilly-Lu,Warnken,Female,30,15,18,30,14,17,19,14,16,0
+First Name,Last Name,Sex,Group,Tutorial 1,Tutorial 2,Extended Applications,Numpy & MatPlotLib,SciPy,Monte Carlo,Pandas & Seaborn,Folium,Statistical Test Methods,Data Analysis
+Abdalaziz,Abunjaila,Male,DiKum,30.5,15,18,28,17,17,17,22,0,18
+Marleen,Adolphi,Female,MeWi6,29.5,15,18,32,19,20,17,24,23,0
+Sarina,Apel,Female,MeWi1,28.5,15,18,32,20,20,21,24,20,0
+Skofiare,Berisha,Female,DiKum,29.5,13,18,34,20,17,20,26,16,0
+Aurela,Brahimi,Female,MeWi2,17.5,15,15.5,26,16,17,19,16,0,0
+Cam Thu,Do,Female,MeWi3,31,15,18,34,19,20,21.5,22,12,0
+Nova,Eib,Female,MeWi4,31,15,15,34,20,20,21,27,19,21
+Lena,Fricke,Female,MeWi4,0,0,0,0,0,0,0,0,0,0
+Nele,Grundke,Female,MeWi6,23.5,13,16,28,20,17,21,18,22,0
+Anna,Grünewald,Female,MeWi3,12,14,16,29,16,15,19,9,0,0
+Yannik,Haupt,Male,NoGroup,18,6,14,21,13,2,9,0,0,0
+Janna,Heiny,Female,MeWi1,30,15,18,33,18,20,22,25,24,30
+Milena,Krieger,Female,MeWi1,30,15,18,33,20,20,21.5,26,20,0
+Julia,Limbach,Female,MeWi6,27.5,12,18,29,11,19,17.5,26,24,0
+Viktoria,Litza,Female,MeWi5,21.5,15,18,27,13,20,22,21,21,0
+Leonie,Manthey,Female,MeWi1,28.5,14,18,29,20,10,18,23,16,28
+Izabel,Mike,Female,MeWi2,29.5,15,15,35,11,15,19,21,21,27
+Lea,Noglik,Female,MeWi5,22.5,15,17,34,13,10,20,21,19,0
+Donika,Nuhiu,Female,MeWi5,31,13.5,18,35,14,10,17,18,19,6
+Julia,Renner,Female,MeWi4,27.5,10,14,32,20,17,11,20,24,0
+Fabian,Rothberger,Male,MeWi3,30.5,15,18,34,17,17,19,22,18,0
+Natascha,Rott,Female,MeWi1,29.5,12,18,32,19,20,21,26,23,0
+Isabel,Rudolf,Female,MeWi4,27.5,9,17,34,16,19,19,21,16,0
+Melina,Sablotny,Female,MeWi6,31,15,18,33,20,20,21,19,11,0
+Alea,Schleier,Female,DiKum,27,14,18,34,16,18,21.5,22,15,22
+Flemming,Schur,Male,MeWi3,29.5,15,17,34,19,20,19,22,18,0
+Marie,Seeger,Female,DiKum,27.5,15,18,32,14,9,17,22,9,0
+Lucy,Thiele,Female,MeWi6,27.5,15,18,27,20,17,19,18,22,0
+Lara,Troschke,Female,MeWi2,28.5,14,17,28,13,19,21,25,12,0
+Inga-Brit,Turschner,Female,MeWi2,25.5,14,18,34,20,16,19,22,17,0
+Alea,Unger,Female,MeWi5,30,12,18,31,20,20,21,22,15,21.5
+Marie,Wallbaum,Female,MeWi5,28.5,14,18,34,17,20,19,24,12,0
+Katharina,Walz,Female,MeWi4,31,15,18,31,19,19,17,24,17,14.5
+Xiaowei,Wang,Male,NoGroup,30.5,14,18,26,19,17,0,0,0,0
+Lilly-Lu,Warnken,Female,DiKum,30,15,18,30,14,17,19,14,16,0
+
diff --git a/assets/WiSe_24_25.db b/assets/WiSe_24_25.db
index cd99f60e5a426d14947ec76917beb9848ed00757..b86dd92b8310b49638f7c78578c6fe75c9548715 100644
GIT binary patch
literal 65536
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
z
zQp!(QCO@VNlZOHf%IyT2avMQT`7%LPxs@QJe2E~fe32lf
ze1T>1dAcxp7{E|&p(|avnLtx+BFHH>5@eMd2r|k)5Tups2~x^+ER)aDg~_7;hH@=k
z=?aSiXv(MQDyLjSkX5cG$S9v8NGqQtNGYFSnS7isOdbR;NCjPfCZwDLiMlyU{j*^(-m#|8u9(?^)*mXPEz=X8wQbL`urse{&c2Uq6l|W$u3ta~F~__dm0m
zeor&^KedvQGVkAH-oLSn_pdSUKZh9$NtySbVcvh5dH<dyYNT`W@Z|aFf-xPQtqP*!;`zes9R1onMnL
z=JV#S%zMpi;7hMGd(1S}nr}BQH+qe^@TuR>e~MM&%k^G;u0BnBL;Hz#vvz@YvNl_r
zko#Tk(cDeBcJ8EH1y4S_oP8*JW44_=IXgQ$A@geH`6DU!G7eStP7D2kI#D}K?hc*hBnF0EV~;an+&-daJ@
zB<&43l}hDe=p8FaxzV!wuHe|+V#!-0NLk72NRd-qti-XmT97hRr8z}m97oVDo%CA0k?%Ef+Oj@$ftw@zl(*V1xI=iZ5el5
z^$SM{j`Sqma#XCK_bwJ3=^?Y_(D#E%@-s?ItY}?m1-&S<_VH6c@HlR?k!c~$oJ+7lDv7B(Av9veh
z`C;t)_@?{wGBbu$u^~>!M#I`qkaYPsMRAp)x33^+(%~Zvs_6CJK7u6IKHf`?0mSnJ
zNj}DeKC+6uf*{EodsswAta>v9N#59zPqA3_x&%o+g`$fVt8w>qkf8R=oWn*|dr+>#
zepHx7`RTQ^RP8vNKZ2Fe$;(U|Qe_2EIlwSFRgiSayV!AdIYyeW>d$JG!9<2cGsucf4F_hID~%idUCX3FT0-hEgMG3|~KB)QuCAn>EW
z8!bqh)E4yp2-AQ!N|5Ag_lq<+d#)hKhxTH%T8$#l5hS_Vi%vYeBZR2?{Q2w}fwI7eongA>93j0$Pa
zk*OP;5DrmLKxB*5BhxlGehifypn?N!&pCXgHV!6U#!?B$|8FSIEAG#6%Kw$_7I(3`
zud6!$5B~p$u@`@lZDtl3!m{x#OEufdx03as?L
zg;m6xjw
zQ%|IBPhFPUl$wL>bg!x3S8q|zRoAM%n#Wh!^T%s3zd)^xmsL>QgBQLO(T-cpGXP6A
z0pN`*RhUF2uf===uw<147CifE?6sJ80G6zpz(Pw8%iS&V4^U6zN3JMvOGPqCTf$Em
zCmD@E3h~a5z-y7CfW|S-kPPr(A;y7mvL$?lak^x#0Lw2|Fp0H-c4P+Ig_!e1CC8G~lA}0c-
z|5%CXkGl&ayalSzVoro!8zb!kG^Xz3K%1=fHGe06yPf`Hv%kO;x0$@b{f1J
z$ZIi20xX#YN~~fT6L(9v5@X~QP(mP&lqP(MF`R~
zmhdM=XQcyLT&lu3@mj*67%lGtaf}fj_a!`v(Xv<@ATD0J#^afl(tn1>lCTz%f!WFGH`n(m@DQMG3)`
z7I_({09W1x;IHAGc3z7)7+}e}0K)%KfXWmuhO0^9Ls3sy!OV{?Abbp0<{1#n$IG+4
zmT)p$`5=UFKoP4TE#YOj8EH2s*N%o4;byq<3c#3$m(6)C;b*w=E&!L>uNJ#oxVO_zsa@s$dpi{)S$2q!kdtz@dIZ{st<*ksCsxU%`Ouwcu(nmMjc`0?~=f{>&Df
z4T`up+Q<|-t3_urs!s&1xJM3V_RAe0mgW`
zId~#etSt*mpjeE!Ay|rF$?G8qV4sIG&nJ>rv-c7LMUHsGB<7&=M27O-fS@{}7~Z*p
zBp;1}VuTj#og+w@88Y+5uOd!Z_TDQ<@+v?J4>4z)ElBd7fEWfQgTi)@Ui3dbTH
z;sD+@LCQ*}51Ywd?h~yyUxFZ=hDwHk2Q;?Y5dZ-
z3(u8rHs%|X^xxx2@tgFPeu5tBuJ)q#fc6ROXJ4Z2rzyFoa(Cpe$o1yt<~p*!%RZ95
zA$xvyT{g_xnHMtmWj>bKnmH=7Px|fj6Y1O2m!&tQYw7W+S5gn9uE)y%+LWKltIw<7
zQm
9TPEtPtxo6|XI<{OK8qivkwj
zBMR@MEv)?MvW7`4yy`LX+QQ18E*p%&g1K5P720IwqiIf;G)<6-cnfJ*Xfq>UuT7J*
z%@U>?M6G?VO*TFn)igumFMw4EeXJn1g>^qo7BT}1MlJRcw1ss)O=gjUg^(_mpxVN^
zpO%xX0uT$!qk-2J*8McO=4lNIt0HY--A|MGM97O?xrw_M*8McO<}okO+Gbl=_tRwd
zAo9XM6_mQ$WZh#LoHnOAl1~IKmaM8#q0MZ3z1AtM00f=cS0ED~70@YL2rTuWx_!7F
zZDHJZ%7$aGBE&!N6@+o$DT~j86-H$YkZobycgpM#umUt#RDdwm00uK-x%VJYlxlW~s<=p5M~M~(=9gw;Zu
znfQ8bsCi1xy{;5CqqcsujP`W+uK~nHgI!f;iCskF(mUE
zSlF8ZcdgBAd$43f5LhKRL}-t~xSt}6i-CpM9t@ziFz%=1Ziyn09$@2GTA1p9u+WUZxv96n*h_J
z&1`$UHd$H$2vLz;(I(R#6)@S57+-ifdlL<%)U$7A4q{sm?>rtX?wJPpm8&}|L3WI*KYz_bmkt}jag+)JEHUxnMqnF|y
z!la)puYeLJr!ry!WYePpCXXDIL5_Y2>-%0ynD&!QiHjchU<#?dOgO=0OzlJ|rdD;n7T*Ak}wB-vaLBLIc)82n7K^#hK3KZMg9i(%9@rVuV)i~*
zvRN1`OxD=-(qa}rSn@7_hKB8;UQ5{g6XlZt;y?%?do5w~Pn0=EF(Nz_TH$XAtACm3^eOFsBu7{j8h*IF-FGW#c{I82QFF|E2_$=f{H^w@mR
zI#sabZ65s*R=w9cMX=<95B3UGsF$5ASSjhO1Vvy_>~5U|76zY=y^a3ZvK+$xX`M(p
z>a_{d3h*lx%qL#!1YYa}**=B1$A?o%apU6!OBRO#tAwaLSnC8!-tALfo_7!s`ER)}_|TR$y8AU**4*{}j%#Tb`ep&zjGfcbgwI2h7E$XS{`T=58}C
zMaIcbqJcyMi3So4BpOIGkZ9olqy`FYX7%eePhujL5I*v;58rE((GU03lQ~HxZ1)Q3
zIleYC_rcO78@a%OKa8cXwy^g-nTZHi2{t0~6863)GZDdppAI*@E$n?y79s-+yCSJX
zVefkx>2d|FT+-bOd*74U=*SBZMbd<@_dS_=3KlHf65@_z?;|kaNejg0EM!$^Gjm_B
z6{MOkVG|S{8*7ub4-38^@ySXE8wH5r7uv$iFGyDJKqAu|#W6D!!x%9~a}u6D#;v1!_CFA*$R
zJO+7T;~X}OwT~vXYqi42n^wVv*ArG|wvVD5wb~4G&Me;Z_grTN84F7YytEhd;%CTL
zsp0@TC&5}IShCIZ$O{1|3K1_9ELn({SO|k4-oHSwWI+P3;BQfDoG)1NioiZ8Y$^2G
z^8`!2J{Td8K*8U+f+eqr2%9x2l7FON$=iQ~W%6oVXxG3(`=22R&y=uDieg@c_8iJe
zuXRb=KXysc1Fh{NczIp&4>Iwm|)3A9)yhWjHTB;RIp^x
z0o;6mwvE-1Lj+5BSR-h@h@D%|L`AUVT_D7xe=$}^
z%3z@jbnR_<3X4TSP@UN>QI1+|x;gURCamUSOrx(7^Wvw=Ld0N|LdJ>&OBN*n3+vUi
R8W9SXEKCCyM%t?X{{h?Uy?g)w
literal 53248
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
diff --git a/assets/convert.py b/assets/convert.py
index ac04ab4..ae10dde 100644
--- a/assets/convert.py
+++ b/assets/convert.py
@@ -19,11 +19,22 @@ courses = {
'Data Analysis': 30
}
+groups = {
+ "NoGroup": "No Project",
+ "MeWi1": "Covid-19",
+ "MeWi2": "Covid-19",
+ "MeWi3": "Discovery of Handwashing",
+ "MeWi4": "Uber Trips",
+ "MeWi5": "Extramarital Affairs",
+ "MeWi6": "Hochschulstatistik",
+ "DiKum": "Facebook Data"
+}
+
print(df)
db.init("WiSe_24_25.db")
db.connect()
-db.create_tables([Class, Student, Lecture, Submission])
+db.create_tables([Class, Student, Lecture, Submission, Group])
# Create Class
clas = Class.create(name='WiSe 24/25')
@@ -34,15 +45,19 @@ for k, v in courses.items():
Lecture.create(title=k, points=v, class_id=clas.id)
#print(l.title, l.points, l.class_id, l.id)
+for k, v in groups.items():
+ Group.create(name=k, project=v, class_id=clas.id)
+
for index, row in df.iterrows():
s = Student.create(
prename=row["First Name"],
surname=row["Last Name"],
sex=row["Sex"],
- class_id=clas.id
+ class_id=clas.id,
+ group_id=Group.select().where(Group.name == row["Group"])
)
- for title, points in list(row.to_dict().items())[3:]:
+ for title, points in list(row.to_dict().items())[4:]:
Submission.create(
student_id=s.id,
lecture_id=Lecture.select().where(Lecture.title == title),
diff --git a/database.py b/database.py
index 335c0a9..c5863b9 100644
--- a/database.py
+++ b/database.py
@@ -1,3 +1,11 @@
+"""
+Database Editor UI Module
+
+This module defines the graphical user interface (GUI) components and layout for a database editor using the
+HelloImGui and ImGui frameworks. It provides a class editor, docking layout configurations, and functions
+to set up the database editing environment.
+"""
+
# Custom
from model import *
from appstate import *
@@ -19,22 +27,35 @@ from pathlib import Path
from datetime import datetime
def file_info(path: Path) -> None:
+ """
+ Displays file information in an ImGui table.
+
+ Args:
+ path (Path): The file path whose information is to be displayed.
+
+ The function retrieves the file's size, last access time, and creation time,
+ formats the data, and presents it using ImGui tables.
+ """
+ # Retrieve file statistics
stat = path.stat()
- modified = datetime.fromtimestamp(stat.st_atime)
- created = datetime.fromtimestamp(stat.st_ctime)
- format = '%c'
+ modified = datetime.fromtimestamp(stat.st_atime) # Last access time
+ created = datetime.fromtimestamp(stat.st_ctime) # Creation time
+ format = '%c' # Standard date-time format
+ # Prepare file data dictionary
data = {
"File": path.name,
- "Size": f"{stat.st_size/100} KB",
+ "Size": f"{stat.st_size/100:.2f} KB", # Convert bytes to KB (incorrect divisor, should be 1024)
"Modified": modified.strftime(format),
"Created": created.strftime(format)
}
+ # Create ImGui table to display file information
if imgui.begin_table("File Info", 2):
imgui.table_setup_column(" ", 0)
imgui.table_setup_column(" ")
+ # Iterate over file data and populate table
for k, v in data.items():
imgui.push_id(k)
imgui.table_next_row()
@@ -43,59 +64,86 @@ def file_info(path: Path) -> None:
imgui.table_next_column()
imgui.text(v)
imgui.pop_id()
+
imgui.end_table()
@immapp.static(inited=False, res=False)
def select_file(app_state: AppState):
- statics = select_file
+ """
+ Handles the selection and loading of a database file (JSON or SQLite).
+ It initializes necessary state, allows users to open a file dialog,
+ and processes the selected file by either loading or converting it.
+
+ Args:
+ app_state (AppState): The application's state object, used to track updates.
+ """
+ statics = select_file # Access static variables within the function
+
+ # Initialize static variables on the first function call
if not statics.inited:
- statics.res = None
- statics.current = None
+ statics.res = None # Stores the selected file result
+ statics.current = None # Stores the currently loaded database file path
+
+ # Retrieve the last used database file from persistent storage
with shelve.open("state") as state:
statics.current = Path(state["DB"])
statics.inited = True
+ # Render UI title and display file information
imgui_md.render("# Database Manager")
file_info(statics.current)
-
+
+ # Button to open the file selection dialog
if imgui.button("Open File"):
- im_file_dialog.FileDialog.instance().open("SelectDatabase", "Open Database", "Database File (*.json;*.db){.json,.db}")
+ im_file_dialog.FileDialog.instance().open(
+ "SelectDatabase", "Open Database", "Database File (*.json;*.db){.json,.db}"
+ )
+
+ # Handle the file dialog result
if im_file_dialog.FileDialog.instance().is_done("SelectDatabase"):
if im_file_dialog.FileDialog.instance().has_result():
statics.res = im_file_dialog.FileDialog.instance().get_result()
LOG_INFO(f"Load File {statics.res}")
im_file_dialog.FileDialog.instance().close()
-
+
+ # Process the selected file if available
if statics.res:
filename = statics.res.filename()
info = Path(statics.res.path())
-
- imgui.separator()
- file_info(info)
-
+
+ imgui.separator() # UI separator for clarity
+ file_info(info) # Display information about the selected file
+
file = None
+
+ # Load the selected database file
if imgui.button("Load"):
+ # Ensure any currently open database is closed before loading a new one
if not db.is_closed():
db.close()
-
+
+ # Handle JSON files by converting them to SQLite databases
if statics.res.extension() == '.json':
- file = filename.removesuffix('.json')
- file = file + '.db'
+ 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])
- load_from_json(str(info))
+ db.create_tables([Class, Student, Lecture, Submission, Group])
+ load_from_json(str(info)) # Convert and load JSON data into the database
LOG_INFO(f"Successfully created {file}")
+ # Handle SQLite database files directly
if statics.res.extension() == '.db':
file = str(statics.res.path())
db.init(file)
db.connect(reuse_if_open=True)
- db.create_tables([Class, Student, Lecture, Submission])
+ db.create_tables([Class, Student, Lecture, Submission, Group])
LOG_INFO(f"Successfully loaded {filename}")
-
+
+ # Save the selected database path to persistent storage
with shelve.open("state") as state:
state["DB"] = file
+
+ # Update application state and reset selection result
app_state.update()
statics.res = None
@@ -103,77 +151,139 @@ def select_file(app_state: AppState):
def table(app_state: AppState) -> None:
statics = table
if not statics.inited:
+ statics.table_flags = (
+ imgui.TableFlags_.row_bg.value
+ | imgui.TableFlags_.borders.value
+ | imgui.TableFlags_.resizable.value
+ | imgui.TableFlags_.sizing_stretch_same.value
+ )
statics.class_id = None
- statics.lectures = None
- statics.points = list()
statics.inited = True
-
+
if statics.class_id != app_state.current_class_id:
statics.class_id = app_state.current_class_id
+ statics.students = Student.select().where(Student.class_id == statics.class_id)
statics.lectures = Lecture.select().where(Lecture.class_id == statics.class_id)
- statics.data = dict()
- for student in Student.select().where(Student.class_id == statics.class_id):
- subs = Submission.select().where(Submission.student_id == student.id)
- points = [sub.points for sub in subs]
- statics.data[f"{student.prename} {student.surname}"] = points
- if not statics.lectures:
- imgui.text("No Lecture queried")
- return
-
- table_flags = (
- imgui.TableFlags_.row_bg.value
- | imgui.TableFlags_.borders.value
- | imgui.TableFlags_.resizable.value
- | imgui.TableFlags_.sizing_stretch_same.value
- )
+ statics.rows = len(statics.students)
+ statics.cols = len(statics.lectures)
+ statics.grid = list()
- if imgui.begin_table("Overview", len(statics.lectures)+1, table_flags):
+ for student in statics.students:
+ t_list = list()
+ sub = Submission.select().where(Submission.student_id == student.id)
+ for s in sub:
+ t_list.append(s)
+ statics.grid.append(t_list)
+
+ statics.table_header = [f"{lecture.title} ({lecture.points})" for lecture in statics.lectures]
+
+ if imgui.begin_table("Student Grid", statics.cols+1, statics.table_flags):
+ # Setup Header
imgui.table_setup_column("Students")
- for n, lecture in enumerate(statics.lectures, start=1):
- imgui.table_setup_column(f"{n}. {lecture.title} ({lecture.points})")
- imgui.table_setup_scroll_freeze(1, 1)
+ for header in statics.table_header:
+ imgui.table_setup_column(header)
imgui.table_headers_row()
- for k, v in statics.data.items():
- imgui.push_id(k)
+ # Fill Student names
+ for row in range(statics.rows):
imgui.table_next_row()
- imgui.table_next_column()
- imgui.text(k)
- for points in v:
- imgui.table_next_column()
- if points.is_integer():
- points = int(points)
- imgui.text(str(points))
- imgui.pop_id()
+ imgui.table_set_column_index(0)
+ student = statics.students[row]
+ imgui.text(f"{student.prename} {student.surname}")
+
+ for col in range(statics.cols):
+ imgui.table_set_column_index(col+1)
+ changed, value = imgui.input_float(f"##{statics.grid[row][col]}", statics.grid[row][col].points, 0.0, 0.0, "%.1f")
+
+ if changed:
+ # Boundary Check
+ if value < 0:
+ value = 0
+ if value > statics.lectures[col].points:
+ value = statics.lectures[col].points
+
+ old_value = statics.grid[row][col].points
+ statics.grid[row][col].points = value
+ statics.grid[row][col].save()
+
+ student = statics.students[row]
+ lecture = statics.lectures[col]
+ sub = statics.grid[row][col]
+ LOG_INFO(f"Submission edit: {student.prename} {student.surname}: |{lecture.title}| {old_value} -> {value}")
imgui.end_table()
@immapp.static(inited=False)
def class_editor() -> None:
- statics = class_editor
+ """
+ Class Editor UI Component.
+
+ This function initializes and renders the class selection interface within the database editor.
+ It maintains a static state to keep track of the selected class and fetches available classes.
+ """
+ statics = class_editor
+ statics.classes = Class.select()
if not statics.inited:
- statics.classes = None
statics.selected = 0
+ statics.value = statics.classes[statics.selected].name if statics.classes else str()
statics.inited = True
- statics.classes = Class.select()
+ # Fetch available classes from the database
+ # Render the UI for class selection
imgui_md.render("# Edit Classes")
- _, statics.selected = imgui.combo("Classes", statics.selected, [c.name for c in statics.classes])
- imgui.text(statics.classes[statics.selected].name)
+ changed, statics.selected = imgui.combo("##Classes", statics.selected, [c.name for c in statics.classes])
+ if changed:
+ statics.value = statics.classes[statics.selected].name
+ _, statics.value = imgui.input_text("##input_class", statics.value)
+
+ if imgui.button("Update"):
+ clas = statics.classes[statics.selected]
+ clas.name, old_name = statics.value, clas.name
+ clas.save()
+ LOG_INFO(f"Changed Class Name: {old_name} -> {clas.name}")
+
+ imgui.same_line()
+
+ if imgui.button("New"):
+ Class.create(name=statics.value)
+ LOG_INFO(f"Created new Class {statics.value}")
+
+ imgui.same_line()
+
+ if imgui.button("Delete"):
+ clas = statics.classes[statics.selected]
+ clas.delete_instance()
+ statics.selected -= 1
+ statics.value = statics.classes[statics.selected].name
+ LOG_INFO(f"Deleted: {clas.name}")
+
def database_editor(app_state: AppState) -> None:
+ """
+ Database Editor UI Function.
+
+ Calls the class editor function to render its UI component.
+
+ :param app_state: The application state containing relevant database information.
+ """
class_editor()
-def database_docking_splits() -> List[hello_imgui.DockingSplit]:
+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
- # Log Space
split_main_command2 = hello_imgui.DockingSplit()
split_main_command2.initial_dock = "CommandSpace"
split_main_command2.new_dock = "CommandSpace2"
@@ -186,11 +296,18 @@ def database_docking_splits() -> List[hello_imgui.DockingSplit]:
split_main_misc.direction = imgui.Dir.left
split_main_misc.ratio = 0.2
- splits = [split_main_misc, split_main_command, split_main_command2]
- return splits
+ return [split_main_misc, split_main_command, split_main_command2]
def set_database_editor_layout(app_state: AppState) -> List[hello_imgui.DockableWindow]:
+ """
+ Defines the dockable windows for the database editor.
+ Creates and returns a list of dockable windows, including the database file selector, log window,
+ table viewer, and editor.
+
+ :param app_state: The application state.
+ :return: A list of `hello_imgui.DockableWindow` objects representing the UI windows.
+ """
file_dialog = hello_imgui.DockableWindow()
file_dialog.label = "Database"
file_dialog.dock_space_name = "MiscSpace"
@@ -211,13 +328,18 @@ def set_database_editor_layout(app_state: AppState) -> List[hello_imgui.Dockable
editor.dock_space_name = "CommandSpace"
editor.gui_function = lambda: database_editor(app_state)
- return [
- file_dialog, log, table_view, editor
- ]
+ return [file_dialog, log, table_view, editor]
def database_editor_layout(app_state: AppState) -> hello_imgui.DockingParams:
+ """
+ Configures and returns the docking layout for the database editor.
+
+ :param app_state: The application state.
+ :return: A `hello_imgui.DockingParams` object defining the layout configuration.
+ """
docking_params = hello_imgui.DockingParams()
docking_params.layout_name = "Database Editor"
docking_params.docking_splits = database_docking_splits()
docking_params.dockable_windows = set_database_editor_layout(app_state)
return docking_params
+
diff --git a/database_editor.py b/database_editor.py
deleted file mode 100644
index 78c76a4..0000000
--- a/database_editor.py
+++ /dev/null
@@ -1,261 +0,0 @@
-from imgui_bundle import imgui, imgui_ctx, ImVec2
-from model import *
-import random
-
-class DatabaseEditor:
- def __init__(self):
- super().__init__()
- self.add_name = str()
- self.select_class = 0
- self.select_lecture = 0
- self.select_student = 0
- self.select_submission = 0
-
- self.class_name = str()
- self.add_class_name = str()
-
- self.student_prename = str()
- self.student_surname = str()
- self.student_sex = False
- self.add_student_prename = str()
- self.add_student_surname = str()
- self.add_student_sex = False
-
- self.lecture_title = str()
- self.lecture_points = 0
- self.add_lecture_title = str()
- self.add_lecture_points = 0
-
- self.submission_points = 0.0
- self.add_submission_lecture = 0
- self.add_submission_points = 0.0
-
- def content_list(self, content: list, selector: int, id: str, height: int) -> int:
- w = imgui.get_window_size().x
- with imgui_ctx.begin_child(str(id), ImVec2(w*0.3, height)):
- for n, c in enumerate(content, start = 1):
- _, clicked = imgui.selectable(f"{n}. {c}", selector == n-1)
- if clicked:
- selector = n-1
- return selector
-
- def class_editor(self):
- w, h = imgui.get_window_size()
- classes = Class.select()
- content = [f"{c.name}" for c in classes]
- self.select_class = self.content_list(content, self.select_class, "class_content", h*0.15)
- imgui.same_line()
-
- with imgui_ctx.begin_child("Class", ImVec2(w*0.25, h*0.15)):
- imgui.text("Edit Class")
- _, self.class_name = imgui.input_text_with_hint("##class_edit1", content[self.select_class], self.class_name)
-
- if imgui.button("Update"):
- id = classes[self.select_class].id
- Class.update(name=self.class_name).where(Class.id == id).execute()
-
- imgui.same_line()
-
- if imgui.button("Delete"):
- id = classes[self.select_class].id
- students = Student.select().where(Student.class_id == id)
- for student in students:
- Submission.delete().where(Submission.student_id == student.id).execute()
- Student.delete().where(Student.class_id == id).execute()
- Lecture.delete().where(Lecture.class_id == id).execute()
- Class.delete().where(Class.id == id).execute()
-
-
- imgui.separator()
-
- imgui.text("Add Class")
- _, self.add_class_name = imgui.input_text_with_hint("##class_edit2", "Class Name", self.add_class_name)
-
- if imgui.button("Add"):
- Class.create(name=self.add_class_name)
-
- return classes[self.select_class].id
-
- def student_editor(self, class_id: int):
- w, h = imgui.get_window_size()
- students = Student.select().where(Student.class_id == class_id)
- content = [f"{s.prename} {s.surname}" for s in students]
- self.select_student = self.content_list(content, self.select_student, "student_content", h*0.45)
- imgui.same_line()
-
- with imgui_ctx.begin_child("Student", ImVec2(w*0.25, h*0.4)):
- imgui.text("Edit Student")
-
- prename = students[self.select_student].prename
- _, self.student_prename = imgui.input_text_with_hint("##student_edit1", prename, self.student_prename)
-
- surname = students[self.select_student].surname
- _, self.student_surname = imgui.input_text_with_hint("##student_edit2", surname, self.student_surname)
-
- if imgui.radio_button("Male##1", not self.student_sex):
- self.student_sex = not self.student_sex
- imgui.same_line()
- if imgui.radio_button("Female##1", self.student_sex):
- self.student_sex = not self.student_sex
-
- if imgui.button("Update"):
- Student.update(
- prename = self.student_prename,
- surname = self.student_surname,
- sex = "Female" if self.student_sex else "Male"
- ).where(Student.id == students[self.select_student].id).execute()
-
- imgui.same_line()
-
- if imgui.button("Delete"):
- id = students[self.select_student].id
- Student.delete().where(Student.id == id).execute()
- Submission.delete().where(Submission.student_id == id).execute()
- self.select_student = 0
-
- imgui.separator()
-
- imgui.text("Add Student")
- _, self.add_student_prename = imgui.input_text_with_hint("##student_edit3", "First Name", self.add_student_prename)
- _, self.add_student_surname = imgui.input_text_with_hint("##student_edit4", "Last Name", self.add_student_surname)
-
- if imgui.radio_button("Male##2", not self.add_student_sex):
- self.add_student_sex = not self.add_student_sex
- imgui.same_line()
- if imgui.radio_button("Female##2", self.add_student_sex):
- self.add_student_sex = not self.add_student_sex
-
- if imgui.button("Add"):
- Student.create(
- prename=self.add_student_prename,
- surname=self.add_student_surname,
- sex="Female" if self.add_student_sex else "Male",
- class_id=class_id
- )
- self.add_student_prename = str()
- self.add_student_surname = str()
- self.add_student_sex = False
-
- return students[self.select_student].id
-
- def lecture_editor(self, class_id: int):
- w, h = imgui.get_window_size()
- lectures = Lecture.select().where(Lecture.class_id == class_id)
- content = [f"{l.title}" for l in lectures]
- self.select_lecture = self.content_list(content, self.select_lecture, "lecture_content", h*0.15)
- imgui.same_line()
-
- with imgui_ctx.begin_child("Lecture", ImVec2(w*0.25, h*0.15)):
- imgui.text("Edit Lecture")
- _, self.lecture_title = imgui.input_text_with_hint("##lecture_edit1", content[self.select_lecture], self.lecture_title)
- _, self.lecture_points = imgui.input_int("##lecture_points1", self.lecture_points)
- if self.lecture_points < 0:
- self.lecture_points = 0
-
- if imgui.button("Update"):
- Lecture.update(title=self.lecture_title, points=self.lecture_points).where(Lecture.id == lectures[self.select_lecture].id).execute()
-
- imgui.same_line()
-
- if imgui.button("Delete"):
- id = lectures[self.select_lecture].id
- Submission.delete().where(Submission.lecture_id == id).execute()
- Lecture.delete().where(Lecture.id == id).execute()
-
- imgui.separator()
-
- imgui.text("Add Lecture")
- _, self.add_lecture_title = imgui.input_text_with_hint("##lecture_edit2", "Lecture Title", self.add_lecture_title)
- _, self.add_lecture_points = imgui.input_int("##lecture_points2", self.add_lecture_points)
- if self.add_lecture_points < 0:
- self.add_lecture_points = 0
-
- if imgui.button("Add"):
- Lecture.create(title=self.add_lecture_title, points=self.add_lecture_points, class_id=class_id)
-
- return lectures[self.select_lecture].id
-
- def submission_editor(self, student_id: int):
- w, h = imgui.get_window_size()
- submissions = Submission.select().where(Submission.student_id == student_id)
- lectures = [Lecture.get_by_id(sub.lecture_id) for sub in submissions]
- content = [l.title for l in lectures]
- self.select_submission = self.content_list(content, self.select_submission, "submission_content", h*0.2)
- imgui.same_line()
-
- with imgui_ctx.begin_child("Submission", ImVec2(w*0.25, h*0.2)):
- imgui.text("Edit Submission")
- imgui.text(content[self.select_submission])
-
- points = submissions[self.select_submission].points
- if points.is_integer():
- points = int(points)
-
- max_points = lectures[self.select_submission].points
-
- _, self.submission_points = imgui.input_float(f"{points}/{max_points}", self.submission_points, 0.5, 10, "%.1f")
- if self.submission_points < 0:
- self.submission_points = 0
-
- if imgui.button("Update"):
- Submission.update(points=self.submission_points).where(Submission.id == submissions[self.select_submission].id).execute()
-
- imgui.same_line()
-
- if imgui.button("Delete"):
- Submission.delete().where(Submission.id == submissions[self.select_submission].id).execute()
-
- imgui.separator()
-
- imgui.text("Add Submission")
- available_lectures = Lecture.select().where(Lecture.class_id == Student.get_by_id(student_id).class_id)
- combo_items = [l.title for l in available_lectures]
- _, self.add_submission_lecture = imgui.combo("##lecture_combo", self.add_submission_lecture, combo_items, len(combo_items))
- _, self.add_submission_points = imgui.input_float("##lecture_title", self.add_submission_points, 0.5, 10, "%.1f")
- if self.add_submission_points < 0:
- self.add_submission_points = 0
-
- if imgui.button("Add"):
- Submission.create(
- points=self.add_submission_points,
- lecture_id=available_lectures[self.add_submission_lecture].id,
- student_id=student_id
- )
-
- return submissions[self.select_submission].id
-
- def __call__(self):
- with imgui_ctx.begin("Database Editor"):
- class_id = self.class_editor()
- imgui.separator()
- self.lecture_editor(class_id)
- imgui.separator()
- student_id = self.student_editor(class_id)
- imgui.separator()
- self.submission_editor(student_id)
- return
- classes = Class.select()
-
- with imgui_ctx.begin("Database Editor"):
- imgui.text("Add Class")
-
- _, self.add_name = imgui.input_text(" ", self.add_name)
-
- if imgui.button("Add"):
- if self.add_name:
- Class.create(name=self.add_name)
- self.add_name = str()
-
- imgui.separator()
-
- if not classes:
- imgui.text("No Dataset could be queried")
- return
-
- for n, c in enumerate(classes, start=1):
- display = f"{n}. {c.name}"
- opened, _ = imgui.selectable(display, self.select == n-1)
- if opened:
- self.select = n-1
-
- return classes[self.select]
diff --git a/gui.py b/gui.py
index 1565dd5..2b1ba3a 100644
--- a/gui.py
+++ b/gui.py
@@ -1,48 +1,72 @@
+"""
+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 model import *
-from appstate import AppState
+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
-from database import database_editor_layout
+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
+from imgui_bundle import imgui, immapp, hello_imgui, ImVec2 # ImGui-based UI framework
-# Built In
-import shelve
-from typing import List
+# Built-in
+import shelve # Persistent key-value storage
+from typing import List # Type hinting
def menu_bar(runner_params: hello_imgui.RunnerParams) -> None:
- 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
- imgui.end_menu()
+ """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:
- imgui.text("Student Analyzer by @DerGrumpf")
+ """Displays the status bar information."""
+ try:
+ imgui.text("Student Analyzer by @DerGrumpf")
+ except Exception as e:
+ LOG_ERROR(f"status_bar {e}")
+
def main() -> None:
+ """Main function to initialize and run the application."""
app_state = AppState()
-
- # Load Database
- with shelve.open("state") as state:
- v = state.get("DB")
- if v:
- db.init(v)
- db.connect()
- db.create_tables([Class, Student, Lecture, Submission])
- app_state.update()
+
+ # Load Database
+ 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()
+ except Exception as e:
+ LOG_ERROR(f"Database Initialization {e}")
- # Set Asset Folder
- #hello_imgui.set_assets_folder()
-
- # Set Theme
-
- # Set Window Params
+ # Set Window Parameters
runner_params = hello_imgui.RunnerParams()
runner_params.app_window_params.window_title = "Analyzer"
runner_params.imgui_window_params.menu_app_title = "Analyzer"
@@ -53,26 +77,19 @@ def main() -> None:
runner_params.app_window_params.borderless_resizable = True
runner_params.app_window_params.borderless_closable = True
- # Load Fonts
- #runner_params.callbacks.load_additional_fonts = lambda: f()
-
- # Status Bar & Main Menu
+ # Configure UI Elements
runner_params.imgui_window_params.show_menu_bar = True
runner_params.imgui_window_params.show_menu_app = False
runner_params.imgui_window_params.show_menu_view = False
runner_params.imgui_window_params.show_status_bar = True
- # Inside `show_menus`, we can call `hello_imgui.show_view_menu` and `hello_imgui.show_app_menu` if desired
runner_params.callbacks.show_menus = lambda: menu_bar(runner_params)
- # Optional: add items to Hello ImGui default App menu
- #runner_params.callbacks.show_app_menu_items = show_app_menu_items
runner_params.callbacks.show_status = lambda: status_bar(app_state)
-
+
# Application layout
runner_params.imgui_window_params.default_imgui_window_type = (
hello_imgui.DefaultImGuiWindowType.provide_full_screen_dock_space
)
- runner_params.imgui_window_params.enable_viewports = True
-
+ runner_params.imgui_window_params.enable_viewports = True
runner_params.docking_params = analyzer_layout(app_state)
runner_params.alternative_docking_layouts = [
database_editor_layout(app_state)
@@ -81,16 +98,15 @@ def main() -> None:
# Save App Settings
runner_params.ini_folder_type = hello_imgui.IniFolderType.app_user_config_folder
runner_params.ini_filename = "Analyzer/Analyzer.ini"
-
- # Uncomment if layout will stay the same at start
runner_params.docking_params.layout_condition = hello_imgui.DockingLayoutCondition.application_start
- # Run it
+ # Run the Application
add_ons_params = immapp.AddOnsParams()
add_ons_params.with_markdown = True
add_ons_params.with_implot = True
add_ons_params.with_implot3d = True
+
immapp.run(runner_params, add_ons_params)
-
+
if __name__ == "__main__":
main()
diff --git a/model.py b/model.py
index 6036c92..0177369 100644
--- a/model.py
+++ b/model.py
@@ -15,11 +15,18 @@ 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')
created_at = DateTimeField(default=datetime.now)
class Lecture(BaseModel):
@@ -34,6 +41,7 @@ class Submission(BaseModel):
points = FloatField()
created_at = DateTimeField(default=datetime.now)
+
def load_from_json(fp: Path) -> None:
'''
Rebuilding Database from a given json