Added: Stuff
This commit is contained in:
		
							
								
								
									
										
											BIN
										
									
								
								assets/Nexus/NexusSansPro-Bold.otf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/Nexus/NexusSansPro-Bold.otf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/Nexus/NexusSansPro-Regular.otf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/Nexus/NexusSansPro-Regular.otf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/Nexus/NexusSerifPro-Bold.otf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/Nexus/NexusSerifPro-Bold.otf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/Nexus/NexusSerifPro-Regular.otf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/Nexus/NexusSerifPro-Regular.otf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -1,36 +1,38 @@
 | 
				
			|||||||
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,Study,Tutorial 1,Tutorial 2,Extended Applications,Numpy & MatPlotLib,SciPy,Monte Carlo,Pandas & Seaborn,Folium,Statistical Test Methods,Data Analysis
 | 
				
			||||||
Abdalaziz,Abunjaila,Male,DiKum,30%,30.5,15,18,28,17,17,17,22,0,18
 | 
					Abdalaziz,Abunjaila,Male,DiKum,30%,Digitale Kommunikation & Medientechnik,30.5,15,18,28,17,17,17,22,0,18
 | 
				
			||||||
Marleen,Adolphi,Female,MeWi6,30%,29.5,15,18,32,19,20,17,24,23,0
 | 
					Marleen,Adolphi,Female,MeWi6,30%,Digitale Kommunikation & Medientechnik,29.5,15,18,32,19,20,17,24,23,0
 | 
				
			||||||
Sarina,Apel,Female,MeWi1,30%,28.5,15,18,32,20,20,21,24,20,23
 | 
					Sarina,Apel,Female,MeWi1,30%,Medienwissenschaften,28.5,15,18,32,20,20,21,24,20,23
 | 
				
			||||||
Skofiare,Berisha,Female,DiKum,30%,29.5,13,18,34,20,17,20,26,16,0
 | 
					Skofiare,Berisha,Female,DiKum,30%,Medienwissenschaften,29.5,13,18,34,20,17,20,26,16,10
 | 
				
			||||||
Aurela,Brahimi,Female,MeWi2,30%,17.5,15,15.5,26,16,17,19,16,0,0
 | 
					Aurela,Brahimi,Female,MeWi2,30%,Medienwissenschaften,17.5,15,15.5,26,16,17,19,16,16,16
 | 
				
			||||||
Cam Thu,Do,Female,MeWi3,30%,31,15,18,34,19,20,21.5,22,12,0
 | 
					Cam Thu,Do,Female,MeWi3,30%,Medienwissenschaften,31,15,18,34,19,20,21.5,22,12,15
 | 
				
			||||||
Nova,Eib,Female,MeWi4,30%,31,15,15,34,20,20,21,27,19,21
 | 
					Nova,Eib,Female,MeWi4,30%,Medienwissenschaften,31,15,15,34,20,20,21,27,19,21
 | 
				
			||||||
Lena,Fricke,Female,MeWi4,30%,0,0,0,0,0,0,0,0,0,0
 | 
					Lena,Fricke,Female,MeWi4,30%,Medienwissenschaften,13,14,15,21,15,17,17,18,19,11
 | 
				
			||||||
Nele,Grundke,Female,MeWi6,30%,23.5,13,16,28,20,17,21,18,22,11
 | 
					Nele,Grundke,Female,MeWi6,30%,Medienwissenschaften,23.5,13,16,28,20,17,21,18,22,11
 | 
				
			||||||
Anna,Grünewald,Female,MeWi3,30%,12,14,16,29,16,15,19,9,0,0
 | 
					Anna,Grünewald,Female,MeWi3,30%,Medienwissenschaften,12,14,16,29,16,15,19,9,16,12
 | 
				
			||||||
Yannik,Haupt,Male,NoGroup,30%,18,6,14,21,13,2,9,0,0,0
 | 
					Yannik,Haupt,Male,NoGroup,30%,Unknown,18,6,14,21,13,2,9,0,0,0
 | 
				
			||||||
Janna,Heiny,Female,MeWi1,30%,30,15,18,33,18,20,22,25,24,30
 | 
					Janna,Heiny,Female,MeWi1,30%,Technologie Orientiertes Managment,30,15,18,33,18,20,22,25,24,30
 | 
				
			||||||
Milena,Krieger,Female,MeWi1,30%,30,15,18,33,20,20,21.5,26,20,22
 | 
					Milena,Krieger,Female,MeWi1,30%,Technologie Orientiertes Managment,30,15,18,33,20,20,21.5,26,20,22
 | 
				
			||||||
Julia,Limbach,Female,MeWi6,30%,27.5,12,18,29,11,19,17.5,26,24,28
 | 
					Julia,Limbach,Female,MeWi6,30%,Medienwissenschaften,27.5,12,18,29,11,19,17.5,26,24,28
 | 
				
			||||||
Viktoria,Litza,Female,MeWi5,30%,21.5,15,18,27,13,20,22,21,21,30
 | 
					Viktoria,Litza,Female,MeWi5,30%,Medienwissenschaften,21.5,15,18,27,13,20,22,21,21,30
 | 
				
			||||||
Leonie,Manthey,Female,MeWi1,30%,28.5,14,18,29,20,10,18,23,16,28
 | 
					Leonie,Manthey,Female,MeWi1,30%,Medienwissenschaften,28.5,14,18,29,20,10,18,23,16,28
 | 
				
			||||||
Izabel,Mike,Female,MeWi2,30%,29.5,15,15,35,11,15,19,21,21,27
 | 
					Izabel,Mike,Female,MeWi2,30%,Medienwissenschaften,29.5,15,15,35,11,15,19,21,21,27
 | 
				
			||||||
Lea,Noglik,Female,MeWi5,30%,22.5,15,17,34,13,10,20,21,19,6
 | 
					Lea,Noglik,Female,MeWi5,30%,Medienwissenschaften,22.5,15,17,34,13,10,20,21,19,6
 | 
				
			||||||
Donika,Nuhiu,Female,MeWi5,30%,31,13.5,18,35,14,10,17,18,19,8
 | 
					Donika,Nuhiu,Female,MeWi5,30%,Medienwissenschaften,31,13.5,18,35,14,10,17,18,19,8
 | 
				
			||||||
Julia,Renner,Female,MeWi4,30%,27.5,10,14,32,20,17,11,20,24,14
 | 
					Julia,Renner,Female,MeWi4,30%,Medienwissenschaften,27.5,10,14,32,20,17,11,20,24,14
 | 
				
			||||||
Fabian,Rothberger,Male,MeWi3,30%,30.5,15,18,34,17,17,19,22,18,30
 | 
					Fabian,Rothberger,Male,MeWi3,30%,Medienwissenschaften,30.5,15,18,34,17,17,19,22,18,30
 | 
				
			||||||
Natascha,Rott,Female,MeWi1,30%,29.5,12,18,32,19,20,21,26,23,26
 | 
					Natascha,Rott,Female,MeWi1,30%,Medienwissenschaften,29.5,12,18,32,19,20,21,26,23,26
 | 
				
			||||||
Isabel,Rudolf,Female,MeWi4,30%,27.5,9,17,34,16,19,19,21,16,14
 | 
					Isabel,Rudolf,Female,MeWi4,30%,Medienwissenschaften,27.5,9,17,34,16,19,19,21,16,14
 | 
				
			||||||
Melina,Sablotny,Female,MeWi6,30%,31,15,18,33,20,20,21,19,11,28
 | 
					Melina,Sablotny,Female,MeWi6,30%,Medienwissenschaften,31,15,18,33,20,20,21,19,11,28
 | 
				
			||||||
Alea,Schleier,Female,DiKum,30%,27,14,18,34,16,18,21.5,22,15,22
 | 
					Alea,Schleier,Female,DiKum,30%,Medienwissenschaften,27,14,18,34,16,18,21.5,22,15,22
 | 
				
			||||||
Flemming,Schur,Male,MeWi3,30%,29.5,15,17,34,19,20,19,22,18,27
 | 
					Flemming,Schur,Male,MeWi3,30%,Medienwissenschaften,29.5,15,17,34,19,20,19,22,18,27
 | 
				
			||||||
Marie,Seeger,Female,DiKum,30%,27.5,15,18,32,14,9,17,22,9,25
 | 
					Marie,Seeger,Female,DiKum,30%,Medienwissenschaften,27.5,15,18,32,14,9,17,22,9,25
 | 
				
			||||||
Lucy,Thiele,Female,MeWi6,30%,27.5,15,18,27,20,17,19,18,22,25
 | 
					Lucy,Thiele,Female,MeWi6,30%,Medienwissenschaften,27.5,15,18,27,20,17,19,18,22,25
 | 
				
			||||||
Lara,Troschke,Female,MeWi2,30%,28.5,14,17,28,13,19,21,25,12,24
 | 
					Lara,Troschke,Female,MeWi2,30%,Medienwissenschaften,28.5,14,17,28,13,19,21,25,12,24
 | 
				
			||||||
Inga-Brit,Turschner,Female,MeWi2,30%,25.5,14,18,34,20,16,19,22,17,30
 | 
					Inga-Brit,Turschner,Female,MeWi2,30%,Medienwissenschaften,25.5,14,18,34,20,16,19,22,17,30
 | 
				
			||||||
Alea,Unger,Female,MeWi5,30%,30,12,18,31,20,20,21,22,15,21.5
 | 
					Alea,Unger,Female,MeWi5,30%,Medienwissenschaften,30,12,18,31,20,20,21,22,15,21.5
 | 
				
			||||||
Marie,Wallbaum,Female,MeWi5,30%,28.5,14,18,34,17,20,19,24,12,22
 | 
					Marie,Wallbaum,Female,MeWi5,30%,Medienwissenschaften,28.5,14,18,34,17,20,19,24,12,22
 | 
				
			||||||
Katharina,Walz,Female,MeWi4,30%,31,15,18,31,19,19,17,24,17,14.5
 | 
					Katharina,Walz,Female,MeWi4,30%,Medienwissenschaften,31,15,18,31,19,19,17,24,17,14.5
 | 
				
			||||||
Xiaowei,Wang,Male,NoGroup,30%,30.5,14,18,26,19,17,0,0,0,0
 | 
					Xiaowei,Wang,Male,NoGroup,30%,Unknown,30.5,14,18,26,19,17,0,0,0,0
 | 
				
			||||||
Lilly-Lu,Warnken,Female,DiKum,30%,30,15,18,30,14,17,19,14,16,24
 | 
					Lilly-Lu,Warnken,Female,DiKum,30%,Medienwissenschaften,30,15,18,30,14,17,19,14,16,24
 | 
				
			||||||
 | 
					,,,,,,,,,,,,,,,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		
		
			
  | 
										
											Binary file not shown.
										
									
								
							@@ -44,19 +44,24 @@ for k, v in courses.items():
 | 
				
			|||||||
    #print(l.title, l.points, l.class_id, l.id)
 | 
					    #print(l.title, l.points, l.class_id, l.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
