Release - Upload

This commit is contained in:
Js_Sman_Omen 2025-03-24 14:34:56 +01:00
commit 89f35ca466
370 changed files with 32904 additions and 0 deletions

54
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,54 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Run and Debug Django Backend",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/backend/manage.py",
"args": [
"runserver",
"127.0.0.1:8000"
],
"django": true,
"autoStartBrowser": true,
"cwd": "${workspaceFolder}/backend",
"console": "integratedTerminal"
},
{
"name": "Debug React Frontend",
"type": "firefox",
"request": "launch",
"reAttach": true,
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/Frontend",
// "runtimeExecutable": "yarn",
// "args": ["dev"],
// "cwd": "${workspaceFolder}/frontend",
// "console": "integratedTerminal"
},
{
"name": "Run React Frontend",
"type": "node",
"request": "launch",
"runtimeExecutable": "yarn",
"args": ["dev"],
"cwd": "${workspaceFolder}/frontend",
"console": "integratedTerminal"
}
],
"compounds": [
{
"name": "Start Django and React",
"configurations": [
"Run and Debug Django Backend",
"Run React Frontend"
]
}
]
}

168
Backend/.gitignore vendored Normal file
View File

@ -0,0 +1,168 @@
# Created by .ignore support plugin (hsz.mobi)
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
.DS_Store
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# IPython Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# dotenv
.env
# virtualenv
venv/
ENV/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject
### VirtualEnv template
# Virtualenv
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
.Python
[Bb]in
[Ii]nclude
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
.venv
pip-selfcheck.json
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
.idea/
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# DB
db.sqlite3
# StudData
studis/xlsmodel/data/
# Logdateien
log/
# Medien
media/
# VSCode
.vscode/launch.json

20
Backend/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,20 @@
{
"workbench.colorCustomizations": {
// Top-Bar
"titleBar.border": "#004F59", // Teal border
"titleBar.activeForeground": "#E0FFFF", // Light cyan text (sky blue feel)
"titleBar.activeBackground": "#007A91", // Vibrant teal background
// Left-Bar
"activityBar.border": "#004F59", // Teal border
"activityBar.activeForeground": "#E0FFFF", // Light cyan active icon
"activityBar.activeBackground": "#00A2C3", // Sky blue for active background
"activityBar.background": "#005F6B", // Deeper teal for overall background
// Bottom-Bar
"statusBar.background": "#004F59", // Teal background
"statusBar.foreground": "#E0FFFF", // Light cyan text
},
"debug.allowBreakpointsEverywhere": false
}

64
Backend/README.md Normal file
View File

@ -0,0 +1,64 @@
# Prüfplanviewer
## Installation
Nach dem laden des Repos und der Installation der Module
aus <code> requirements.txt </code> sollten noch folgende
<code> manage.py tasks </code> ausgeführt werden:
- <code> migrate </code> (Anlegen der Datenbanktabellen)
- Befehl aus READ.ME im Verzeichnis <code> fixtures </code>.
Dadurch werden Berechtigungsgruppen angelegt.
Für den Prüfungsplan wird aktuell nur die
Gruppe <code> PP_ADMIN </code> benötigt.
- <code> createsuperuser </code> (Anlage eines ersten Admin-Users)
Danach kann die Anwendung gestartet werden. Anmeldung mit dem
gerade generierten Superuser. Im Bereich "Administration" können
weitere User angelegt werden und ggf. der Gruppe <code> PP_ADMIN </code>
zugeordnet werden. Benutzer in dieser Gruppe können den
Prüfplan nicht nur sehen, sondern auch neue Daten laden.
## Daten
Die Daten werden von Prof. Mahr und Prof. Hopf generiert.
Bei Bedarf müssen wird dort nachfragen. Alte Daten gibt es
auch bei Prof. Hofmann.
## Neue Anforderungen
Nach dem Testbetrieb wurden einige neue Amforderungen
identifiziert:
- Anzeige von "Präsenz"-Pflicht in der
Dozentensicht (ob und wie dies in den Daten
enthalten ist, muss noch geklärt werden)
- Anzeige von "Pendler"-Eigenschaft in der
Dozentensicht (ob und wie dies in den Daten
enthalten ist, muss noch geklärt werden)
- Anzeige von ungeplanten Prüfungen
(also z.B. alternative Prüfungsformen während COVID)
(ob und wie dies in den Daten
enthalten ist, muss noch geklärt werden)
- Anzeige des Versionsstands der importierten Daten
(ob und wie dies in den Daten
enthalten ist, muss noch geklärt werden)
- Import der Daten auch von Kommandozeile
(nicht nur über Web-Upload)
- iCal-Export von Prüfungen
zum Import in den Kalender
- bei der Detailansicht einer Prüfung sollen
alle Prüfer, Räume und die jeweilig Anzahl
der Studierenden angezeigt werden.
(ob und wie dies in den Daten
enthalten ist, muss noch geklärt werden)
- Bessere Anzeige auf mobilen Geräten (responsive)
## ToDOs
- Klären der Datenschnittstelle
- Überarbeiten des Datenimports (neue Daten, Kommandozeile)
- Erweitern der Anzeige
* entweder in der bestehenden App
* oder in einer Single-Page-App
* oder beides

0
Backend/RestTest.http Normal file
View File

0
Backend/api/__init__.py Normal file
View File

576
Backend/api/serializer.py Normal file
View File

@ -0,0 +1,576 @@
import datetime
import inspect
import json
from rest_framework import serializers
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from pruefplan_parser.models import JsonParser
from pruefplan_viewer.models import *
# from django.utils.encoding import smart_str
# Class to generate a UserToken with custom fields
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user) # Default Token
# Add custom informations to the Token
token["username"] = user.username
token["password"] = user.password
return token
class API_ExamDataSet_ForStudentSerializer(serializers.ModelSerializer):
class Meta:
model = API_ExamDataSet_ForStudent
fields = '__all__'
class PruefPlan_JSON_Serializer(serializers.Serializer):
def create(self, validated_data):
return super().create(validated_data)
def validate(self, data):
if "array" not in data or not isinstance(data["array"], list):
raise serializers.ValidationError(
"Das 'array'-Feld ist erforderlich und muss als Python-Liste verfügbar sein."
)
return {"dataArray": data["array"]} # Rückgabe des validierten Datenarrays
class Lecturer_JSON_Serializer(serializers.Serializer):
# Definiert die Felder, die der Serializer erwartet
revision = serializers.CharField(required=True)
array = serializers.ListField()
def create(self, validated_data):
# Definieren der erwarteten Schlüssel im JSON-Daten
idKey = "kennung"
firstNameKey = "vorname"
lastNameKey = "nachname"
titleKey = "titel"
created_lecturers = [] # Liste zum Speichern der erstellten Lecturer-Objekte
try:
# Schleife über die Daten, um Lecturer-Objekte zu erstellen
for iteration_number, d in enumerate(validated_data["dataArray"]):
# Lecturer-Objekt erstellen und speichern
entry = Lecturer(
identification=d[idKey],
firstName=d[firstNameKey],
lastName=d[lastNameKey],
title=d[titleKey],
)
entry.save()
created_lecturers.append(entry) # Objekt der Liste hinzufügen
except KeyError as e:
# Fehlerbehandlung bei fehlenden Schlüsseln
calling_function = inspect.stack()[0][3]
template = "Exception of type [{0}] occurred in function {1} in iteration {2} of the loop.. Arguments: {3!r}. Message: [{4}] entry might be missing"
message = template.format(
type(e).__name__,
calling_function,
iteration_number,
e.args,
e,
)
JsonParser.exceptionMessages.append(message) # Fehlermeldung speichern
except Exception as e:
# Allgemeine Fehlerbehandlung
calling_function = inspect.stack()[0][3]
template = "Exception of type [{0}] occurred in function {1} in iteration {2} of the loop. Arguments: {3!r}. Message: {4}."
message = template.format(
type(e).__name__,
calling_function,
iteration_number,
e.args,
e.__str__(),
)
JsonParser.exceptionMessages.append(message) # Fehlermeldung speichern
return {
"Serializer": "Lecturer",
"status": "success",
"created_count": len(created_lecturers),
} # Rückgabe des Erfolgsstatus
def validate(self, data):
# Validierung, um sicherzustellen, dass 'revision' und 'array' Felder vorhanden sind
if "revision" not in data.keys():
raise serializers.ValidationError("Das 'revision'-Feld ist erforderlich.")
if "array" not in data or not isinstance(data["array"], list):
raise serializers.ValidationError(
"Das 'array'-Feld ist erforderlich und muss als Python-Liste verfügbar sein."
)
return {"dataArray": data["array"]} # Rückgabe des validierten Datenarrays
class Attendance_JSON_Serializer(serializers.Serializer):
# Definiert die Felder, die der Serializer erwartet
revision = serializers.CharField(required=True)
array = serializers.ListField()
def create(self, validated_data):
# Definieren der erwarteten Schlüssel im JSON-Daten
idKey = "dozentenkennung"
yearKey = "jahr"
monthKey = "monat"
dayKey = "tag"
hourKey = "stunde"
minuteKey = "minute"
weekdayKey = "wochentag"
dateKey = "termin"
created_attendences = (
[]
) # Liste zum Speichern der erstellten Attendance-Objekte
try:
# Schleife über die Daten, um Attendance-Objekte zu erstellen
for iteration_number, d in enumerate(validated_data["dataArray"]):
# Überprüfung, ob das 'termin'-Feld vorhanden ist
keyAvailable = "termin" in d
if not keyAvailable:
# Warnung ausgeben, wenn kein Termin zugewiesen wurde
template = "Warning. Following Exam was not assigned any examination date ['termin']: [{1}]"
message = template.format(d[idKey])
JsonParser.warningMessages.append(message)
continue
# Datum und Uhrzeit aus den Daten extrahieren
date = datetime.date(
d[dateKey][yearKey], d[dateKey][monthKey], d[dateKey][dayKey]
)
time = datetime.time(d[dateKey][hourKey], d[dateKey][minuteKey])
# Attendance-Objekt erstellen und speichern
entry = Attendance(
identification=d[idKey],
time=time,
date=date,
dateTimeText=d[dateKey]["datumText"],
weekday=d[dateKey]["wochentag"],
)
entry.save()
created_attendences.append(entry) # Objekt der Liste hinzufügen
try:
# Dozent aus der Datenbank abrufen und mit der Anwesenheit verknüpfen
lecturer = Lecturer.objects.get(identification=d[idKey])
lecturer.attendance_set.add(
entry
) # Attendance dem Lecturer zuweisen
except Exception as e:
# Fehlerbehandlung, wenn der Dozent nicht gefunden wird
template = "Exception of type [{0}]. Arguments: {1!r}. Message: [{2}] entry might be missing"
message = template.format(type(e).__name__, e.args, idKey)
JsonParser.exceptionMessages.append(
message
) # Fehlermeldung speichern
except KeyError as e:
# Fehlerbehandlung bei fehlenden Schlüsseln
calling_function = inspect.stack()[0][3]
template = "Exception of type [{0}] occurred in function {1} in iteration {2} of the loop.. Arguments: {3!r}. Message: [{4}] entry might be missing"
message = template.format(
type(e).__name__,
calling_function,
iteration_number,
e.args,
e,
)
JsonParser.exceptionMessages.append(message) # Fehlermeldung speichern
except Exception as e:
# Allgemeine Fehlerbehandlung
calling_function = inspect.stack()[0][3]
template = "Exception of type [{0}] occurred in function {1} in iteration {2} of the loop. Arguments: {3!r}. Message: {4}."
message = template.format(
type(e).__name__,
calling_function,
iteration_number,
e.args,
e.__str__(),
)
JsonParser.exceptionMessages.append(message) # Fehlermeldung speichern
return {
"Serializer": "Attendance",
"status": "success",
"created_count": len(created_attendences),
} # Rückgabe des Erfolgsstatus
def validate(self, data):
# Validierung, um sicherzustellen, dass 'revision' und 'array' Felder vorhanden sind
if "revision" not in data.keys():
raise serializers.ValidationError("Das 'revision'-Feld ist erforderlich.")
if "array" not in data or not isinstance(data["array"], list):
raise serializers.ValidationError(
"Das 'array'-Feld ist erforderlich und muss als Python-Liste verfügbar sein."
)
return {"dataArray": data["array"]} # Rückgabe des validierten Datenarrays
class Subject_JSON_Serializer(serializers.Serializer):
# Definiert die Felder, die der Serializer erwartet
revision = serializers.CharField(required=True)
array = serializers.ListField()
def create(self, validated_data):
# Definieren der erwarteten Schlüssel im JSON-Daten
idKey = "kennung"
initialsKey = "kurztext"
nameKey = "langtext"
ohmIdKey = "fachkennungOhm"
created_subjects = [] # Liste zum Speichern der erstellten Subject-Objekte
try:
# Schleife über die Daten, um Subject-Objekte zu erstellen
for iteration_number, d in enumerate(validated_data["dataArray"]):
# Subject-Objekt erstellen und speichern
entry = Subject(
identification=d[idKey],
initials=d[ohmIdKey][initialsKey],
name=d[ohmIdKey][nameKey],
)
entry.save()
created_subjects.append(entry) # Objekt der Liste hinzufügen
except KeyError as e:
# Fehlerbehandlung bei fehlenden Schlüsseln
calling_function = inspect.stack()[0][3]
template = "Exception of type [{0}] occurred in function {1} in iteration {2} of the loop.. Arguments: {3!r}. Message: [{4}] entry might be missing"
message = template.format(
type(e).__name__,
calling_function,
iteration_number,
e.args,
e,
)
JsonParser.exceptionMessages.append(message) # Fehlermeldung speichern
except Exception as e:
# Allgemeine Fehlerbehandlung
calling_function = inspect.stack()[0][3]
template = "Exception of type [{0}] occurred in function {1} in iteration {2} of the loop. Arguments: {3!r}. Message: {4}."
message = template.format(
type(e).__name__,
calling_function,
iteration_number,
e.args,
e.__str__(),
)
JsonParser.exceptionMessages.append(message) # Fehlermeldung speichern
return {
"Serializer": "Subject",
"status": "success",
"created_count": len(created_subjects),
} # Rückgabe des Erfolgsstatus
def validate(self, data):
# Validierung, um sicherzustellen, dass 'revision' und 'array' Felder vorhanden sind
if "revision" not in data.keys():
raise serializers.ValidationError("Das 'revision'-Feld ist erforderlich.")
if "array" not in data or not isinstance(data["array"], list):
raise serializers.ValidationError(
"Das 'array'-Feld ist erforderlich und muss als Python-Liste verfügbar sein."
)
return {"dataArray": data["array"]} # Rückgabe des validierten Datenarrays
class Exam_JSON_Serializer(serializers.Serializer):
# Define the fields expected by the serializer
revision = serializers.CharField(required=True)
array = serializers.ListField()
def create(self, validated_data):
# Define the expected keys in the JSON data
idKey = "kennung"
yearKey = "jahr"
monthKey = "monat"
dayKey = "tag"
hourKey = "stunde"
minuteKey = "minute"
weekdayKey = "wochentag"
dateKey = "termin"
partialExamsKey = "teilpruefungen"
partialExamIdKey = "kennung"
numRegStudKey = "anzahlAnmeldungen"
subjectIdsKey = "fachkennungen"
examExecutionsKey = "pruefungsdurchfuehrungen"
lecturerIdKey = "dozentenkennung"
supervisorTypeKey = "typ"
locationKey = "raum"
created_exams = [] # List to store created Exam objects
created_partialexams = [] # List to store created Exam objects
ignored_Vorgezogen_exams = 0 # List to ignored Exam objects
ignored_KeinTermin_exams = 0 # List to ignored Exam objects
try:
# Iterate over the data to create Exam objects
for iteration_number, d in enumerate(validated_data["dataArray"]):
# Certain exam entries are ignored
if d["pruefungsart"] == "LN_VORGEZOGEN": # These entries can be ignored
ignored_Vorgezogen_exams += 1
continue
keyAvailable = "termin" in d
if not keyAvailable:
# Warning if no examination date is assigned
template = "Warning. Following Exam was not assigned any examination date ['termin']: [{0}]"
message = template.format(d[idKey])
JsonParser.warningMessages.append(message)
ignored_KeinTermin_exams += 1
continue
# Extract date and time for the exam
date = datetime.date(
d[dateKey][yearKey], d[dateKey][monthKey], d[dateKey][dayKey]
)
time = datetime.time(d[dateKey][hourKey], d[dateKey][minuteKey])
# Create and save a new Exam entry
examEntry = Exam(
identification=d[idKey],
date=date,
time=time,
weekday=d[dateKey][weekdayKey],
)
examEntry.save()
created_exams.append(examEntry)
# Create PartialExam entries and link them with the main exam
for teilpruef in d[partialExamsKey]:
partialExamId = teilpruef[partialExamIdKey]
regStudCount = teilpruef[numRegStudKey]
# Create and save a new PartialExam entry
partialExam = PartialExam(
identification=partialExamId, regStudCount=regStudCount
)
partialExam.save()
created_partialexams.append(partialExam)
# Link Exam and PartialExam (One-to-Many relationship)
examEntry.partialexam_set.add(partialExam)
# Link subjects to the PartialExams
for subId in teilpruef[subjectIdsKey]:
try:
# Check if the subject exists and link it
subject = Subject.objects.get(identification=subId)
partialExam.subjectIds.add(
subject
) # Many-to-Many relationship
except Exception as e:
# Log an error if the subject is not found
template = "Exception of type [{0}]. Arguments: {1!r}. Message: [{2}] entry might be missing"
message = template.format(type(e).__name__, e.args, subId)
JsonParser.exceptionMessages.append(message)
# Link exam executions to PartialExams
for pruefdurch in teilpruef[examExecutionsKey]:
lecId = pruefdurch[lecturerIdKey]
supervisorType = pruefdurch[supervisorTypeKey]
location = pruefdurch[locationKey]
# Create and save a new ExamExecution entry
examExecution = ExamExecution(
supervisorType=supervisorType, location=location
)
examExecution.save()
# Check if the lecturer exists and link the exam execution
try:
lecturer = Lecturer.objects.get(pk=lecId)
lecturer.examexecution_set.add(
examExecution
) # One-to-Many relationship
except Exception as e:
# Log an error if the lecturer is not found
template = "Exception of type [{0}]. Arguments: {1!r}. Message: [{2}] entry might be missing"
message = template.format(type(e).__name__, e.args, lecId)
JsonParser.exceptionMessages.append(message)
# Link subjects to the ExamExecution
for subId in pruefdurch[subjectIdsKey]:
try:
# Check if the subject exists and link it
subject = Subject.objects.get(identification=subId)
examExecution.subjectIds.add(
subject
) # Many-to-Many relationship
except Exception as e:
# Log an error if the subject is not found
template = "Exception of type [{0}]. Arguments: {1!r}. Message: [{2}] entry might be missing"
message = template.format(
type(e).__name__, e.args, subId
)
JsonParser.exceptionMessages.append(message)
# Link PartialExam and ExamExecution (One-to-Many relationship)
partialExam.examexecution_set.add(examExecution)
except KeyError as e:
# Error handling for missing keys in the dict
calling_function = inspect.stack()[0][3]
template = "Exception of type [{0}] occurred in function {1} in iteration {2} of the loop.. Arguments: {3!r}. Message: [{4}] entry might be missing"
message = template.format(
type(e).__name__,
calling_function,
iteration_number,
e.args,
e,
)
JsonParser.exceptionMessages.append(message)
except Exception as e:
# General error handling
calling_function = inspect.stack()[0][3]
template = "Exception of type [{0}] occurred in function {1} in iteration {2} of the loop. Arguments: {3!r}. Message: {4}."
message = template.format(
type(e).__name__,
calling_function,
iteration_number,
e.args,
e.__str__(),
)
JsonParser.exceptionMessages.append(message)
return {
"Serializer": "Exam",
"status": "success",
"created_Exams": len(created_exams),
"created_PartialExams_count": len(created_partialexams),
"ignored_Exams -> Vorgezogen": ignored_Vorgezogen_exams,
"ignored_Exams -> Kein Termin": ignored_KeinTermin_exams,
}
def validate(self, data):
# Validation for the 'revision' field
if "revision" not in data.keys():
raise serializers.ValidationError("The 'revision' field is required.")
# Validation for the 'array' field
if "array" not in data or not isinstance(data["array"], list):
raise serializers.ValidationError(
"The 'array' field is required and must be a Python list."
)
return {"dataArray": data["array"]}
class RoomAllocation_JSON_Serializer(serializers.Serializer):
# Define the fields expected by the serializer
revision = serializers.CharField(required=True)
array = serializers.ListField()
def create(self, validated_data):
# Define the expected keys in the JSON data
idKey = "kennungPruefung"
yearKey = "jahr"
monthKey = "monat"
dayKey = "tag"
hourKey = "stunde"
minuteKey = "minute"
weekdayKey = "wochentag"
locationKey = "raum"
dateKey = "termin"
matrikelKey = "matrikel"
created_roomAllocations = [] # List to store created StudentExam objects
try:
# Iterate over the data to create StudentExam objects
for iteration_number, d in enumerate(validated_data["dataArray"]):
# Extract date and time
date = datetime.date(
d[dateKey][yearKey], d[dateKey][monthKey], d[dateKey][dayKey]
)
time = datetime.time(d[dateKey][hourKey], d[dateKey][minuteKey])
# Find the corresponding Exam entry
exam = Exam.objects.filter(
identification=d[idKey], date=date, time=time
).first()
# Iterate through partial exams and their executions
partialExams = exam.partialexam_set.all()
for partialExam in partialExams:
examExecutions = partialExam.examexecution_set.all()
for examExecution in examExecutions:
# Check if the location matches and create StudentExam entry
if examExecution.location == d[locationKey]:
studentExamEntry = StudentExam(
examIdentification=d[idKey],
date=date,
time=time,
weekday=d[dateKey][weekdayKey],
location=d[locationKey],
examExecution=examExecution,
)
studentExamEntry.save()
created_roomAllocations.append(studentExamEntry)
# Create or update Student entries
for matrikel in d[matrikelKey]:
studentEntry = Student.objects.filter(matrikel=matrikel).exists()
if not studentEntry:
# Create a new Student entry if it doesn't exist
entry = Student(matrikel=matrikel)
entry.save()
else:
entry = Student.objects.all().filter(matrikel=matrikel).first()
entry.exams.add(studentExamEntry)
except KeyError as e:
# Error handling for missing keys in the dict
calling_function = inspect.stack()[0][3]
template = "Exception of type [{0}] occurred in function {1} in iteration {2} of the loop.. Arguments: {3!r}. Message: [{4}] entry might be missing"
message = template.format(
type(e).__name__,
calling_function,
iteration_number,
e.args,
e,
)
JsonParser.exceptionMessages.append(message)
except Exception as e:
# General error handling
calling_function = inspect.stack()[0][3]
template = "Exception of type [{0}] occurred in function {1} in iteration {2} of the loop. Arguments: {3!r}. Message: {4}."
message = template.format(
type(e).__name__,
calling_function,
iteration_number,
e.args,
e.__str__(),
)
JsonParser.exceptionMessages.append(message)
return {
"Serializer": "RoomAllocation",
"status": "success",
"created_RoomAllocations": len(created_roomAllocations),
}
def validate(self, data):
# Validation for the 'revision' field
if "revision" not in data.keys():
raise serializers.ValidationError("The 'revision' field is required.")
# Validation for the 'array' field
if "array" not in data or not isinstance(data["array"], list):
raise serializers.ValidationError(
"The 'array' field is required and must be a Python list."
)
return {"dataArray": data["array"]}

