From c320b2766486d5e53ad8a31bb7c777d3bd65a0fd Mon Sep 17 00:00:00 2001
From: DerGrumpf
Date: Fri, 7 Feb 2025 14:44:03 +0100
Subject: [PATCH] Changed: Projects
---
analyzer.py | 185 +++++++++------------------------
assets/Student_list.csv | 73 ++++++-------
assets/WiSe_24_25.db | Bin 65536 -> 65536 bytes
assets/convert.py | 5 +-
grader/tests/base_grader.py | 76 ++++++++++++++
grader/valuation.py | 197 ++++++++++++++++++++++++++++++++++++
model.py | 1 +
7 files changed, 361 insertions(+), 176 deletions(-)
create mode 100644 grader/tests/base_grader.py
create mode 100644 grader/valuation.py
diff --git a/analyzer.py b/analyzer.py
index 4a50928..badbdc8 100644
--- a/analyzer.py
+++ b/analyzer.py
@@ -1,6 +1,7 @@
# Custom
from model import *
from appstate import AppState
+from grader.valuation import *
# External
from imgui_bundle import (
@@ -22,129 +23,56 @@ from numpy.typing import NDArray
# Built In
from typing import List, Any
-def student_list(app_state: AppState) -> None:
- statics = student_list
- if not app_state.current_class_id:
- imgui.text("No Class found in Database")
- return
+@immapp.static(inited=False)
+def select_class(app_state: AppState) -> None:
+ statics = select_class
- if not hasattr(statics, "select"):
- statics.select = 0
-
- statics.students = Student.select().where(Student.class_id == app_state.current_class_id)
- statics.students = statics.students if statics.students else None
- if not statics.students:
- imgui.text(f"No Stundents found in {Class.get_by_id(app_state.current_class_id).name}")
+ if not app_state.current_class_id:
+ imgui.text("No class found in Database")
return
+ if not statics.inited:
+ statics.select = 0
+ statics.classes = Class.select()
+ statics.labels = [c.name for c in statics.classes]
+ statics.inited = True
+
+ changed, statics.select = imgui.combo("Classes", statics.select, statics.labels)
+ if changed:
+ app_state.current_class_id = statics.classes[statics.select].id
+
+@immapp.static(inited=False)
+def select_student(app_state: AppState) -> None:
+ statics = select_student
+
+ if not app_state.current_class_id:
+ return
+
+ if not statics.inited:
+ statics.select = 0
+ statics.students = Student.select().where(Student.class_id == app_state.current_class_id)
+ statics.inited = True
+
+ if not statics.students:
+ imgui.text("No Studends found")
+ return
+
for n, student in enumerate(statics.students, start=1):
display = f"{n}. {student.prename} {student.surname}"
_, clicked = imgui.selectable(display, statics.select == n-1)
if clicked:
statics.select = n-1
-
- app_state.current_student_id = statics.students[statics.select].id
+ 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
+
+@immapp.static(inited=False)
+def student_list(app_state: AppState) -> None:
+ statics = student_list
- if not hasattr(statics, "select"):
- statics.select = 0
+ select_class(app_state)
+ imgui.separator()
+ select_student(app_state)
- 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
-
- if not app_state.current_class_id:
- imgui.text("No class found in Database")
- return
-
- lectures = Lecture.select().where(Lecture.class_id == app_state.current_class_id)
-
- if not lectures:
- imgui.text(f"No Lectures found for {Class.get_by_id(app_state.current_class_id).name}")
- return
-
- if not hasattr(statics, "select"):
- statics.select = 0
-
- for n, lecture in enumerate(lectures, start=1):
- display = f"{n}. {lecture.title}"
- _, clicked = imgui.selectable(display, statics.select == n-1)
- if clicked:
- statics.select = n-1
-
- app_state.current_lecture_id = lectures[statics.select].id
-
-def class_list(app_state: AppState) -> None:
- statics = class_list
-
- if db.is_closed():
- imgui.text("No Database loaded")
- return
-
- classes = Class.select() if db else None
- if not classes:
- imgui.text("No Classes currently in Database")
- return
-
- if not hasattr(statics, "select"):
- statics.select = 0
-
- for n, clas in enumerate(classes, start=1):
- display = f"{n}. {clas.name}"
- _, clicked = imgui.selectable(display, statics.select == n-1)
- if clicked:
- statics.select = n-1
-
- app_state.current_class_id = classes[statics.select].id
-
-def submissions_list(app_state: AppState) -> None:
- statics = submissions_list
-
- if not app_state.current_lecture_id:
- imgui.text("No Lecture found")
- return
- if not app_state.current_student_id:
- imgui.text("No Student found")
- return
-
- submissions = Submission.select().where(Submission.lecture_id == app_state.current_lecture_id and Submission.student_id == app_state.current_student_id)
-
- if not submissions:
- student = Student.get_by_id(app_state.current_student_id)
- lecture = Lecture.get_by_id(app_state.current_lecture_id)
- imgui.text(f"{student.prename} {student.surname} didn't submitted for {lecture.title}")
- return
-
- if not hasattr(statics, "select"):
- statics.select = 0
-
- for n, sub in enumerate(submissions, start=1):
- lecture = Lecture.get_by_id(sub.lecture_id)
- points = sub.points
- if points.is_integer():
- points = int(points)
- display = f"{n}. {lecture.title} {points}/{lecture.points}"
- _, clicked = imgui.selectable(display, statics.select == n-1)
- if clicked:
- statics.select = n-1
-
- app_state.current_submission_id = submissions[statics.select].id
def plot_bar_line_percentage(data: np.array, labels: list, avg: float) -> None:
if not data.size > 0:
@@ -212,6 +140,8 @@ def student_graph(app_state: AppState) -> None:
statics.points = np.sum(statics.sub_points)
if statics.points.is_integer():
statics.points = int(statics.points)
+ statics.grader = get_grader("Oberstufe")
+ #statics.grader = get_grader(statics.student.grader)
statics.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)]
@@ -219,7 +149,7 @@ def student_graph(app_state: AppState) -> None:
w, h = imgui.get_window_size()
imgui_md.render(f"# {statics.student.prename} {statics.student.surname} ({statics.group.name})")
- imgui_md.render(f"### {statics.points}/{statics.max_points}")
+ imgui_md.render(f"### {statics.points}/{statics.max_points} ({statics.student.grader})")
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)
@@ -227,8 +157,8 @@ def student_graph(app_state: AppState) -> None:
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
- imgui.text_colored(COLOR, f"{n}. {lecture.title}")
+ COLOR = statics.grader.get_grade_color(points, lecture.points)
+ imgui.text_colored(COLOR, f"{n}. {lecture.title} {points}/{lecture.points} ({statics.grader.get_grade(points, lecture.points)}) ")
@immapp.static(inited=False)
def sex_graph(app_state: AppState) -> None:
@@ -415,26 +345,6 @@ def set_analyzer_layout(app_state: AppState) -> List[hello_imgui.DockableWindow]
student_selector.label = "Students"
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"
- lecture_selector.gui_function = lambda: lecture_list(app_state)
-
- class_selector = hello_imgui.DockableWindow()
- class_selector.label = "Classes"
- class_selector.dock_space_name = "CommandSpace2"
- class_selector.gui_function = lambda: class_list(app_state)
-
- submission_selector = hello_imgui.DockableWindow()
- submission_selector.label = "Submissions"
- submission_selector.dock_space_name = "CommandSpace"
- submission_selector.gui_function = lambda: submissions_list(app_state)
student_info = hello_imgui.DockableWindow()
student_info.label = "Student Analyzer"
@@ -452,10 +362,9 @@ def set_analyzer_layout(app_state: AppState) -> List[hello_imgui.DockableWindow]
student_ranking.gui_function = lambda: ranking(app_state)
return [
- class_selector, student_selector,
- lecture_selector, submission_selector,
+ student_selector,
student_info, sex_info,
- student_ranking, group_selector
+ student_ranking,
]
def analyzer_layout(app_state: AppState) -> hello_imgui.DockingParams:
diff --git a/assets/Student_list.csv b/assets/Student_list.csv
index fa13c8e..5271b0d 100644
--- a/assets/Student_list.csv
+++ b/assets/Student_list.csv
@@ -1,37 +1,38 @@
-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
+First Name,Last Name,Sex,Group,Grader,Tutorial 1,Tutorial 2,Extended Applications,Numpy & MatPlotLib,SciPy,Monte Carlo,Pandas & Seaborn,Folium,Statistical Test Methods,Data Analysis
+Abdalaziz,Abunjaila,Male,DiKum,30 Percent,30.5,15,18,28,17,17,17,22,0,18
+Marleen,Adolphi,Female,MeWi6,30 Percent,29.5,15,18,32,19,20,17,24,23,0
+Sarina,Apel,Female,MeWi1,30 Percent,28.5,15,18,32,20,20,21,24,20,23
+Skofiare,Berisha,Female,DiKum,30 Percent,29.5,13,18,34,20,17,20,26,16,0
+Aurela,Brahimi,Female,MeWi2,30 Percent,17.5,15,15.5,26,16,17,19,16,0,0
+Cam Thu,Do,Female,MeWi3,30 Percent,31,15,18,34,19,20,21.5,22,12,0
+Nova,Eib,Female,MeWi4,30 Percent,31,15,15,34,20,20,21,27,19,21
+Lena,Fricke,Female,MeWi4,30 Percent,0,0,0,0,0,0,0,0,0,0
+Nele,Grundke,Female,MeWi6,30 Percent,23.5,13,16,28,20,17,21,18,22,11
+Anna,Grünewald,Female,MeWi3,30 Percent,12,14,16,29,16,15,19,9,0,0
+Yannik,Haupt,Male,NoGroup,30 Percent,18,6,14,21,13,2,9,0,0,0
+Janna,Heiny,Female,MeWi1,30 Percent,30,15,18,33,18,20,22,25,24,30
+Milena,Krieger,Female,MeWi1,30 Percent,30,15,18,33,20,20,21.5,26,20,22
+Julia,Limbach,Female,MeWi6,30 Percent,27.5,12,18,29,11,19,17.5,26,24,28
+Viktoria,Litza,Female,MeWi5,30 Percent,21.5,15,18,27,13,20,22,21,21,30
+Leonie,Manthey,Female,MeWi1,30 Percent,28.5,14,18,29,20,10,18,23,16,28
+Izabel,Mike,Female,MeWi2,30 Percent,29.5,15,15,35,11,15,19,21,21,27
+Lea,Noglik,Female,MeWi5,30 Percent,22.5,15,17,34,13,10,20,21,19,6
+Donika,Nuhiu,Female,MeWi5,30 Percent,31,13.5,18,35,14,10,17,18,19,8
+Julia,Renner,Female,MeWi4,30 Percent,27.5,10,14,32,20,17,11,20,24,14
+Fabian,Rothberger,Male,MeWi3,30 Percent,30.5,15,18,34,17,17,19,22,18,30
+Natascha,Rott,Female,MeWi1,30 Percent,29.5,12,18,32,19,20,21,26,23,26
+Isabel,Rudolf,Female,MeWi4,30 Percent,27.5,9,17,34,16,19,19,21,16,14
+Melina,Sablotny,Female,MeWi6,30 Percent,31,15,18,33,20,20,21,19,11,28
+Alea,Schleier,Female,DiKum,30 Percent,27,14,18,34,16,18,21.5,22,15,22
+Flemming,Schur,Male,MeWi3,30 Percent,29.5,15,17,34,19,20,19,22,18,27
+Marie,Seeger,Female,DiKum,30 Percent,27.5,15,18,32,14,9,17,22,9,25
+Lucy,Thiele,Female,MeWi6,30 Percent,27.5,15,18,27,20,17,19,18,22,25
+Lara,Troschke,Female,MeWi2,30 Percent,28.5,14,17,28,13,19,21,25,12,24
+Inga-Brit,Turschner,Female,MeWi2,30 Percent,25.5,14,18,34,20,16,19,22,17,30
+Alea,Unger,Female,MeWi5,30 Percent,30,12,18,31,20,20,21,22,15,21.5
+Marie,Wallbaum,Female,MeWi5,30 Percent,28.5,14,18,34,17,20,19,24,12,22
+Katharina,Walz,Female,MeWi4,30 Percent,31,15,18,31,19,19,17,24,17,14.5
+Xiaowei,Wang,Male,NoGroup,30 Percent,30.5,14,18,26,19,17,0,0,0,0
+Lilly-Lu,Warnken,Female,DiKum,30 Percent,30,15,18,30,14,17,19,14,16,24
+,,,,,,,,,,,,,,
diff --git a/assets/WiSe_24_25.db b/assets/WiSe_24_25.db
index b86dd92b8310b49638f7c78578c6fe75c9548715..8e2e87587de8f5e50c15903ef23e6a37545dcda0 100644
GIT binary patch
literal 65536
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
MEKdU#J^U5_AN!y-+5i9m
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
diff --git a/assets/convert.py b/assets/convert.py
index ae10dde..7c31ae1 100644
--- a/assets/convert.py
+++ b/assets/convert.py
@@ -54,10 +54,11 @@ for index, row in df.iterrows():
surname=row["Last Name"],
sex=row["Sex"],
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"]
)
- for title, points in list(row.to_dict().items())[4:]:
+ for title, points in list(row.to_dict().items())[5:]:
Submission.create(
student_id=s.id,
lecture_id=Lecture.select().where(Lecture.title == title),
diff --git a/grader/tests/base_grader.py b/grader/tests/base_grader.py
new file mode 100644
index 0000000..94e8d85
--- /dev/null
+++ b/grader/tests/base_grader.py
@@ -0,0 +1,76 @@
+import sys
+sys.path.append("..")
+
+from valuation import BaseGrading
+
+# Testing
+import unittest
+from unittest.mock import patch
+class TestBaseGrading(unittest.TestCase):
+ test_schema = {"Grade1": 0.1, "Grade2": 0.3}
+
+ @patch.multiple(BaseGrading, __abstractmethods__=set())
+ def get_base_grader(self):
+ return BaseGrading(self.test_schema, "TestGrader")
+
+ def test_getter(self):
+ grader = self.get_base_grader()
+ self.assertEqual(grader.get("Grade1"), self.test_schema["Grade1"])
+ self.assertEqual(grader.get("grade1"), self.test_schema["Grade1"])
+
+ def test_len(self):
+ grader = self.get_base_grader()
+ self.assertEqual(len(grader), len(self.test_schema))
+
+ def test_contains(self):
+ grader = self.get_base_grader()
+ self.assertTrue(0.1 in grader)
+ self.assertTrue(0.9 in grader)
+ self.assertFalse(100 in grader)
+ self.assertFalse(None in grader)
+ self.assertTrue("Grade1" in grader)
+ self.assertTrue("gRADE2" in grader)
+
+ def test_iter(self):
+ grader = self.get_base_grader()
+ for grade, test in zip(grader, self.test_schema):
+ self.assertEqual(grade, test)
+
+ def test_reversed(self):
+ grader = self.get_base_grader()
+ for grade, test in zip(reversed(grader), reversed(self.test_schema)):
+ self.assertEqual(grade, test)
+
+ def test_str(self):
+ grader = self.get_base_grader()
+ self.assertEqual(str(grader), "TestGrader")
+
+ def test_repr(self):
+ grader = self.get_base_grader()
+ self.assertEqual(repr(grader), f"")
+
+ def test_eq(self):
+ grader = self.get_base_grader()
+ self.assertTrue(grader == grader)
+ self.assertTrue(grader != grader)
+
+ def test_keys(self):
+ grader = self.get_base_grader()
+ for k1, t1 in zip(grader.keys(), self.test_schema.keys()):
+ self.assertEqual(k1, t1)
+
+ def test_items(self):
+ grader = self.get_base_grader()
+ for v1, t1 in zip(grader.values(), self.test_schema.values()):
+ self.assertEqual(v1, t1)
+
+ def test_items(self):
+ grader = self.get_base_grader()
+ for g1, t1 in zip(grader.items(), self.test_schema.items()):
+ k, v = g1
+ tk, tv = t1
+ self.assertEqual(k, tk)
+ self.assertEqual(v, tv)
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/grader/valuation.py b/grader/valuation.py
new file mode 100644
index 0000000..ef42f74
--- /dev/null
+++ b/grader/valuation.py
@@ -0,0 +1,197 @@
+from collections.abc import Sequence, Iterable, Mapping
+from typing import Any
+import inspect
+from abc import ABC, abstractmethod
+import weakref
+
+from PIL import ImageColor
+from colour import Color
+PASSED: str = "#1AFE49"
+FAILED: str = "#FF124F"
+
+def hex_to_rgba(color: str) -> tuple:
+ return tuple([e/255 for e in ImageColor.getcolor(color, "RGBA")])
+
+def gradient(color1: str, color2: str, num: int) -> list[tuple]:
+ c1, c2 = Color(color1), Color(color2)
+ colors = list(c2.range_to(c1, num))
+ colors = [hex_to_rgba(str(c)) for c in colors]
+ return colors
+
+class BaseGrading(Mapping, ABC):
+ __instances: list[Mapping] = list()
+
+ def __init__(self, schema: dict[str, int | float], name=None, alt_name=None):
+ all_str = all(isinstance(k, str) for k in schema.keys())
+ assert all_str or all(isinstance(k, int) for k in schema.keys()), "Keys must be all of type (str, int)"
+ assert all(isinstance(v, float) for v in schema.values()), "All values must be floats in range(0,1)"
+ assert all(v <= 1 and v >= 0 for v in schema.values()), "All values must be floats in range(0,1)"
+ if all_str:
+ self.schema = dict()
+ for k, v in schema.items():
+ self.schema[k.title()] = v
+ else:
+ self.schema = schema
+
+ self.__class__.__instances.append(weakref.proxy(self))
+ self.name = name
+ self.alt_name = alt_name
+
+ def __getitem__(self, index):
+ if index >= len(self):
+ raise IndexError
+ return self.schema[index]
+
+ def __len__(self) -> int:
+ return len(self.schema)
+
+ def __contains__(self, item: int | str | float) -> bool:
+ if isinstance(item, (int, str)):
+ if isinstance(item, str):
+ item = item.title()
+ return item in self.schema
+ if isinstance(item, float):
+ return item <= 1 and item >= 0
+ return False
+
+ def __iter__(self) -> Iterable:
+ yield from self.schema
+
+ def __reversed__(self) -> Iterable:
+ yield from reversed(self.schema)
+
+ def __str__(self) -> str:
+ return self.name
+
+ def __repr__(self) -> str:
+ return f"<{self.name}: ({str(self.schema)})>"
+
+ def __eq__(self, other) -> bool:
+ if other == self:
+ return True
+ if isinstance(other, BaseEval):
+ return self.schema == other.schema
+ return NotImplemented
+
+ def __ne__(self, other) -> bool:
+ return not self.__eq__(other)
+
+ def get(self, key: int | str) -> float | None:
+ if isinstance(key, str):
+ key = key.title()
+ if key in self:
+ return self.schema[key]
+ return None
+
+ def keys(self) -> tuple:
+ return list(self.schema.keys())
+
+ def values(self) -> tuple:
+ return list(self.schema.values())
+
+ def items(self) -> list[tuple]:
+ return list(self.schema.items())
+
+ @abstractmethod
+ def has_passed(self, value: int | float, max: int | float) -> bool:
+ pass
+
+ @abstractmethod
+ def get_grade(self, value: int | float, max: int | float) -> str | int:
+ pass
+
+ @abstractmethod
+ def get_grade_color(self, value: int | float, max: int | float) -> tuple:
+ pass
+
+ @classmethod
+ def get_instances(cls):
+ yield from cls.__instances
+
+ @classmethod
+ def get_instance(cls, name: str):
+ for instance in cls.__instances:
+ if instance.alt_name == name:
+ return instance
+
+get_gradings = lambda: BaseGrading.get_instances()
+get_grader = lambda name: BaseGrading.get_instance(name)
+
+class StdPercentRule(BaseGrading):
+ def has_passed(self, value: int | float, max: int | float) -> bool:
+ return value >= max * self.schema["Passed"]
+
+ def get_grade(self, value: int | float, max: int | float) -> str:
+ return "Passed" if self.has_passed(value, max) else "Not Passed"
+
+ def get_grade_color(self, value: int | float, max: int | float) -> tuple:
+ if self.has_passed(value, max):
+ return hex_to_rgba(PASSED)
+ return hex_to_rgba(FAILED)
+
+class StdGermanGrading(BaseGrading):
+ def has_passed(self, value: int | float, max: int | float) -> bool:
+ return value/max >= 0.45
+
+ def search_grade(self, value: float) -> int:
+ if value <= 0:
+ return min(self.schema.keys())
+
+ searched = max(self.schema.keys())
+ found = False
+ while not found:
+ if self.schema[searched] <= value:
+ found = True
+ else:
+ searched -= 1
+ return searched
+
+ def get_grade(self, value: int | float, max: int | float) -> int:
+ return self.search_grade(value/max)
+
+ def get_grade_color(self, value: float, max: int | float) -> tuple:
+ grade = self.get_grade(value, max)
+ colors = gradient(PASSED, FAILED, len(self.schema))
+ return colors[grade]
+
+# Definitions
+Std30PercentRule = StdPercentRule({
+ "pAssed": 0.3,
+ "Failed": 0.0
+}, "Std30PercentRule", "30 Percent")
+
+Std50PercentRule = StdPercentRule({
+ "Passed": 0.5,
+ "Failed": 0.0
+}, "Std50PercentRule", "50 Percent")
+
+StdGermanGradingMiddleSchool = StdGermanGrading({
+ 1: 0.96,
+ 2: 0.80,
+ 3: 0.60,
+ 4: 0.45,
+ 5: 0.16,
+ 6: 0.00
+}, "StdGermanGradingMiddleSchool", "Secondary School")
+
+StdGermanGradingHighSchool = StdGermanGrading({
+ 15: 0.95,
+ 14: 0.90,
+ 13: 0.85,
+ 12: 0.80,
+ 11: 0.75,
+ 10: 0.70,
+ 9: 0.65,
+ 8: 0.60,
+ 7: 0.55,
+ 6: 0.50,
+ 5: 0.45,
+ 4: 0.40,
+ 3: 0.33,
+ 2: 0.27,
+ 1: 0.20,
+ 0: 0.00
+}, "StdGermanGradingHighSchool", "Oberstufe")
+
+
+#print(StdGermanGradingHighSchool.get_grade(0.0, 24))
diff --git a/model.py b/model.py
index 0177369..e99b546 100644
--- a/model.py
+++ b/model.py
@@ -27,6 +27,7 @@ class Student(BaseModel):
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):