for k, v in groups.items():
 | 
					for k, v in groups.items():
 | 
				
			||||||
    Group.create(name=k, project=v, has_passed=True, class_id=clas.id)
 | 
					    Group.create(name=k, project=v, has_passed=True if k != 'NoGroup' else False, class_id=clas.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
for index, row in df.iterrows():
 | 
					for index, row in df.iterrows():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    study, _ = Study.get_or_create(name=row['Study'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    s = Student.create(
 | 
					    s = Student.create(
 | 
				
			||||||
        prename=row["First Name"],
 | 
					        prename=row["First Name"],
 | 
				
			||||||
        surname=row["Last Name"],
 | 
					        surname=row["Last Name"],
 | 
				
			||||||
        sex=row["Sex"],
 | 
					        sex=row["Sex"],
 | 
				
			||||||
 | 
					        study_id=study.id,
 | 
				
			||||||
        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"],
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    for title, points in list(row.to_dict().items())[5:]:
 | 
					    for title, points in list(row.to_dict().items())[6:]:
 | 
				
			||||||
        Submission.create(
 | 
					        Submission.create(
 | 
				
			||||||
            student_id=s.id,
 | 
					            student_id=s.id,
 | 
				
			||||||
            lecture_id=Lecture.select().where(Lecture.title == title),
 | 
					            lecture_id=Lecture.select().where(Lecture.title == title),
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										13874
									
								
								assets/documents/document.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13874
									
								
								assets/documents/document.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										13298
									
								
								assets/documents/wise-23-24-fri-07-03-2025-01-14.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13298
									
								
								assets/documents/wise-23-24-fri-07-03-2025-01-14.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										13400
									
								
								assets/documents/wise-23-24-fri-07-03-2025-01-15.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13400
									
								
								assets/documents/wise-23-24-fri-07-03-2025-01-15.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										13400
									
								
								assets/documents/wise-23-24-fri-07-03-2025-01-15a.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13400
									
								
								assets/documents/wise-23-24-fri-07-03-2025-01-15a.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										13569
									
								
								assets/documents/wise-24-25-fri-07-03-2025-00-47.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13569
									
								
								assets/documents/wise-24-25-fri-07-03-2025-00-47.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										13153
									
								
								assets/documents/wise-24-25-thu-06-03-2025-21-33.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13153
									
								
								assets/documents/wise-24-25-thu-06-03-2025-21-33.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										13615
									
								
								assets/documents/wise-24-25-thu-06-03-2025-22-52.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13615
									
								
								assets/documents/wise-24-25-thu-06-03-2025-22-52.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										13642
									
								
								assets/documents/wise-24-25-thu-06-03-2025-22-53.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13642
									
								
								assets/documents/wise-24-25-thu-06-03-2025-22-53.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										13642
									
								
								assets/documents/wise-24-25-thu-06-03-2025-22-54.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13642
									
								
								assets/documents/wise-24-25-thu-06-03-2025-22-54.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										13570
									
								
								assets/documents/wise-24-25-thu-06-03-2025-22-55.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13570
									
								
								assets/documents/wise-24-25-thu-06-03-2025-22-55.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										13570
									
								
								assets/documents/wise-24-25-thu-06-03-2025-22-56.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13570
									
								
								assets/documents/wise-24-25-thu-06-03-2025-22-56.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										29
									
								
								assets/learnlytics.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								assets/learnlytics.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					<svg
 | 
				
			||||||
 | 
					    width="300" height="150" viewBox="0 0 300 150"
 | 
				
			||||||
 | 
					    xmlns="http://www.w3.org/2000/svg"
 | 
				
			||||||
 | 
					    fill="none" stroke-linecap="round" stroke-linejoin="round"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					    <!-- Background Grid -->
 | 
				
			||||||
 | 
					    <rect width="100%" height="100%" fill="#0D1B2A" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- Bar Chart -->
 | 
				
			||||||
 | 
					    <rect x="50" y="90" width="30" height="30" fill="#2EC4B6" />
 | 
				
			||||||
 | 
					    <rect x="90" y="60" width="30" height="60" fill="#2EC4B6" />
 | 
				
			||||||
 | 
					    <rect x="130" y="80" width="30" height="40" fill="#2EC4B6" />
 | 
				
			||||||
 | 
					    <rect x="170" y="40" width="30" height="80" fill="#2EC4B6" />
 | 
				
			||||||
 | 
					    <rect x="210" y="75" width="30" height="45" fill="#2EC4B6" />
 | 
				
			||||||
 | 
					    <rect x="250" y="30" width="30" height="90" fill="#2EC4B6" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- Analytics Chart - Dynamic Graph Lines -->
 | 
				
			||||||
 | 
					    <polyline points="50,110 90,70 130,100 170,50 210,90 250,40" stroke="#F4A261" stroke-width="6" stroke-dasharray="8 4" />
 | 
				
			||||||
 | 
					    <circle cx="50" cy="110" r="5" fill="#F4A261" />
 | 
				
			||||||
 | 
					    <circle cx="90" cy="70" r="5" fill="#F4A261" />
 | 
				
			||||||
 | 
					    <circle cx="130" cy="100" r="5" fill="#F4A261" />
 | 
				
			||||||
 | 
					    <circle cx="170" cy="50" r="5" fill="#F4A261" />
 | 
				
			||||||
 | 
					    <circle cx="210" cy="90" r="5" fill="#F4A261" />
 | 
				
			||||||
 | 
					    <circle cx="250" cy="40" r="5" fill="#F4A261" />
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					    <!-- Text: Learnlytics -->
 | 
				
			||||||
 | 
					    <text x="100" y="140" fill="#E0E1DD" font-family="Arial, sans-serif" font-size="20" font-weight="bold">Learnlytics</text>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 1.3 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/logo_IFN.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/logo_IFN.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 6.3 KiB  | 
							
								
								
									
										198
									
								
								assets/logo_IFN.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								assets/logo_IFN.svg
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
		 After Width: | Height: | Size: 108 KiB  | 
@@ -52,6 +52,14 @@ class Group(BaseModel):
 | 
				
			|||||||
    created_at = DateTimeField(default=datetime.now)
 | 
					    created_at = DateTimeField(default=datetime.now)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Study(BaseModel):
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    Table for Storing a Study Program
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    name = CharField()
 | 
				
			||||||
 | 
					    created_at = DateTimeField(default=datetime.now)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Student(BaseModel):
 | 
					class Student(BaseModel):
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    Table for Storing a Student and linking him to appropiate Tables
 | 
					    Table for Storing a Student and linking him to appropiate Tables
 | 
				
			||||||
@@ -59,6 +67,7 @@ class Student(BaseModel):
 | 
				
			|||||||
    prename = CharField()
 | 
					    prename = CharField()
 | 
				
			||||||
    surname = CharField()
 | 
					    surname = CharField()
 | 
				
			||||||
    sex = CharField()
 | 
					    sex = CharField()
 | 
				
			||||||
 | 
					    study = ForeignKeyField(Study, backref='study')
 | 
				
			||||||
    class_id = ForeignKeyField(Class, backref='class')
 | 
					    class_id = ForeignKeyField(Class, backref='class')
 | 
				
			||||||
    group_id = ForeignKeyField(Group, backref='group')
 | 
					    group_id = ForeignKeyField(Group, backref='group')
 | 
				
			||||||
    grader = CharField()
 | 
					    grader = CharField()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ from typing import Any
 | 
				
			|||||||
import inspect
 | 
					import inspect
 | 
				
			||||||
from abc import ABC, abstractmethod
 | 
					from abc import ABC, abstractmethod
 | 
				
			||||||
import weakref
 | 
					import weakref
 | 
				
			||||||
 | 
					from collections import Counter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from PIL import ImageColor
 | 
					from PIL import ImageColor
 | 
				
			||||||
from colour import Color
 | 
					from colour import Color
 | 
				
			||||||
@@ -97,7 +98,15 @@ class BaseGrading(Mapping, ABC):
 | 
				
			|||||||
        pass
 | 
					        pass
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    @abstractmethod
 | 
					    @abstractmethod
 | 
				
			||||||
    def get_grade(self, value: int | float, max: int | float) -> str | int:
 | 
					    def has_passed_course(self, values: list[int | float], maxs: list[int | float]) -> bool:
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def get_grade(self, value: int | float, max: int | float, is_html: bool) -> str | int:
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					   
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def get_final_grade(self, values: list[int | float], maxs: list[int | float], is_html: bool) -> str | int:
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @abstractmethod
 | 
					    @abstractmethod
 | 
				
			||||||
@@ -114,25 +123,43 @@ class BaseGrading(Mapping, ABC):
 | 
				
			|||||||
            if instance.alt_name == name:
 | 
					            if instance.alt_name == name:
 | 
				
			||||||
                return instance
 | 
					                return instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
get_gradings = lambda: BaseGrading.get_instances()
 | 
					get_gradings = lambda: BaseGrading.get_instances()
 | 
				
			||||||
get_grader = lambda name: BaseGrading.get_instance(name)
 | 
					get_grader = lambda name: BaseGrading.get_instance(name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StdPercentRule(BaseGrading):
 | 
					class StdPercentRule(BaseGrading):
 | 
				
			||||||
    def has_passed(self, value: int | float, max: int | float) -> bool:
 | 
					    def has_passed(self, value: int | float, max: int | float) -> bool:
 | 
				
			||||||
        return value >= max * self.schema["Passed"]
 | 
					        return value >= max * self.schema["Passed"]
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    def get_grade(self, value: int | float, max: int | float) -> str:
 | 
					    def has_passed_course(self, values: list[int | float], maxs: list[int | float]) -> bool:
 | 
				
			||||||
        return "Passed" if self.has_passed(value, max) else "Not Passed"
 | 
					        checks = [self.has_passed(value, max) for value, max in zip(values, maxs)]
 | 
				
			||||||
 | 
					        check = Counter(checks)
 | 
				
			||||||
 | 
					        return check[False] < 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_grade(self, value: int | float, max: int | float, is_html=True) -> str:
 | 
				
			||||||
 | 
					        if is_html:
 | 
				
			||||||
 | 
					            return '✓' if self.has_passed(value, max) else '✗'
 | 
				
			||||||
 | 
					        return 'Passed' if self.has_passed(value, max) else 'Failed'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_final_grade(self, values: list[int | float], maxs: int | float, is_html=True) -> str:
 | 
				
			||||||
 | 
					        if is_html:
 | 
				
			||||||
 | 
					            return '✓' if self.has_passed_course(values, maxs) else '✗'
 | 
				
			||||||
 | 
					        return 'Passed' if self.has_passed_course(values, maxs) else 'Failed'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_grade_color(self, value: int | float, max: int | float) -> tuple:
 | 
					    def get_grade_color(self, value: int | float, max: int | float) -> tuple:
 | 
				
			||||||
        if self.has_passed(value, max):
 | 
					        if self.has_passed(value, max):
 | 
				
			||||||
            return hex_to_rgba(PASSED)
 | 
					            return hex_to_rgba(PASSED)
 | 
				
			||||||
        return hex_to_rgba(FAILED)
 | 
					        return hex_to_rgba(FAILED)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StdGermanGrading(BaseGrading):
 | 
					
 | 
				
			||||||
 | 
					class StdGermanHighSchoolGrading(BaseGrading):
 | 
				
			||||||
    def has_passed(self, value: int | float, max: int | float) -> bool:
 | 
					    def has_passed(self, value: int | float, max: int | float) -> bool:
 | 
				
			||||||
        return value/max >= 0.45 
 | 
					        return value/max >= 0.45 
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    def has_passed_course(self, values: list[int | float], maxs: list[int | float]) -> bool:
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def search_grade(self, value: float) -> int:
 | 
					    def search_grade(self, value: float) -> int:
 | 
				
			||||||
        if value <= 0:
 | 
					        if value <= 0:
 | 
				
			||||||
            return min(self.schema.keys())
 | 
					            return min(self.schema.keys())
 | 
				
			||||||
@@ -146,14 +173,50 @@ class StdGermanGrading(BaseGrading):
 | 
				
			|||||||
                searched -= 1
 | 
					                searched -= 1
 | 
				
			||||||
        return searched
 | 
					        return searched
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_grade(self, value: int | float, max: int | float) -> int:
 | 
					    def get_grade(self, value: int | float, max: int | float, is_html=False) -> int:
 | 
				
			||||||
        return self.search_grade(value/max)
 | 
					        return self.search_grade(value/max)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_final_grade(self, values: list[int | float], maxs: list[int | float], is_html=False) -> int:
 | 
				
			||||||
 | 
					        return self.search_grade(sum(values)/sum(maxs))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_grade_color(self, value: float, max: int | float) -> tuple:
 | 
					    def get_grade_color(self, value: float, max: int | float) -> tuple:
 | 
				
			||||||
        grade = self.get_grade(value, max)
 | 
					        grade = self.get_grade(value, max)
 | 
				
			||||||
        colors = gradient(PASSED, FAILED, len(self.schema)) 
 | 
					        colors = gradient(PASSED, FAILED, len(self.schema)) 
 | 
				
			||||||
        return colors[grade]
 | 
					        return colors[grade]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StdGermanMiddleSchoolGrading(BaseGrading):
 | 
				
			||||||
 | 
					    def has_passed(self, value: int | float, max: int | float) -> bool:
 | 
				
			||||||
 | 
					        return value/max >= 0.45 
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def has_passed_course(self, values: list[int | float], maxs: list[int | float]) -> bool:
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def search_grade(self, value: float) -> int:
 | 
				
			||||||
 | 
					        if value <= 0:
 | 
				
			||||||
 | 
					            return max(self.schema.keys())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        searched = min(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, is_html=False) -> int:
 | 
				
			||||||
 | 
					        return self.search_grade(value/max)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def get_final_grade(self, values: list[int | float], maxs: list[int | float], is_html=False) -> int:
 | 
				
			||||||
 | 
					        return self.search_grade(sum(values)/sum(maxs))
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    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
 | 
					# Definitions
 | 
				
			||||||
Std30PercentRule = StdPercentRule({
 | 
					Std30PercentRule = StdPercentRule({
 | 
				
			||||||
    "pAssed": 0.3,
 | 
					    "pAssed": 0.3,
 | 
				
			||||||
@@ -165,7 +228,7 @@ Std50PercentRule = StdPercentRule({
 | 
				
			|||||||
    "Failed": 0.0
 | 
					    "Failed": 0.0
 | 
				
			||||||
}, "Std50PercentRule", "50%")
 | 
					}, "Std50PercentRule", "50%")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
StdGermanGradingMiddleSchool = StdGermanGrading({
 | 
					StdGermanGradingMiddleSchool = StdGermanMiddleSchoolGrading({
 | 
				
			||||||
    1: 0.96,
 | 
					    1: 0.96,
 | 
				
			||||||
    2: 0.80,
 | 
					    2: 0.80,
 | 
				
			||||||
    3: 0.60,
 | 
					    3: 0.60,
 | 
				
			||||||
@@ -174,7 +237,7 @@ StdGermanGradingMiddleSchool = StdGermanGrading({
 | 
				
			|||||||
    6: 0.00
 | 
					    6: 0.00
 | 
				
			||||||
}, "StdGermanGradingMiddleSchool", "Mittelstufe")
 | 
					}, "StdGermanGradingMiddleSchool", "Mittelstufe")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
StdGermanGradingHighSchool = StdGermanGrading({
 | 
					StdGermanGradingHighSchool = StdGermanHighSchoolGrading({
 | 
				
			||||||
    15: 0.95,
 | 
					    15: 0.95,
 | 
				
			||||||
    14: 0.90,
 | 
					    14: 0.90,
 | 
				
			||||||
    13: 0.85,
 | 
					    13: 0.85,
 | 
				
			||||||
@@ -194,5 +257,5 @@ StdGermanGradingHighSchool = StdGermanGrading({
 | 
				
			|||||||
}, "StdGermanGradingHighSchool", "Oberstufe")
 | 
					}, "StdGermanGradingHighSchool", "Oberstufe")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#print(StdGermanGradingHighSchool.get_grade(0.0, 24))
 | 
					#print(StdGermanGradingMiddleSchool.get_grade(189.5, 242))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,8 @@ from .student_list import student_list
 | 
				
			|||||||
from .student_graph import student_graph
 | 
					from .student_graph import student_graph
 | 
				
			||||||
from .group_graph import group_graph
 | 
					from .group_graph import group_graph
 | 
				
			||||||
from .class_graph import class_graph
 | 
					from .class_graph import class_graph
 | 
				
			||||||
 | 
					from .submission_table import submission_table
 | 
				
			||||||
 | 
					from .document_creator import document_creator
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def analyzer_docking_splits() -> List[hello_imgui.DockingSplit]:
 | 
					def analyzer_docking_splits() -> List[hello_imgui.DockingSplit]:
 | 
				
			||||||
    split_main_misc = hello_imgui.DockingSplit()
 | 
					    split_main_misc = hello_imgui.DockingSplit()
 | 
				
			||||||
@@ -52,16 +53,23 @@ def set_analyzer_layout() -> List[hello_imgui.DockableWindow]:
 | 
				
			|||||||
    class_info.dock_space_name = "MainDockSpace"
 | 
					    class_info.dock_space_name = "MainDockSpace"
 | 
				
			||||||
    class_info.gui_function = lambda: class_graph()
 | 
					    class_info.gui_function = lambda: class_graph()
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    #student_ranking = hello_imgui.DockableWindow()
 | 
					    submission_info = hello_imgui.DockableWindow()
 | 
				
			||||||
    #student_ranking.label = "Ranking"
 | 
					    submission_info.label = "Submissions"
 | 
				
			||||||
    #student_ranking.dock_space_name = "MainDockSpace"
 | 
					    submission_info.dock_space_name = "MainDockSpace"
 | 
				
			||||||
    #student_ranking.gui_function = lambda: ranking()
 | 
					    submission_info.gui_function = lambda: submission_table()
 | 
				
			||||||
 | 
					   
 | 
				
			||||||
 | 
					    document = hello_imgui.DockableWindow()
 | 
				
			||||||
 | 
					    document.label = "Document Creator"
 | 
				
			||||||
 | 
					    document.dock_space_name = "MainDockSpace"
 | 
				
			||||||
 | 
					    document.gui_function = lambda: document_creator()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return [
 | 
					    return [
 | 
				
			||||||
        student_selector,
 | 
					        student_selector,
 | 
				
			||||||
        student_info,
 | 
					        student_info,
 | 
				
			||||||
        group_info,
 | 
					        group_info,
 | 
				
			||||||
        class_info
 | 
					        class_info,
 | 
				
			||||||
 | 
					        submission_info,
 | 
				
			||||||
 | 
					        document
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def analyzer_layout() -> hello_imgui.DockingParams:
 | 
					def analyzer_layout() -> hello_imgui.DockingParams:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,15 +2,28 @@ from imgui_bundle import (
 | 
				
			|||||||
    imgui,
 | 
					    imgui,
 | 
				
			||||||
    immapp,
 | 
					    immapp,
 | 
				
			||||||
    imgui_md,
 | 
					    imgui_md,
 | 
				
			||||||
 | 
					    im_file_dialog,
 | 
				
			||||||
 | 
					    immvision,
 | 
				
			||||||
    ImVec2
 | 
					    ImVec2
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from peewee import fn
 | 
					from peewee import fn
 | 
				
			||||||
 | 
					from slugify import slugify
 | 
				
			||||||
 | 
					import cairosvg
 | 
				
			||||||
 | 
					from PIL import Image
 | 
				
			||||||
 | 
					import numpy as np
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from pathlib import Path
 | 
					from pathlib import Path
 | 
				
			||||||
import subprocess, os, platform
 | 
					import subprocess, os, platform
 | 
				
			||||||
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					from itertools import compress
 | 
				
			||||||
 | 
					import io
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from dbmodel import *
 | 
					from dbmodel import *
 | 
				
			||||||
 | 
					from grader import get_grader, get_gradings
 | 
				
			||||||
from .analyzer_state import AnalyzerState
 | 
					from .analyzer_state import AnalyzerState
 | 
				
			||||||
from .plotter import plot_html
 | 
					from .plotter import plot_pdf
 | 
				
			||||||
 | 
					from pdf import *
 | 
				
			||||||
 | 
					
 | 
				
			||||||
state = AnalyzerState()
 | 
					state = AnalyzerState()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -25,7 +38,7 @@ def ranking(class_id: int) -> None:
 | 
				
			|||||||
    if not statics.inited:
 | 
					    if not statics.inited:
 | 
				
			||||||
        statics.class_id = class_id
 | 
					        statics.class_id = class_id
 | 
				
			||||||
        statics.data = [
 | 
					        statics.data = [
 | 
				
			||||||
            (student, Submission.select(fn.SUM(Submission.points)).where(Submission.student_id == student.id).scalar()) 
 | 
					            (f"{student.prename} {student.surname}", Submission.select(fn.SUM(Submission.points)).where(Submission.student_id == student.id).scalar()) 
 | 
				
			||||||
            for student in Student.select().where(Student.class_id == class_id)
 | 
					            for student in Student.select().where(Student.class_id == class_id)
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        statics.data.sort(key = lambda tup: tup[1], reverse=True)
 | 
					        statics.data.sort(key = lambda tup: tup[1], reverse=True)
 | 
				
			||||||
@@ -34,20 +47,368 @@ def ranking(class_id: int) -> None:
 | 
				
			|||||||
    if statics.class_id != class_id:
 | 
					    if statics.class_id != class_id:
 | 
				
			||||||
        statics.inited = False
 | 
					        statics.inited = False
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    imgui_md.render(f"# Ranking - {Class.get_by_id(statics.class_id).name}")
 | 
					    imgui_md.render_unindented(f"# Ranking - {Class.get_by_id(statics.class_id).name}")
 | 
				
			||||||
    imgui.text("")
 | 
					 | 
				
			||||||
   
 | 
					   
 | 
				
			||||||
    if imgui.begin_table("Ranking1", len(statics.data), imgui.TableFlags_.sizing_fixed_fit.value):
 | 
					    if len(statics.data) < 1:
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if imgui.begin_table("Ranking", 3, imgui.TableFlags_.sizing_fixed_fit.value):
 | 
				
			||||||
        for n, d in enumerate(statics.data, start=1):
 | 
					        for n, d in enumerate(statics.data, start=1):
 | 
				
			||||||
            student, points = d 
 | 
					            student, points = d 
 | 
				
			||||||
            if points.is_integer():
 | 
					            if points.is_integer():
 | 
				
			||||||
                points = int(points)
 | 
					                points = int(points)
 | 
				
			||||||
            imgui.table_next_row()
 | 
					            imgui.table_next_row()
 | 
				
			||||||
            imgui.table_next_column()
 | 
					            imgui.table_next_column()
 | 
				
			||||||
            #imgui.set_next_item_width(-1)
 | 
					            imgui.text(f"{n}.")
 | 
				
			||||||
            imgui.text(f"{n}. {student.prename} {student.surname} - {points} Points")
 | 
					            imgui.table_next_column()
 | 
				
			||||||
 | 
					            imgui.text(student)
 | 
				
			||||||
 | 
					            imgui.table_next_column()
 | 
				
			||||||
 | 
					            imgui.text(f"{points} Points")
 | 
				
			||||||
        imgui.end_table()
 | 
					        imgui.end_table()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
 | 
					def lecture_list(class_id: int) -> None:
 | 
				
			||||||
 | 
					    imgui_md.render_unindented("# Lectures")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if class_id < 1:
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    statics = lecture_list
 | 
				
			||||||
 | 
					    if not statics.inited:
 | 
				
			||||||
 | 
					        statics.class_id = class_id
 | 
				
			||||||
 | 
					        statics.data = [
 | 
				
			||||||
 | 
					            (lecture, Submission.select().where(Submission.lecture_id == lecture.id).count())
 | 
				
			||||||
 | 
					             for lecture in Lecture.select().where(Lecture.class_id == class_id)
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        statics.inited = True 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if statics.class_id != class_id:
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if imgui.button("Add Lecture"):
 | 
				
			||||||
 | 
					        imgui.open_popup("Lecture")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if add_lecture(statics.class_id):
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if len(statics.data) < 1:
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if imgui.begin_table("Lectures", 2, imgui.TableFlags_.sizing_fixed_fit.value):
 | 
				
			||||||
 | 
					        for n, d in enumerate(statics.data, start=1):
 | 
				
			||||||
 | 
					            lecture, sub_count = d 
 | 
				
			||||||
 | 
					            imgui.table_next_row()
 | 
				
			||||||
 | 
					            imgui.table_next_column()
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if imgui.button(f"X##{lecture.title}"):
 | 
				
			||||||
 | 
					                lecture.delete_instance()
 | 
				
			||||||
 | 
					                statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            imgui.same_line()
 | 
				
			||||||
 | 
					            imgui.text(lecture.title)
 | 
				
			||||||
 | 
					            imgui.table_next_column()
 | 
				
			||||||
 | 
					            imgui.text(f"{lecture.points} Points")
 | 
				
			||||||
 | 
					        imgui.end_table()        
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
 | 
					def add_lecture(class_id: int) -> bool:
 | 
				
			||||||
 | 
					    statics = add_lecture
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not statics.inited:
 | 
				
			||||||
 | 
					        statics.title = str()
 | 
				
			||||||
 | 
					        statics.points = float()
 | 
				
			||||||
 | 
					        statics.inited = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    center = imgui.get_main_viewport().get_center()
 | 
				
			||||||
 | 
					    imgui.set_next_window_pos(center, imgui.Cond_.appearing.value, ImVec2(0.5, 0.5))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if imgui.begin_popup("Lecture", imgui.WindowFlags_.always_auto_resize.value):
 | 
				
			||||||
 | 
					        imgui_md.render_unindented("# New Lecture")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _, statics.title = imgui.input_text("Title", statics.title)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        _, statics.points = imgui.input_float("Points", statics.points, 0.5, 2.0, "%.1f")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if statics.points < 0:
 | 
				
			||||||
 | 
					            statics.points = float()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if imgui.button("Add"):
 | 
				
			||||||
 | 
					            lecture = Lecture.create(
 | 
				
			||||||
 | 
					                title = statics.title,
 | 
				
			||||||
 | 
					                points = statics.points,
 | 
				
			||||||
 | 
					                class_id = class_id
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					            sub_data = [
 | 
				
			||||||
 | 
					                {"student_id": s.id, "lecture_id": lecture.id, "class_id": class_id, "points": 0.0}
 | 
				
			||||||
 | 
					                for s in Student.select().where(Student.class_id == class_id)
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            Submission.insert_many(sub_data).execute()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            statics.inited = False
 | 
				
			||||||
 | 
					            imgui.close_current_popup()
 | 
				
			||||||
 | 
					            imgui.end_popup()
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        imgui.same_line()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if imgui.button("Cancel"):
 | 
				
			||||||
 | 
					            imgui.close_current_popup()
 | 
				
			||||||
 | 
					            statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        imgui.end_popup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
 | 
					def group_list(class_id: int) -> None:
 | 
				
			||||||
 | 
					    imgui_md.render_unindented('# Groups')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if class_id < 1:
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    statics = group_list
 | 
				
			||||||
 | 
					    if not statics.inited:
 | 
				
			||||||
 | 
					        statics.class_id = class_id
 | 
				
			||||||
 | 
					        statics.groups = list(Group.select().where(Group.class_id == class_id))
 | 
				
			||||||
 | 
					        statics.inited = True
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if statics.class_id != class_id:
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					   
 | 
				
			||||||
 | 
					    if imgui.button("Add Group"):
 | 
				
			||||||
 | 
					        imgui.open_popup("Group")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if add_group(statics.class_id):
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if imgui.begin_table("Groups", 2, imgui.TableFlags_.sizing_fixed_fit.value):
 | 
				
			||||||
 | 
					        for n, group in enumerate(statics.groups, start=1):
 | 
				
			||||||
 | 
					             
 | 
				
			||||||
 | 
					            imgui.table_next_row()
 | 
				
			||||||
 | 
					            imgui.table_next_column()
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if group.name != 'NoGroup': 
 | 
				
			||||||
 | 
					                if imgui.button(f"X##{group.name}"):
 | 
				
			||||||
 | 
					                    # Put Students in NoGroup
 | 
				
			||||||
 | 
					                    students = Student.select().where(Student.group_id == group.id)
 | 
				
			||||||
 | 
					                    nogroup = Group.select().where(Group.class_id == statics.class_id and Group.name == 'NoGroup').get()
 | 
				
			||||||
 | 
					                    for student in students:
 | 
				
			||||||
 | 
					                        student.group_id = nogroup.id 
 | 
				
			||||||
 | 
					                    with db.atomic():
 | 
				
			||||||
 | 
					                        Student.bulk_update(students, fields=[Student.group_id], batch_size=50)
 | 
				
			||||||
 | 
					                    group.delete_instance()
 | 
				
			||||||
 | 
					                    statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                imgui.same_line()
 | 
				
			||||||
 | 
					            imgui.text(group.name)
 | 
				
			||||||
 | 
					            imgui.table_next_column()
 | 
				
			||||||
 | 
					            imgui.text(f"{group.project}")
 | 
				
			||||||
 | 
					        imgui.end_table()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
 | 
					def add_group(class_id: int) -> bool:
 | 
				
			||||||
 | 
					    statics = add_group
 | 
				
			||||||
 | 
					    if not statics.inited:
 | 
				
			||||||
 | 
					        statics.class_id = class_id
 | 
				
			||||||
 | 
					        statics.name = str()
 | 
				
			||||||
 | 
					        statics.project = str()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        nogroup = None
 | 
				
			||||||
 | 
					        for group in Group.select().where(Group.class_id == class_id):
 | 
				
			||||||
 | 
					            if group.name == 'NoGroup':
 | 
				
			||||||
 | 
					                nogroup = group 
 | 
				
			||||||
 | 
					                break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        statics.students = list(Student.select().where(Student.class_id == class_id and Student.group_id == nogroup.id))
 | 
				
			||||||
 | 
					        statics.selected = [False] * len(statics.students)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        statics.inited = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if statics.class_id != class_id:
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    center = imgui.get_main_viewport().get_center()
 | 
				
			||||||
 | 
					    imgui.set_next_window_pos(center, imgui.Cond_.appearing.value, ImVec2(0.5, 0.5))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if imgui.begin_popup("Group", imgui.WindowFlags_.always_auto_resize.value):
 | 
				
			||||||
 | 
					        imgui_md.render_unindented("# New Group")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _, statics.name = imgui.input_text("Name", statics.name)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        _, statics.project = imgui.input_text("Project", statics.project)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for n, student in enumerate(statics.students):
 | 
				
			||||||
 | 
					            changed, _ = imgui.checkbox(f'{student.prename} {student.surname}', statics.selected[n])
 | 
				
			||||||
 | 
					            if changed:
 | 
				
			||||||
 | 
					                statics.selected[n] = not statics.selected[n]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if imgui.button("Add"):
 | 
				
			||||||
 | 
					            if not statics.name:
 | 
				
			||||||
 | 
					                imgui.close_current_popup()
 | 
				
			||||||
 | 
					                imgui.end_popup()
 | 
				
			||||||
 | 
					                return False 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if not statics.project:
 | 
				
			||||||
 | 
					                statics.project = "NoProject"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            group = Group.create(
 | 
				
			||||||
 | 
					                name = statics.name,
 | 
				
			||||||
 | 
					                project = statics.project,
 | 
				
			||||||
 | 
					                has_passed = False,
 | 
				
			||||||
 | 
					                class_id = class_id
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            students = list(compress(statics.students, statics.selected))
 | 
				
			||||||
 | 
					            if students:
 | 
				
			||||||
 | 
					                for student in students:
 | 
				
			||||||
 | 
					                    student.group_id = group.id
 | 
				
			||||||
 | 
					                with db.atomic():
 | 
				
			||||||
 | 
					                    Student.bulk_update(students, fields=[Student.group_id], batch_size=50)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            statics.inited = False
 | 
				
			||||||
 | 
					            imgui.close_current_popup()
 | 
				
			||||||
 | 
					            imgui.end_popup()
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        imgui.same_line()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if imgui.button("Cancel"):
 | 
				
			||||||
 | 
					            imgui.close_current_popup()
 | 
				
			||||||
 | 
					            statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        imgui.end_popup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
 | 
					def student_list(class_id: int) -> None:
 | 
				
			||||||
 | 
					    imgui_md.render_unindented('# Students')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if class_id < 1:
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    statics = student_list
 | 
				
			||||||
 | 
					    if not statics.inited:
 | 
				
			||||||
 | 
					        statics.class_id = class_id
 | 
				
			||||||
 | 
					        statics.students = list(Student.select().where(Student.class_id == class_id))
 | 
				
			||||||
 | 
					        statics.inited = True
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if statics.class_id != class_id:
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					   
 | 
				
			||||||
 | 
					    if imgui.button("Add Student"):
 | 
				
			||||||
 | 
					        imgui.open_popup("Student")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if add_student(statics.class_id):
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if imgui.begin_table("Students", 2, imgui.TableFlags_.sizing_fixed_fit.value):
 | 
				
			||||||
 | 
					        for n, student in enumerate(statics.students, start=1):
 | 
				
			||||||
 | 
					             
 | 
				
			||||||
 | 
					            imgui.table_next_row()
 | 
				
			||||||
 | 
					            imgui.table_next_column()
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if imgui.button(f"X##{student.surname}{student.id}"):
 | 
				
			||||||
 | 
					                Submission.delete().where(Submission.student_id == student.id).execute()
 | 
				
			||||||
 | 
					                student.delete_instance()
 | 
				
			||||||
 | 
					                statics.inited = False
 | 
				
			||||||
 | 
					                imgui.end_table()
 | 
				
			||||||
 | 
					                return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            imgui.same_line()
 | 
				
			||||||
 | 
					            imgui.text(f'{student.prename} {student.surname}')
 | 
				
			||||||
 | 
					            #imgui.table_next_column()
 | 
				
			||||||
 | 
					            #imgui.text(f"{group.project}")
 | 
				
			||||||
 | 
					        imgui.end_table()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from playhouse.shortcuts import model_to_dict
 | 
				
			||||||
 | 
					from pprint import pprint
 | 
				
			||||||
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
 | 
					def add_student(class_id: int) -> bool:
 | 
				
			||||||
 | 
					    statics = add_student
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not statics.inited:
 | 
				
			||||||
 | 
					        statics.class_id = class_id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        statics.prename = str()
 | 
				
			||||||
 | 
					        statics.surname = str()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        statics.genders = ["Male", "Female"]
 | 
				
			||||||
 | 
					        statics.gender_select = 0
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        statics.studys = list(Study.select())
 | 
				
			||||||
 | 
					        statics.study_labels = [study.name for study in statics.studys]
 | 
				
			||||||
 | 
					        statics.study_select = 0 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        statics.groups = list(Group.select().where(Group.class_id == class_id))
 | 
				
			||||||
 | 
					        statics.group_labels = [group.name for group in statics.groups]
 | 
				
			||||||
 | 
					        statics.group_select = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        statics.graders = [grader.alt_name for grader in get_gradings()]
 | 
				
			||||||
 | 
					        statics.grader_select = 0
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        statics.inited = True
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if statics.class_id != class_id or len(statics.studys) != Study.select().count() or len(statics.groups) != Group.select().where(Group.class_id == statics.class_id).count():
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    center = imgui.get_main_viewport().get_center()
 | 
				
			||||||
 | 
					    imgui.set_next_window_pos(center, imgui.Cond_.appearing.value, ImVec2(0.5, 0.5))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if imgui.begin_popup("Student", imgui.WindowFlags_.always_auto_resize.value):
 | 
				
			||||||
 | 
					        imgui_md.render_unindented("# New Student")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        _, statics.prename = imgui.input_text("First Name", statics.prename)
 | 
				
			||||||
 | 
					        _, statics.surname = imgui.input_text("Last Name", statics.surname)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _, statics.gender_select = imgui.combo("Gender", statics.gender_select, statics.genders)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        _, statics.study_select = imgui.combo("Study Progamm", statics.study_select, statics.study_labels)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _, statics.group_select = imgui.combo("Group", statics.group_select, statics.group_labels)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _, statics.grader_select = imgui.combo("Grader", statics.grader_select, statics.graders)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if imgui.button("Add"):
 | 
				
			||||||
 | 
					            s = Student.create(
 | 
				
			||||||
 | 
					                prename = statics.prename,
 | 
				
			||||||
 | 
					                surname = statics.surname,
 | 
				
			||||||
 | 
					                sex = statics.genders[statics.gender_select],
 | 
				
			||||||
 | 
					                study_id = statics.studys[statics.study_select].id,
 | 
				
			||||||
 | 
					                class_id = class_id,
 | 
				
			||||||
 | 
					                group_id = statics.groups[statics.group_select].id,
 | 
				
			||||||
 | 
					                grader = statics.graders[statics.grader_select]
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            data = [
 | 
				
			||||||
 | 
					                {'student_id': s.id, 'lecture_id': lecture.id, 'class_id': s.class_id, 'points': 0.0}
 | 
				
			||||||
 | 
					                for lecture in Lecture.select().where(Lecture.class_id == s.class_id)
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					            Submission.insert_many(data).execute()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            statics.inited = False
 | 
				
			||||||
 | 
					            imgui.close_current_popup()
 | 
				
			||||||
 | 
					            imgui.end_popup()
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        imgui.same_line()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if imgui.button("Cancel"):
 | 
				
			||||||
 | 
					            imgui.close_current_popup()
 | 
				
			||||||
 | 
					            statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        imgui.end_popup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def class_graph() -> None:
 | 
					def class_graph() -> None:
 | 
				
			||||||
    if db.is_closed():
 | 
					    if db.is_closed():
 | 
				
			||||||
        imgui.text("No DB loaded")
 | 
					        imgui.text("No DB loaded")
 | 
				
			||||||
@@ -55,22 +416,28 @@ def class_graph() -> None:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    w, h = imgui.get_content_region_avail()
 | 
					    w, h = imgui.get_content_region_avail()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if imgui.begin_child("Ranking", ImVec2(w*0.3, h*0.5), imgui.ChildFlags_.borders.value):
 | 
					    if imgui.begin_child("Group1", ImVec2(w, h*0.5)):        
 | 
				
			||||||
 | 
					        w1, h1 = imgui.get_content_region_avail()
 | 
				
			||||||
 | 
					        if imgui.begin_child("Groups", ImVec2(w1/3, h1), imgui.ChildFlags_.borders.value):
 | 
				
			||||||
 | 
					            group_list(state.class_id)
 | 
				
			||||||
 | 
					            imgui.end_child()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        imgui.same_line()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if imgui.begin_child("Lectures", ImVec2(w1/3, h1), imgui.ChildFlags_.borders.value):
 | 
				
			||||||
 | 
					            lecture_list(state.class_id)
 | 
				
			||||||
 | 
					            imgui.end_child()
 | 
				
			||||||
 | 
					   
 | 
				
			||||||
 | 
					        imgui.same_line()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if imgui.begin_child("Students", ImVec2(w1*0.32, h1), imgui.ChildFlags_.borders.value):
 | 
				
			||||||
 | 
					            student_list(state.class_id)
 | 
				
			||||||
 | 
					            imgui.end_child()
 | 
				
			||||||
 | 
					        imgui.end_child()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if imgui.begin_child("Group2", ImVec2(w, h*0.5)):
 | 
				
			||||||
 | 
					        w1, h1 = imgui.get_content_region_avail()
 | 
				
			||||||
 | 
					        if imgui.begin_child("Ranking", ImVec2(w1/3, h1), imgui.ChildFlags_.borders.value):
 | 
				
			||||||
            ranking(state.class_id)
 | 
					            ranking(state.class_id)
 | 
				
			||||||
            imgui.end_child()
 | 
					            imgui.end_child()
 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    imgui.same_line()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if imgui.begin_child("Presentations", ImVec2(w*0.7, h*0.7), imgui.ChildFlags_.borders.value):
 | 
					 | 
				
			||||||
        html = Path("/storage/programming/Learnlytics/assets/covid_faelle_MeWi_2.html")
 | 
					 | 
				
			||||||
        plot_html(html)
 | 
					 | 
				
			||||||
        imgui.same_line()
 | 
					 | 
				
			||||||
        if imgui.button("Open in Browser"):
 | 
					 | 
				
			||||||
            # I Hate everything about it 
 | 
					 | 
				
			||||||
            if platform.system() == 'Darwin': # MacOS
 | 
					 | 
				
			||||||
                subprocess.Popen(('open', html))
 | 
					 | 
				
			||||||
            elif platform.system() == 'Windows': # Windows 
 | 
					 | 
				
			||||||
                os.startfile(html)
 | 
					 | 
				
			||||||
            else: # Linux & Variants
 | 
					 | 
				
			||||||
                subprocess.Popen(('xdg-open', html))
 | 
					 | 
				
			||||||
        imgui.end_child()
 | 
					        imgui.end_child()
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										259
									
								
								learnlytics/gui/analyzer/document_creator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										259
									
								
								learnlytics/gui/analyzer/document_creator.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,259 @@
 | 
				
			|||||||
 | 
					from imgui_bundle import (
 | 
				
			||||||
 | 
					    imgui,
 | 
				
			||||||
 | 
					    immapp,
 | 
				
			||||||
 | 
					    imgui_md,
 | 
				
			||||||
 | 
					    immvision,
 | 
				
			||||||
 | 
					    im_file_dialog,
 | 
				
			||||||
 | 
					    ImVec2
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					from itertools import compress 
 | 
				
			||||||
 | 
					import io
 | 
				
			||||||
 | 
					import pickle
 | 
				
			||||||
 | 
					from dataclasses import dataclass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from PIL import Image
 | 
				
			||||||
 | 
					import numpy as np 
 | 
				
			||||||
 | 
					import cairosvg
 | 
				
			||||||
 | 
					from slugify import slugify
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from dbmodel import *
 | 
				
			||||||
 | 
					from pdf import *
 | 
				
			||||||
 | 
					from .plotter import plot_pdf
 | 
				
			||||||
 | 
					from .analyzer_state import AnalyzerState 
 | 
				
			||||||
 | 
					state = AnalyzerState()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass
 | 
				
			||||||
 | 
					class DocumentProperties:
 | 
				
			||||||
 | 
					    logo: Path
 | 
				
			||||||
 | 
					    save_dir: Path 
 | 
				
			||||||
 | 
					    file_name: str
 | 
				
			||||||
 | 
					    author: str
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def svg_to_png(svg: Path) -> Image:
 | 
				
			||||||
 | 
					    with open(svg) as f:
 | 
				
			||||||
 | 
					        svg = f.read()
 | 
				
			||||||
 | 
					    return Image.open(io.BytesIO(cairosvg.svg2png(svg))).convert("RGBA")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def dump_properties(properties: DocumentProperties) -> None:
 | 
				
			||||||
 | 
					    with open('./pickles/document_properties.pkl', 'wb') as f:
 | 
				
			||||||
 | 
					        pickle.dump(properties, f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def load_properties() -> DocumentProperties:
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        with open('./pickles/document_properties.pkl', 'rb') as f:
 | 
				
			||||||
 | 
					            properties = pickle.load(f)
 | 
				
			||||||
 | 
					        return properties
 | 
				
			||||||
 | 
					    except FileNotFoundError:
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
 | 
					def document_properties(class_id: int) -> None:
 | 
				
			||||||
 | 
					    # TO DO: Store Properties persistent
 | 
				
			||||||
 | 
					    statics = document_properties
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not statics.inited:
 | 
				
			||||||
 | 
					        statics.properties = load_properties()
 | 
				
			||||||
 | 
					        if not statics.properties:
 | 
				
			||||||
 | 
					            statics.properties = DocumentProperties(
 | 
				
			||||||
 | 
					                logo = Path("./assets/learnlytics.svg"),
 | 
				
			||||||
 | 
					                save_dir = Path.home(),
 | 
				
			||||||
 | 
					                file_name = "document",
 | 
				
			||||||
 | 
					                author = "Learnlytics"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        statics.image = svg_to_png(statics.properties.logo) if statics.properties.logo.suffix == ".svg" else Image.open(statics.properties.logo)        
 | 
				
			||||||
 | 
					        statics.inited = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    imgui_md.render_unindented('### Properties')
 | 
				
			||||||
 | 
					    imgui.text("")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if imgui.begin_table("Properties", 2, imgui.TableFlags_.sizing_fixed_fit.value):
 | 
				
			||||||
 | 
					        imgui.table_next_row()
 | 
				
			||||||
 | 
					        imgui.table_next_column()
 | 
				
			||||||
 | 
					        imgui.text("Author:")
 | 
				
			||||||
 | 
					        imgui.table_next_column()
 | 
				
			||||||
 | 
					        changed, statics.properties.author = imgui.input_text("", statics.properties.author)
 | 
				
			||||||
 | 
					        if changed:
 | 
				
			||||||
 | 
					            dump_properties(statics.properties)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        imgui.table_next_row()
 | 
				
			||||||
 | 
					        imgui.table_next_column()
 | 
				
			||||||
 | 
					        if imgui.button("Add Logo"):
 | 
				
			||||||
 | 
					            im_file_dialog.FileDialog.instance().open(
 | 
				
			||||||
 | 
					                "SelectLogo", "Open Logo", "Logo (*.png; *.jpg; *.jpeg; *.svg){.png,.jpg,.jpeg,.svg}", False, str(Path.home())
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        imgui.table_next_column()
 | 
				
			||||||
 | 
					        imgui.text(str(statics.properties.logo))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        imgui.table_next_row()
 | 
				
			||||||
 | 
					        imgui.table_next_column()
 | 
				
			||||||
 | 
					        if imgui.button("Save to"):
 | 
				
			||||||
 | 
					            im_file_dialog.FileDialog.instance().open(
 | 
				
			||||||
 | 
					                "SelectSaveDir", "Select Save Directory", "", False, str(Path.home())
 | 
				
			||||||
 | 
					            ) 
 | 
				
			||||||
 | 
					        imgui.table_next_column()
 | 
				
			||||||
 | 
					        imgui.text(str(statics.properties.save_dir))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        imgui.table_next_row()
 | 
				
			||||||
 | 
					        imgui.table_next_column()
 | 
				
			||||||
 | 
					        if imgui.button("Generate Name"):
 | 
				
			||||||
 | 
					            date = datetime.now()
 | 
				
			||||||
 | 
					            statics.properties.file_name = slugify(f'{Class.get_by_id(class_id).name}_{date.strftime("%a_%d.%m.%Y_%H:%M")}')
 | 
				
			||||||
 | 
					        imgui.table_next_column()
 | 
				
			||||||
 | 
					        changed, statics.properties.file_name = imgui.input_text(".pdf", statics.properties.file_name)
 | 
				
			||||||
 | 
					        if changed:
 | 
				
			||||||
 | 
					            dump_properties(statics.properties)
 | 
				
			||||||
 | 
					        imgui.end_table()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if statics.properties.logo:
 | 
				
			||||||
 | 
					        w, h = imgui.get_content_region_avail()
 | 
				
			||||||
 | 
					        x, y = statics.image.size
 | 
				
			||||||
 | 
					        size = (
 | 
				
			||||||
 | 
					            int(x) if x < w*0.8 else int(w*0.8),
 | 
				
			||||||
 | 
					            int(y) if y < h*0.5 else int(h*0.5)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        immvision.image_display("Logo", np.array(statics.image), size, True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if im_file_dialog.FileDialog.instance().is_done("SelectLogo"):
 | 
				
			||||||
 | 
					        if im_file_dialog.FileDialog.instance().has_result():
 | 
				
			||||||
 | 
					            statics.properties.logo = im_file_dialog.FileDialog.instance().get_result()
 | 
				
			||||||
 | 
					            statics.properties.logo = Path(statics.properties.logo.path()).relative_to(Path('.').resolve())
 | 
				
			||||||
 | 
					            statics.image = svg_to_png(statics.properties.logo) if statics.properties.logo.suffix == ".svg" else Image.open(statics.properties.logo)
 | 
				
			||||||
 | 
					            dump_properties(statics.properties)
 | 
				
			||||||
 | 
					        im_file_dialog.FileDialog.instance().close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if im_file_dialog.FileDialog.instance().is_done("SelectSaveDir"):
 | 
				
			||||||
 | 
					        if im_file_dialog.FileDialog.instance().has_result():
 | 
				
			||||||
 | 
					            save_dir = im_file_dialog.FileDialog.instance().get_result()
 | 
				
			||||||
 | 
					            statics.properties.save_dir = Path(save_dir.path())
 | 
				
			||||||
 | 
					            dump_properties(statics.properties)
 | 
				
			||||||
 | 
					        im_file_dialog.FileDialog.instance().close()   
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return statics.properties
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
 | 
					def filters() -> list[Study]:
 | 
				
			||||||
 | 
					    statics = filters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not statics.inited:
 | 
				
			||||||
 | 
					        statics.studys = list(Study.select())
 | 
				
			||||||
 | 
					        statics.checked = [True] * len(statics.studys)
 | 
				
			||||||
 | 
					        statics.inited = True
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    imgui_md.render_unindented("### Study Filters")
 | 
				
			||||||
 | 
					    imgui.text("")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if imgui.begin_table("Create", 2, imgui.TableFlags_.sizing_fixed_fit.value):
 | 
				
			||||||
 | 
					        for n, study in enumerate(statics.studys):
 | 
				
			||||||
 | 
					            imgui.table_next_row()
 | 
				
			||||||
 | 
					            imgui.table_next_column()
 | 
				
			||||||
 | 
					            _, statics.checked[n] = imgui.checkbox(f"##{study.name}", statics.checked[n])
 | 
				
			||||||
 | 
					            imgui.table_next_column() 
 | 
				
			||||||
 | 
					            imgui.text(study.name)
 | 
				
			||||||
 | 
					        imgui.end_table()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return list(compress(statics.studys, statics.checked))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
 | 
					def order_by() -> str:
 | 
				
			||||||
 | 
					    statics = order_by
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not statics.inited:
 | 
				
			||||||
 | 
					        statics.order_by = ["Study Program", "Group", "Ranking"]
 | 
				
			||||||
 | 
					        statics.selected = 1
 | 
				
			||||||
 | 
					        statics.inited = True
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    imgui_md.render_unindented("### Order by")
 | 
				
			||||||
 | 
					    imgui.text("")
 | 
				
			||||||
 | 
					    if imgui.begin_table("Order", 2, imgui.TableFlags_.sizing_fixed_fit.value):
 | 
				
			||||||
 | 
					        for n, order in enumerate(statics.order_by):
 | 
				
			||||||
 | 
					            imgui.table_next_row()
 | 
				
			||||||
 | 
					            imgui.table_next_column()
 | 
				
			||||||
 | 
					            if imgui.radio_button(f"## {order}", statics.selected == n):
 | 
				
			||||||
 | 
					                statics.selected = n
 | 
				
			||||||
 | 
					            imgui.table_next_column()
 | 
				
			||||||
 | 
					            imgui.text(order)
 | 
				
			||||||
 | 
					        imgui.end_table()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return statics.order_by[statics.selected]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
 | 
					def create_document(class_id: int) -> None:
 | 
				
			||||||
 | 
					    imgui_md.render_unindented("# Create Documents")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    statics = create_document
 | 
				
			||||||
 | 
					    if not statics.inited:
 | 
				
			||||||
 | 
					        statics.class_id = class_id
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            with open("./pickles/last_file.txt") as f:
 | 
				
			||||||
 | 
					                statics.file = Path(f.read())
 | 
				
			||||||
 | 
					        except FileNotFoundError:
 | 
				
			||||||
 | 
					            statics.file = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        statics.order_by = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        statics.inited = True
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if statics.class_id != class_id:
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    w, h = imgui.get_content_region_avail()
 | 
				
			||||||
 | 
					    if imgui.begin_child("PDF", ImVec2(w*0.45, h), imgui.ChildFlags_.borders.value):
 | 
				
			||||||
 | 
					        imgui_md.render_unindented('### Document')
 | 
				
			||||||
 | 
					        imgui.text("")
 | 
				
			||||||
 | 
					        if statics.file:
 | 
				
			||||||
 | 
					            plot_pdf(statics.file, "Viewer")
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            imgui.text("No document created yet.")
 | 
				
			||||||
 | 
					    imgui.end_child()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    imgui.same_line()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if imgui.begin_child("Filters", ImVec2(w*0.54, h), imgui.ChildFlags_.borders.value):
 | 
				
			||||||
 | 
					        w1, h1 = imgui.get_content_region_avail()
 | 
				
			||||||
 | 
					        if imgui.begin_child("Study Filters", ImVec2(w1, h1*0.2)):
 | 
				
			||||||
 | 
					            study_filters = filters()  
 | 
				
			||||||
 | 
					            imgui.end_child()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if imgui.begin_child("Order By", ImVec2(w1, h1*0.2)):
 | 
				
			||||||
 | 
					            statics.order_by = order_by()
 | 
				
			||||||
 | 
					            imgui.end_child()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if imgui.begin_child("Document Properties", ImVec2(w1, h1*0.58)):
 | 
				
			||||||
 | 
					            properties = document_properties(statics.class_id)
 | 
				
			||||||
 | 
					            if imgui.button("Create"):
 | 
				
			||||||
 | 
					                pdf, css = get_pdf(statics.class_id)
 | 
				
			||||||
 | 
					                header = get_pdf_header(state.class_id, [study.name for study in study_filters], properties.author, properties.logo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                match statics.order_by:
 | 
				
			||||||
 | 
					                    case "Group":
 | 
				
			||||||
 | 
					                        sections = create_group_pdf(study_filters) 
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                    case "Study Program":    
 | 
				
			||||||
 | 
					                        sections = create_study_pdf(study_filters) 
 | 
				
			||||||
 | 
					                     
 | 
				
			||||||
 | 
					                    case 'Ranking':
 | 
				
			||||||
 | 
					                        sections = create_ranking_pdf(study_filters)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                pdf.add_section(header, user_css=css)
 | 
				
			||||||
 | 
					                for section in sections:
 | 
				
			||||||
 | 
					                    pdf.add_section(section, user_css=css)
 | 
				
			||||||
 | 
					                statics.file = properties.save_dir / (properties.file_name + ".pdf")
 | 
				
			||||||
 | 
					                pdf.save(statics.file)
 | 
				
			||||||
 | 
					                with open("./pickles/last_file.txt", "w") as f:
 | 
				
			||||||
 | 
					                    f.write(str(statics.file))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            imgui.end_child()
 | 
				
			||||||
 | 
					        imgui.end_child()
 | 
				
			||||||
 | 
					    imgui.same_line()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def document_creator():
 | 
				
			||||||
 | 
					    create_document(state.class_id)
 | 
				
			||||||
@@ -23,6 +23,7 @@ def header(group_id: int) -> None:
 | 
				
			|||||||
    statics = header 
 | 
					    statics = header 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if group_id < 1:
 | 
					    if group_id < 1:
 | 
				
			||||||
 | 
					        imgui_md.render_unindented("# Student Ranking")
 | 
				
			||||||
        return 
 | 
					        return 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if not statics.inited:
 | 
					    if not statics.inited:
 | 
				
			||||||
@@ -46,6 +47,13 @@ def header(group_id: int) -> None:
 | 
				
			|||||||
        if changed:
 | 
					        if changed:
 | 
				
			||||||
            statics.group.has_passed = not statics.group.has_passed
 | 
					            statics.group.has_passed = not statics.group.has_passed
 | 
				
			||||||
            statics.group.save()
 | 
					            statics.group.save()
 | 
				
			||||||
 | 
					        imgui.same_line()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if imgui.button("Edit Group"):
 | 
				
			||||||
 | 
					            imgui.open_popup("EditGroup")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if edit_group(statics.group_id):
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if imgui.begin_table("Students", len(statics.data), imgui.TableFlags_.sizing_fixed_fit.value):
 | 
					    if imgui.begin_table("Students", len(statics.data), imgui.TableFlags_.sizing_fixed_fit.value):
 | 
				
			||||||
        for n, d in enumerate(statics.data, start=1):
 | 
					        for n, d in enumerate(statics.data, start=1):
 | 
				
			||||||
@@ -59,12 +67,62 @@ def header(group_id: int) -> None:
 | 
				
			|||||||
        imgui.end_table()
 | 
					        imgui.end_table()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
 | 
					def edit_group(group_id: int) -> bool:
 | 
				
			||||||
 | 
					    statics = edit_group
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not statics.inited:
 | 
				
			||||||
 | 
					        statics.group_id = group_id
 | 
				
			||||||
 | 
					        statics.group = Group.get_by_id(group_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        statics.name = statics.group.name
 | 
				
			||||||
 | 
					        statics.project = statics.group.project
 | 
				
			||||||
 | 
					        statics.has_passed = statics.group.has_passed
 | 
				
			||||||
 | 
					        statics.inited = True
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if statics.group_id != group_id:
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    center = imgui.get_main_viewport().get_center()
 | 
				
			||||||
 | 
					    imgui.set_next_window_pos(center, imgui.Cond_.appearing.value, ImVec2(0.5, 0.5))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if imgui.begin_popup("EditGroup", imgui.WindowFlags_.always_auto_resize.value):
 | 
				
			||||||
 | 
					        imgui_md.render_unindented(f"# Edit {statics.group.name}")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        _, statics.name = imgui.input_text("Name", statics.name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _, statics.project = imgui.input_text("Project", statics.project)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        changed, _ = imgui.checkbox("Passed?", statics.has_passed)
 | 
				
			||||||
 | 
					        if changed:
 | 
				
			||||||
 | 
					            statics.has_passed = not statics.has_passed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if imgui.button("Change"):
 | 
				
			||||||
 | 
					            statics.group.name = statics.name
 | 
				
			||||||
 | 
					            statics.group.project = statics.project
 | 
				
			||||||
 | 
					            statics.group.has_passed = statics.has_passed
 | 
				
			||||||
 | 
					            statics.group.save()
 | 
				
			||||||
 | 
					            imgui.close_current_popup()
 | 
				
			||||||
 | 
					            imgui.end_popup()
 | 
				
			||||||
 | 
					            statics.inited = False
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        imgui.same_line()
 | 
				
			||||||
 | 
					        if imgui.button("Cancel"):
 | 
				
			||||||
 | 
					            imgui.close_current_popup()
 | 
				
			||||||
 | 
					            statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        imgui.end_popup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return False
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@immapp.static(inited=False)
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
def plot(group_id: int) -> None:
 | 
					def plot(group_id: int) -> None:
 | 
				
			||||||
    statics = plot 
 | 
					    statics = plot 
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    if group_id < 1:
 | 
					    if group_id < 1:
 | 
				
			||||||
 | 
					        imgui_md.render_unindented("# Group Performance Graph")
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if not statics.inited:
 | 
					    if not statics.inited:
 | 
				
			||||||
@@ -94,6 +152,7 @@ def presentation(group_id: int) -> None:
 | 
				
			|||||||
    statics = presentation
 | 
					    statics = presentation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if group_id < 1:
 | 
					    if group_id < 1:
 | 
				
			||||||
 | 
					        imgui_md.render_unindented("# Group Presentation")
 | 
				
			||||||
        return 
 | 
					        return 
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    if not statics.inited:
 | 
					    if not statics.inited:
 | 
				
			||||||
@@ -154,6 +213,10 @@ def group_plot(class_id: int) -> None:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    if not statics.inited:
 | 
					    if not statics.inited:
 | 
				
			||||||
        statics.class_id = class_id
 | 
					        statics.class_id = class_id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if Student.select().where(Student.class_id == class_id).count() < 1:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        max_points = Lecture.select(fn.SUM(Lecture.points)).where(Lecture.class_id == class_id).scalar() 
 | 
					        max_points = Lecture.select(fn.SUM(Lecture.points)).where(Lecture.class_id == class_id).scalar() 
 | 
				
			||||||
        data = {
 | 
					        data = {
 | 
				
			||||||
            group.name: 
 | 
					            group.name: 
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -105,7 +105,6 @@ def plot_html(html: Path, label: str = "Presentation") -> None:
 | 
				
			|||||||
        statics.quality = 100 if statics.high_quality else 10
 | 
					        statics.quality = 100 if statics.high_quality else 10
 | 
				
			||||||
        statics.reload = True
 | 
					        statics.reload = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    imgui.same_line()
 | 
					    imgui.same_line()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if imgui.button("Back"):
 | 
					    if imgui.button("Back"):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,7 @@ import numpy as np
 | 
				
			|||||||
from peewee import fn
 | 
					from peewee import fn
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from dbmodel import *
 | 
					from dbmodel import *
 | 
				
			||||||
 | 
					from grader import get_gradings, get_grader
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .analyzer_state import AnalyzerState 
 | 
					from .analyzer_state import AnalyzerState 
 | 
				
			||||||
from .plotter import plot_bar_line_percentage 
 | 
					from .plotter import plot_bar_line_percentage 
 | 
				
			||||||
@@ -19,28 +20,42 @@ PROGRESS_BAR_COLOR = ImVec4(190, 190, 40, 255)/255
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@immapp.static(inited=False)
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
def header(student_id: int) -> None:
 | 
					def header(student_id: int, reset: bool) -> None:
 | 
				
			||||||
    statics = header
 | 
					    statics = header
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    if student_id < 1:
 | 
					    if student_id < 1:
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if reset:
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if not statics.inited:
 | 
					    if not statics.inited:
 | 
				
			||||||
        statics.student = Student.get_by_id(student_id)
 | 
					        statics.student = Student.get_by_id(student_id)
 | 
				
			||||||
 | 
					        statics.study = Study.get_by_id(statics.student.study.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        points = [sub.points for sub in Submission.select().where(Submission.student_id == student_id)]
 | 
				
			||||||
 | 
					        max_points = [lecture.points for lecture in Lecture.select().where(Lecture.class_id == statics.student.class_id)]
 | 
				
			||||||
 | 
					        statics.has_passed = get_grader(statics.student.grader).get_final_grade(points, max_points, False)
 | 
				
			||||||
        statics.inited = True
 | 
					        statics.inited = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if statics.student.id != student_id:
 | 
					    if statics.student.id != student_id:
 | 
				
			||||||
        statics.inited = False
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    imgui_md.render(f"# {statics.student.prename} {statics.student.surname}")
 | 
					    imgui_md.render_unindented(f"# {statics.student.prename} {statics.student.surname} - {statics.has_passed}")
 | 
				
			||||||
 | 
					    imgui_md.render_unindented(f"Degree Program: **{statics.study.name}**")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@immapp.static(inited=False)
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
def plot(student_id: int) -> None:
 | 
					def plot(student_id: int, reset: bool) -> None:
 | 
				
			||||||
    statics = plot 
 | 
					    statics = plot 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if student_id < 1:
 | 
					    if student_id < 1:
 | 
				
			||||||
 | 
					        imgui_md.render_unindented("# Student Analyzer")
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    if reset:
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if not statics.inited:
 | 
					    if not statics.inited:
 | 
				
			||||||
        statics.student = Student.get_by_id(student_id)
 | 
					        statics.student = Student.get_by_id(student_id)
 | 
				
			||||||
        submissions = Submission.select().where(Submission.student_id == statics.student.id)
 | 
					        submissions = Submission.select().where(Submission.student_id == statics.student.id)
 | 
				
			||||||
@@ -66,15 +81,144 @@ def plot(student_id: int) -> None:
 | 
				
			|||||||
    imgui.pop_style_color()
 | 
					    imgui.pop_style_color()
 | 
				
			||||||
    plot_bar_line_percentage(statics.data, statics.labels, statics.avg*100)
 | 
					    plot_bar_line_percentage(statics.data, statics.labels, statics.avg*100)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
 | 
					def dialog(student_id: int) -> None:
 | 
				
			||||||
 | 
					    imgui_md.render_unindented("# Student Attributes")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if student_id < 1:
 | 
				
			||||||
 | 
					        return 
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    statics = dialog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not statics.inited:
 | 
				
			||||||
 | 
					        statics.student_id = student_id
 | 
				
			||||||
 | 
					        statics.student = Student.get_by_id(student_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        statics.prename = statics.student.prename
 | 
				
			||||||
 | 
					        statics.surname = statics.student.surname
 | 
				
			||||||
 | 
					        statics.sex = 0 if statics.student.sex == "Male" else 1
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        statics.studys = list(Study.select())
 | 
				
			||||||
 | 
					        s = Study.get_by_id(statics.student.study_id)
 | 
				
			||||||
 | 
					        statics.study = statics.studys.index(s)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        statics.groups = list(Group.select().where(Group.class_id == statics.student.class_id))
 | 
				
			||||||
 | 
					        g = Group.get_by_id(statics.student.group_id)
 | 
				
			||||||
 | 
					        statics.group = statics.groups.index(g)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        statics.graders = [grade.alt_name for grade in get_gradings()]
 | 
				
			||||||
 | 
					        statics.grader = statics.graders.index(statics.student.grader)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        statics.inited = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if statics.student_id != student_id:
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if imgui.begin_table("Attributes", 2):#, imgui.TableFlags_.sizing_fixed_fit.value):
 | 
				
			||||||
 | 
					        imgui.table_next_row()
 | 
				
			||||||
 | 
					        imgui.table_next_column()
 | 
				
			||||||
 | 
					        imgui.text("First Name")
 | 
				
			||||||
 | 
					        imgui.table_next_column()
 | 
				
			||||||
 | 
					        _, statics.prename = imgui.input_text("##First Name", statics.prename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        imgui.table_next_row()
 | 
				
			||||||
 | 
					        imgui.table_next_column()
 | 
				
			||||||
 | 
					        imgui.text("Last Name")
 | 
				
			||||||
 | 
					        imgui.table_next_column()
 | 
				
			||||||
 | 
					        _, statics.surname = imgui.input_text("##Last Name", statics.surname)
 | 
				
			||||||
 | 
					       
 | 
				
			||||||
 | 
					        imgui.table_next_row()
 | 
				
			||||||
 | 
					        imgui.table_next_column()
 | 
				
			||||||
 | 
					        imgui.text("Sex")
 | 
				
			||||||
 | 
					        imgui.table_next_column()
 | 
				
			||||||
 | 
					        _, statics.sex = imgui.combo("##Sex", statics.sex, ["Male", "Female"])
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        imgui.table_next_row()
 | 
				
			||||||
 | 
					        imgui.table_next_column()
 | 
				
			||||||
 | 
					        imgui.text("Study Program")
 | 
				
			||||||
 | 
					        imgui.table_next_column()
 | 
				
			||||||
 | 
					        _, statics.study = imgui.combo("##Study", statics.study, [s.name for s in statics.studys])
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        imgui.table_next_row()
 | 
				
			||||||
 | 
					        imgui.table_next_column()
 | 
				
			||||||
 | 
					        imgui.text("Group")
 | 
				
			||||||
 | 
					        imgui.table_next_column()
 | 
				
			||||||
 | 
					        _, statics.group = imgui.combo("##Group", statics.group, [g.name for g in statics.groups])
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        imgui.table_next_row()
 | 
				
			||||||
 | 
					        imgui.table_next_column()
 | 
				
			||||||
 | 
					        imgui.text("Grader")
 | 
				
			||||||
 | 
					        imgui.table_next_column()
 | 
				
			||||||
 | 
					        _, statics.grader = imgui.combo("##Grader", statics.grader, statics.graders)
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					        imgui.end_table()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if imgui.button("Save"):
 | 
				
			||||||
 | 
					        statics.student.prename = statics.prename
 | 
				
			||||||
 | 
					        statics.student.surname = statics.surname
 | 
				
			||||||
 | 
					        statics.student.sex = "Male" if statics.sex == 0 else "Female"
 | 
				
			||||||
 | 
					        statics.student.study_id = statics.studys[statics.study].id
 | 
				
			||||||
 | 
					        statics.student.group_id = statics.groups[statics.group].id
 | 
				
			||||||
 | 
					        statics.student.grader = get_grader(statics.graders[statics.grader]).alt_name
 | 
				
			||||||
 | 
					        statics.student.save()
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					        return True 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
 | 
					def lecture_lists(student_id: int) -> None:
 | 
				
			||||||
 | 
					    imgui_md.render_unindented("# Lectures")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if student_id < 1:
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    statics = lecture_lists
 | 
				
			||||||
 | 
					    if not statics.inited:
 | 
				
			||||||
 | 
					        statics.student_id = student_id
 | 
				
			||||||
 | 
					        statics.student = Student.get_by_id(student_id)
 | 
				
			||||||
 | 
					        statics.subs = list(Submission.select().where(Submission.student_id == statics.student.id))
 | 
				
			||||||
 | 
					        statics.inited = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if statics.student_id != student_id:
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if imgui.begin_table("Lecture List", 2, imgui.TableFlags_.none.value):
 | 
				
			||||||
 | 
					        for sub in statics.subs:
 | 
				
			||||||
 | 
					            imgui.table_next_row()
 | 
				
			||||||
 | 
					            imgui.table_next_column()
 | 
				
			||||||
 | 
					            imgui.text(sub.lecture_id.title)
 | 
				
			||||||
 | 
					            imgui.table_next_column()
 | 
				
			||||||
 | 
					            imgui.text(f'{int(sub.points) if sub.points.is_integer() else sub.points}/{sub.lecture_id.points} Points')
 | 
				
			||||||
 | 
					        imgui.end_table()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
def student_graph() -> None:
 | 
					def student_graph() -> None:
 | 
				
			||||||
    if db.is_closed():
 | 
					    if db.is_closed():
 | 
				
			||||||
        imgui.text("No DB loaded")
 | 
					        imgui.text("No DB loaded")
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    statics = student_graph
 | 
				
			||||||
 | 
					    if not statics.inited:
 | 
				
			||||||
 | 
					        statics.reset = False
 | 
				
			||||||
 | 
					        statics.inited = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    w, h = imgui.get_content_region_avail()
 | 
					    w, h = imgui.get_content_region_avail()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if imgui.begin_child("Header", ImVec2(w, h*0.5), imgui.ChildFlags_.borders.value):
 | 
					    if imgui.begin_child("Header", ImVec2(w, h*0.65), imgui.ChildFlags_.borders.value):
 | 
				
			||||||
        header(state.student_id)
 | 
					        header(state.student_id, statics.reset)
 | 
				
			||||||
        plot(state.student_id)
 | 
					        plot(state.student_id, statics.reset)
 | 
				
			||||||
        imgui.end_child()
 | 
					        imgui.end_child()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    imgui.separator()
 | 
					    imgui.separator()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if imgui.begin_child("Dialog", ImVec2(w*0.4, h*0.34), imgui.ChildFlags_.borders.value):
 | 
				
			||||||
 | 
					        statics.reset = dialog(state.student_id)
 | 
				
			||||||
 | 
					        imgui.end_child()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    imgui.same_line()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if imgui.begin_child("Lectures", ImVec2(w*0.3, h*0.34), imgui.ChildFlags_.borders.value):
 | 
				
			||||||
 | 
					        lecture_lists(state.student_id)
 | 
				
			||||||
 | 
					        imgui.end_child()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,8 @@
 | 
				
			|||||||
from imgui_bundle import (
 | 
					from imgui_bundle import (
 | 
				
			||||||
    imgui,
 | 
					    imgui,
 | 
				
			||||||
    immapp,
 | 
					    immapp,
 | 
				
			||||||
    imgui_md
 | 
					    imgui_md,
 | 
				
			||||||
 | 
					    ImVec2
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from dbmodel import *
 | 
					from dbmodel import *
 | 
				
			||||||
@@ -20,8 +21,119 @@ def class_selector() -> int:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    labels = (c.name for c in Class.select())
 | 
					    labels = (c.name for c in Class.select())
 | 
				
			||||||
    _, statics.selector = imgui.combo("##Classes", statics.selector, list(labels))
 | 
					    _, statics.selector = imgui.combo("##Classes", statics.selector, list(labels))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    current = Class.select()[statics.selector].id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if imgui.button("New Class"):
 | 
				
			||||||
 | 
					        imgui.open_popup("NewC")
 | 
				
			||||||
 | 
					    new_class()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    imgui.same_line()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if imgui.button("Delete Class"):
 | 
				
			||||||
 | 
					        imgui.open_popup("DeleteC")
 | 
				
			||||||
 | 
					    if delete_class(current):
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    imgui.separator()
 | 
					    imgui.separator()
 | 
				
			||||||
    return Class.select()[statics.selector].id
 | 
					    return current 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
 | 
					def new_class() -> None:
 | 
				
			||||||
 | 
					    statics = new_class
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not statics.inited:
 | 
				
			||||||
 | 
					        statics.name = str()
 | 
				
			||||||
 | 
					        statics.inited = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    center = imgui.get_main_viewport().get_center()
 | 
				
			||||||
 | 
					    imgui.set_next_window_pos(center, imgui.Cond_.appearing.value, ImVec2(0.5, 0.5))
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if imgui.begin_popup("NewC"):
 | 
				
			||||||
 | 
					        imgui_md.render_unindented("# New Class")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					        _, statics.name = imgui.input_text("Name", statics.name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if imgui.button("Add"):
 | 
				
			||||||
 | 
					            clas = Class.create(name=statics.name)
 | 
				
			||||||
 | 
					            Group.create(
 | 
				
			||||||
 | 
					                name="NoGroup",
 | 
				
			||||||
 | 
					                project="NoProject",
 | 
				
			||||||
 | 
					                has_passed=False,
 | 
				
			||||||
 | 
					                class_id = clas.id
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            imgui.close_current_popup()
 | 
				
			||||||
 | 
					            statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        imgui.same_line()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if imgui.button("Cancel"):
 | 
				
			||||||
 | 
					            imgui.close_current_popup()
 | 
				
			||||||
 | 
					            statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        imgui.end_popup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
 | 
					def delete_class(class_id: int) -> bool:
 | 
				
			||||||
 | 
					    statics = delete_class
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not statics.inited:
 | 
				
			||||||
 | 
					        statics.class_id = class_id
 | 
				
			||||||
 | 
					        statics.data = {
 | 
				
			||||||
 | 
					            "Students": Student.select().where(Student.class_id == class_id).count(),
 | 
				
			||||||
 | 
					            "Groups": Group.select().where(Group.class_id == class_id).count(),
 | 
				
			||||||
 | 
					            "Lectures": Lecture.select().where(Lecture.class_id == class_id).count(),
 | 
				
			||||||
 | 
					            "Submissions": Submission.select().where(Submission.class_id == class_id).count()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        statics.inited = True
 | 
				
			||||||
 | 
					        statics.ret = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if statics.class_id != class_id:
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    center = imgui.get_main_viewport().get_center()
 | 
				
			||||||
 | 
					    imgui.set_next_window_pos(center, imgui.Cond_.appearing.value, ImVec2(0.5, 0.5))
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    size = imgui.get_main_viewport().size
 | 
				
			||||||
 | 
					    imgui.set_next_window_size(ImVec2(size.x * 0.1, size.y * 0.25))
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if imgui.begin_popup("DeleteC"):
 | 
				
			||||||
 | 
					        imgui_md.render_unindented("# Delete Class")
 | 
				
			||||||
 | 
					        imgui_md.render_unindented("**Are you sure?**")
 | 
				
			||||||
 | 
					        imgui_md.render_unindented("This Operation deletes:")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if imgui.begin_table("Attributes", len(statics.data)):
 | 
				
			||||||
 | 
					            for attr, count in statics.data.items():
 | 
				
			||||||
 | 
					                imgui.table_next_row()
 | 
				
			||||||
 | 
					                imgui.table_next_column()
 | 
				
			||||||
 | 
					                imgui.text(attr)
 | 
				
			||||||
 | 
					                imgui.table_next_column()
 | 
				
			||||||
 | 
					                imgui.text(str(count))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            imgui.end_table()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if imgui.button("Delete"):
 | 
				
			||||||
 | 
					            Submission.delete().where(Submission.class_id == statics.class_id).execute()
 | 
				
			||||||
 | 
					            Group.delete().where(Group.class_id == statics.class_id).execute()
 | 
				
			||||||
 | 
					            Lecture.delete().where(Lecture.class_id == statics.class_id).execute()
 | 
				
			||||||
 | 
					            Student.delete().where(Student.class_id == statics.class_id).execute()
 | 
				
			||||||
 | 
					            Class.get_by_id(statics.class_id).delete_instance()
 | 
				
			||||||
 | 
					            imgui.close_current_popup()
 | 
				
			||||||
 | 
					            statics.inited = False
 | 
				
			||||||
 | 
					            statics.ret = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        imgui.same_line()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if imgui.button("Cancel"):
 | 
				
			||||||
 | 
					            imgui.close_current_popup()
 | 
				
			||||||
 | 
					            statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        imgui.end_popup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return statics.ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@immapp.static(inited=False)
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
def tree(class_id: int) -> None:
 | 
					def tree(class_id: int) -> None:
 | 
				
			||||||
@@ -36,11 +148,19 @@ def tree(class_id: int) -> None:
 | 
				
			|||||||
        statics.selected = -1
 | 
					        statics.selected = -1
 | 
				
			||||||
        statics.ret = (-1, -1)
 | 
					        statics.ret = (-1, -1)
 | 
				
			||||||
        statics.flags = imgui.TreeNodeFlags_.none.value
 | 
					        statics.flags = imgui.TreeNodeFlags_.none.value
 | 
				
			||||||
 | 
					        statics.counter = 0
 | 
				
			||||||
        statics.inited = True
 | 
					        statics.inited = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if statics.class_id != class_id:
 | 
					    if statics.class_id != class_id:
 | 
				
			||||||
        statics.inited = False
 | 
					        statics.inited = False
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    if statics.counter > 60:
 | 
				
			||||||
 | 
					        statics.data = {
 | 
				
			||||||
 | 
					            group: list(Student.select().where(Student.group_id == group.id))
 | 
				
			||||||
 | 
					            for group in Group.select().where(Group.class_id == statics.class_id)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        statics.counter = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if imgui.button("Expand"):
 | 
					    if imgui.button("Expand"):
 | 
				
			||||||
        statics.flags = imgui.TreeNodeFlags_.default_open.value
 | 
					        statics.flags = imgui.TreeNodeFlags_.default_open.value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -61,6 +181,7 @@ def tree(class_id: int) -> None:
 | 
				
			|||||||
            n += 1
 | 
					            n += 1
 | 
				
			||||||
            imgui.tree_pop()
 | 
					            imgui.tree_pop()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    statics.counter += 1
 | 
				
			||||||
    return statics.ret
 | 
					    return statics.ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def student_list() -> None:
 | 
					def student_list() -> None:
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										94
									
								
								learnlytics/gui/analyzer/submission_table.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								learnlytics/gui/analyzer/submission_table.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
				
			|||||||
 | 
					from imgui_bundle import (
 | 
				
			||||||
 | 
					    imgui,
 | 
				
			||||||
 | 
					    immapp,
 | 
				
			||||||
 | 
					    imgui_md
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from dbmodel import *
 | 
				
			||||||
 | 
					from .analyzer_state import AnalyzerState
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					state = AnalyzerState()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
 | 
					def table(class_id: int) -> None:
 | 
				
			||||||
 | 
					    if class_id < 1:
 | 
				
			||||||
 | 
					        return 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    statics = table 
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if not statics.inited:
 | 
				
			||||||
 | 
					        statics.class_id = class_id
 | 
				
			||||||
 | 
					        statics.table_flags = (
 | 
				
			||||||
 | 
					            imgui.TableFlags_.row_bg.value
 | 
				
			||||||
 | 
					            | imgui.TableFlags_.borders.value 
 | 
				
			||||||
 | 
					            | imgui.TableFlags_.resizable.value 
 | 
				
			||||||
 | 
					            | imgui.TableFlags_.sizing_stretch_same.value
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        statics.students = Student.select().where(Student.class_id == statics.class_id)
 | 
				
			||||||
 | 
					        statics.lectures = Lecture.select().where(Lecture.class_id == statics.class_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        statics.rows = len(statics.students)
 | 
				
			||||||
 | 
					        statics.cols = len(statics.lectures)
 | 
				
			||||||
 | 
					        statics.grid = [
 | 
				
			||||||
 | 
					            list(Submission.select().where(Submission.student_id == student.id))
 | 
				
			||||||
 | 
					            for student in statics.students
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        statics.table_header = [f"{lecture.title} ({lecture.points})" for lecture in statics.lectures]
 | 
				
			||||||
 | 
					        statics.inited = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if statics.class_id != class_id:
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if statics.cols != Lecture.select().where(Lecture.class_id == statics.class_id).count():
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if len(statics.students) != Student.select().where(Student.class_id == statics.class_id).count():
 | 
				
			||||||
 | 
					        statics.inited = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if imgui.begin_table("Student Grid", statics.cols+1, statics.table_flags):
 | 
				
			||||||
 | 
					        # Setup Header 
 | 
				
			||||||
 | 
					        imgui.table_setup_column("Students")
 | 
				
			||||||
 | 
					        for header in statics.table_header:
 | 
				
			||||||
 | 
					            imgui.table_setup_column(header)
 | 
				
			||||||
 | 
					        imgui.table_headers_row()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Fill Student names
 | 
				
			||||||
 | 
					        for row in range(statics.rows):
 | 
				
			||||||
 | 
					            imgui.table_next_row()
 | 
				
			||||||
 | 
					            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]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        imgui.end_table()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def submission_table() -> None:
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    imgui_md.render_unindented("# Submissions")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if db.is_closed():
 | 
				
			||||||
 | 
					        imgui.text("No DB loaded")
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    table(state.class_id)
 | 
				
			||||||
@@ -142,75 +142,6 @@ def select_file() -> None:
 | 
				
			|||||||
            # Update application state and reset selection result
 | 
					            # Update application state and reset selection result
 | 
				
			||||||
            statics.res = None
 | 
					            statics.res = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@immapp.static(inited=False)
 | 
					 | 
				
			||||||
def table() -> None:
 | 
					 | 
				
			||||||
    statics = table
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    if db.is_closed():
 | 
					 | 
				
			||||||
        imgui.text("DB")
 | 
					 | 
				
			||||||
        return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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.inited = True
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        statics.students = Student.select().where(Student.class_id == statics.class_id)
 | 
					 | 
				
			||||||
        statics.lectures = Lecture.select().where(Lecture.class_id == statics.class_id)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        statics.rows = len(statics.students)
 | 
					 | 
				
			||||||
        statics.cols = len(statics.lectures)
 | 
					 | 
				
			||||||
        statics.grid = list()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        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 header in statics.table_header:
 | 
					 | 
				
			||||||
            imgui.table_setup_column(header)
 | 
					 | 
				
			||||||
        imgui.table_headers_row()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Fill Student names
 | 
					 | 
				
			||||||
        for row in range(statics.rows):
 | 
					 | 
				
			||||||
            imgui.table_next_row()
 | 
					 | 
				
			||||||
            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]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        imgui.end_table()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@immapp.static(inited=False)
 | 
					@immapp.static(inited=False)
 | 
				
			||||||
def editor() -> None:
 | 
					def editor() -> None:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								learnlytics/pdf/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								learnlytics/pdf/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					from .utils import get_pdf, get_pdf_header
 | 
				
			||||||
 | 
					from .ranking_pdf import create_ranking_pdf
 | 
				
			||||||
 | 
					from .study_pdf import create_study_pdf
 | 
				
			||||||
 | 
					from .group_pdf import create_group_pdf
 | 
				
			||||||
							
								
								
									
										98
									
								
								learnlytics/pdf/document_style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								learnlytics/pdf/document_style.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,98 @@
 | 
				
			|||||||
 | 
					/* All */ 
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'NexusSansPro-Regular';
 | 
				
			||||||
 | 
					  src: url('./assets/Nexus/NexusSansPro-Regular.otf');
 | 
				
			||||||
 | 
					  font-weight: normal;
 | 
				
			||||||
 | 
					  font-style: normal;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'NexusSansPro-Bold';
 | 
				
			||||||
 | 
					  src: url('./assets/Nexus/NexusSansPro-Bold.otf');
 | 
				
			||||||
 | 
					  font-weight: normal;
 | 
				
			||||||
 | 
					  font-style: normal;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* {
 | 
				
			||||||
 | 
					  font-size: 120%;
 | 
				
			||||||
 | 
					  font-family: 'NexusSansPro-Regular';
 | 
				
			||||||
 | 
					  color: #000;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Headers */
 | 
				
			||||||
 | 
					h1 {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  font-size: 28px;
 | 
				
			||||||
 | 
					  font-family: 'NexusSansPro-Bold';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					h2 {
 | 
				
			||||||
 | 
					  font-size: 22px;
 | 
				
			||||||
 | 
					  padding-bottom: -15px;
 | 
				
			||||||
 | 
					  font-family: 'NexusSansPro-Bold';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					h4 {
 | 
				
			||||||
 | 
					  font-size: 16px;
 | 
				
			||||||
 | 
					  padding-bottom: -10px;
 | 
				
			||||||
 | 
					  font-family: 'NexusSansPro-Bold';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					h5 {
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  padding-bottom: -15px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Image */ 
 | 
				
			||||||
 | 
					img {
 | 
				
			||||||
 | 
					  padding: inherit;
 | 
				
			||||||
 | 
					  margin: 0 auto;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  width: 50%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* List */
 | 
				
			||||||
 | 
					ol {
 | 
				
			||||||
 | 
					  list-style-type: upper-roman;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					li {
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Miscelaneous */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					p {
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					hr {
 | 
				
			||||||
 | 
					  border: 3px solid #be1e3c;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Table */ 
 | 
				
			||||||
 | 
					table {
 | 
				
			||||||
 | 
					  margin: 0 auto;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					th, td {
 | 
				
			||||||
 | 
					  padding-right: 40px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Table Header */
 | 
				
			||||||
 | 
					th {
 | 
				
			||||||
 | 
					  text-align: left;
 | 
				
			||||||
 | 
					  font-size: 16px;
 | 
				
			||||||
 | 
					  font-family: 'NexusSansPro-Bold';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Table Content */
 | 
				
			||||||
 | 
					td {
 | 
				
			||||||
 | 
					  text-align: left;
 | 
				
			||||||
 | 
					  font-size: 13px;
 | 
				
			||||||
 | 
					  padding-left: 10px;
 | 
				
			||||||
 | 
					  padding-top: -1px;
 | 
				
			||||||
 | 
					  padding-bottom: 5px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										52
									
								
								learnlytics/pdf/group_pdf.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								learnlytics/pdf/group_pdf.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					from dbmodel import *
 | 
				
			||||||
 | 
					from grader import get_grader
 | 
				
			||||||
 | 
					from markdown_pdf import Section
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from functools import reduce
 | 
				
			||||||
 | 
					import operator
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def create_group_pdf(filter: list[Study]) -> list[Section]:
 | 
				
			||||||
 | 
					    sections = list()
 | 
				
			||||||
 | 
					    text = ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    filter = [(Student.study_id == study.id) for study in filter]
 | 
				
			||||||
 | 
					    expr = reduce(operator.or_, filter)
 | 
				
			||||||
 | 
					    students = list(Student.select().where(expr))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    group_ids = set([student.group_id for student in students])
 | 
				
			||||||
 | 
					    group_filter = [(Group.id == id) for id in group_ids]
 | 
				
			||||||
 | 
					    group_expr = reduce(operator.or_, group_filter)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    data = {
 | 
				
			||||||
 | 
					        group: [student for student in Student.select().where(Student.group_id == group.id) if student.study in filter]
 | 
				
			||||||
 | 
					        for group in Group.select().where(group_expr)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    max_points = [lecture.points for lecture in Lecture.select().where(Lecture.class_id == next(iter(data.keys())).class_id)]
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    n = 0
 | 
				
			||||||
 | 
					    for group, students in data.items():
 | 
				
			||||||
 | 
					        text += f'## {group.name}\n'
 | 
				
			||||||
 | 
					        text += f'#### Project: {group.project}\n'
 | 
				
			||||||
 | 
					        passed = '✓' if group.has_passed else '✗'
 | 
				
			||||||
 | 
					        text += f'Passed Exam {passed}\n'
 | 
				
			||||||
 | 
					        text += '|Student|Study Program|Passed?|\n'
 | 
				
			||||||
 | 
					        text += '|-|-|-|\n'
 | 
				
			||||||
 | 
					        for student in students:
 | 
				
			||||||
 | 
					            grader = get_grader(student.grader)
 | 
				
			||||||
 | 
					            overall_points = [sub.points for sub in Submission.select().where(Submission.student_id == student.id)]
 | 
				
			||||||
 | 
					            passed = grader.get_final_grade(overall_points, max_points)
 | 
				
			||||||
 | 
					           
 | 
				
			||||||
 | 
					            text += f'|{student.prename} {student.surname}|{student.study.name}|{passed}|\n'
 | 
				
			||||||
 | 
					        text += '\n---\n\n'
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        n += 1 
 | 
				
			||||||
 | 
					        if n == 2:
 | 
				
			||||||
 | 
					            sections.append(Section(text))
 | 
				
			||||||
 | 
					            n = 0 
 | 
				
			||||||
 | 
					            text = ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if text:
 | 
				
			||||||
 | 
					        sections.append(Section(text))
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return sections
 | 
				
			||||||
							
								
								
									
										58
									
								
								learnlytics/pdf/ranking_pdf.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								learnlytics/pdf/ranking_pdf.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					from dbmodel import *
 | 
				
			||||||
 | 
					from peewee import fn 
 | 
				
			||||||
 | 
					from markdown_pdf import Section
 | 
				
			||||||
 | 
					from functools import reduce 
 | 
				
			||||||
 | 
					from itertools import compress
 | 
				
			||||||
 | 
					import operator
 | 
				
			||||||
 | 
					from grader import get_grader
 | 
				
			||||||
 | 
					from collections import Counter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def create_header(number_of_students: int) -> str:
 | 
				
			||||||
 | 
					    text = '## Ranking\n'
 | 
				
			||||||
 | 
					    text += f'#### Number of Students: {number_of_students}\n'
 | 
				
			||||||
 | 
					    text += '|Student|Points|Percentage|Passed|\n'
 | 
				
			||||||
 | 
					    text += '|-|-|-|-|\n'
 | 
				
			||||||
 | 
					    return text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def create_ranking_pdf(filter: list[int]) -> list[Section]:
 | 
				
			||||||
 | 
					    filter = [(Student.study_id == id) for id in filter]
 | 
				
			||||||
 | 
					    expr = reduce(operator.or_, filter)
 | 
				
			||||||
 | 
					    students = [
 | 
				
			||||||
 | 
					        (student, Submission.select(fn.SUM(Submission.points)).where(Submission.student_id == student.id).scalar())
 | 
				
			||||||
 | 
					        for student in Student.select().where(expr)
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    students.sort(key = lambda tup: tup[1], reverse=True)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    sections = list()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    max_points = [lecture.points for lecture in Lecture.select().where(Lecture.class_id == students[0][0].class_id)]
 | 
				
			||||||
 | 
					    max = sum(max_points)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Table Header
 | 
				
			||||||
 | 
					    text = create_header(len(students))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    limit = 0
 | 
				
			||||||
 | 
					    for n, tup in enumerate(students, start=1):
 | 
				
			||||||
 | 
					        student, points = tup
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        student_data = f'{n}. {student.prename} {student.surname}' 
 | 
				
			||||||
 | 
					        points = int(points) if points.is_integer() else points
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        grader = get_grader(student.grader)
 | 
				
			||||||
 | 
					        p = [sub.points for sub in Submission.select().where(Submission.student_id == student.id)]
 | 
				
			||||||
 | 
					        passed = grader.get_final_grade(p, max_points)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        text += f'|{student_data}|{points}/{max}|{points/max:.1%}|{passed}|\n'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        limit += 1
 | 
				
			||||||
 | 
					        if limit > 29:
 | 
				
			||||||
 | 
					            sections.append(Section(text))
 | 
				
			||||||
 | 
					            limit = 0
 | 
				
			||||||
 | 
					            text = create_header(len(students))      
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if text:
 | 
				
			||||||
 | 
					        sections.append(Section(text))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return sections
 | 
				
			||||||
							
								
								
									
										37
									
								
								learnlytics/pdf/study_pdf.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								learnlytics/pdf/study_pdf.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					from dbmodel import *
 | 
				
			||||||
 | 
					from grader import get_grader
 | 
				
			||||||
 | 
					from markdown_pdf import Section
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def create_study_pdf(filter: list[int]) -> list[Section]:
 | 
				
			||||||
 | 
					    sections = list()
 | 
				
			||||||
 | 
					    text = ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    data = {
 | 
				
			||||||
 | 
					        study: list(Student.select().where(Student.study_id == study.id))
 | 
				
			||||||
 | 
					        for study in filter
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    max_points = [lecture.points for lecture in Lecture.select().where(Lecture.class_id == next(iter(data.values()))[0].class_id)]
 | 
				
			||||||
 | 
					    max = sum(max_points)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    for study, students in data.items():
 | 
				
			||||||
 | 
					        text += f'## {study.name}\n'
 | 
				
			||||||
 | 
					        text += f'#### Number of Students: {Student.select().where(Student.study_id == study.id).count()}\n'
 | 
				
			||||||
 | 
					        text += '|Student|Points|Percentage|Passed?|\n'
 | 
				
			||||||
 | 
					        text += '|-|-|-|-|\n'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for student in students:                    
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            grader = get_grader(student.grader)
 | 
				
			||||||
 | 
					            overall_points = [sub.points for sub in Submission.select().where(Submission.student_id == student.id)]
 | 
				
			||||||
 | 
					            passed = grader.get_final_grade(overall_points, max_points)
 | 
				
			||||||
 | 
					            points = sum(overall_points)
 | 
				
			||||||
 | 
					            points = int(points) if points.is_integer() else points
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            text += f'|{student.prename} {student.surname}|{points}/{max}|{points/max:.1%}|{passed}|\n'
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        sections.append(Section(text))
 | 
				
			||||||
 | 
					        text = ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return sections
 | 
				
			||||||
							
								
								
									
										47
									
								
								learnlytics/pdf/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								learnlytics/pdf/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					from dbmodel import Class
 | 
				
			||||||
 | 
					from markdown_pdf import MarkdownPdf, Section
 | 
				
			||||||
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					from collections.abc import Sequence
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_pdf(class_id: int) -> tuple[MarkdownPdf, str]:
 | 
				
			||||||
 | 
					    clas = Class.get_by_id(class_id)
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					    # Create PDF 
 | 
				
			||||||
 | 
					    pdf = MarkdownPdf(toc_level=2, mode='gfm-like')
 | 
				
			||||||
 | 
					    pdf.meta['title'] = clas.name 
 | 
				
			||||||
 | 
					    pdf.meta['author'] = 'Learnlytics by @DerGrumpf'
 | 
				
			||||||
 | 
					    pdf.meta['producer'] = 'Learnlytics'
 | 
				
			||||||
 | 
					    pdf.meta['subject'] = f'Passed List - {clas.name}'
 | 
				
			||||||
 | 
					    pdf.meta['keywords'] = f'Passed List,{clas.name}'
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    css = None
 | 
				
			||||||
 | 
					    with open('./learnlytics/pdf/document_style.css') as f:
 | 
				
			||||||
 | 
					        css = f.read()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (pdf, css)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_pdf_header(class_id: int, filter: Sequence[str], author: str, logo_file: Path = None) -> Section:
 | 
				
			||||||
 | 
					    text = ''
 | 
				
			||||||
 | 
					    # Append Logo
 | 
				
			||||||
 | 
					    if logo_file:
 | 
				
			||||||
 | 
					        text += f'\n' 
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Title
 | 
				
			||||||
 | 
					    text += f'# Passed List - {Class.get_by_id(class_id).name}\n'
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Filters applied
 | 
				
			||||||
 | 
					    text += '#### Study Programms:\n' 
 | 
				
			||||||
 | 
					    for n, filter_el in enumerate(filter, start=1):
 | 
				
			||||||
 | 
					        text += f'{n}. {filter_el}\n'
 | 
				
			||||||
 | 
					    text += '---\n\n'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Metadata
 | 
				
			||||||
 | 
					    text += f'Author: {author}\n\n'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    date = datetime.now()
 | 
				
			||||||
 | 
					    text += f'Created: {date.strftime("%A %d.%m.%Y %H:%M")}\n\n'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    text += 'This document is system-generated and may not require manual signatures.'
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return Section(text)
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								pickles/document_properties.pkl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								pickles/document_properties.pkl
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								pickles/last_file.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								pickles/last_file.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					/storage/programming/Learnlytics/assets/documents/wise-23-24-fri-07-03-2025-01-15a.pdf
 | 
				
			||||||
							
								
								
									
										364
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										364
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							@@ -11,6 +11,126 @@ files = [
 | 
				
			|||||||
    {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
 | 
					    {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "cairocffi"
 | 
				
			||||||
 | 
					version = "1.7.1"
 | 
				
			||||||
 | 
					description = "cffi-based cairo bindings for Python"
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = ">=3.8"
 | 
				
			||||||
 | 
					files = [
 | 
				
			||||||
 | 
					    {file = "cairocffi-1.7.1-py3-none-any.whl", hash = "sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f"},
 | 
				
			||||||
 | 
					    {file = "cairocffi-1.7.1.tar.gz", hash = "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.dependencies]
 | 
				
			||||||
 | 
					cffi = ">=1.1.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.extras]
 | 
				
			||||||
 | 
					doc = ["sphinx", "sphinx_rtd_theme"]
 | 
				
			||||||
 | 
					test = ["numpy", "pikepdf", "pytest", "ruff"]
 | 
				
			||||||
 | 
					xcb = ["xcffib (>=1.4.0)"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "cairosvg"
 | 
				
			||||||
 | 
					version = "2.7.1"
 | 
				
			||||||
 | 
					description = "A Simple SVG Converter based on Cairo"
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = ">=3.5"
 | 
				
			||||||
 | 
					files = [
 | 
				
			||||||
 | 
					    {file = "CairoSVG-2.7.1-py3-none-any.whl", hash = "sha256:8a5222d4e6c3f86f1f7046b63246877a63b49923a1cd202184c3a634ef546b3b"},
 | 
				
			||||||
 | 
					    {file = "CairoSVG-2.7.1.tar.gz", hash = "sha256:432531d72347291b9a9ebfb6777026b607563fd8719c46ee742db0aef7271ba0"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.dependencies]
 | 
				
			||||||
 | 
					cairocffi = "*"
 | 
				
			||||||
 | 
					cssselect2 = "*"
 | 
				
			||||||
 | 
					defusedxml = "*"
 | 
				
			||||||
 | 
					pillow = "*"
 | 
				
			||||||
 | 
					tinycss2 = "*"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.extras]
 | 
				
			||||||
 | 
					doc = ["sphinx", "sphinx-rtd-theme"]
 | 
				
			||||||
 | 
					test = ["flake8", "isort", "pytest"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "cffi"
 | 
				
			||||||
 | 
					version = "1.17.1"
 | 
				
			||||||
 | 
					description = "Foreign Function Interface for Python calling C code."
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = ">=3.8"
 | 
				
			||||||
 | 
					files = [
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
 | 
				
			||||||
 | 
					    {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.dependencies]
 | 
				
			||||||
 | 
					pycparser = "*"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "colour"
 | 
					name = "colour"
 | 
				
			||||||
version = "0.1.5"
 | 
					version = "0.1.5"
 | 
				
			||||||
@@ -25,6 +145,36 @@ files = [
 | 
				
			|||||||
[package.extras]
 | 
					[package.extras]
 | 
				
			||||||
test = ["nose"]
 | 
					test = ["nose"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "cssselect2"
 | 
				
			||||||
 | 
					version = "0.8.0"
 | 
				
			||||||
 | 
					description = "CSS selectors for Python ElementTree"
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = ">=3.9"
 | 
				
			||||||
 | 
					files = [
 | 
				
			||||||
 | 
					    {file = "cssselect2-0.8.0-py3-none-any.whl", hash = "sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e"},
 | 
				
			||||||
 | 
					    {file = "cssselect2-0.8.0.tar.gz", hash = "sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.dependencies]
 | 
				
			||||||
 | 
					tinycss2 = "*"
 | 
				
			||||||
 | 
					webencodings = "*"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.extras]
 | 
				
			||||||
 | 
					doc = ["furo", "sphinx"]
 | 
				
			||||||
 | 
					test = ["pytest", "ruff"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "defusedxml"
 | 
				
			||||||
 | 
					version = "0.7.1"
 | 
				
			||||||
 | 
					description = "XML bomb protection for Python stdlib modules"
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 | 
				
			||||||
 | 
					files = [
 | 
				
			||||||
 | 
					    {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
 | 
				
			||||||
 | 
					    {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "flatpak-pip-generator"
 | 
					name = "flatpak-pip-generator"
 | 
				
			||||||
version = "24.0.0"
 | 
					version = "24.0.0"
 | 
				
			||||||
@@ -186,6 +336,76 @@ PyOpenGL = "*"
 | 
				
			|||||||
[package.extras]
 | 
					[package.extras]
 | 
				
			||||||
test = ["pytest"]
 | 
					test = ["pytest"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "linkify-it-py"
 | 
				
			||||||
 | 
					version = "2.0.3"
 | 
				
			||||||
 | 
					description = "Links recognition library with FULL unicode support."
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = ">=3.7"
 | 
				
			||||||
 | 
					files = [
 | 
				
			||||||
 | 
					    {file = "linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048"},
 | 
				
			||||||
 | 
					    {file = "linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.dependencies]
 | 
				
			||||||
 | 
					uc-micro-py = "*"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.extras]
 | 
				
			||||||
 | 
					benchmark = ["pytest", "pytest-benchmark"]
 | 
				
			||||||
 | 
					dev = ["black", "flake8", "isort", "pre-commit", "pyproject-flake8"]
 | 
				
			||||||
 | 
					doc = ["myst-parser", "sphinx", "sphinx-book-theme"]
 | 
				
			||||||
 | 
					test = ["coverage", "pytest", "pytest-cov"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "markdown-it-py"
 | 
				
			||||||
 | 
					version = "3.0.0"
 | 
				
			||||||
 | 
					description = "Python port of markdown-it. Markdown parsing, done right!"
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = ">=3.8"
 | 
				
			||||||
 | 
					files = [
 | 
				
			||||||
 | 
					    {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
 | 
				
			||||||
 | 
					    {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.dependencies]
 | 
				
			||||||
 | 
					mdurl = ">=0.1,<1.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.extras]
 | 
				
			||||||
 | 
					benchmarking = ["psutil", "pytest", "pytest-benchmark"]
 | 
				
			||||||
 | 
					code-style = ["pre-commit (>=3.0,<4.0)"]
 | 
				
			||||||
 | 
					compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
 | 
				
			||||||
 | 
					linkify = ["linkify-it-py (>=1,<3)"]
 | 
				
			||||||
 | 
					plugins = ["mdit-py-plugins"]
 | 
				
			||||||
 | 
					profiling = ["gprof2dot"]
 | 
				
			||||||
 | 
					rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
 | 
				
			||||||
 | 
					testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "markdown-pdf"
 | 
				
			||||||
 | 
					version = "1.3.3"
 | 
				
			||||||
 | 
					description = "Markdown to pdf renderer"
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = ">=3.8"
 | 
				
			||||||
 | 
					files = [
 | 
				
			||||||
 | 
					    {file = "markdown_pdf-1.3.3-py3-none-any.whl", hash = "sha256:0b9b425030fbe4871cbb5842a30ba8d23c6ca1f5ba40a2bb3bd4f286e17d28fc"},
 | 
				
			||||||
 | 
					    {file = "markdown_pdf-1.3.3.tar.gz", hash = "sha256:5cdb054afa20de0b590a5cbffc0569545bf3681f70263d6ffdc15adf99b515aa"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.dependencies]
 | 
				
			||||||
 | 
					markdown-it-py = "3.0.0"
 | 
				
			||||||
 | 
					PyMuPDF = "1.24.6"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "mdurl"
 | 
				
			||||||
 | 
					version = "0.1.2"
 | 
				
			||||||
 | 
					description = "Markdown URL utilities"
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = ">=3.7"
 | 
				
			||||||
 | 
					files = [
 | 
				
			||||||
 | 
					    {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
 | 
				
			||||||
 | 
					    {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "munch"
 | 
					name = "munch"
 | 
				
			||||||
version = "4.0.0"
 | 
					version = "4.0.0"
 | 
				
			||||||
@@ -490,6 +710,17 @@ files = [
 | 
				
			|||||||
greenlet = ">=3.1.1,<4.0.0"
 | 
					greenlet = ">=3.1.1,<4.0.0"
 | 
				
			||||||
pyee = ">=12,<13"
 | 
					pyee = ">=12,<13"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "pycparser"
 | 
				
			||||||
 | 
					version = "2.22"
 | 
				
			||||||
 | 
					description = "C parser in Python"
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = ">=3.8"
 | 
				
			||||||
 | 
					files = [
 | 
				
			||||||
 | 
					    {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
 | 
				
			||||||
 | 
					    {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "pydantic"
 | 
					name = "pydantic"
 | 
				
			||||||
version = "2.10.6"
 | 
					version = "2.10.6"
 | 
				
			||||||
@@ -639,6 +870,66 @@ typing-extensions = "*"
 | 
				
			|||||||
[package.extras]
 | 
					[package.extras]
 | 
				
			||||||
dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "pytest", "pytest-asyncio", "pytest-trio", "sphinx", "toml", "tox", "trio", "trio", "trio-typing", "twine", "twisted", "validate-pyproject[all]"]
 | 
					dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "pytest", "pytest-asyncio", "pytest-trio", "sphinx", "toml", "tox", "trio", "trio", "trio-typing", "twine", "twisted", "validate-pyproject[all]"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "pymupdf"
 | 
				
			||||||
 | 
					version = "1.24.6"
 | 
				
			||||||
 | 
					description = "A high performance Python library for data extraction, analysis, conversion & manipulation of PDF (and other) documents."
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = ">=3.8"
 | 
				
			||||||
 | 
					files = [
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:3a3689394c7ab2851be73f3c82300747e2a089dc37d563be8bda3f71c603c5c4"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:64fb6b2c00b3d3fa31c36d3735cb7694e5f459635883e02b086fdc44fb9398ee"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp310-none-manylinux2014_x86_64.whl", hash = "sha256:83c2b34c7d6ee261e5b6db643947ec22e1aa85d3fa2ab826af50e9909c2b739f"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp310-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e0ca0051f55063e366c26bf8619784c36f796114840b0506958297d58dcfda1"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp310-none-win32.whl", hash = "sha256:d6caebcaffba5179d3ac62df29858b88dba026ea15e987b4a619ec7e72114b7c"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp310-none-win_amd64.whl", hash = "sha256:57f49d90c546bca7ec46c89d1d6e4c3a1c861a6f04a5e038e12cf3419532fb4d"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:9bd685fadc2e7e94af8cd0a92adf9c84dbcf246f4471d180186087d908e0f0c4"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:cb095bae3bf9be8543128e8834d5cd9101b4ab43e3d84e58a73b8a72c93ca9a7"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp311-none-manylinux2014_x86_64.whl", hash = "sha256:6ce5703d30ebe8d710b58d4cfd1a8c6d0627c2f9a34bf9a9a1aef2e496a5df7b"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp311-none-musllinux_1_2_x86_64.whl", hash = "sha256:5fdccd3fdbe61f1cc93d38d9b234880b7f9d7fc703d0221198be788a74de0a77"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp311-none-win32.whl", hash = "sha256:90856c84c8babb5692f5d64504f371eb5f147c33cd0a51724a4e1e530b7a1e4b"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp311-none-win_amd64.whl", hash = "sha256:b0ce01fe4a3153604ded32a78e48c647f30bad80ce0a051563f11db85980da5a"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp312-none-macosx_10_9_x86_64.whl", hash = "sha256:aee2999e8cd042ded9ff5ba4997dfbcbbb4e4f8a09c5e95c9a3c293a651a919d"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:c85b8c4e389a71d57aba4c8e6115d7f20ec3b5025018023f3360cf176bbd294a"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp312-none-manylinux2014_x86_64.whl", hash = "sha256:64cb67a3938c32614c2ba41cbd37168223aa9983bc882dc9a6c8c6bb207ade60"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp312-none-musllinux_1_2_x86_64.whl", hash = "sha256:7e099cc4a0deca70173692fe10b08eb486ca86222377d34bfcadb3bcb2da3ff8"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp312-none-win32.whl", hash = "sha256:24f11aa94e606f466e11163bc4fb5ab3328236549c75c26991c6269342d8dcba"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp312-none-win_amd64.whl", hash = "sha256:10c373c9dce565779eced2a88730229e12c57d9c388cc1e184b1565e641979f7"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:407ae5cfc32cae18fd50c47e4a40b5cee77ffc27b285f9fa28c50d088a5d9624"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:4b62e9c088de896e962864d948da8b263f12572d89e5e65a8929bd51280a12ca"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp38-none-manylinux2014_x86_64.whl", hash = "sha256:5073416516e97bb1323076ae295224684fb11d3ddf7d39d9c142eed824648067"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp38-none-musllinux_1_2_x86_64.whl", hash = "sha256:71ed2e7b23d1cb50e577258e8b1dee986d8c06fca2261239c12872b6c43c40df"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp38-none-win32.whl", hash = "sha256:c915f5b019c4fd4afa47d39ee5871440200328d11c2377a43a364f5cb70d3c0d"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp38-none-win_amd64.whl", hash = "sha256:b9742faefcddda1ee793ef26a686370f468128ade356cd90d2ea2e01e98b07a1"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:bb55d6ce5165c7a8eac2fdce3ba83c71a227816bbbfd3da3aa48af38ad91cced"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:fdc464085549998a88b53d49aef44312e77e1c2a5ef2d2b7b03a623b9fff982f"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp39-none-manylinux2014_x86_64.whl", hash = "sha256:7b74e0ae95b8449069966cbeb6be8e1536f61f388ceb74ff3924f91cd788462a"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp39-none-musllinux_1_2_x86_64.whl", hash = "sha256:e059984723e3c64c5c49b9edf481b42c50d66b5d59f50ab86d56101d8e378a02"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp39-none-win32.whl", hash = "sha256:9b7ce753a3e6c2625963815df171f268ad4002243d1d950b246c0181183bd919"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6-cp39-none-win_amd64.whl", hash = "sha256:b1879e5c175cacee0cc140a7ee19dae901310b95b066a02d6a3829ccbfc4a5fa"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDF-1.24.6.tar.gz", hash = "sha256:029dd99df1cebcbcd4240940809c5e373353d12e6c8483934d42f59ceacfb037"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.dependencies]
 | 
				
			||||||
 | 
					PyMuPDFb = "1.24.6"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "pymupdfb"
 | 
				
			||||||
 | 
					version = "1.24.6"
 | 
				
			||||||
 | 
					description = "MuPDF shared libraries for PyMuPDF."
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = ">=3.8"
 | 
				
			||||||
 | 
					files = [
 | 
				
			||||||
 | 
					    {file = "PyMuPDFb-1.24.6-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:21e3ed890f736def68b9a031122ae1fb854d5cb9a53aa144b6e2ca3092416a6b"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDFb-1.24.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8704d2dfadc9448ce184597d8b0f9c30143e379ac948a517f9c4db7c0c71ed51"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDFb-1.24.6-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01662584d5cfa7a91f77585f13fc23a12291cfd76a57e0a28dd5a56bf521cb2c"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDFb-1.24.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e1f7657353529ae3f88575c83ee49eac9adea311a034b9c97248a65cee7df0e5"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDFb-1.24.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cebc2cedb870d1e1168e2f502eb06f05938f6df69103b0853a2b329611ec19a7"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDFb-1.24.6-py3-none-win32.whl", hash = "sha256:ac4b865cd1e239db04674f85e02844a0e405f8255ee7a74dfee0d86aad0d3576"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDFb-1.24.6-py3-none-win_amd64.whl", hash = "sha256:9224e088a0d3c188dea03831807789e245b812fbd071c8d498da8f7cc33142b2"},
 | 
				
			||||||
 | 
					    {file = "PyMuPDFb-1.24.6.tar.gz", hash = "sha256:f5a40b1732d65a1e519916d698858b9ce7473e23edf9001ddd085c5293d59d30"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "pyopengl"
 | 
					name = "pyopengl"
 | 
				
			||||||
version = "3.1.9"
 | 
					version = "3.1.9"
 | 
				
			||||||
@@ -664,6 +955,23 @@ files = [
 | 
				
			|||||||
[package.dependencies]
 | 
					[package.dependencies]
 | 
				
			||||||
six = ">=1.5"
 | 
					six = ">=1.5"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "python-slugify"
 | 
				
			||||||
 | 
					version = "8.0.4"
 | 
				
			||||||
 | 
					description = "A Python slugify application that also handles Unicode"
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = ">=3.7"
 | 
				
			||||||
 | 
					files = [
 | 
				
			||||||
 | 
					    {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"},
 | 
				
			||||||
 | 
					    {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.dependencies]
 | 
				
			||||||
 | 
					text-unidecode = ">=1.3"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.extras]
 | 
				
			||||||
 | 
					unidecode = ["Unidecode (>=1.1.1)"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "pytz"
 | 
					name = "pytz"
 | 
				
			||||||
version = "2025.1"
 | 
					version = "2025.1"
 | 
				
			||||||
@@ -763,6 +1071,35 @@ files = [
 | 
				
			|||||||
    {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
 | 
					    {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "text-unidecode"
 | 
				
			||||||
 | 
					version = "1.3"
 | 
				
			||||||
 | 
					description = "The most basic Text::Unidecode port"
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = "*"
 | 
				
			||||||
 | 
					files = [
 | 
				
			||||||
 | 
					    {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"},
 | 
				
			||||||
 | 
					    {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "tinycss2"
 | 
				
			||||||
 | 
					version = "1.4.0"
 | 
				
			||||||
 | 
					description = "A tiny CSS parser"
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = ">=3.8"
 | 
				
			||||||
 | 
					files = [
 | 
				
			||||||
 | 
					    {file = "tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289"},
 | 
				
			||||||
 | 
					    {file = "tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.dependencies]
 | 
				
			||||||
 | 
					webencodings = ">=0.4"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.extras]
 | 
				
			||||||
 | 
					doc = ["sphinx", "sphinx_rtd_theme"]
 | 
				
			||||||
 | 
					test = ["pytest", "ruff"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "types-setuptools"
 | 
					name = "types-setuptools"
 | 
				
			||||||
version = "75.8.0.20250225"
 | 
					version = "75.8.0.20250225"
 | 
				
			||||||
@@ -813,7 +1150,32 @@ tzdata = {version = "*", markers = "platform_system == \"Windows\""}
 | 
				
			|||||||
[package.extras]
 | 
					[package.extras]
 | 
				
			||||||
devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"]
 | 
					devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "uc-micro-py"
 | 
				
			||||||
 | 
					version = "1.0.3"
 | 
				
			||||||
 | 
					description = "Micro subset of unicode data files for linkify-it-py projects."
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = ">=3.7"
 | 
				
			||||||
 | 
					files = [
 | 
				
			||||||
 | 
					    {file = "uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a"},
 | 
				
			||||||
 | 
					    {file = "uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.extras]
 | 
				
			||||||
 | 
					test = ["coverage", "pytest", "pytest-cov"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "webencodings"
 | 
				
			||||||
 | 
					version = "0.5.1"
 | 
				
			||||||
 | 
					description = "Character encoding aliases for legacy web content"
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = "*"
 | 
				
			||||||
 | 
					files = [
 | 
				
			||||||
 | 
					    {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
 | 
				
			||||||
 | 
					    {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[metadata]
 | 
					[metadata]
 | 
				
			||||||
lock-version = "2.0"
 | 
					lock-version = "2.0"
 | 
				
			||||||
python-versions = "^3.12"
 | 
					python-versions = "^3.12"
 | 
				
			||||||
content-hash = "bb8a55ca695106ae590ae540a5261bed6905ea6686e6d38a3e0487e5ebb16d1d"
 | 
					content-hash = "0f368a19ff46c53164f16b88dee00580f42b9559212a4fa7fa71b9aad0721824"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,6 +25,10 @@ pytz = "^2025.1"
 | 
				
			|||||||
tzlocal = "^5.3"
 | 
					tzlocal = "^5.3"
 | 
				
			||||||
pdf2image = "^1.17.0"
 | 
					pdf2image = "^1.17.0"
 | 
				
			||||||
playwright = "^1.50.0"
 | 
					playwright = "^1.50.0"
 | 
				
			||||||
 | 
					markdown-pdf = "^1.3.3"
 | 
				
			||||||
 | 
					linkify-it-py = "^2.0.3"
 | 
				
			||||||
 | 
					python-slugify = "^8.0.4"
 | 
				
			||||||
 | 
					cairosvg = "^2.7.1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[build-system]
 | 
					[build-system]
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user