24
Backend/api/urls.py Normal file
View File

@ -0,0 +1,24 @@
from django.urls import path
from . import views
from rest_framework_simplejwt.views import TokenRefreshView
from rest_framework.authtoken.views import obtain_auth_token
urlpatterns = [
path(
"user/token",
views.MyTokenObtainPairView.as_view(),
), # URL to classview fpr token interactions "as_view()"
path(
"user/token/refresh/", TokenRefreshView.as_view()
), # Lib view from rest_framework to refresh the given token
path("user/logoutUser", views.logoutUser),
path("user/checkUser", views.checkLoginStatus),
path("fetchExamData", views.getAPI_ExamDataSet_ForStudent),
path("pruefplanUpload/", views.PruefPlanUploader.as_view()),
path("api-token-auth/", obtain_auth_token),
path("addlecturer", views.addLecturer),
path("addAttendance", views.addAttendance),
path("addSubject", views.addSubject),
path("addExam", views.addExam),
path("addRoomAllocation", views.addRoomAllocation),
]

250
Backend/api/views.py Normal file
View File

@ -0,0 +1,250 @@
from datetime import date
import time
from rest_framework.response import Response
from rest_framework.decorators import api_view
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from pruefplan_viewer.models import *
from .serializer import *
from django.contrib.auth import authenticate, login, logout
from django.shortcuts import render
from api import serializer as api_serializer
from rest_framework_simplejwt.views import TokenObtainPairView
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from django.utils.encoding import force_str
# Create a REST API view class to interact with the custom token serializer class
class MyTokenObtainPairView(TokenObtainPairView):
# Setup the serializer -> using custom serializer
serializer_class = api_serializer.MyTokenObtainPairSerializer
# Func to handle a post request on this API endpoint
def post(self, request, *args, **kwargs):
username = request.data.get("username")
password = request.data.get("password")
# User authentication using LADP agains the TH-Server -> Setup LADP in settings.py
user = authenticate(username=username, password=password)
if not user:
return Response({"error": "Invalid credentials"}, status=400)
# Login the user for Backend authentication
login(request=request, user=user)
# Delegate the post request to the TokenObptainPairView class
# Executing the standart jwt creation workflow -> using the custom serializer
return super().post(request, *args, **kwargs)
@api_view(["POST"])
def logoutUser(request):
logout(request)
return Response()
# Extract the location key and map it to a URL
def map_location(location_str):
URL_MAP = {
"B": "https://www.th-nuernberg.de/wie-erreichen-sie-uns/anfahrt/b-standort-bahnhofstrasse/",
"F": "https://www.th-nuernberg.de/wie-erreichen-sie-uns/anfahrt/f-standort-auf-aeg/",
"H": "https://www.th-nuernberg.de/wie-erreichen-sie-uns/anfahrt/h-standort-hohfederstrasse/",
"K": "https://www.th-nuernberg.de/wie-erreichen-sie-uns/anfahrt/k-standort-kesslerplatz/",
"NM": "https://www.th-nuernberg.de/wie-erreichen-sie-uns/anfahrt/nm-standort-neumarkt-in-der-oberpfalz/",
"SC": "https://www.th-nuernberg.de/wie-erreichen-sie-uns/anfahrt/sc-standort-innere-cramer-klett-strasse/",
"SG": "https://www.th-nuernberg.de/wie-erreichen-sie-uns/anfahrt/sg-standort-karl-grillenberger-strasse/",
"SK": "https://www.th-nuernberg.de/wie-erreichen-sie-uns/anfahrt/sk-standort-prof-ernst-nathan-strasse/",
"SP": "https://www.th-nuernberg.de/wie-erreichen-sie-uns/anfahrt/sp-standort-kesslerstrasse/",
"SR": "https://www.th-nuernberg.de/wie-erreichen-sie-uns/anfahrt/sr-standort-rednitzhembach/",
"W": "https://www.th-nuernberg.de/wie-erreichen-sie-uns/anfahrt/w-standort-wassertorstrasse/",
"OL": "Online",
"MC": "https://www.th-nuernberg.de/wie-erreichen-sie-uns/anfahrt/pruefungsstandort-mensa-insel-schuett/",
"Default": "https://www.th-nuernberg.de/wie-erreichen-sie-uns/anfahrt/",
}
if not location_str:
return URL_MAP["Default"] # Fallback URL if location is empty or None
location_key = location_str.split(".")[0] # Get the first part before '.'
# Find the matching key
for key in URL_MAP.keys():
if location_key.startswith(key):
return URL_MAP[key]
return URL_MAP["Default"] # If no match is found, return the default URL
def get_Subject(exam):
partialExam = PartialExam.objects.filter(identification=exam).first()
subjects = partialExam.subjectIds
result = [subject.name for subject in subjects]
pass
class PruefPlanUploader(APIView):
permission_classes = (IsAuthenticated,)
def post(self, request):
timeStart = time.perf_counter()
JsonParser().clearTable()
dataSets = [dataSet for dataSet in request.data]
serializers = [
Lecturer_JSON_Serializer(data=dataSets[0]),
Attendance_JSON_Serializer(data=dataSets[1]),
Subject_JSON_Serializer(data=dataSets[2]),
Exam_JSON_Serializer(data=dataSets[3]),
RoomAllocation_JSON_Serializer(data=dataSets[4]),
]
respondList = [
serializer.save() for serializer in serializers if serializer.is_valid()
]
partial_delta = generate_ExamDataSet_ForStudent()
timeEnd = time.perf_counter()
total_delta = timeEnd - timeStart
respondList.append({"Uploadtime_in_Sekonds": {round(total_delta, ndigits=2)}})
respondList.append({"ExamDataSettime_in_Sekonds": {round(partial_delta, ndigits=2)}})
return Response(respondList)
@api_view(["GET"])
def getAPI_ExamDataSet_ForStudent(request):
exam_data = API_ExamDataSet_ForStudent.objects.all()
# Serialize the data using the ModelSerializer
serializer = API_ExamDataSet_ForStudentSerializer(exam_data, many=True)
return Response(serializer.data) # This will return the data as JSON
@api_view(["POST"])
def pruefplanUpload(request):
timeStart = time.perf_counter()
dataSets = [dataSet for dataSet in request.data]
serializers = [
Lecturer_JSON_Serializer(data=dataSets[0]),
Attendance_JSON_Serializer(data=dataSets[1]),
Subject_JSON_Serializer(data=dataSets[2]),
Exam_JSON_Serializer(data=dataSets[3]),
RoomAllocation_JSON_Serializer(data=dataSets[4]),
]
respondList = [
serializer.save() for serializer in serializers if serializer.is_valid()
]
generate_ExamDataSet_ForStudent()
timeEnd = time.perf_counter()
delta = timeEnd - timeStart
print(f"Time for Upload: {round(delta)}s")
respondList.append(delta)
return Response(respondList)
def generate_ExamDataSet_ForStudent():
timeStart = time.perf_counter()
students = Student.objects.all()
for student in students:
for studentExam in student.exams.all():
examDataSet_ForStudent = API_ExamDataSet_ForStudent(
date=studentExam.date,
time=studentExam.time,
weekday=studentExam.weekday,
room=studentExam.location,
location=map_location(studentExam.location),
subjectIdent=list(
{
subject.name
for subject in studentExam.examExecution.partialExam.subjectIds.all()
}
)[0],
examIdent=studentExam.examIdentification,
examStudCount=studentExam.examExecution.partialExam.regStudCount,
supervisor=f"{studentExam.examExecution.lecturer.firstName} {studentExam.examExecution.lecturer.lastName}",
studMartNr=student.matrikel,
)
examDataSet_ForStudent.save()
timeEnd = time.perf_counter()
delta = timeEnd - timeStart
return delta
@api_view(["POST"])
def checkLoginStatus(request):
username = request.data["username"]
if User.objects.filter(username=username).exists():
usr = User.objects.get(username=username)
if usr.is_authenticated:
return Response(data="IsLoggedin")
else:
return Response(data="IsNotLoggedin")
else:
return Response(data="No user exist with this {} username.".format(username))
@api_view(["POST"])
def addLecturer(request):
serializer = Lecturer_JSON_Serializer(data=request.data)
if serializer.is_valid():
Lecturer_JASON_Serializer_Respond = (
serializer.save()
) # Hier wird der Rückgabewert von create() erfasst
# Erstelle eine Response basierend auf den erstellten Lecturer-Objekten
return Response(Lecturer_JASON_Serializer_Respond)
@api_view(["POST"])
def addAttendance(request):
serializer = Attendance_JSON_Serializer(data=request.data)
if serializer.is_valid():
Attendance_JSON_Serializer_Respond = (
serializer.save()
) # Hier wird der Rückgabewert von create() erfasst
# Erstelle eine Response basierend auf den erstellten Lecturer-Objekten
return Response(Attendance_JSON_Serializer_Respond)
@api_view(["POST"])
def addSubject(request):
serializer = Subject_JSON_Serializer(data=request.data)
if serializer.is_valid():
Subject_JASON_Serializer_Respond = (
serializer.save()
) # Hier wird der Rückgabewert von create() erfasst
# Erstelle eine Response basierend auf den erstellten Lecturer-Objekten
return Response(Subject_JASON_Serializer_Respond)
@api_view(["POST"])
def addExam(request):
serializer = Exam_JSON_Serializer(data=request.data)
if serializer.is_valid():
Exam_JASON_Serializer_Respond = (
serializer.save()
) # Hier wird der Rückgabewert von create() erfasst
# Erstelle eine Response basierend auf den erstellten Lecturer-Objekten
return Response(Exam_JASON_Serializer_Respond)
@api_view(["POST"])
def addRoomAllocation(request):
serializer = RoomAllocation_JSON_Serializer(data=request.data)
if serializer.is_valid():
RoomAllocation_JASON_Serializer_Respond = (
serializer.save()
) # Hier wird der Rückgabewert von create() erfasst
# Erstelle eine Response basierend auf den erstellten Lecturer-Objekten
return Response(RoomAllocation_JASON_Serializer_Respond)

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class AuthStuffConfig(AppConfig):
name = 'authstuff'

View File

@ -0,0 +1,167 @@
import logging
import datetime
import pytz
import string
import threading
import time
from ldap3 import Server, Connection, ALL, NTLM, ALL_ATTRIBUTES
from django.core.exceptions import ValidationError
from django.utils import timezone
from authstuff.models import ActiveDirectoryEntry
from django.conf import settings
logger = logging.getLogger(__name__)
LOGIN_FIELD = "sAMAccountName"
EXPIRE_FIELD = "accountExpires"
DEPARTMENT_FIELD = "department"
GIVENNAME_FIELD = "givenName"
LASTNAME_FIELD = "sn"
ROLE_FIELD = "description"
class UpdateThread(threading.Thread):
def __init__(self, connection, domain, delta):
super(UpdateThread, self).__init__(group=None)
self.logger = logging.getLogger(__name__)
self.connection = connection
self.domain = domain
self.delta = delta
def run(self):
if has_to_update(self.delta):
try:
startchars = string.ascii_lowercase[:26] + "_"
for char in startchars:
self.logger.info('Update AD entries with leading {}'.format(char))
update_entries_with_leading_char(self.connection, self.domain, char)
time.sleep(5)
except Exception as e:
self.logger.error(str(e))
self.connection.unbind()
def last_update():
result = datetime.datetime.combine(datetime.date.min, datetime.time.min)
try:
entry = ActiveDirectoryEntry.objects.latest('modified')
result = entry.modified
except:
logger.debug('No last update found')
logger.info('Last update: {}'.format(result))
return result
def has_to_update(delta):
deadline = timezone.now() - delta
deadline = deadline.replace(tzinfo=pytz.UTC)
last = last_update().replace(tzinfo=pytz.UTC)
result = settings.LDAP_FORCE_UPDATE or (last < deadline)
logger.info('Has to update: {}'.format(result))
return result
def update_entries(user, pwd, delta):
dom = "ADS1"
conn = get_connection(user, pwd, dom)
update_entries_with_conn(conn, dom, delta)
def update_entries_with_conn(conn, dom, delta):
asyncThread = UpdateThread(conn, dom, delta)
asyncThread.start()
def update_entries_with_leading_char(conn, dom, char):
conn.search(
search_base='OU=users,OU=EFI,OU=Faculties,DC=' + dom + ',DC=fh-nuernberg,DC=de',
search_filter='(&(objectclass=user)(CN=' + char + '*))',
attributes=ALL_ATTRIBUTES)
logger.info('Found {} entries'.format(len(conn.entries)))
for entry in conn.entries:
try:
clear_and_update_entry(entry)
except Exception as e:
logger.error(str(e))
def clear_and_update_entry(entry):
dic = parse_entry_dic(entry)
dic = clear_expire_date(dic)
dic = clear_department(dic)
dic = clear_description(dic)
update_entry(dic)
def update_entry(entry_dic):
login = entry_dic[LOGIN_FIELD]
try:
entry = ActiveDirectoryEntry.objects.get(canonicalName=login)
logger.debug('Update entry {}'.format(login))
except:
logger.debug('New entry {}'.format(login))
entry = ActiveDirectoryEntry()
entry.canonicalName = entry_dic[LOGIN_FIELD]
entry.accountExpires = entry_dic[EXPIRE_FIELD]
entry.department = entry_dic[DEPARTMENT_FIELD]
entry.givenName = entry_dic[GIVENNAME_FIELD]
entry.lastName = entry_dic[LASTNAME_FIELD]
entry.role = entry_dic[ROLE_FIELD]
entry.modified = timezone.now()
logger.debug(entry)
try:
entry.full_clean()
except ValidationError as e:
logger.warning('AD entry not valid: {}'.format(e.message_dict))
entry.save()
def get_connection(user, pwd, dom):
server = Server('gso1.ads1.fh-nuernberg.de', get_info=ALL)
conn = Connection(
server,
user=dom + "\\" + user,
password=pwd,
authentication=NTLM)
conn.bind()
return conn
def parse_entry_dic(entry):
def value_of_field(f):
try:
return entry[f].value
except:
return None
fieldNames = [LOGIN_FIELD, EXPIRE_FIELD, DEPARTMENT_FIELD, GIVENNAME_FIELD, LASTNAME_FIELD, ROLE_FIELD]
fields = map(value_of_field, fieldNames )
return dict(zip(fieldNames, fields))
def clear_expire_date(dic):
if dic[EXPIRE_FIELD] is not None:
expire_datetime = dic[EXPIRE_FIELD].replace(tzinfo=pytz.UTC)
else:
expire_datetime = datetime.datetime(datetime.MAXYEAR, 12, 31, tzinfo=pytz.UTC)
if expire_datetime.year == datetime.MAXYEAR:
expire_datetime = expire_datetime.replace(year=2099, hour=0, minute=0, second=0, microsecond=0)
dic[EXPIRE_FIELD] = expire_datetime
return dic
def clear_department(dic):
dic[DEPARTMENT_FIELD] = string_adjust(dic[DEPARTMENT_FIELD])
return dic
def clear_description(dic):
dic[ROLE_FIELD] = string_adjust(dic[ROLE_FIELD])
return dic
def string_adjust(s):
if s is not None:
return s[:32]
else:
return "N.N."

View File

@ -0,0 +1,28 @@
# Generated by Django 2.1.4 on 2018-12-10 11:55
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='ActiveDirectoryEntry',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(editable=False)),
('modified', models.DateTimeField()),
('accountExpires', models.DateTimeField()),
('canonicalName', models.CharField(max_length=32)),
('department', models.CharField(max_length=32)),
('givenName', models.CharField(max_length=32)),
('lastName', models.CharField(max_length=32)),
('role', models.CharField(max_length=32)),
],
),
]

View File

View File

@ -0,0 +1,25 @@
from django.db import models
from django.utils import timezone
class ActiveDirectoryEntry(models.Model):
# id = models.CharField(primary_key=True, max_length=64)
# created = models.DateTimeField(auto_now_add=True)
# modified = models.DateTimeField(auto_now=True)
created = models.DateTimeField(editable=False)
modified = models.DateTimeField()
accountExpires = models.DateTimeField()
canonicalName = models.CharField(max_length=32) #sAMAccountName
department = models.CharField(max_length=32)
givenName = models.CharField(max_length=32)
lastName = models.CharField(max_length=32) #sn
role = models.CharField(max_length=32) #description
def save(self, *args, **kwargs):
''' On save, update timestamps '''
if not self.id:
self.created = timezone.now()
self.modified = timezone.now()
return super(ActiveDirectoryEntry, self).save(*args, **kwargs)
def __str__(self):
return "AD-Entry %s (%s %s)" % (self.canonicalName, self.givenName, self.lastName)

View File

@ -0,0 +1,9 @@
from django import template
from django.contrib.auth.models import Group
register = template.Library()
@register.filter(name='has_group')
def has_group(user, group_name):
group = Group.objects.get(name=group_name)
return True if group in user.groups.all() else False

View File

@ -0,0 +1,17 @@
from django.test import TestCase
import authstuff.ldap_access
import datetime
import secrets
class ActiveDirTestCase(TestCase):
def setUp(self):
pass
def test_import_ad(self):
delta = datetime.timedelta() # datetime.timedelta(days=2)
last_update = authstuff.ldap_access.last_update()
authstuff.ldap_access.update_entries(secrets.TEST_AD_USER, secrets.TEST_AD_PASSWORD, delta)
self.assertGreater(authstuff.ldap_access.last_update(), last_update)

View File

@ -0,0 +1,20 @@
import logging
from authstuff.models import ActiveDirectoryEntry
from django.http import JsonResponse
logger = logging.getLogger(__name__)
def detail(request, login):
result = {'canonicalName' : login, 'found' : False}
try:
addata = ActiveDirectoryEntry.objects.get(canonicalName=login)
result['found'] = True
result['role'] = addata.role
result['updated'] = addata.modified
except ActiveDirectoryEntry.DoesNotExist:
pass
response = JsonResponse(result)
return response

22
Backend/manage.py Normal file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "medinf.settings")
try:
from django.core.management import execute_from_command_line
except ImportError:
# The above import may fail for some other reason. Ensure that the
# issue is really that Django is missing to avoid masking other
# exceptions on Python 2.
try:
import django
except ImportError:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
)
raise
execute_from_command_line(sys.argv)

View File

@ -0,0 +1,36 @@
import getpass
from ldap3 import Server, Connection, ALL, NTLM, ALL_ATTRIBUTES
print("Authentifizierung gegen ein Active Directory")
# Eingaben
dom = input("Domain [ADS1]:") or "ADS1"
user = input("User [hofmannol]:") or "hofmannol"
print('Password:', end='')
pwd = getpass.getpass()
# Binden an das AD
server = Server('gso1.ads1.fh-nuernberg.de', get_info=ALL)
conn = Connection(
server,
user=dom+"\\"+user,
password=pwd,
authentication=NTLM)
conn.bind()
print(conn)
print(conn.extend.standard.who_am_i())
print(conn.bound)
# Suche nach dem gerade verbundenen User
conn.search(
search_base='DC='+dom+',DC=fh-nuernberg,DC=de',
search_filter='(&(objectclass=user)(CN='+user+'))',
attributes=ALL_ATTRIBUTES)
print(conn.entries[0])
conn.unbind()

View File

View File

@ -0,0 +1,4 @@
from django.conf import settings # import the settings file
def logout_redirect(request):
return {'LOGOUT_REDIRECT_URL': settings.LOGOUT_REDIRECT_URL}

View File

@ -0,0 +1 @@
loaddata medinf/fixtures/groups.json

View File

@ -0,0 +1,30 @@
[
{
"model": "auth.group",
"pk": 1,
"fields": {
"name": "PP_ADMIN"
}
},
{
"model": "auth.group",
"pk": 10,
"fields": {
"name": "STUD_VIEW"
}
},
{
"model": "auth.group",
"pk": 11,
"fields": {
"name": "STUD_VIEW_EXTENDED"
}
},
{
"model": "auth.group",
"pk": 12,
"fields": {
"name": "STUD_ADMIN"
}
}
]

View File

@ -0,0 +1,92 @@
import logging
import traceback
import datetime
from django.contrib.auth.models import User
from ldap3 import Server, Connection, ALL, NTLM, ALL_ATTRIBUTES
from ldap3.core.exceptions import LDAPSocketOpenError
from django.conf import settings
from authstuff.ldap_access import update_entries_with_conn
class LdapBackend(object):
"""
Authenticate against a LDAP directory.
"""
log = logging.getLogger(__name__)
def check_login(self, username, password):
self.log.info("check login {0}".format(username))
server = Server(settings.LDAP_SERVER, connect_timeout=8)
qualified_user = settings.LDAP_DOMAIN + '\\' + username
conn = Connection(server, qualified_user, password=password, authentication=NTLM)
try:
conn.bind()
except LDAPSocketOpenError:
# LDAP Server nicht erreichbar
self.log.warning("LDAP check_login: Server " + settings.LDAP_SERVER + " not reachable.")
return None
except:
var = traceback.format_exc()
self.log.info("LDAP check_login(bind): Unexpected Error %s" % var)
return None
result = None
self.log.info("Bind successful")
try:
if conn.extend.standard.who_am_i() != None:
conn.search(
search_base='DC=' + settings.LDAP_DOMAIN + ',DC=fh-nuernberg,DC=de',
search_filter='(&(objectclass=user)(CN=' + username + '))', attributes=ALL_ATTRIBUTES)
info = conn.entries[0]
result = {'lastname' : str(info.sn),
'givenname' : str(info.givenName),
'login' : str(info.cn),
'department' : str(info.department),
'role' : str(info.description)}
self.log.info("LDAP check_login: %s" % result)
except:
var = traceback.format_exc()
self.log.warning("LDAP check_login: Unexpected Error %s" % var)
try:
delta = datetime.timedelta(days=settings.LDAP_UPDATE_TIMEOUT_DAYS)
update_entries_with_conn(conn, settings.LDAP_DOMAIN, delta)
except:
var = traceback.format_exc()
self.log.warning("LDAP check_login: Update AD Entries failed %s" % var)
return result
def authenticate(self, request, username=None, password=None):
ldap_user = self.check_login(username,password)
if ldap_user:
# {'lastname': 'Hofmann', 'givenname': 'Oliver', 'login': 'hofmannol', 'department': 'EFI', 'role': 'PF'}
# {'lastname': 'Wimmer', 'givenname': 'Martin', 'login': 'wimmerma', 'department': 'EFI', 'role': 'MA'}
# {'lastname': 'Mueller', 'givenname': 'Vincent', 'login': 'muellervi56608', 'department': 'EFI', 'role': 'ST'}
# {'lastname': 'Poehlau', 'givenname': 'Frank', 'login': 'poehlaufr', 'department': 'EFI', 'role': 'PF'}
try:
user = User.objects.get(username=ldap_user['login'])
except User.DoesNotExist:
self.log.info("LDAP authenticate: create new user %s" % ldap_user['login'])
user = User(username=ldap_user['login'])
user.first_name = ldap_user['givenname']
user.last_name = ldap_user['lastname']
user.is_staff = (ldap_user['role'] != 'ST')
user.is_superuser = False
user.save()
return user
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None

306
Backend/medinf/settings.py Normal file
View File

@ -0,0 +1,306 @@
"""
Django settings for medinf project.
Generated by 'django-admin startproject' using Django 1.11.2.
For more information on this file, see
https://docs.djangoproject.com/en/1.11/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
"""
from datetime import timedelta
import os
import re
import socket
# Development or Production
r = re.search(r"^172.17", socket.gethostbyname(socket.gethostname()))
DEVELOPMENT = r == None
DEBUG = True
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = DEVELOPMENT
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Log Configuration
if DEBUG:
LOGLEVEL = "DEBUG"
else:
LOGLEVEL = "INFO"
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s",
"datefmt": "%d/%b/%Y %H:%M:%S",
},
},
"handlers": {
"null": {
"level": "DEBUG",
"class": "logging.NullHandler",
},
"logfile": {
"level": "DEBUG",
"class": "logging.handlers.RotatingFileHandler",
"filename": os.path.join(BASE_DIR, "log/log.txt"),
"maxBytes": 1024 * 1024 * 5,
"backupCount": 5,
"formatter": "standard",
},
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "standard",
},
},
"loggers": {
"": {
"level": "WARNING",
"handlers": ["logfile"],
},
"django.db.backends": {
"handlers": ["console"],
"level": "INFO", # set DEBUG if needed
"propagate": False,
},
"medinf": {
"handlers": ["console"],
"level": LOGLEVEL,
"propagate": True,
},
"studis": {
"handlers": ["console"],
"level": LOGLEVEL,
"propagate": True,
},
"authstuff": {
"handlers": ["console"],
"level": "INFO",
"propagate": True,
},
},
}
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "XXXXXn!i2lx(&)gq7l!art_pr&#9et*$r)r&ogu&j-+wm0^ni5"
USE_X_FORWARDED_HOST = True
ALLOWED_HOSTS = [
"medinf.efi.th-nuernberg.de",
"api.efi.th-nuernberg.de",
"localhost",
"127.0.0.1",
]
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# Custom Apps
"authstuff.apps.AuthStuffConfig",
"pruefplan_viewer.apps.PruefplanViewerConfig",
"pruefplan_parser.apps.PruefplanParserConfig",
# Third Party Apps
"rest_framework",
"rest_framework_simplejwt.token_blacklist",
"rest_framework.authtoken",
"corsheaders",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "medinf.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [os.path.join(BASE_DIR, "templates")],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"medinf.context_processors.logout_redirect",
],
},
},
]
WSGI_APPLICATION = "medinf.wsgi.application"
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
if DEVELOPMENT:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}
else:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": "django-app",
"USER": "django-app",
"PASSWORD": "8TFXHv9X",
"HOST": "mysql",
"PORT": "3306",
"OPTIONS": {"init_command": "SET sql_mode='STRICT_TRANS_TABLES'"},
}
}
# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/
LANGUAGE_CODE = "de-de"
TIME_ZONE = "Europe/Berlin"
USE_I18N = True
USE_L10N = True
USE_TZ = True
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
# Nach dem Deployment werden die statischen Inhalte von medinf geholt.
# Sie müssen in das Verzeichnis <HTMLROOT>/static kopiert werden
STATIC_ROOT = os.path.join("/tmp", "static")
STATIC_URL = "/static/"
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"),
]
# Konfiguration des Auth-Systems
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
}
LDAP_DOMAIN = "ADS1"
LDAP_SERVER = "gso1.ads1.fh-nuernberg.de"
LDAP_FORCE_UPDATE = False
LDAP_UPDATE_TIMEOUT_DAYS = 2
if DEVELOPMENT:
LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"
LOGIN_URL = "/accounts/login/"
else:
LOGIN_REDIRECT_URL = "/app/"
LOGOUT_REDIRECT_URL = "/app/"
LOGIN_URL = "/app/accounts/login/"
if DEVELOPMENT:
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
]
else:
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"medinf.ldap_backend.LdapBackend",
]
# Config the JWT Token with static settings -> from docs
# https://django-rest-framework-simplejwt.readthedocs.io/en/latest/
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
"REFRESH_TOKEN_LIFETIME": timedelta(days=50),
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
"UPDATE_LAST_LOGIN": False,
"ALGORITHM": "HS256",
"VERIFYING_KEY": None,
"AUDIENCE": None,
"ISSUER": None,
"JWK_URL": None,
"LEEWAY": 0,
"AUTH_HEADER_TYPES": ("Bearer",),
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
"USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
"TOKEN_TYPE_CLAIM": "token_type",
"TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
"JTI_CLAIM": "jti",
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
}
# Set coresheader to allow all origin
CORS_ALLOW_ALL_ORIGINS = True
# Konfiguration Prüfungsplan
PP_UPLOAD_DIR = os.path.join(BASE_DIR, "pruefplan_parser", "data")
PP_TEACHER_FILE = os.path.join(PP_UPLOAD_DIR, "Dozenten.json")
PP_SUBJECT_FILE = os.path.join(PP_UPLOAD_DIR, "Faecher.json")
PP_ATTENDANCE_FILE = os.path.join(PP_UPLOAD_DIR, "Praesenzen.json")
PP_EXAM_FILE = os.path.join(PP_UPLOAD_DIR, "Pruefungen.json")
PP_STUDENT_FILE = os.path.join(PP_UPLOAD_DIR, "Saaleinteilung.json")

37
Backend/medinf/urls.py Normal file
View File

@ -0,0 +1,37 @@
"""medinf URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.11/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf.urls import include
from django.contrib import admin
from django.urls import path
from django.views.generic import TemplateView
import medinf.views
urlpatterns = [
path("", TemplateView.as_view(template_name="index.html")),
path("navlogin/", medinf.views.navlogin, name="navlogin"),
path("admin/", admin.site.urls),
path("accounts/", include("django.contrib.auth.urls")),
path("pruefplan_viewer/", include("pruefplan_viewer.urls")),
path("pruefplan_parser/", include("pruefplan_parser.urls")),
path("api/", include("api.urls")), # Link the api urls to the main access point
path(
"authenticate/",
medinf.views.AuthenticateView.as_view(),
name="authenticateView",
), # REST call to authenticate a user in the backend
]

59
Backend/medinf/views.py Normal file
View File

@ -0,0 +1,59 @@
from django.contrib.auth import authenticate, login, logout
from django.shortcuts import render, redirect
from yaml import serialize
from api.serializer import MyTokenObtainPairSerializer
from api.views import MyTokenObtainPairView
import medinf.settings
import logging
import medinf.ldap_backend
def navlogin(request):
log = logging.getLogger("medinf")
logout(request)
error = ""
if request.POST:
username = request.POST.get("username", "?")
password = request.POST.get("password", "?")
user = authenticate(username=username, password=password)
if user is not None:
if user.is_active:
login(request, user)
return redirect(medinf.settings.LOGIN_REDIRECT_URL)
else:
log.info("Inactive user {} tried to login".format(username))
error = "Ihre Benutzerkennung wurde deaktiviert."
else:
log.info("Login failed for {}".format(username))
error = "Benutzername oder Kennwort falsch."
context = {"error": error}
return render(request, "index.html", context)
################ New REST Token Auth #########################################
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework.views import APIView
from rest_framework.response import Response
from django.contrib.auth import authenticate
class AuthenticateView(APIView):
def post(self, request):
username = request.data.get("username")
password = request.data.get("password")
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user) #login the user in the backend
# refresh = RefreshToken.for_user(user)
serializer = MyTokenObtainPairSerializer(data={"username":username, "password":password})
serializer.is_valid(raise_exception=True)
token = serializer.validated_data
return Response(token)
return Response({"error": "Invalid credentials"}, status=400)

21
Backend/medinf/wsgi.py Normal file
View File

@ -0,0 +1,21 @@
"""
WSGI config for medinf project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
"""
import os
import logging
import medinf.settings
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "medinf.settings")
log = logging.getLogger(__name__)
log.info("Starting in %s Environment" % ('DEV' if medinf.settings.DEVELOPMENT else 'PROD'))
application = get_wsgi_application()

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class PruefplanParserConfig(AppConfig):
name = 'pruefplan_parser'

View File

@ -0,0 +1,229 @@
{
"revision": "2648:2829M",
"array": [
{
"kennung": "259",
"vorname": "Christine",
"nachname": "Rademacher",
"titel": "Prof. Dr.",
"fakultaet": "AMP",
"istLehrbeauftragter": false
},
{
"kennung": "315",
"vorname": "J\u00c3\u00b6rg",
"nachname": "Steinbach",
"titel": "Prof. Dr.",
"fakultaet": "AMP",
"istLehrbeauftragter": false
},
{
"kennung": "385",
"vorname": "Peter",
"nachname": "Jonas",
"titel": "Prof. Dr.",
"fakultaet": "AMP",
"istLehrbeauftragter": false
},
{
"kennung": "71655",
"vorname": "Melanie",
"nachname": "Basting",
"titel": "Prof. Dr.",
"fakultaet": "AMP",
"istLehrbeauftragter": false
},
{
"kennung": "103",
"vorname": "Thomas",
"nachname": "Giesler",
"titel": "Prof. Dr.",
"fakultaet": "EFI",
"istLehrbeauftragter": false
},
{
"kennung": "70901",
"vorname": "Sven",
"nachname": "Loquai",
"titel": "Prof. Dr.-Ing.",
"fakultaet": "EFI",
"istLehrbeauftragter": false
},
{
"kennung": "166",
"vorname": "Bernd",
"nachname": "Klehn",
"titel": "Prof. Dr.",
"fakultaet": "EFI",
"istLehrbeauftragter": false
},
{
"kennung": "382",
"vorname": "Michael",
"nachname": "Zwanger",
"titel": "Prof. Dr.",
"fakultaet": "EFI",
"istLehrbeauftragter": false
},
{
"kennung": "74090",
"vorname": "Olga",
"nachname": "Battermann",
"titel": "Dr.",
"fakultaet": "EFI",
"istLehrbeauftragter": true
},
{
"kennung": "73247",
"vorname": "Michael",
"nachname": "Mayle",
"titel": "Prof. Dr.",
"fakultaet": "AMP",
"istLehrbeauftragter": false
},
{
"kennung": "316",
"vorname": "Florian",
"nachname": "Steinmeyer",
"titel": "Prof. Dr.",
"fakultaet": "AMP",
"istLehrbeauftragter": false
},
{
"kennung": "49",
"vorname": "Bernd",
"nachname": "Braun",
"titel": "Prof. Dr.",
"fakultaet": "AMP",
"istLehrbeauftragter": false
},
{
"kennung": "73630",
"vorname": "Peter",
"nachname": "Eberhardt",
"titel": "Prof. Dr.",
"fakultaet": "EFI",
"istLehrbeauftragter": true
},
{
"kennung": "576",
"vorname": "J\u00c3\u00bcrgen",
"nachname": "Krumm",
"titel": "Prof.",
"fakultaet": "EFI",
"istLehrbeauftragter": false
},
{
"kennung": "219",
"vorname": "Stefan",
"nachname": "May",
"titel": "Prof. Dr.",
"fakultaet": "EFI",
"istLehrbeauftragter": false
},
{
"kennung": "75398",
"vorname": "Christian",
"nachname": "Pfitzner",
"titel": "Prof. Dr.",
"fakultaet": "EFI",
"istLehrbeauftragter": false
},
{
"kennung": "508",
"vorname": "Wolfgang",
"nachname": "M\u00c3\u00b6nch",
"titel": "Prof. Dr.",
"fakultaet": "EFI",
"istLehrbeauftragter": false
},
{
"kennung": "585",
"vorname": "Rainer",
"nachname": "Engelbrecht",
"titel": "Prof. Dr.-Ing.",
"fakultaet": "EFI",
"istLehrbeauftragter": false
},
{
"kennung": "73998",
"vorname": "Stefan",
"nachname": "Wenhardt",
"titel": "",
"fakultaet": "AMP",
"istLehrbeauftragter": true
},
{
"kennung": "74065",
"vorname": "Matthias",
"nachname": "Gleich",
"titel": "",
"fakultaet": "EFI",
"istLehrbeauftragter": true
},
{
"kennung": "376",
"vorname": "Olaf",
"nachname": "Ziemann",
"titel": "Prof. Dr.",
"fakultaet": "EFI",
"istLehrbeauftragter": false
},
{
"kennung": "350",
"vorname": "Sebastian",
"nachname": "Walter",
"titel": "Prof. Dr.",
"fakultaet": "EFI",
"istLehrbeauftragter": false
},
{
"kennung": "75807",
"vorname": "Andreas",
"nachname": "Stute",
"titel": "Prof. Dr.",
"fakultaet": "EFI",
"istLehrbeauftragter": false
},
{
"kennung": "492",
"vorname": "G\u00c3\u00bcnter",
"nachname": "Kie\u00c3\u0178ling",
"titel": "Prof. Dr.",
"fakultaet": "EFI",
"istLehrbeauftragter": false
},
{
"kennung": "84399",
"vorname": "Anja",
"nachname": "Freudenreich",
"titel": "Prof. Dr.",
"fakultaet": "EFI",
"istLehrbeauftragter": false
},
{
"kennung": "38496",
"vorname": "Jan",
"nachname": "Paulus",
"titel": "Prof. Dr.",
"fakultaet": "EFI",
"istLehrbeauftragter": false
},
{
"kennung": "62711",
"vorname": "Hritam",
"nachname": "Dutta",
"titel": "Prof. Dr.",
"fakultaet": "EFI",
"istLehrbeauftragter": false
},
{
"kennung": "73323",
"vorname": "Enrico",
"nachname": "Schr\u00c3\u00b6der;",
"titel": "Prof. Dr.",
"fakultaet": "EFI",
"istLehrbeauftragter": false
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,161 @@
{
"revision": "2648:2829M",
"array": [
{
"dozentenkennung": "103",
"termin": {
"datumText": "22.07.2024 11:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 22,
"stunde": 11,
"minute": 0,
"wochentag": "MONTAG",
"pruefungswoche": 2
}
},
{
"dozentenkennung": "70901",
"termin": {
"datumText": "11.07.2024 08:30 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 11,
"stunde": 8,
"minute": 30,
"wochentag": "DONNERSTAG",
"pruefungswoche": 1
}
},
{
"dozentenkennung": "166",
"termin": {
"datumText": "16.07.2024 16:30 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 16,
"stunde": 16,
"minute": 30,
"wochentag": "DIENSTAG",
"pruefungswoche": 1
}
},
{
"dozentenkennung": "382",
"termin": {
"datumText": "16.07.2024 14:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 16,
"stunde": 14,
"minute": 0,
"wochentag": "DIENSTAG",
"pruefungswoche": 1
}
},
{
"dozentenkennung": "576",
"termin": {
"datumText": "03.07.2024 11:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 3,
"stunde": 11,
"minute": 0,
"wochentag": "MITTWOCH",
"pruefungswoche": 0
}
},
{
"dozentenkennung": "75398",
"termin": {
"datumText": "09.07.2024 11:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 9,
"stunde": 11,
"minute": 0,
"wochentag": "DIENSTAG",
"pruefungswoche": 0
}
},
{
"dozentenkennung": "508",
"termin": {
"datumText": "13.07.2024 11:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 13,
"stunde": 11,
"minute": 0,
"wochentag": "SAMSTAG",
"pruefungswoche": 1
}
},
{
"dozentenkennung": "585",
"termin": {
"datumText": "13.07.2024 11:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 13,
"stunde": 11,
"minute": 0,
"wochentag": "SAMSTAG",
"pruefungswoche": 1
}
},
{
"dozentenkennung": "492",
"termin": {
"datumText": "19.07.2024 11:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 19,
"stunde": 11,
"minute": 0,
"wochentag": "FREITAG",
"pruefungswoche": 2
}
},
{
"dozentenkennung": "84399",
"termin": {
"datumText": "26.07.2024 16:30 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 26,
"stunde": 16,
"minute": 30,
"wochentag": "FREITAG",
"pruefungswoche": 3
}
},
{
"dozentenkennung": "38496",
"termin": {
"datumText": "05.07.2024 11:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 5,
"stunde": 11,
"minute": 0,
"wochentag": "FREITAG",
"pruefungswoche": 0
}
},
{
"dozentenkennung": "62711",
"termin": {
"datumText": "04.07.2024 14:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 4,
"stunde": 14,
"minute": 0,
"wochentag": "DONNERSTAG",
"pruefungswoche": 0
}
}
]
}

View File

@ -0,0 +1,803 @@
{
"revision": "2648:2829M",
"array": [
{
"kennung": "M1:SU:BEI:1",
"pruefungsart": "LN_STUDIENBEGLEITEND",
"termin": {
"datumText": "10.07.2024 11:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 10,
"stunde": 11,
"minute": 0,
"wochentag": "MITTWOCH",
"pruefungswoche": 0
},
"teilpruefungen": [
{
"kennung": "M1:SU:BEI:1",
"anzahlAnmeldungen": 63,
"fachkennungen": [
"BEI:20141:1110"
],
"pruefungsdurchfuehrungen": [
{
"dozentenkennung": "259",
"typ": "PRUEFER",
"raum": "BB.103",
"fachkennungen": [
"BEI:20141:1110"
]
},
{
"dozentenkennung": "315",
"typ": "PRUEFER",
"raum": "KA.450",
"fachkennungen": [
"BEI:20141:1110"
]
},
{
"dozentenkennung": "385",
"typ": "PRUEFER",
"raum": "BB.111",
"fachkennungen": [
"BEI:20141:1110"
]
},
{
"dozentenkennung": "71655",
"typ": "PRUEFER",
"raum": "BB.103",
"fachkennungen": [
"BEI:20141:1110"
]
}
]
}
],
"dozentenKennungPendler": ""
},
{
"kennung": "BMED:20141:5150:\"103,166\"_BMED:20141:5150:\"70901\"_BMED:20201:5150:\"103,166\"",
"pruefungsart": "PRUEFUNG",
"termin": {
"datumText": "11.07.2024 11:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 11,
"stunde": 11,
"minute": 0,
"wochentag": "DONNERSTAG",
"pruefungswoche": 1
},
"teilpruefungen": [
{
"kennung": "BMED:20141:5150:\"103,166\"",
"anzahlAnmeldungen": 5,
"fachkennungen": [
"BMED:20141:5150"
],
"pruefungsdurchfuehrungen": [
{
"dozentenkennung": "103",
"typ": "PRUEFER",
"raum": "KA.111",
"fachkennungen": [
"BMED:20141:5150"
]
},
{
"dozentenkennung": "70901",
"typ": "PRUEFER",
"raum": "KA.213",
"fachkennungen": [
"BMED:20141:5150"
]
},
{
"dozentenkennung": "166",
"typ": "PRUEFER",
"raum": "KA.111",
"fachkennungen": [
"BMED:20141:5150"
]
}
]
},
{
"kennung": "BMED:20141:5150:\"70901\"",
"anzahlAnmeldungen": 0,
"fachkennungen": [
"BMED:20141:5150"
],
"pruefungsdurchfuehrungen": [
{
"dozentenkennung": "103",
"typ": "PRUEFER",
"raum": "KA.111",
"fachkennungen": [
"BMED:20141:5150"
]
},
{
"dozentenkennung": "70901",
"typ": "PRUEFER",
"raum": "KA.213",
"fachkennungen": [
"BMED:20141:5150"
]
},
{
"dozentenkennung": "166",
"typ": "PRUEFER",
"raum": "KA.111",
"fachkennungen": [
"BMED:20141:5150"
]
}
]
},
{
"kennung": "BMED:20201:5150:\"103,166\"",
"anzahlAnmeldungen": 41,
"fachkennungen": [
"BMED:20201:5150"
],
"pruefungsdurchfuehrungen": [
{
"dozentenkennung": "103",
"typ": "PRUEFER",
"raum": "KA.111",
"fachkennungen": [
"BMED:20201:5150"
]
},
{
"dozentenkennung": "70901",
"typ": "PRUEFER",
"raum": "KA.213",
"fachkennungen": [
"BMED:20201:5150"
]
},
{
"dozentenkennung": "166",
"typ": "PRUEFER",
"raum": "KA.111",
"fachkennungen": [
"BMED:20201:5150"
]
}
]
}
],
"dozentenKennungPendler": ""
},
{
"kennung": "MZ1PH:SU:BMED:2_PH:SU:BMED:2",
"pruefungsart": "PRUEFUNG",
"termin": {
"datumText": "24.07.2024 14:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 24,
"stunde": 14,
"minute": 0,
"wochentag": "MITTWOCH",
"pruefungswoche": 2
},
"teilpruefungen": [
{
"kennung": "MZ1PH:SU:BMED:2",
"anzahlAnmeldungen": 2,
"fachkennungen": [
"BMED:20141:1370"
],
"pruefungsdurchfuehrungen": [
{
"dozentenkennung": "382",
"typ": "PRUEFER",
"raum": "KA.107",
"fachkennungen": [
"BMED:20141:1370"
]
},
{
"dozentenkennung": "74090",
"typ": "PRUEFER",
"raum": "KA.107",
"fachkennungen": [
"BMED:20141:1370"
]
}
]
},
{
"kennung": "PH:SU:BMED:2",
"anzahlAnmeldungen": 101,
"fachkennungen": [
"BMED:20201:1600"
],
"pruefungsdurchfuehrungen": [
{
"dozentenkennung": "73247",
"typ": "PRUEFER",
"raum": "ML.034",
"fachkennungen": [
"BMED:20201:1600"
]
},
{
"dozentenkennung": "316",
"typ": "PRUEFER",
"raum": "KA.140",
"fachkennungen": [
"BMED:20201:1600"
]
},
{
"dozentenkennung": "49",
"typ": "AUFSICHT",
"raum": "ML.034",
"fachkennungen": [
"BMED:20201:1600"
]
}
]
}
],
"dozentenKennungPendler": ""
},
{
"kennung": "126efi:WPF:EFI:0",
"pruefungsart": "LN_VORGEZOGEN",
"teilpruefungen": [],
"dozentenKennungPendler": ""
},
{
"kennung": "MUK:SU:BME:6",
"pruefungsart": "PRUEFUNG",
"teilpruefungen": [
{
"kennung": "MUK:SU:BME:6",
"anzahlAnmeldungen": 0,
"fachkennungen": [
"BME:20131:4520"
],
"pruefungsdurchfuehrungen": []
}
],
"dozentenKennungPendler": ""
},
{
"kennung": "ET1:SU:BMF:1",
"pruefungsart": "LN_STUDIENBEGLEITEND",
"termin": {
"datumText": "03.07.2024 08:30 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 3,
"stunde": 8,
"minute": 30,
"wochentag": "MITTWOCH",
"pruefungswoche": 0
},
"teilpruefungen": [
{
"kennung": "ET1:SU:BMF:1",
"anzahlAnmeldungen": 12,
"fachkennungen": [
"BMF:20171:1180"
],
"pruefungsdurchfuehrungen": [
{
"dozentenkennung": "73630",
"typ": "PRUEFER",
"raum": "KH.108",
"fachkennungen": [
"BMF:20171:1180"
]
},
{
"dozentenkennung": "576",
"typ": "PRUEFER",
"raum": "KH.108",
"fachkennungen": [
"BMF:20171:1180"
]
}
]
}
],
"dozentenKennungPendler": ""
},
{
"kennung": "mAUT4#AB:SU:MSY:1",
"pruefungsart": "PRUEFUNG",
"termin": {
"datumText": "30.07.2024 11:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 30,
"stunde": 11,
"minute": 0,
"wochentag": "DIENSTAG",
"pruefungswoche": 3
},
"teilpruefungen": [
{
"kennung": "mAUT4#AB:SU:MSY:1",
"anzahlAnmeldungen": 31,
"fachkennungen": [
"MAPR:20201:8425",
"MSY:20091:8425"
],
"pruefungsdurchfuehrungen": [
{
"dozentenkennung": "219",
"typ": "PRUEFER",
"raum": "KA.213",
"fachkennungen": [
"MAPR:20201:8425",
"MSY:20091:8425"
]
},
{
"dozentenkennung": "75398",
"typ": "PRUEFER",
"raum": "KA.213",
"fachkennungen": [
"MAPR:20201:8425",
"MSY:20091:8425"
]
}
]
}
],
"dozentenKennungPendler": ""
},
{
"kennung": "MAPR:20201:5020:\"393,159,355\"",
"pruefungsart": "LN_VORGEZOGEN",
"teilpruefungen": [],
"dozentenKennungPendler": ""
},
{
"kennung": "TO:SU:BMF:6_TO:SU:BMMF:4",
"pruefungsart": "PRUEFUNG",
"termin": {
"datumText": "13.07.2024 08:30 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 13,
"stunde": 8,
"minute": 30,
"wochentag": "SAMSTAG",
"pruefungswoche": 1
},
"teilpruefungen": [
{
"kennung": "TO:SU:BMMF:4",
"anzahlAnmeldungen": 30,
"fachkennungen": [
"BMED:20141:5350",
"BMED:20201:5350"
],
"pruefungsdurchfuehrungen": [
{
"dozentenkennung": "508",
"typ": "PRUEFER",
"raum": "KA.440",
"fachkennungen": [
"BMED:20141:5350",
"BMED:20201:5350"
]
},
{
"dozentenkennung": "585",
"typ": "PRUEFER",
"raum": "KA.107",
"fachkennungen": [
"BMED:20141:5350",
"BMED:20201:5350"
]
}
]
},
{
"kennung": "TO:SU:BMF:6",
"anzahlAnmeldungen": 29,
"fachkennungen": [
"BMF:20171:4130"
],
"pruefungsdurchfuehrungen": [
{
"dozentenkennung": "508",
"typ": "PRUEFER",
"raum": "KA.440",
"fachkennungen": [
"BMF:20171:4130"
]
},
{
"dozentenkennung": "585",
"typ": "PRUEFER",
"raum": "KA.107",
"fachkennungen": [
"BMF:20171:4130"
]
}
]
}
],
"dozentenKennungPendler": ""
},
{
"kennung": "M1:SU:BMED:1",
"pruefungsart": "LN_STUDIENBEGLEITEND",
"termin": {
"datumText": "10.07.2024 16:30 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 10,
"stunde": 16,
"minute": 30,
"wochentag": "MITTWOCH",
"pruefungswoche": 0
},
"teilpruefungen": [
{
"kennung": "M1:SU:BMED:1",
"anzahlAnmeldungen": 35,
"fachkennungen": [
"BMED:20141:1210",
"BMED:20201:1510"
],
"pruefungsdurchfuehrungen": [
{
"dozentenkennung": "73998",
"typ": "PRUEFER",
"raum": "MC.100",
"fachkennungen": [
"BMED:20141:1210",
"BMED:20201:1510"
]
},
{
"dozentenkennung": "259",
"typ": "PRUEFER",
"raum": "MC.100",
"fachkennungen": [
"BMED:20141:1210",
"BMED:20201:1510"
]
}
]
}
],
"dozentenKennungPendler": ""
},
{
"kennung": "FO:SU:BME:2",
"pruefungsart": "LN_VORGEZOGEN",
"teilpruefungen": [],
"dozentenKennungPendler": ""
},
{
"kennung": "BW:SU:BEI:5",
"pruefungsart": "PRUEFUNG",
"termin": {
"datumText": "11.07.2024 08:30 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 11,
"stunde": 8,
"minute": 30,
"wochentag": "DONNERSTAG",
"pruefungswoche": 1
},
"teilpruefungen": [
{
"kennung": "BW:SU:BEI:5",
"anzahlAnmeldungen": 49,
"fachkennungen": [
"BEI:20141:3800"
],
"pruefungsdurchfuehrungen": [
{
"dozentenkennung": "74065",
"typ": "PRUEFER",
"raum": "MC.100",
"fachkennungen": [
"BEI:20141:3800"
]
},
{
"dozentenkennung": "376",
"typ": "AUFSICHT",
"raum": "KA.111",
"fachkennungen": [
"BEI:20141:3800"
]
}
]
}
],
"dozentenKennungPendler": ""
},
{
"kennung": "MAPR:20201:5810:\"472,60104\"",
"pruefungsart": "LN_VORGEZOGEN",
"teilpruefungen": [],
"dozentenKennungPendler": ""
},
{
"kennung": "MK:SU:BMF:4",
"pruefungsart": "PRUEFUNG",
"termin": {
"datumText": "26.07.2024 08:30 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 26,
"stunde": 8,
"minute": 30,
"wochentag": "FREITAG",
"pruefungswoche": 3
},
"teilpruefungen": [
{
"kennung": "MK:SU:BMF:4",
"anzahlAnmeldungen": 43,
"fachkennungen": [
"BMF:20171:4070"
],
"pruefungsdurchfuehrungen": [
{
"dozentenkennung": "350",
"typ": "PRUEFER",
"raum": "MC.100",
"fachkennungen": [
"BMF:20171:4070"
]
}
]
}
],
"dozentenKennungPendler": ""
},
{
"kennung": "024efi:WPF:EFI:0",
"pruefungsart": "LN_VORGEZOGEN",
"teilpruefungen": [],
"dozentenKennungPendler": ""
},
{
"kennung": "ELK1:SU:BEI:3_IBT:20141:6160:\"166\"",
"pruefungsart": "PRUEFUNG",
"termin": {
"datumText": "27.07.2024 11:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 27,
"stunde": 11,
"minute": 0,
"wochentag": "SAMSTAG",
"pruefungswoche": 3
},
"teilpruefungen": [
{
"kennung": "ELK1:SU:BEI:3",
"anzahlAnmeldungen": 36,
"fachkennungen": [
"BEI:20141:5120",
"BMED:20141:5120"
],
"pruefungsdurchfuehrungen": [
{
"dozentenkennung": "166",
"typ": "PRUEFER",
"raum": "MC.100",
"fachkennungen": [
"BEI:20141:5120",
"BMED:20141:5120"
]
}
]
},
{
"kennung": "IBT:20141:6160:\"166\"",
"anzahlAnmeldungen": 6,
"fachkennungen": [
"IBT:20141:6160"
],
"pruefungsdurchfuehrungen": [
{
"dozentenkennung": "166",
"typ": "PRUEFER",
"raum": "MC.100",
"fachkennungen": [
"IBT:20141:6160"
]
}
]
}
],
"dozentenKennungPendler": ""
},
{
"kennung": "mINF6/1:SU:MSY:2",
"pruefungsart": "PRUEFUNG",
"teilpruefungen": [
{
"kennung": "mINF6/1:SU:MSY:2",
"anzahlAnmeldungen": 2,
"fachkennungen": [
"MAPR:20201:8835",
"MSY:20091:8835"
],
"pruefungsdurchfuehrungen": []
}
],
"dozentenKennungPendler": ""
},
{
"kennung": "MSY:20091:8361:\"49,75807\"",
"pruefungsart": "PRUEFUNG",
"termin": {
"datumText": "15.07.2024 08:30 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 15,
"stunde": 8,
"minute": 30,
"wochentag": "MONTAG",
"pruefungswoche": 1
},
"teilpruefungen": [
{
"kennung": "MSY:20091:8361:\"49,75807\"",
"anzahlAnmeldungen": 8,
"fachkennungen": [
"MSY:20091:8361"
],
"pruefungsdurchfuehrungen": [
{
"dozentenkennung": "75807",
"typ": "PRUEFER",
"raum": "KA.217",
"fachkennungen": [
"MSY:20091:8361"
]
},
{
"dozentenkennung": "49",
"typ": "PRUEFER",
"raum": "KA.217",
"fachkennungen": [
"MSY:20091:8361"
]
}
]
}
],
"dozentenKennungPendler": ""
},
{
"kennung": "ET2:SU:BMF:2",
"pruefungsart": "PRUEFUNG",
"termin": {
"datumText": "19.07.2024 08:30 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 19,
"stunde": 8,
"minute": 30,
"wochentag": "FREITAG",
"pruefungswoche": 2
},
"teilpruefungen": [
{
"kennung": "ET2:SU:BMF:2",
"anzahlAnmeldungen": 61,
"fachkennungen": [
"BMF:20171:1190"
],
"pruefungsdurchfuehrungen": [
{
"dozentenkennung": "73630",
"typ": "PRUEFER",
"raum": "BB.103",
"fachkennungen": [
"BMF:20171:1190"
]
},
{
"dozentenkennung": "492",
"typ": "PRUEFER",
"raum": "SP.467",
"fachkennungen": [
"BMF:20171:1190"
]
}
]
}
],
"dozentenKennungPendler": ""
},
{
"kennung": "I2#AB:SU:BEI:3",
"pruefungsart": "LN_STUDIENBEGLEITEND",
"termin": {
"datumText": "04.07.2024 11:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 4,
"stunde": 11,
"minute": 0,
"wochentag": "DONNERSTAG",
"pruefungswoche": 0
},
"teilpruefungen": [
{
"kennung": "I2#AB:SU:BEI:3",
"anzahlAnmeldungen": 63,
"fachkennungen": [
"BEI:20141:5160",
"BEI:20141:5160",
"BMED:20141:5160",
"BMED:20141:5160"
],
"pruefungsdurchfuehrungen": [
{
"dozentenkennung": "84399",
"typ": "PRUEFER",
"raum": "BB.103",
"fachkennungen": [
"BEI:20141:5160",
"BEI:20141:5160",
"BMED:20141:5160",
"BMED:20141:5160"
]
},
{
"dozentenkennung": "38496",
"typ": "PRUEFER",
"raum": "KA.450",
"fachkennungen": [
"BEI:20141:5160",
"BEI:20141:5160",
"BMED:20141:5160",
"BMED:20141:5160"
]
},
{
"dozentenkennung": "62711",
"typ": "PRUEFER",
"raum": "BB.210",
"fachkennungen": [
"BEI:20141:5160",
"BEI:20141:5160",
"BMED:20141:5160",
"BMED:20141:5160"
]
},
{
"dozentenkennung": "73323",
"typ": "PRUEFER",
"raum": "BB.103",
"fachkennungen": [
"BEI:20141:5160",
"BEI:20141:5160",
"BMED:20141:5160",
"BMED:20141:5160"
]
}
]
}
],
"dozentenKennungPendler": ""
}
]
}

View File

@ -0,0 +1 @@
Hier landen die hochgeladenen Dateien

View File

@ -0,0 +1,577 @@
{
"revision": "2648:2829M",
"array": [
{
"kennungPruefung": "M1:SU:BEI:1",
"termin": {
"datumText": "10.07.2024 11:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 10,
"stunde": 11,
"minute": 0,
"wochentag": "MITTWOCH",
"pruefungswoche": 0
},
"raum": "KA.450",
"kennungFaecher": [
"BEI:20141:1110"
],
"matrikel": [
3752511,
3743531,
3726002,
3756960,
3682959,
3747191,
3726312,
3727575,
3677443,
3724433,
3724165,
3748172,
3726338,
3671524,
3684924,
3571059,
3725229,
3678411,
3726268,
3303621,
3732643
]
},
{
"kennungPruefung": "BMED:20141:5150:\"103,166\"_BMED:20141:5150:\"70901\"_BMED:20201:5150:\"103,166\"",
"termin": {
"datumText": "11.07.2024 11:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 11,
"stunde": 11,
"minute": 0,
"wochentag": "DONNERSTAG",
"pruefungswoche": 1
},
"raum": "KA.213",
"kennungFaecher": [
"BMED:20141:5150",
"BMED:20141:5150",
"BMED:20201:5150"
],
"matrikel": [
3691111,
3503148,
3680786,
3670499,
3634750,
3706613,
3527650,
3645338,
3706064,
3674877,
3291852,
3676053,
3578540,
3690651,
3621462,
3623453,
3623581,
3673432,
2747059,
3569726,
3673458,
3535000,
3623411
]
},
{
"kennungPruefung": "MZ1PH:SU:BMED:2_PH:SU:BMED:2",
"termin": {
"datumText": "24.07.2024 14:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 24,
"stunde": 14,
"minute": 0,
"wochentag": "MITTWOCH",
"pruefungswoche": 2
},
"raum": "KA.107",
"kennungFaecher": [
"BMED:20141:1370"
],
"matrikel": [
3504398,
3516845
]
},
{
"kennungPruefung": "ET1:SU:BMF:1",
"termin": {
"datumText": "03.07.2024 08:30 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 3,
"stunde": 8,
"minute": 30,
"wochentag": "MITTWOCH",
"pruefungswoche": 0
},
"raum": "KH.108",
"kennungFaecher": [
"BMF:20171:1180"
],
"matrikel": [
3584222,
3569599,
3565942,
3749645,
3756449,
3698347,
3628502,
3182142,
3673742,
3719352,
3693298,
3756564
]
},
{
"kennungPruefung": "mAUT4#AB:SU:MSY:1",
"termin": {
"datumText": "30.07.2024 11:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 30,
"stunde": 11,
"minute": 0,
"wochentag": "DIENSTAG",
"pruefungswoche": 3
},
"raum": "KA.213",
"kennungFaecher": [
"MAPR:20201:8425",
"MSY:20091:8425"
],
"matrikel": [
3716009,
3518430,
3506909,
3513240,
3503119,
3724136,
3767142,
3767155,
3707610,
3508565,
3503429,
3581599,
3754052,
3516832,
3772463,
3508680,
3505960,
3569630,
3577853,
3244724,
3502563,
3503094,
3580352,
3164060,
3502873,
3559848,
3723960,
3758612,
3513224,
3289528,
3768800
]
},
{
"kennungPruefung": "TO:SU:BMF:6_TO:SU:BMMF:4",
"termin": {
"datumText": "13.07.2024 08:30 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 13,
"stunde": 8,
"minute": 30,
"wochentag": "SAMSTAG",
"pruefungswoche": 1
},
"raum": "KA.107",
"kennungFaecher": [
"BMED:20141:5350",
"BMED:20201:5350",
"BMF:20171:4130"
],
"matrikel": [
3622836,
3574261,
3529162,
3617023,
3616170,
3615636,
3629947,
3584699,
3624405,
3556537,
3616138,
3513422,
3619874,
3621178,
3623929,
2984098,
2931071,
3513266,
2267607
]
},
{
"kennungPruefung": "M1:SU:BMED:1",
"termin": {
"datumText": "10.07.2024 16:30 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 10,
"stunde": 16,
"minute": 30,
"wochentag": "MITTWOCH",
"pruefungswoche": 0
},
"raum": "MC.100",
"kennungFaecher": [
"BMED:20141:1210",
"BMED:20201:1510"
],
"matrikel": [
3733583,
3746008,
3751361,
3673445,
3739767,
3749629,
3719307,
3742617,
3740220,
3720860,
3721838,
3504653,
3754586,
3726479,
3561069,
3727869,
3740275,
3728051,
3724967,
3512920,
3581490,
3711062,
3759862,
3649154,
3756551,
3636093,
3719266,
3635348,
3719563,
3727393,
3761041,
3691393,
3638266,
3633414,
3622852
]
},
{
"kennungPruefung": "BW:SU:BEI:5",
"termin": {
"datumText": "11.07.2024 08:30 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 11,
"stunde": 8,
"minute": 30,
"wochentag": "DONNERSTAG",
"pruefungswoche": 1
},
"raum": "MC.100",
"kennungFaecher": [
"BEI:20141:3800"
],
"matrikel": [
3616675,
3647725,
3627406,
3572030,
3235621,
3161199,
3512045,
3628672,
3308517,
3235250,
3634396,
3616703,
3627381,
3085850,
3620436,
3622159,
3569205,
3628205,
3263104,
3516957,
3628630,
3615850,
3647866,
3627419,
3641161,
3627831,
3622191,
3017956
]
},
{
"kennungPruefung": "MK:SU:BMF:4",
"termin": {
"datumText": "26.07.2024 08:30 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 26,
"stunde": 8,
"minute": 30,
"wochentag": "FREITAG",
"pruefungswoche": 3
},
"raum": "MC.100",
"kennungFaecher": [
"BMF:20171:4070"
],
"matrikel": [
3544163,
3643800,
3621545,
3668827,
3569599,
3678479,
3246109,
3698350,
3583791,
3678437,
3693511,
3619902,
3699683,
3733244,
3627394,
3667523,
3700385,
3678226,
3698839,
3678495,
3036482,
3678440,
3684474,
3664621,
3698347,
3693243,
3564309,
3629947,
3584699,
3645721,
3687433,
3693269,
3678242,
3638914,
3619874,
3678424,
3234192,
3682438,
3669428,
3665967,
3693272,
3706626,
3665222
]
},
{
"kennungPruefung": "ELK1:SU:BEI:3_IBT:20141:6160:\"166\"",
"termin": {
"datumText": "27.07.2024 11:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 27,
"stunde": 11,
"minute": 0,
"wochentag": "SAMSTAG",
"pruefungswoche": 3
},
"raum": "MC.100",
"kennungFaecher": [
"BEI:20141:5120",
"BMED:20141:5120",
"IBT:20141:6160"
],
"matrikel": [
3569979,
3647684,
3572030,
3633737,
3530133,
3509559,
3627381,
3085850,
3706121,
3670064,
3513381,
3263104,
3670402,
3670936,
3536119,
3692598,
3615272,
3692639,
3692668,
3671962,
3667932,
3672617,
3692837,
3674822,
3671805,
3513026,
3670051,
3693214,
3577712,
3668517,
3673586,
3672068,
3702392,
3722314,
3645792,
3534748,
3638394,
3305701,
3291335,
3637256,
3589933,
3589988
]
},
{
"kennungPruefung": "MSY:20091:8361:\"49,75807\"",
"termin": {
"datumText": "15.07.2024 08:30 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 15,
"stunde": 8,
"minute": 30,
"wochentag": "MONTAG",
"pruefungswoche": 1
},
"raum": "KA.217",
"kennungFaecher": [
"MSY:20091:8361"
],
"matrikel": [
3581599,
2761145,
3132088,
3757011,
2694152,
3768798,
3713545,
3506826
]
},
{
"kennungPruefung": "ET2:SU:BMF:2",
"termin": {
"datumText": "19.07.2024 08:30 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 19,
"stunde": 8,
"minute": 30,
"wochentag": "FREITAG",
"pruefungswoche": 2
},
"raum": "SP.467",
"kennungFaecher": [
"BMF:20171:1190"
],
"matrikel": [
3726495,
3756449,
3721148,
3664621,
3721825,
3698347,
3628502,
3720831,
3182142,
3742505,
3250956,
3720901,
3688315,
3719253,
3720844,
3673742,
3513422,
3726239,
3719237,
3753899,
2984098,
3234192,
3719352,
3257531,
3693298,
3198761,
3756564,
3726099,
3719914
]
},
{
"kennungPruefung": "I2#AB:SU:BEI:3",
"termin": {
"datumText": "04.07.2024 11:00 MESZ",
"jahr": 2024,
"monat": 7,
"tag": 4,
"stunde": 11,
"minute": 0,
"wochentag": "DONNERSTAG",
"pruefungswoche": 0
},
"raum": "KA.450",
"kennungFaecher": [
"BEI:20141:5160",
"BEI:20141:5160",
"BMED:20141:5160",
"BMED:20141:5160"
],
"matrikel": [
3641161,
3670936,
3565575,
3536119,
3570982,
3615272,
3692613,
3570627,
3629017,
3590623,
3629033,
3667932,
3706118,
3702389,
3630286,
3283310,
3715284,
3671087,
3271030,
3674822,
3665277
]
}
]
}

View File

@ -0,0 +1,9 @@
from django import forms
class UploadFileForm(forms.Form):
student_file = forms.FileField(required=False)
exams_file = forms.FileField(required=False)
lecturers_file = forms.FileField(required=False)
subjects_file = forms.FileField(required=False)
attendance_file = forms.FileField(required=False)

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2 on 2021-04-30 12:03
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='JsonParser',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
]

View File

@ -0,0 +1,454 @@
import inspect
from django.db import models
from medinf.settings import (
PP_EXAM_FILE,
PP_ATTENDANCE_FILE,
PP_STUDENT_FILE,
PP_SUBJECT_FILE,
PP_TEACHER_FILE,
)
from pruefplan_viewer.models import (
API_ExamDataSet_ForStudent,
Attendance,
Subject,
Exam,
Lecturer,
PartialExam,
ExamExecution,
Student,
StudentExam,
)
import json
import datetime
class JsonParser(models.Model):
exceptionMessages = []
warningMessages = []
def clearTable(self):
JsonParser.exceptionMessages.clear()
JsonParser.warningMessages.clear()
Subject.objects.all().delete()
Exam.objects.all().delete()
Attendance.objects.all().delete()
Lecturer.objects.all().delete()
Student.objects.all().delete()
PartialExam.objects.all().delete()
ExamExecution.objects.all().delete()
StudentExam.objects.all().delete()
API_ExamDataSet_ForStudent.objects.all().delete()
def fillLecturerTable(self):
idKey = "kennung"
firstNameKey = "vorname"
lastNameKey = "nachname"
titleKey = "titel"
try:
with open(PP_TEACHER_FILE, encoding="utf-8") as json_file:
data = json.load(json_file)
revision = data["revision"] # TODO -> Do Something with Revision
for d in data["array"]:
entry = Lecturer(
identification=d[idKey],
firstName=d[firstNameKey],
lastName=d[lastNameKey],
title=d[titleKey],
) # Create new Lecturer entry
entry.save()
except FileNotFoundError as e:
JsonParser.exceptionMessages.append(
"Exception of type [FileNotFound]. Message: " + str(e)
)
except KeyError as e:
calling_function = inspect.stack()[0][3]
iteration_number = data["array"].index(d)
template = "Exception of type [{0}] occurred in function {1} in iteration {2} of the loop.. Arguments: {3!r}. Message: [{4}] entry might be missing in {5!r}"
message = template.format(
type(e).__name__,
calling_function,
iteration_number,
e.args,
e,
PP_TEACHER_FILE,
)
JsonParser.exceptionMessages.append(message)
except Exception as e:
calling_function = inspect.stack()[0][3]
template = "Exception of type [{0}] occurred in function {1} in iteration {2} of the loop. Arguments: {3!r}. Message: {4}."
message = template.format(
type(e).__name__,
calling_function,
iteration_number,
e.args,
e.__str__(),
)
JsonParser.exceptionMessages.append(message)
def fillAttendanceTable(self):
idKey = "dozentenkennung"
yearKey = "jahr"
monthKey = "monat"
dayKey = "tag"
hourKey = "stunde"
minuteKey = "minute"
weekdayKey = "wochentag"
dateKey = "termin"
try:
with open(PP_ATTENDANCE_FILE, encoding="utf-8") as json_file:
data = json.load(json_file)
revision = data["revision"] # TODO -> Do Something with Revision
for d in data["array"]:
keyAvailable = "termin" in d
if (
not keyAvailable
): # These attendance entries were not assigned any examination date
template = "Warning. Location: {0!r}. Following Exam was not assigned any examination date ['termin']: [{1}]"
message = template.format(PP_EXAM_FILE, d[idKey])
JsonParser.warningMessages.append(message)
continue
date = datetime.date(
d[dateKey][yearKey], d[dateKey][monthKey], d[dateKey][dayKey]
)
time = datetime.time(d[dateKey][hourKey], d[dateKey][minuteKey])
entry = Attendance(
identification=d[idKey],
time=time,
date=date,
dateTimeText=d[dateKey]["datumText"],
weekday=d[dateKey]["wochentag"],
) # Create new Lecturer entry
entry.save()
try:
# Get the Lecturer with the "Dozentenkennung" of this attendance entry
lecturer = Lecturer.objects.get(identification=d[idKey])
# Link this attendance entry to the lecturer
lecturer.attendance_set.add(entry)
except Exception as e:
template = "Exception of type [{0}]. Arguments: {1!r}. Message: [{2}] entry might be missing in {3!r}"
message = template.format(
type(e).__name__, e.args, idKey, PP_TEACHER_FILE
)
JsonParser.exceptionMessages.append(message)
except FileNotFoundError as e:
JsonParser.exceptionMessages.append(
"Exception of type [FileNotFound]. Message: " + str(e)
)
except KeyError as e:
calling_function = inspect.stack()[0][3]
iteration_number = data["array"].index(d)
template = "Exception of type [{0}] occurred in function {1} in iteration {2} of the loop.. Arguments: {3!r}. Message: [{4}] entry might be missing in {5!r}"
message = template.format(
type(e).__name__,
calling_function,
iteration_number,
e.args,
e,
PP_ATTENDANCE_FILE,
)
JsonParser.exceptionMessages.append(message)
except Exception as e:
calling_function = inspect.stack()[0][3]
iteration_number = data["array"].index(d)
template = "Exception of type [{0}] occurred in function {1} in iteration {2} of the loop. Arguments: {3!r}. Message: {4}."
message = template.format(
type(e).__name__,
calling_function,
iteration_number,
e.args,
e.__str__(),
)
JsonParser.exceptionMessages.append(message)
def fillSubjectTable(self):
idKey = "kennung"
initialsKey = "kurztext"
nameKey = "langtext"
ohmIdKey = "fachkennungOhm"
try:
with open(PP_SUBJECT_FILE, encoding="utf-8") as json_file:
data = json.load(json_file)
revision = data["revision"] # TODO -> Do Something with Revision
for d in data["array"]:
entry = Subject(
identification=d[idKey],
initials=d[ohmIdKey][initialsKey],
name=d[ohmIdKey][nameKey],
) # Create new Subject entry
entry.save()
except FileNotFoundError as e:
JsonParser.exceptionMessages.append(
"Exception of type [FileNotFound]. Message:" + str(e)
)
except KeyError as e:
calling_function = inspect.stack()[0][3]
iteration_number = data["array"].index(d)
template = "Exception of type [{0}] occurred in function {1} in iteration {2} of the loop.. Arguments: {3!r}. Message: [{4}] entry might be missing in {5!r}"
message = template.format(
type(e).__name__,
calling_function,
iteration_number,
e.args,
e,
PP_SUBJECT_FILE,
)
JsonParser.exceptionMessages.append(message)
except Exception as e:
calling_function = inspect.stack()[0][3]
iteration_number = data["array"].index(d)
template = "Exception of type [{0}] occurred in function {1} in iteration {2} of the loop. Arguments: {3!r}. Message: {4}."
message = template.format(
type(e).__name__,
calling_function,
iteration_number,
e.args,
e.__str__(),
)
JsonParser.exceptionMessages.append(message)
def fillExamTable(self):
idKey = "kennung"
yearKey = "jahr"
monthKey = "monat"
dayKey = "tag"
hourKey = "stunde"
minuteKey = "minute"
weekdayKey = "wochentag"
dateKey = "termin"
partialExamsKey = "teilpruefungen"
partialExamIdKey = "kennung"
numRegStudKey = "anzahlAnmeldungen"
subjectIdsKey = "fachkennungen"
examExecutionsKey = "pruefungsdurchfuehrungen"
lecturerIdKey = "dozentenkennung"
supervisorTypeKey = "typ"
locationKey = "raum"
try:
with open(PP_EXAM_FILE, encoding="utf-8") as json_file:
data = json.load(json_file)
revision = data["revision"] # TODO -> Do Something with Revision
for d in data["array"]:
if (
d["pruefungsart"] == "LN_VORGEZOGEN"
): # These entries can be ignored
continue
keyAvailable = "termin" in d
if (
not keyAvailable
): # These exam entries were not assigned any examination date
template = "Warning. Location: {0!r}. Following Exam was not assigned any examination date ['termin']: [{1}]"
message = template.format(PP_EXAM_FILE, d[idKey])
JsonParser.warningMessages.append(message)
continue
date = datetime.date(
d[dateKey][yearKey], d[dateKey][monthKey], d[dateKey][dayKey]
)
time = datetime.time(d[dateKey][hourKey], d[dateKey][minuteKey])
examEntry = Exam(
identification=d[idKey],
date=date,
time=time,
weekday=d[dateKey][weekdayKey],
) # Create new Exam entry
examEntry.save()
for teilpruef in d[partialExamsKey]:
partialExamId = teilpruef[partialExamIdKey]
regStudCount = teilpruef[numRegStudKey]
partialExam = PartialExam(
identification=partialExamId, regStudCount=regStudCount
) # Create new Partialexam entry
partialExam.save()
examEntry.partialexam_set.add(
partialExam
) # Add connection Exam(One) to PartialExam(Many)
for subId in teilpruef[subjectIdsKey]:
# check if subject exists
try:
subject = Subject.objects.get(identification=subId)
partialExam.subjectIds.add(
subject
) # Add connection PartialExam(Many) to Subject(Many)
except Exception as e:
template = "Exception of type [{0}]. Arguments: {1!r}. Message: [{2}] entry might be missing in {3!r}"
message = template.format(
type(e).__name__, e.args, subId, PP_SUBJECT_FILE
)
JsonParser.exceptionMessages.append(message)
for pruefdurch in teilpruef[examExecutionsKey]:
lecId = pruefdurch[lecturerIdKey]
supervisorType = pruefdurch[supervisorTypeKey]
location = pruefdurch[locationKey]
examExecution = ExamExecution(
supervisorType=supervisorType, location=location
) # Create new ExamExecution entry
examExecution.save()
# check if lecturer exists
try:
lecturer = Lecturer.objects.get(pk=lecId)
lecturer.examexecution_set.add(
examExecution
) # Add connection Lecturer (One) to ExamExecution (Many)
except Exception as e:
template = "Exception of type [{0}]. Arguments: {1!r}. Message: [{2}] entry might be missing in {3!r}"
message = template.format(
type(e).__name__, e.args, lecId, PP_TEACHER_FILE
)
JsonParser.exceptionMessages.append(message)
for subId in pruefdurch[subjectIdsKey]:
# check if subject exists
try:
subject = Subject.objects.get(identification=subId)
examExecution.subjectIds.add(
subject
) # Add connection ExamExecution(Many) to Subject(Many)
except Exception as e:
template = "Exception of type [{0}]. Arguments: {1!r}. Message: [{2}] entry might be missing in {3!r}"
message = template.format(
type(e).__name__, e.args, subId, PP_SUBJECT_FILE
)
JsonParser.exceptionMessages.append(message)
partialExam.examexecution_set.add(
examExecution
) # Add connection PartialExam(One) to ExamExecution(Many)
except FileNotFoundError as e:
JsonParser.exceptionMessages.append(
"Exception of type [FileNotFound]. Message" + str(e)
)
except KeyError as e:
calling_function = inspect.stack()[0][3]
iteration_number = data["array"].index(d)
template = "Exception of type [{0}] occurred in function {1} in iteration {2} of the loop.. Arguments: {3!r}. Message: [{4}] entry might be missing in {5!r}"
message = template.format(
type(e).__name__,
calling_function,
iteration_number,
e.args,
e,
PP_EXAM_FILE,
)
JsonParser.exceptionMessages.append(message)
except Exception as e:
calling_function = inspect.stack()[0][3]
iteration_number = data["array"].index(d)
template = "Exception of type [{0}] occurred in function {1} in iteration {2} of the loop. Arguments: {3!r}. Message: {4}."
message = template.format(
type(e).__name__,
calling_function,
iteration_number,
e.args,
e.__str__(),
)
JsonParser.exceptionMessages.append(message)
def fillStudentExamTable(self):
idKey = "kennungPruefung"
yearKey = "jahr"
monthKey = "monat"
dayKey = "tag"
hourKey = "stunde"
minuteKey = "minute"
weekdayKey = "wochentag"
locationKey = "raum"
dateKey = "termin"
matrikelKey = "matrikel"
try:
with open(PP_STUDENT_FILE, encoding="utf-8") as json_file:
data = json.load(json_file)
revision = data["revision"] # TODO -> Do Something with Revision
for d in data["array"]:
date = datetime.date(
d[dateKey][yearKey], d[dateKey][monthKey], d[dateKey][dayKey]
)
time = datetime.time(d[dateKey][hourKey], d[dateKey][minuteKey])
exam = Exam.objects.filter(
identification=d[idKey], date=date, time=time
).first()
partialExams = exam.partialexam_set.all()
for partialExam in partialExams:
examExecutions = partialExam.examexecution_set.all()
for examExecution in examExecutions:
if examExecution.location == d[locationKey]:
studentExamEntry = StudentExam(
examIdentification=d[idKey],
date=date,
time=time,
weekday=d[dateKey][weekdayKey],
location=d[locationKey],
examExecution=examExecution,
) # Create new StudentExam entry
studentExamEntry.save()
for matrikel in d[matrikelKey]:
studentEntry = Student.objects.filter(
matrikel=matrikel
).exists()
if (
studentEntry is False
): # Create new Student entry if does not already exists
entry = Student(matrikel=matrikel)
entry.save()
else:
entry = (
Student.objects.all().filter(matrikel=matrikel).first()
)
entry.exams.add(studentExamEntry)
except FileNotFoundError as e:
JsonParser.exceptionMessages.append(
"Exception of type [FileNotFound]. Message:" + str(e)
)
except KeyError as e:
calling_function = inspect.stack()[0][3]
iteration_number = data["array"].index(d)
template = "Exception of type [{0}] occurred in function {1} in iteration {2} of the loop.. Arguments: {3!r}. Message: [{4}] entry might be missing in {5!r}"
message = template.format(
type(e).__name__,
calling_function,
iteration_number,
e.args,
e,
PP_STUDENT_FILE,
)
JsonParser.exceptionMessages.append(message)
except Exception as e:
calling_function = inspect.stack()[0][3]
iteration_number = data["array"].index(d)
template = "Exception of type [{0}] occurred in function {1} in iteration {2} of the loop. Arguments: {3!r}. Message: {4}."
message = template.format(
type(e).__name__,
calling_function,
iteration_number,
e.args,
e.__str__(),
)
JsonParser.exceptionMessages.append(message)

View File

@ -0,0 +1,10 @@
from django.urls import path
from . import views
app_name = 'pp_parser'
urlpatterns = [
path('parse', views.parse, name='parse'),
path('clearTable', views.clearTable, name='clearTable'),
path('upload_file', views.upload_file, name='upload_file'),
]

View File

@ -0,0 +1,7 @@
def handle_uploaded_file(f, location):
if f is not None:
with open(location, 'wb+') as destination:
for chunk in f.chunks():
destination.write(chunk)

View File

@ -0,0 +1,65 @@
from django.shortcuts import render
from .models import JsonParser
from pruefplan_parser.models import JsonParser
from django.shortcuts import render
from django.contrib.auth.decorators import user_passes_test
from .forms import UploadFileForm
from pruefplan_parser.utils import handle_uploaded_file
from medinf.settings import (
PP_EXAM_FILE,
PP_ATTENDANCE_FILE,
PP_STUDENT_FILE,
PP_SUBJECT_FILE,
PP_TEACHER_FILE,
)
def is_pp_admin(user):
return user.groups.filter(name="PP_ADMIN").exists()
def parse():
parser = JsonParser()
parser.clearTable()
parser.fillLecturerTable()
parser.fillAttendanceTable()
parser.fillSubjectTable()
parser.fillExamTable()
parser.fillStudentExamTable()
@user_passes_test(is_pp_admin)
def clearTable(request):
parser = JsonParser()
parser.clearTable()
return render(request, "pruefplan/clearTable.html")
@user_passes_test(is_pp_admin)
def upload_file(request):
if request.method == "POST":
form = UploadFileForm(request.POST, request.FILES)
if form.is_valid():
print(request.FILES)
handle_uploaded_file(
request.FILES.get("attendance_file"), PP_ATTENDANCE_FILE
)
handle_uploaded_file(request.FILES.get("student_file"), PP_STUDENT_FILE)
handle_uploaded_file(request.FILES.get("exams_file"), PP_EXAM_FILE)
handle_uploaded_file(request.FILES.get("lecturers_file"), PP_TEACHER_FILE)
handle_uploaded_file(request.FILES.get("subjects_file"), PP_SUBJECT_FILE)
parse()
if (
len(JsonParser.exceptionMessages) != 0
or len(JsonParser.warningMessages) != 0
):
context = {
"exceptions": JsonParser.exceptionMessages,
"warnings": JsonParser.warningMessages,
}
return render(request, "pruefplan/exceptionHandling.html", context)
else:
return render(request, "pruefplan/parse.html")
else:
form = UploadFileForm()
return render(request, "pruefplan/upload.html", {"form": form})

View File

View File

@ -0,0 +1,4 @@
#from django.contrib import admin
#from .models import Lecturer
#admin.site.register(Lecturer)

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class PruefplanViewerConfig(AppConfig):
name = 'pruefplan_viewer'

View File

@ -0,0 +1,94 @@
# Generated by Django 3.2 on 2021-04-30 12:03
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Exam',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('identification', models.CharField(max_length=200, null=True)),
('date', models.DateField(blank=True, null=True)),
('time', models.TimeField(blank=True, null=True)),
('weekday', models.CharField(max_length=200, null=True)),
],
),
migrations.CreateModel(
name='ExamExecution',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('supervisorType', models.CharField(max_length=200, null=True)),
('location', models.CharField(max_length=200, null=True)),
],
),
migrations.CreateModel(
name='Lecturer',
fields=[
('identification', models.CharField(max_length=200, primary_key=True, serialize=False)),
('firstName', models.CharField(max_length=200, null=True)),
('lastName', models.CharField(max_length=200, null=True)),
('title', models.CharField(max_length=200, null=True)),
],
),
migrations.CreateModel(
name='Subject',
fields=[
('identification', models.CharField(max_length=200, primary_key=True, serialize=False)),
('initials', models.CharField(max_length=200, null=True)),
('name', models.CharField(max_length=200, null=True)),
],
),
migrations.CreateModel(
name='StudentExam',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('examIdentification', models.CharField(max_length=200, null=True)),
('date', models.DateField(blank=True, null=True)),
('time', models.TimeField(blank=True, null=True)),
('weekday', models.CharField(max_length=200, null=True)),
('location', models.CharField(max_length=200, null=True)),
('examExecution', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pruefplan_viewer.examexecution')),
],
),
migrations.CreateModel(
name='Student',
fields=[
('matrikel', models.CharField(max_length=200, primary_key=True, serialize=False)),
('exams', models.ManyToManyField(to='pruefplan_viewer.StudentExam')),
],
),
migrations.CreateModel(
name='PartialExam',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('identification', models.CharField(max_length=200, null=True)),
('regStudCount', models.CharField(max_length=200, null=True)),
('exam', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='pruefplan_viewer.exam')),
('subjectIds', models.ManyToManyField(to='pruefplan_viewer.Subject')),
],
),
migrations.AddField(
model_name='examexecution',
name='lecturer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pruefplan_viewer.lecturer'),
),
migrations.AddField(
model_name='examexecution',
name='partialExam',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='pruefplan_viewer.partialexam'),
),
migrations.AddField(
model_name='examexecution',
name='subjectIds',
field=models.ManyToManyField(to='pruefplan_viewer.Subject'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2024-02-26 17:04
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("pruefplan_viewer", "0001_initial"),
]
operations = [
migrations.AlterModelOptions(
name="examexecution",
options={
"ordering": ("partialExam__exam__date", "partialExam__exam__time")
},
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.0.2 on 2024-04-11 13:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pruefplan_viewer', '0002_alter_examexecution_options'),
]
operations = [
migrations.CreateModel(
name='Presences',
fields=[
('identification', models.CharField(max_length=200, primary_key=True, serialize=False)),
],
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 5.0.2 on 2024-04-11 16:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pruefplan_viewer', '0003_presences'),
]
operations = [
migrations.AddField(
model_name='presences',
name='date',
field=models.DateField(blank=True, null=True),
),
migrations.AddField(
model_name='presences',
name='dateTimeText',
field=models.CharField(max_length=200, null=True),
),
migrations.AddField(
model_name='presences',
name='time',
field=models.TimeField(blank=True, null=True),
),
migrations.AddField(
model_name='presences',
name='weekday',
field=models.CharField(max_length=200, null=True),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.0.3 on 2024-04-24 09:13
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pruefplan_viewer', '0004_presences_date_presences_datetimetext_presences_time_and_more'),
]
operations = [
migrations.AddField(
model_name='presences',
name='lecturer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='pruefplan_viewer.lecturer'),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 5.0.3 on 2024-05-17 10:43
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('pruefplan_viewer', '0005_presences_lecturer'),
]
operations = [
migrations.RenameModel(
old_name='Presences',
new_name='Attendance',
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.1.4 on 2025-01-30 14:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pruefplan_viewer', '0006_rename_presences_attendance'),
]
operations = [
migrations.CreateModel(
name='API_ExamDataSet_ForStudent',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
]

View File

@ -0,0 +1,63 @@
# Generated by Django 5.1.4 on 2025-01-30 14:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pruefplan_viewer', '0007_api_examdataset_forstudent'),
]
operations = [
migrations.AddField(
model_name='api_examdataset_forstudent',
name='date',
field=models.DateField(blank=True, null=True),
),
migrations.AddField(
model_name='api_examdataset_forstudent',
name='examIdent',
field=models.CharField(max_length=200, null=True),
),
migrations.AddField(
model_name='api_examdataset_forstudent',
name='examStudCount',
field=models.CharField(max_length=200, null=True),
),
migrations.AddField(
model_name='api_examdataset_forstudent',
name='location',
field=models.CharField(max_length=200, null=True),
),
migrations.AddField(
model_name='api_examdataset_forstudent',
name='room',
field=models.CharField(max_length=200, null=True),
),
migrations.AddField(
model_name='api_examdataset_forstudent',
name='studMartNr',
field=models.CharField(max_length=200, null=True),
),
migrations.AddField(
model_name='api_examdataset_forstudent',
name='subjectIdent',
field=models.CharField(max_length=200, null=True),
),
migrations.AddField(
model_name='api_examdataset_forstudent',
name='supervisor',
field=models.CharField(max_length=200, null=True),
),
migrations.AddField(
model_name='api_examdataset_forstudent',
name='time',
field=models.TimeField(blank=True, null=True),
),
migrations.AddField(
model_name='api_examdataset_forstudent',
name='weekday',
field=models.CharField(max_length=200, null=True),
),
]

View File

@ -0,0 +1,148 @@
from __future__ import annotations # Python 3.10 (Production still on 3.9)
from dataclasses import asdict, dataclass, field
import datetime as dt
from django.db import models
class Subject(models.Model):
identification = models.CharField(max_length=200, primary_key=True)
initials = models.CharField(max_length=200, null=True)
name = models.CharField(max_length=200, null=True)
def __str__(self):
return self.name
class Exam(models.Model):
identification = models.CharField(max_length=200, null=True)
date = models.DateField(blank=True, null=True)
time = models.TimeField(blank=True, null=True)
weekday = models.CharField(max_length=200, null=True)
class PartialExam(models.Model):
identification = models.CharField(max_length=200, null=True)
regStudCount = models.CharField(max_length=200, null=True)
subjectIds = models.ManyToManyField(Subject)
exam = models.ForeignKey("Exam", blank=True, null=True, on_delete=models.SET_NULL)
class Lecturer(models.Model):
identification = models.CharField(max_length=200, primary_key=True)
firstName = models.CharField(max_length=200, null=True)
lastName = models.CharField(max_length=200, null=True)
title = models.CharField(max_length=200, null=True)
class Attendance(models.Model):
identification = models.CharField(max_length=200, primary_key=True)
date = models.DateField(blank=True, null=True)
time = models.TimeField(blank=True, null=True)
dateTimeText = models.CharField(max_length=200, null=True)
weekday = models.CharField(max_length=200, null=True)
lecturer = models.ForeignKey(
"Lecturer", blank=True, null=True, on_delete=models.SET_NULL
)
class StudentExam(models.Model):
examIdentification = models.CharField(max_length=200, null=True)
date = models.DateField(blank=True, null=True)
time = models.TimeField(blank=True, null=True)
weekday = models.CharField(max_length=200, null=True)
location = models.CharField(max_length=200, null=True)
examExecution = models.ForeignKey(
"ExamExecution", blank=True, null=True, on_delete=models.CASCADE
)
class Student(models.Model):
matrikel = models.CharField(max_length=200, primary_key=True)
exams = models.ManyToManyField(StudentExam)
class ExamExecution(models.Model):
lecturer = models.ForeignKey(
"Lecturer", blank=True, null=True, on_delete=models.CASCADE
)
supervisorType = models.CharField(max_length=200, null=True)
location = models.CharField(max_length=200, null=True)
subjectIds = models.ManyToManyField(Subject)
partialExam = models.ForeignKey(
"PartialExam", blank=True, null=True, on_delete=models.SET_NULL
)
class Meta:
ordering = ("partialExam__exam__date", "partialExam__exam__time")
# @dataclass()
# class API_ExamDataSet_ForStudent(models.Model):
# date: dt.date
# time: dt.time
# weekday: str
# room: str | None
# location: str | None
# subjectIdent: str | None
# examIdent: str | None
# examStudCount: int | None
# supervisor: str
# studMartNr: str | None
# def to_dict(self):
# return asdict(self)
class API_ExamDataSet_ForStudent(models.Model):
date = models.DateField(blank=True, null=True)
time = models.TimeField(blank=True, null=True)
weekday = models.CharField(max_length=200, null=True)
room = models.CharField(max_length=200, null=True)
location = models.CharField(max_length=200, null=True)
subjectIdent = models.CharField(max_length=200, null=True)
examIdent = models.CharField(max_length=200, null=True)
examStudCount = models.CharField(max_length=200, null=True)
supervisor = models.CharField(max_length=200, null=True)
studMartNr = models.CharField(max_length=200, null=True)
def to_dict(self):
return asdict(self)
@dataclass(order=True)
class ExamLines:
sort_index: dt.datetime = field(init=False)
date: dt.date
time: dt.time
weekday: str
location: str | None
subjectIds: list[Subject] | None
examIdent: str | None
examStudCound: int | None
supervisorType: str
executionSet: list | None
partialExamIdent: str | None
def __post_init__(self):
self.sort_index = dt.datetime.combine(self.date, self.time)
def sort_and_group_datetime(examLines: list):
examLine_Datetime_Groups = {}
for examLine in examLines:
if examLine.sort_index not in examLine_Datetime_Groups:
examLine_Datetime_Groups[examLine.sort_index] = []
examLine_Datetime_Groups[examLine.sort_index].append(examLine)
sorted_groups = sorted(examLine_Datetime_Groups.items(), key=lambda x: x[0])
sorted_result = []
for _, group in sorted_groups:
sorted_result.extend(group)
return sorted_result

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,13 @@
from django.urls import path
from . import views
app_name = 'pp_viewer'
urlpatterns = [
path('lecturer', views.lecturer, name='lecturer'),
path('subject', views.subject, name='subject'),
path('exam', views.exam, name='exam'),
path('student', views.student, name='student'),
path('studentExam/<int:student_id>/', views.examsForStudent, name='examForStudent'),
path('lecturerExam/<str:lecturer_id>/', views.examsForLecturer, name='examForLecturer'),
]

View File

@ -0,0 +1,115 @@
from django.shortcuts import render
from django.template.defaulttags import register
from django.http import Http404
from django.contrib.auth.decorators import login_required
from .models import ExamLines, Lecturer, Subject, Exam, Student, sort_and_group_datetime
import datetime as dt
@login_required
def lecturer(request):
lecturers = Lecturer.objects.all()
context = {"lecturers": lecturers}
return render(request, "pruefplan/lecturer.html", context)
@login_required
def subject(request):
subjects = Subject.objects.all()
context = {"subjects": subjects}
return render(request, "pruefplan/subject.html", context)
@login_required
def student(request):
students = Student.objects.all()
context = {"students": students}
return render(request, "pruefplan/student.html", context)
@login_required
def exam(request):
exams = Exam.objects.all()
supervisorSetDict = {}
for ex in exams:
examSupervisors = []
for partialExam in ex.partialexam_set.all():
for examExe in partialExam.examexecution_set.all():
supervisor = (
examExe.lecturer.firstName + " " + examExe.lecturer.lastName
)
if supervisor not in examSupervisors:
examSupervisors.append(supervisor)
supervisorSetDict[ex.identification] = examSupervisors
context = {"exams": exams, "supervisorSetDict": supervisorSetDict}
return render(request, "pruefplan/exam.html", context)
@register.filter
def get_item(dictionary, key):
return dictionary.get(key)
def examsForLecturer(request, lecturer_id):
lecturer = Lecturer.objects.filter(pk=lecturer_id).first()
if lecturer is not None:
lecExamExes = lecturer.examexecution_set.all()
lecAttendance = lecturer.attendance_set.all()
lecExamLines = mergeModels(lecExamExes, lecAttendance)
context = {
"lecturer": lecturer,
"lecExamLines": lecExamLines,
"lecExamExes": lecExamExes,
}
return render(request, "pruefplan/lecturerExam.html", context)
else:
raise Http404("Dozentenkennung ungültig.")
def examsForStudent(request, student_id):
student = Student.objects.all().filter(matrikel=student_id).first()
if student is not None:
exams = student.exams.all()
context = {"student": student, "exams": exams}
return render(request, "pruefplan/studentExam.html", context)
else:
raise Http404("Matrikelnummer ungültig.")
def mergeModels(lecExamExes, lecAttendance):
result = [
ExamLines(
date=examExes.partialExam.exam.date,
time=examExes.partialExam.exam.time,
weekday=examExes.partialExam.exam.weekday,
location=examExes.location,
subjectIds=examExes.subjectIds,
examIdent=examExes.partialExam.identification,
examStudCound=examExes.partialExam.regStudCount,
supervisorType=examExes.supervisorType,
executionSet=list(examExes.partialExam.examexecution_set.all()),
partialExamIdent=examExes.partialExam.exam.identification,
)
for examExes in lecExamExes
]
result += [
ExamLines(
date=attendance.date,
time=attendance.time,
weekday=attendance.weekday,
location=None,
subjectIds=None,
examIdent=None,
examStudCound=None,
supervisorType="PRÄSENZ",
executionSet=None,
partialExamIdent=None,
)
for attendance in lecAttendance
]
result = sort_and_group_datetime(result)
return result

49
Backend/requirements.txt Normal file
View File

@ -0,0 +1,49 @@
pyasn1>=0.3.7
Django>=2
ldap3>=2.3
openpyxl>=2.4.8
pytz>=2018.7
asgiref>=3.7.2
boto3>=1.20.26
botocore>=1.23.54
certifi>=2023.11.17
cffi>=1.16.0
charset-normalizer>=3.3.2
cryptography>=41.0.7
dj-database-url>=2.1.0
Django>=4.2.7
django-anymail>=10.2
django-cors-headers>=3.14.0
django-jazzmin>=2.6.0
django-storages>=1.12.3
django-ckeditor-5
djangorestframework>=3.14.0
djangorestframework-simplejwt>=5.2.2
drf-yasg>=1.21.7
environs>=10.0.0
gunicorn>=21.2.0
idna>=3.6
inflection>=0.5.1
jmespath>=0.10.0
marshmallow>=3.20.1
packaging>=23.2
psycopg2>=2.9.9
pycparser>=2.21
PyJWT>=2.6.0
python-dateutil>=2.8.2
python-dotenv>=1.0.0
pytz>=2023.3.post1
PyYAML>=6.0.1
requests>=2.31.0
s3transfer>=0.5.2
shortuuid>=1.0.11
six>=1.16.0
sqlparse>=0.4.4
stripe>=7.9.0
typing_extensions>=4.9.0
tzdata>=2023.3
uritemplate>=4.1.1
urllib3>=1.26.18
requests

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,342 @@
/*!
* Bootstrap Reboot v4.0.0-beta.2 (https://getbootstrap.com)
* Copyright 2011-2017 The Bootstrap Authors
* Copyright 2011-2017 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
-ms-overflow-style: scrollbar;
-webkit-tap-highlight-color: transparent;
}
@-ms-viewport {
width: device-width;
}
article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
text-align: left;
background-color: #fff;
}
[tabindex="-1"]:focus {
outline: none !important;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 0.5rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
border-bottom: 0;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: .5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
dfn {
font-style: italic;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 80%;
}
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -.25em;
}
sup {
top: -.5em;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
-webkit-text-decoration-skip: objects;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
a:not([href]):not([tabindex]) {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus {
outline: 0;
}
pre,
code,
kbd,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
pre {
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
-ms-overflow-style: scrollbar;
}
figure {
margin: 0 0 1rem;
}
img {
vertical-align: middle;
border-style: none;
}
svg:not(:root) {
overflow: hidden;
}
a,
area,
button,
[role="button"],
input:not([type="range"]),
label,
select,
summary,
textarea {
-ms-touch-action: manipulation;
touch-action: manipulation;
}
table {
border-collapse: collapse;
}
caption {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
color: #868e96;
text-align: left;
caption-side: bottom;
}
th {
text-align: inherit;
}
label {
display: inline-block;
margin-bottom: .5rem;
}
button {
border-radius: 0;
}
button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
button,
html [type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
padding: 0;
border-style: none;
}
input[type="radio"],
input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"] {
-webkit-appearance: listbox;
}
textarea {
overflow: auto;
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
display: block;
width: 100%;
max-width: 100%;
padding: 0;
margin-bottom: .5rem;
font-size: 1.5rem;
line-height: inherit;
color: inherit;
white-space: normal;
}
progress {
vertical-align: baseline;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
outline-offset: -2px;
-webkit-appearance: none;
}
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
summary {
display: list-item;
}
template {
display: none;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v4.0.0-beta.2 (https://getbootstrap.com)
* Copyright 2011-2017 The Bootstrap Authors
* Copyright 2011-2017 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}[role=button],a,area,button,input:not([type=range]),label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#868e96;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */

File diff suppressed because one or more lines are too long

8374
Backend/static/bootstrap/css/bootstrap.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3850
Backend/static/bootstrap/js/bootstrap.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

231
Backend/static/css/bots.css Normal file
View File

@ -0,0 +1,231 @@
@import url('http://fonts.googleapis.com/css?family=Kalam');
body {
background-color: black;
font-family: 'ChicagoFLFRegular';
font-weight: normal;
font-style: normal;
font-size: 90%;
color: white;
height: 100%;
}
header {
height: 200px;
position: relative;
}
section {
position: relative;
}
#sectionhash {
height: 20px;
}
#sectionwees {
height: 340px;
}
#sectionanswer {
height: 150px;
}
#sectionform {
height: 400px;
}
footer {
height: 200px;
position: relative;
}
td {
vertical-align: top;
padding-right:10px;
padding-bottom: 10px;
}
form table {
width: 100%;
border-collapse: collapse;
}
input {
width: 100%;
outline-color: #b4363b;
}
input.levelradio {
width: 10%;
}
#hashtags {
font-size: x-small;
max-width:400px;
width: 100%;
overflow: auto;
margin: auto;
position: absolute;
top: 0; left: 0; bottom: 0; right: 0;
text-align: center;
}
.headline {
padding-bottom: 4px;
}
#header {
width: 50%;
overflow: auto;
margin: auto;
position: absolute;
left: 0; bottom: 0; right: 0;
text-align: center;
display: table-cell;
vertical-align: middle;
}
#headerimg {
max-width:400px;
width: 100%;
}
#wees {
max-width:400px;
width: 100%;
overflow: auto;
margin: auto;
position: absolute;
top: 0; left: 0; bottom: 0; right: 0;
text-align: left;
}
#answer {
max-width:400px;
width: 100%;
overflow: auto;
margin: auto;
position: absolute;
top: 0; left: 0; bottom: 0; right: 0;
text-align: left;
}
#applyform {
max-width:400px;
width: 100%;
overflow: auto;
margin: auto;
position: absolute;
top: 0; left: 0; bottom: 0; right: 0;
text-align: left;
}
#apply {
background-color: #b4363b;
border: medium solid #b4363b;
font-family: 'ChicagoFLFRegular';
font-weight: normal;
font-style: normal;
color: white;
font-size: medium;
margin-top: 20px;
}
#footer {
max-width:400px;
width: 100%;
overflow: auto;
margin: auto;
position: absolute;
top: 0; left: 0; bottom: 0; right: 0;
text-align: left;
}
#planetdiv {
position: fixed;
padding: 10px;
bottom: 0;
left: 0;
right: 0;
height: 330px;
overflow: hidden;
z-index: -1;
}
#planetcut {
width: 100%;
height: auto;
max-height: 320px;
overflow: auto;
margin: auto;
position: absolute;
left: 0; bottom: 0; right: 0;
text-align: center;
display: table-cell;
vertical-align: middle;
z-index: -2;
}
#kurzlogo {
width: 150px;
overflow: auto;
margin: auto;
position: absolute;
left: 30px; bottom: 10px;
text-align: center;
display: table-cell;
vertical-align: middle;
z-index: -1;
}
#thlogo {
width: 210px;
overflow: auto;
margin: auto;
position: absolute;
left: 210px; bottom: 10px;
text-align: center;
display: table-cell;
vertical-align: middle;
z-index: -1;
}
#skydiv {
position: fixed;
padding: 10px;
top: 0;
left: 0;
right: 0;
height: 600px;
overflow: hidden;
z-index: -1;
}
#skyl{
overflow: auto;
margin: auto;
position: absolute;
top: 0; left: 0; bottom: 0;
}
#skyr {
overflow: auto;
margin: auto;
position: absolute;
top: 0; right: 0; bottom: 0;
}
a {
color: white;
text-decoration: none;
font-weight: bold;
}

1
Backend/static/css/dropzone.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,58 @@
#navbar-efi {
background-color: #ffe240;
margin-bottom: 20px;
}
/* Login Dropdown */
#login-dp{
min-width: 250px;
padding: 14px 14px 0;
overflow:hidden;
background-color:rgba(255,255,255,.8);
}
#login-dp .bottom{
background-color:rgba(255,255,255,.8);
border-top:1px solid #ddd;
clear:both;
padding:14px;
}
#login-dp .form-group {
margin-bottom: 10px;
}
#login-button {
text-align: right;
min-width: 250px;
}
#login-button:focus {
border-color: rgba(0, 0, 0, 0.8);
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.075) inset, 0 0 8px rgba(126, 239, 104, 0.6);
outline: 0 none;
}
.icon {
width: 16px;
height: 16px;
}
.iconic-warning {
fill: #ffe240;
}
.svg-hidden {
display: none;
}
@media(max-width:768px){
#login-dp{
background-color: inherit;
color: #fff;
}
#login-dp .bottom{
background-color: inherit;
border-top:0 none;
}
}

View File

@ -0,0 +1,3 @@
.table-nostriped tbody tr:nth-of-type(odd) {
background-color:transparent;
}

BIN
Backend/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,23 @@
.bfd-dropfield {
display: inline;
padding: 5px; }
.bfd-dropfield-inner {
border: 5px dashed #888;
color: #888;
cursor: pointer;
font-size: 32px;
text-align: center; }
.bfd-dropfield-inner:hover, .bfd-dropfield-inner.bfd-dragover {
border-color: #bbb;
color: #bbb; }
.bfd-info {
overflow: hidden;
white-space: nowrap; }
.bfd-error-message {
text-align: center; }
.bfd-remove {
cursor: pointer; }

View File

@ -0,0 +1 @@
!function(e){"use strict";e.FileDialog=function(a){var o=e.extend(e.FileDialog.defaults,a),t=e(["<div class='modal fade'>"," <div class='modal-dialog'>"," <div class='modal-content'>"," <div class='modal-header'>"," <button type='button' class='close' data-dismiss='modal'>"," <span aria-hidden='true'>&times;</span>"," <span class='sr-only'>",o.cancel_button," </span>"," </button>"," <h4 class='modal-title'>",o.title," </h4>"," </div>"," <div class='modal-body'>"," <input type='file' />"," <div class='bfd-dropfield'>"," <div class='bfd-dropfield-inner'>",o.drag_message," </div>"," </div>"," <div class='container-fluid bfd-files'>"," </div>"," </div>"," <div class='modal-footer'>"," <button type='button' class='btn btn-primary bfd-ok'>",o.ok_button," </button>"," <button type='button' class='btn btn-default bfd-cancel'"," data-dismiss='modal'>",o.cancel_button," </button>"," </div>"," </div>"," </div>","</div>"].join("")),n=!1,r=e("input:file",t),i=e(".bfd-dropfield",t),s=e(".bfd-dropfield-inner",i);s.css({height:o.dropheight,"padding-top":o.dropheight/2-32}),r.attr({accept:o.accept,multiple:o.multiple}),i.on("click.bfd",function(){r.trigger("click")});var d=[],l=[],c=function(a){var n,r,i=new FileReader;l.push(i),i.onloadstart=function(){},i.onerror=function(e){e.target.error.code!==e.target.error.ABORT_ERR&&n.parent().html(["<div class='bg-danger bfd-error-message'>",o.error_message,"</div>"].join("\n"))},i.onprogress=function(a){var o=Math.round(100*a.loaded/a.total)+"%";n.attr("aria-valuenow",a.loaded),n.css("width",o),e(".sr-only",n).text(o)},i.onload=function(e){a.content=e.target.result,d.push(a),n.removeClass("active")};var s=e(["<div class='col-xs-7 col-sm-4 bfd-info'>"," <span class='glyphicon glyphicon-remove bfd-remove'></span>&nbsp;"," <span class='glyphicon glyphicon-file'></span>&nbsp;"+a.name,"</div>","<div class='col-xs-5 col-sm-8 bfd-progress'>"," <div class='progress'>"," <div class='progress-bar progress-bar-striped active' role='progressbar'"," aria-valuenow='0' aria-valuemin='0' aria-valuemax='"+a.size+"'>"," <span class='sr-only'>0%</span>"," </div>"," </div>","</div>"].join(""));n=e(".progress-bar",s),e(".bfd-remove",s).tooltip({container:"body",html:!0,placement:"top",title:o.remove_message}).on("click.bfd",function(){var e=d.indexOf(a);e>=0&&d.pop(e),r.fadeOut();try{i.abort()}catch(o){}}),r=e("<div class='row'></div>"),r.append(s),e(".bfd-files",t).append(r),i["readAs"+o.readAs](a)},f=function(e){Array.prototype.forEach.apply(e,[c])};return r.change(function(e){e=e.originalEvent;var a=e.target.files;f(a);var o=r.clone(!0);r.replaceWith(o),r=o}),i.on("dragenter.bfd",function(){s.addClass("bfd-dragover")}).on("dragover.bfd",function(e){e=e.originalEvent,e.stopPropagation(),e.preventDefault(),e.dataTransfer.dropEffect="copy"}).on("dragleave.bfd drop.bfd",function(){s.removeClass("bfd-dragover")}).on("drop.bfd",function(e){e=e.originalEvent,e.stopPropagation(),e.preventDefault();var a=e.dataTransfer.files;0===a.length,f(a)}),e(".bfd-ok",t).on("click.bfd",function(){var a=e.Event("files.bs.filedialog");a.files=d,t.trigger(a),n=!0,t.modal("hide")}),t.on("hidden.bs.modal",function(){if(l.forEach(function(e){try{e.abort()}catch(a){}}),!n){var a=e.Event("cancel.bs.filedialog");t.trigger(a)}t.remove()}),e(document.body).append(t),t.modal(),t},e.FileDialog.defaults={accept:"*",cancel_button:"Close",drag_message:"Drop files here",dropheight:400,error_message:"An error occured while loading file",multiple:!0,ok_button:"OK",readAs:"DataURL",remove_message:"Remove&nbsp;file",title:"Load file(s)"}}(jQuery);

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

1
Backend/static/js/dropzone.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
Backend/static/js/jquery-3.5.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More