Inhalt
Aktueller Ordner:
duesseldorfer-schuelerinventar-python-clientduesk_client3.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Düsseldorfer Schülerinventar (DÜSK) - Python Client
Komplette Desktop-Anwendung für Windows/Mac/Linux
"""
import sys
import subprocess
import requests
import math
import json
from datetime import datetime
# PDF Export
try:
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
except ImportError:
subprocess.check_call([sys.executable, "-m", "pip", "install", "reportlab"])
# GUI
try:
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLabel, QLineEdit, QPushButton,
QTableWidget, QTableWidgetItem, QMessageBox,
QHeaderView, QDialog, QDialogButtonBox,
QFormLayout, QProgressDialog, QGroupBox,
QTabWidget, QComboBox, QScrollArea, QTextEdit,
QSplitter, QRadioButton, QButtonGroup,
QFileDialog, QToolBar, QMenu)
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from PyQt6.QtGui import QFont, QPainter, QPen, QBrush, QColor, QAction
except ImportError:
subprocess.check_call([sys.executable, "-m", "pip", "install", "PyQt6", "requests"])
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLabel, QLineEdit, QPushButton,
QTableWidget, QTableWidgetItem, QMessageBox,
QHeaderView, QDialog, QDialogButtonBox,
QFormLayout, QProgressDialog, QGroupBox,
QTabWidget, QComboBox, QScrollArea, QTextEdit,
QSplitter, QRadioButton, QButtonGroup,
QFileDialog, QToolBar, QMenu)
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from PyQt6.QtGui import QFont, QPainter, QPen, QBrush, QColor, QAction
# ============ API KONFIGURATION ============
API_BASE_URL = "https://paul-koop.org/api/"
# ============ NORMWERTE AUS IHRER DATENBANK ============
NORM_SE_HS = {1: [21.33, 25.33, 29.33, 33.32, 37.32], 2: [20.87, 24.95, 29.03, 33.13, 37.18],
3: [17.93, 21.37, 24.80, 28.23, 31.67], 4: [13.98, 17.71, 21.44, 25.17, 28.90],
5: [24.60, 28.55, 33.04, 37.53, 42.01], 6: [15.53, 18.97, 22.40, 25.83, 29.27]}
NORM_FE_HS = {1: [12.66, 18.16, 23.66, 29.16, 34.66], 2: [13.33, 18.42, 23.51, 28.60, 33.69],
3: [10.75, 15.41, 20.07, 24.73, 29.39], 4: [14.22, 15.30, 16.38, 17.46, 18.54],
5: [14.12, 20.21, 26.30, 32.39, 38.48], 6: [10.53, 14.51, 18.49, 22.47, 26.45]}
NORM_SE_FS = {1: [17.54, 24.03, 30.53, 37.02, 43.51], 2: [17.80, 24.26, 30.73, 37.19, 43.65],
3: [18.03, 22.41, 26.79, 31.17, 35.55], 4: [14.28, 15.55, 16.83, 18.10, 19.37],
5: [20.69, 27.49, 34.29, 41.09, 47.89], 6: [12.44, 18.06, 23.68, 29.29, 34.91]}
NORM_FE_FS = {1: [15.30, 19.79, 24.28, 28.77, 33.26], 2: [14.63, 18.94, 23.25, 27.56, 31.87],
3: [14.62, 17.81, 21.00, 24.19, 27.38], 4: [15.00, 15.55, 16.10, 16.65, 17.20],
5: [18.44, 22.61, 26.78, 30.95, 35.12], 6: [9.79, 13.97, 18.15, 22.33, 26.51]}
class APIWorker(QThread):
finished = pyqtSignal(object)
error = pyqtSignal(str)
def __init__(self, endpoint, method="GET", data=None, params=None, user_id=None, session=None):
super().__init__()
self.endpoint = endpoint
self.method = method
self.data = data
self.params = params
self.user_id = user_id
self.session = session
def run(self):
try:
url = API_BASE_URL + self.endpoint
headers = {'Content-Type': 'application/json'}
if self.user_id and self.session:
headers['X-User-ID'] = str(self.user_id)
headers['X-Session'] = self.session
if self.method == "GET":
response = requests.get(url, headers=headers, params=self.params, timeout=30)
elif self.method == "POST":
response = requests.post(url, json=self.data, headers=headers, timeout=30)
elif self.method == "PUT":
response = requests.put(url, json=self.data, headers=headers, timeout=30)
elif self.method == "DELETE":
response = requests.delete(url, headers=headers, params=self.params, timeout=30)
else:
self.error.emit(f"Unbekannte Methode: {self.method}")
return
if response.status_code == 200:
self.finished.emit(response.json())
else:
self.error.emit(f"HTTP {response.status_code}")
except Exception as e:
self.error.emit(str(e))
class ProfileChart(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setMinimumSize(500, 250)
self.se_values = [3, 3, 3, 3, 3, 3]
self.fe_values = [3, 3, 3, 3, 3, 3]
self.labels = ["Arbeitsverhalten", "Lernverhalten", "Sozialverhalten",
"Fachkompetenz", "Personale Kompetenz", "Methodenkompetenz"]
def set_values(self, se, fe):
self.se_values = se[:6]
self.fe_values = fe[:6]
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
w, h = self.width(), self.height()
top, left = 40, 110
row_h = (h - 80) / 6
chart_w = w - 130
# Gitter
painter.setPen(QPen(QColor(200, 200, 200), 1))
for i in range(7):
y = top + i * row_h
painter.drawLine(left - 5, y, left + chart_w, y)
for i in range(6):
x = left + i * (chart_w / 5)
painter.drawLine(x, top, x, top + row_h * 6)
# Beschriftungen
painter.setFont(QFont("Arial", 9))
painter.setPen(QColor(0, 0, 0))
for i, label in enumerate(self.labels):
y = top + i * row_h + row_h / 2 + 5
painter.drawText(5, int(y), label)
for i in range(5):
x = left + i * (chart_w / 5) + (chart_w / 5) / 2
painter.drawText(int(x) - 4, top - 10, str(i + 1))
# SE Linie (blau)
se_points = []
for i, v in enumerate(self.se_values):
x = left + (v - 1) * (chart_w / 4)
y = top + i * row_h + row_h / 2
se_points.append((int(x), int(y)))
painter.setPen(QPen(QColor(0, 100, 200), 3))
for i in range(5):
painter.drawLine(se_points[i][0], se_points[i][1], se_points[i+1][0], se_points[i+1][1])
painter.setBrush(QBrush(QColor(0, 100, 200, 100)))
for x, y in se_points:
painter.drawEllipse(x - 6, y - 6, 12, 12)
# FE Linie (rot)
fe_points = []
for i, v in enumerate(self.fe_values):
x = left + (v - 1) * (chart_w / 4)
y = top + i * row_h + row_h / 2
fe_points.append((int(x), int(y)))
painter.setPen(QPen(QColor(200, 0, 0), 3))
for i in range(5):
painter.drawLine(fe_points[i][0], fe_points[i][1], fe_points[i+1][0], fe_points[i+1][1])
painter.setBrush(QBrush(QColor(200, 0, 0, 100)))
for x, y in fe_points:
painter.drawEllipse(x - 6, y - 6, 12, 12)
# Legende
painter.setPen(QColor(0, 0, 0))
painter.setBrush(QBrush(QColor(0, 100, 200)))
painter.drawEllipse(left + chart_w - 50, top - 20, 8, 8)
painter.drawText(left + chart_w - 38, top - 14, "SE")
painter.setBrush(QBrush(QColor(200, 0, 0)))
painter.drawEllipse(left + chart_w - 20, top - 20, 8, 8)
painter.drawText(left + chart_w - 8, top - 14, "FE")
class ProfileViewDialog(QDialog):
def __init__(self, profile_data, parent=None):
super().__init__(parent)
self.profile = profile_data
self.current_norm = "HS"
self.init_ui()
self.calculate()
def init_ui(self):
self.setWindowTitle(f"Profil: {self.profile.get('name', 'Unbekannt')}")
self.setMinimumSize(1100, 800)
layout = QVBoxLayout()
# Kopfzeile
header = QHBoxLayout()
header.addWidget(QLabel(f"<h2>{self.profile.get('name', 'Unbekannt')}</h2>"))
header.addStretch()
pdf_btn = QPushButton("PDF Export")
pdf_btn.setStyleSheet("background-color: #f44336; color: white; padding: 5px 15px;")
pdf_btn.clicked.connect(self.export_pdf)
header.addWidget(pdf_btn)
layout.addLayout(header)
# Normauswahl
norm_row = QHBoxLayout()
norm_row.addWidget(QLabel("Normtabelle:"))
self.norm_combo = QComboBox()
self.norm_combo.addItem("Hauptschule (HS)", "HS")
self.norm_combo.addItem("Förderschule (FS)", "FS")
self.norm_combo.currentIndexChanged.connect(self.on_norm_change)
norm_row.addWidget(self.norm_combo)
norm_row.addStretch()
layout.addLayout(norm_row)
# Diagramm
chart_group = QGroupBox("Profil-Diagramm")
self.chart = ProfileChart()
chart_group.setLayout(QVBoxLayout())
chart_group.layout().addWidget(self.chart)
layout.addWidget(chart_group)
# Tabellen nebeneinander
tables = QHBoxLayout()
self.se_table = self.create_table("Selbsteinschätzung (SE)")
self.fe_table = self.create_table("Fremdeinschätzung (FE)")
tables.addWidget(self.se_table)
tables.addWidget(self.fe_table)
layout.addLayout(tables)
# Statistik
stats = QGroupBox("Statistische Auswertung")
stats_layout = QVBoxLayout()
self.corr_label = QLabel()
self.agree_label = QLabel()
self.analysis = QTextEdit()
self.analysis.setReadOnly(True)
self.analysis.setMaximumHeight(120)
stats_layout.addWidget(self.corr_label)
stats_layout.addWidget(self.agree_label)
stats_layout.addWidget(self.analysis)
stats.setLayout(stats_layout)
layout.addWidget(stats)
# Items (erste 10)
items_group = QGroupBox("Item-Werte (erste 10)")
self.items_table = QTableWidget(10, 3)
self.items_table.setHorizontalHeaderLabels(["Item", "SE", "FE"])
self.items_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
items_group.setLayout(QVBoxLayout())
items_group.layout().addWidget(self.items_table)
layout.addWidget(items_group)
# Schließen
close_btn = QPushButton("Schließen")
close_btn.setStyleSheet("background-color: #6699CC; color: white; padding: 8px;")
close_btn.clicked.connect(self.accept)
layout.addWidget(close_btn)
self.setLayout(layout)
def create_table(self, title):
widget = QWidget()
layout = QVBoxLayout(widget)
layout.addWidget(QLabel(f"<b>{title}</b>", alignment=Qt.AlignmentFlag.AlignCenter))
table = QTableWidget(6, 5)
table.setHorizontalHeaderLabels(["1", "2", "3", "4", "5"])
table.setVerticalHeaderLabels(["Arbeitsverhalten", "Lernverhalten", "Sozialverhalten",
"Fachkompetenz", "Personale Kompetenz", "Methodenkompetenz"])
table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
layout.addWidget(table)
return widget
def on_norm_change(self):
self.current_norm = self.norm_combo.currentData()
self.calculate()
def calculate_sums(self, items):
sums = [0] * 7
sums[1] = sum(items[0:10])
sums[2] = sum(items[10:20])
sums[3] = sum(items[20:28]) + items[8] + items[9]
sums[4] = sum(items[28:36])
sums[5] = items[0] + items[1] + items[5] + items[6] + items[7] + items[8] + items[9] + items[11] + items[12] + items[13] + items[14]
sums[6] = items[2] + items[3] + items[4] + items[8] + items[9] + items[10] + items[16] + items[17]
return sums
def get_profile_values(self, sums, norm):
values = [0] * 6
for k in range(1, 7):
placed = False
for p in range(5):
if sums[k] < norm[k][p]:
values[k-1] = p
placed = True
break
if not placed:
values[k-1] = 4
return values
def fill_table(self, table, values):
t = table.findChild(QTableWidget)
for i, v in enumerate(values):
for j in range(5):
t.setItem(i, j, QTableWidgetItem("X" if j == v else ""))
t.item(i, j).setTextAlignment(Qt.AlignmentFlag.AlignCenter)
t.resizeColumnsToContents()
def calculate(self):
# Items auslesen
se_items = [int(self.profile.get(f'item{i}', 2)) for i in range(1, 37)]
fe_items = [int(self.profile.get(f'feitem{i}', 2)) for i in range(1, 37)]
# Summen
se_sums = self.calculate_sums(se_items)
fe_sums = self.calculate_sums(fe_items)
# Normen
if self.current_norm == "HS":
norm_se, norm_fe = NORM_SE_HS, NORM_FE_HS
else:
norm_se, norm_fe = NORM_SE_FS, NORM_FE_FS
# Profile (0-4)
se_prof = self.get_profile_values(se_sums, norm_se)
fe_prof = self.get_profile_values(fe_sums, norm_fe)
# Für Diagramm (1-5)
se_chart = [v + 1 for v in se_prof]
fe_chart = [v + 1 for v in fe_prof]
# Tabellen füllen
self.fill_table(self.se_table, se_prof)
self.fill_table(self.fe_table, fe_prof)
self.chart.set_values(se_chart, fe_chart)
# Korrelation
corr = self.calc_correlation(se_chart, fe_chart)
agree = sum(1 for s, f in zip(se_items, fe_items) if s == f) * 100 / 36
self.corr_label.setText(f"<b>Korrelation:</b> {corr:.2f}")
self.agree_label.setText(f"<b>Übereinstimmung:</b> {agree:.1f}%")
# Auswertung
ratings = ["weit unterdurchschnittlich", "unterdurchschnittlich", "durchschnittlich",
"überdurchschnittlich", "weit überdurchschnittlich"]
komp = ["Arbeitsverhalten", "Lernverhalten", "Sozialverhalten",
"Fachkompetenz", "Personale Kompetenz", "Methodenkompetenz"]
text = "Selbsteinschätzung:\n"
for i, v in enumerate(se_chart):
text += f"• {komp[i]}: {ratings[v-1]}\n"
text += "\nFremdeinschätzung:\n"
for i, v in enumerate(fe_chart):
text += f"• {komp[i]}: {ratings[v-1]}\n"
text += f"\nKorrelation {corr:.2f}: "
if corr >= 0.8:
text += "sehr gute Übereinstimmung"
elif corr >= 0.5:
text += "mäßige Übereinstimmung"
elif corr >= 0.3:
text += "schwache Übereinstimmung"
else:
text += "keine signifikante Übereinstimmung"
self.analysis.setText(text)
# Item-Tabelle
names = ["Zuverlässigkeit", "Arbeitstempo", "Arbeitsplanung", "Organisationsfähigkeit",
"Geschicklichkeit", "Ordnung", "Sorgfalt", "Kreativität", "Problemlösungsfähigkeit",
"Abstraktionsvermögen"]
for i, name in enumerate(names):
self.items_table.setItem(i, 0, QTableWidgetItem(name))
self.items_table.setItem(i, 1, QTableWidgetItem(str(se_items[i])))
self.items_table.setItem(i, 2, QTableWidgetItem(str(fe_items[i])))
def calc_correlation(self, a, b):
ma, mb = sum(a)/6, sum(b)/6
num = sum((a[i]-ma)*(b[i]-mb) for i in range(6))
den = math.sqrt(sum((a[i]-ma)**2 for i in range(6)) * sum((b[i]-mb)**2 for i in range(6)))
return num/den if den != 0 else 0
def export_pdf(self):
filename, _ = QFileDialog.getSaveFileName(self, "PDF speichern",
f"{self.profile.get('name', 'profil')}.pdf",
"PDF (*.pdf)")
if not filename:
return
try:
doc = SimpleDocTemplate(filename, pagesize=A4)
styles = getSampleStyleSheet()
story = []
story.append(Paragraph(f"DÜSK - Profil: {self.profile.get('name', 'Unbekannt')}",
ParagraphStyle('Title', parent=styles['Heading1'], fontSize=16, alignment=1)))
story.append(Spacer(1, 20))
story.append(Paragraph(f"Erstellt: {datetime.now().strftime('%d.%m.%Y %H:%M')}", styles['Normal']))
story.append(Spacer(1, 20))
# Tabellen für PDF
story.append(Paragraph("Selbsteinschätzung", styles['Heading2']))
data = [["Kompetenz", "1", "2", "3", "4", "5"]]
komp = ["Arbeitsverhalten", "Lernverhalten", "Sozialverhalten", "Fachkompetenz",
"Personale Kompetenz", "Methodenkompetenz"]
t = self.se_table.findChild(QTableWidget)
for i, k in enumerate(komp):
row = [k]
for j in range(5):
row.append("X" if t.item(i, j) and t.item(i, j).text() == "X" else "")
data.append(row)
tbl = Table(data)
tbl.setStyle(TableStyle([('GRID', (0,0), (-1,-1), 1, colors.black), ('ALIGN', (0,0), (-1,-1), 'CENTER')]))
story.append(tbl)
story.append(Spacer(1, 20))
story.append(Paragraph("Fremdeinschätzung", styles['Heading2']))
data = [["Kompetenz", "1", "2", "3", "4", "5"]]
t = self.fe_table.findChild(QTableWidget)
for i, k in enumerate(komp):
row = [k]
for j in range(5):
row.append("X" if t.item(i, j) and t.item(i, j).text() == "X" else "")
data.append(row)
tbl = Table(data)
tbl.setStyle(TableStyle([('GRID', (0,0), (-1,-1), 1, colors.black), ('ALIGN', (0,0), (-1,-1), 'CENTER')]))
story.append(tbl)
story.append(Spacer(1, 20))
story.append(Paragraph(self.corr_label.text(), styles['Normal']))
story.append(Paragraph(self.agree_label.text(), styles['Normal']))
story.append(Spacer(1, 10))
story.append(Paragraph(self.analysis.toPlainText(), styles['Normal']))
doc.build(story)
QMessageBox.information(self, "PDF Export", f"Gespeichert: {filename}")
except Exception as e:
QMessageBox.critical(self, "Fehler", str(e))
class ProfileEditDialog(QDialog):
ITEMS = ["Zuverlässigkeit", "Arbeitstempo", "Arbeitsplanung", "Organisationsfähigkeit",
"Geschicklichkeit", "Ordnung", "Sorgfalt", "Kreativität", "Problemlösungsfähigkeit",
"Abstraktionsvermögen", "Selbstständigkeit", "Belastbarkeit", "Konzentrationsfähigkeit",
"Verantwortungsbewusstsein", "Eigeninitiative", "Leistungsbereitschaft", "Auffassungsgabe",
"Merkfähigkeit", "Motivationsfähigkeit", "Reflektionsfähigkeit", "Teamfähigkeit",
"Hilfsbereitschaft", "Kontaktfähigkeit", "Respektvoller Umgang", "Kommunikationsfähigkeit",
"Einfühlungsvermögen", "Konfliktfähigkeit", "Kritikfähigkeit", "Schreiben", "Lesen",
"Mathematik", "Naturwissenschaft", "Fremdsprachen", "Präsentationsfähigkeit",
"PC Kenntnisse", "Fächerübergreifendes Denken"]
def __init__(self, user_id, session, profile=None, groups=None, parent=None):
super().__init__(parent)
self.user_id, self.session = user_id, session
self.profile = profile or {}
self.is_edit = profile is not None
self.groups = groups or []
self.se_btns, self.fe_btns = {}, {}
self.init_ui()
def init_ui(self):
self.setWindowTitle("Profil bearbeiten" if self.is_edit else "Neues Profil")
self.setMinimumSize(900, 700)
layout = QVBoxLayout()
# Name
form = QFormLayout()
self.name_edit = QLineEdit()
self.name_edit.setText(self.profile.get('name', ''))
form.addRow("Name:", self.name_edit)
self.group_combo = QComboBox()
for g in self.groups:
self.group_combo.addItem(g.get('name', ''), g.get('gruppeID'))
if self.profile.get('gruppeID'):
idx = self.group_combo.findData(self.profile.get('gruppeID'))
if idx >= 0:
self.group_combo.setCurrentIndex(idx)
form.addRow("Gruppe:", self.group_combo)
self.new_group = QLineEdit()
self.new_group.setPlaceholderText("Neue Gruppe (optional)")
form.addRow("Neue Gruppe:", self.new_group)
layout.addLayout(form)
# Tabs
tabs = QTabWidget()
tabs.addTab(self.create_item_tab("Selbsteinschätzung", "se"), "Selbsteinschätzung")
tabs.addTab(self.create_item_tab("Fremdeinschätzung", "fe"), "Fremdeinschätzung")
layout.addWidget(tabs)
# Buttons
btns = QHBoxLayout()
save = QPushButton("Speichern")
save.setStyleSheet("background-color: #4CAF50; color: white; padding: 8px;")
save.clicked.connect(self.save)
cancel = QPushButton("Abbrechen")
cancel.clicked.connect(self.reject)
btns.addStretch()
btns.addWidget(save)
btns.addWidget(cancel)
layout.addLayout(btns)
self.setLayout(layout)
self.load_values()
def create_item_tab(self, title, prefix):
scroll = QScrollArea()
scroll.setWidgetResizable(True)
container = QWidget()
layout = QVBoxLayout(container)
btns = self.se_btns if prefix == "se" else self.fe_btns
for i, name in enumerate(self.ITEMS, 1):
group = QGroupBox(f"{i}. {name}")
hbox = QHBoxLayout()
bg = QButtonGroup()
for val, label in [(4, "trifft voll zu"), (3, "trifft zu"), (2, "trifft teilweise zu"), (1, "trifft nicht zu")]:
rb = QRadioButton(label)
hbox.addWidget(rb)
bg.addButton(rb, val)
group.setLayout(hbox)
layout.addWidget(group)
btns[i] = bg
scroll.setWidget(container)
return scroll
def load_values(self):
for i in range(1, 37):
if i in self.se_btns:
val = self.profile.get(f'item{i}', 2)
if btn := self.se_btns[i].button(val):
btn.setChecked(True)
if i in self.fe_btns:
val = self.profile.get(f'feitem{i}', 2)
if btn := self.fe_btns[i].button(val):
btn.setChecked(True)
def save(self):
name = self.name_edit.text().strip()
if not name:
QMessageBox.warning(self, "Fehler", "Bitte Name eingeben")
return
data = {'name': name}
if gid := self.group_combo.currentData():
data['gruppeID'] = gid
if ng := self.new_group.text().strip():
data['namegruppe'] = ng
for i in range(1, 37):
data[f'item{i}'] = self.se_btns[i].checkedId() if i in self.se_btns and self.se_btns[i].checkedId() != -1 else 2
data[f'feitem{i}'] = self.fe_btns[i].checkedId() if i in self.fe_btns and self.fe_btns[i].checkedId() != -1 else 2
if self.is_edit:
data['profilID'] = self.profile.get('profilID')
worker = APIWorker("api_profiles.php", "PUT", data, user_id=self.user_id, session=self.session)
else:
worker = APIWorker("api_profiles.php", "POST", data, user_id=self.user_id, session=self.session)
worker.finished.connect(lambda r: self.save_done(r))
worker.error.connect(lambda e: QMessageBox.critical(self, "Fehler", e))
worker.start()
self.progress = QProgressDialog("Speichern...", None, 0, 0, self)
self.progress.show()
def save_done(self, resp):
self.progress.close()
if resp.get('success'):
QMessageBox.information(self, "Erfolg", "Profil gespeichert")
self.accept()
else:
QMessageBox.critical(self, "Fehler", resp.get('error', 'Unbekannter Fehler'))
class LoginDialog(QDialog):
def __init__(self):
super().__init__()
self.user_id = self.session = None
self.setWindowTitle("DÜSK - Anmeldung")
self.setModal(True)
self.setMinimumSize(350, 250)
layout = QVBoxLayout()
layout.addWidget(QLabel("DÜSK - Düsseldorfer Schülerinventar",
alignment=Qt.AlignmentFlag.AlignCenter,
font=QFont("Arial", 16, QFont.Weight.Bold)))
form = QFormLayout()
self.user_edit = QLineEdit()
self.user_edit.setText("gast")
self.pw_edit = QLineEdit()
self.pw_edit.setText("gast")
self.pw_edit.setEchoMode(QLineEdit.EchoMode.Password)
form.addRow("Benutzername:", self.user_edit)
form.addRow("Passwort:", self.pw_edit)
layout.addLayout(form)
btns = QDialogButtonBox()
login = btns.addButton("Anmelden", QDialogButtonBox.ButtonRole.AcceptRole)
cancel = btns.addButton("Abbrechen", QDialogButtonBox.ButtonRole.RejectRole)
login.clicked.connect(self.do_login)
cancel.clicked.connect(self.reject)
layout.addWidget(btns)
layout.addWidget(QLabel("Server: paul-koop.org\nBenutzung mit gast/gast möglich",
alignment=Qt.AlignmentFlag.AlignCenter,
styleSheet="color: gray; font-size: 10px;"))
self.setLayout(layout)
def do_login(self):
worker = APIWorker("api_login.php", "POST",
{"username": self.user_edit.text().strip(),
"password": self.pw_edit.text().strip()})
worker.finished.connect(self.on_success)
worker.error.connect(lambda e: QMessageBox.critical(self, "Fehler", e))
worker.start()
self.progress = QProgressDialog("Anmeldung...", None, 0, 0, self)
self.progress.show()
def on_success(self, resp):
self.progress.close()
if resp.get('success') or resp.get('userID'):
self.user_id = resp['userID']
self.session = resp['session']
self.accept()
else:
QMessageBox.warning(self, "Fehler", "Anmeldung fehlgeschlagen")
class MainWindow(QMainWindow):
def __init__(self, user_id, session):
super().__init__()
self.user_id, self.session = user_id, session
self.profiles = []
self.setWindowTitle("DÜSK - Düsseldorfer Schülerinventar")
self.setMinimumSize(1000, 600)
central = QWidget()
self.setCentralWidget(central)
layout = QVBoxLayout(central)
# Header
header = QLabel("DÜSK - Düsseldorfer Schülerinventar")
header.setAlignment(Qt.AlignmentFlag.AlignCenter)
header.setStyleSheet("font-size: 24px; font-weight: bold; background-color: #6699CC; color: white; padding: 15px;")
layout.addWidget(header)
# Toolbar
toolbar = QToolBar()
new_btn = QPushButton("Neues Profil")
new_btn.clicked.connect(self.new_profile)
toolbar.addWidget(new_btn)
refresh_btn = QPushButton("Aktualisieren")
refresh_btn.clicked.connect(self.load_profiles)
toolbar.addWidget(refresh_btn)
toolbar.addSeparator()
logout_btn = QPushButton("Abmelden")
logout_btn.setStyleSheet("background-color: #f44336; color: white;")
logout_btn.clicked.connect(self.logout)
toolbar.addWidget(logout_btn)
self.addToolBar(toolbar)
# Tabelle
self.table = QTableWidget()
self.table.setColumnCount(4)
self.table.setHorizontalHeaderLabels(["Name", "Gruppe", "ProfilID", "Aktion"])
self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
layout.addWidget(self.table)
# Footer
footer = QLabel("Paul Koop M.A. - Thomashofstrasse 19, 52070 Aachen")
footer.setAlignment(Qt.AlignmentFlag.AlignCenter)
footer.setStyleSheet("background-color: #6699CC; color: white; padding: 10px; margin-top: 10px;")
layout.addWidget(footer)
self.load_profiles()
self.load_groups()
def load_groups(self):
worker = APIWorker("api_groups.php", "GET", user_id=self.user_id, session=self.session)
worker.finished.connect(lambda r: setattr(self, 'groups', r if isinstance(r, list) else []))
worker.start()
def load_profiles(self):
worker = APIWorker("api_profiles.php", "GET", user_id=self.user_id, session=self.session)
worker.finished.connect(self.on_profiles)
worker.error.connect(lambda e: QMessageBox.critical(self, "Fehler", e))
worker.start()
def on_profiles(self, resp):
if isinstance(resp, list):
self.profiles = resp
self.table.setRowCount(len(resp))
for i, p in enumerate(resp):
self.table.setItem(i, 0, QTableWidgetItem(p.get('name', '')))
self.table.setItem(i, 1, QTableWidgetItem(p.get('gruppename', '')))
self.table.setItem(i, 2, QTableWidgetItem(str(p.get('profilID', ''))))
actions = QWidget()
hbox = QHBoxLayout(actions)
hbox.setContentsMargins(0, 0, 0, 0)
view = QPushButton("Anzeigen")
view.setStyleSheet("background-color: #4CAF50; color: white;")
view.clicked.connect(lambda _, pid=p.get('profilID'): self.view_profile(pid))
hbox.addWidget(view)
edit = QPushButton("Bearbeiten")
edit.clicked.connect(lambda _, pid=p.get('profilID'): self.edit_profile(pid))
hbox.addWidget(edit)
delete = QPushButton("Löschen")
delete.setStyleSheet("background-color: #f44336; color: white;")
delete.clicked.connect(lambda _, pid=p.get('profilID'): self.delete_profile(pid))
hbox.addWidget(delete)
self.table.setCellWidget(i, 3, actions)
self.table.resizeRowsToContents()
def view_profile(self, pid):
worker = APIWorker("api_profiles.php", "GET", params={"id": pid},
user_id=self.user_id, session=self.session)
worker.finished.connect(lambda r: ProfileViewDialog(r).exec() if isinstance(r, dict) else None)
worker.error.connect(lambda e: QMessageBox.critical(self, "Fehler", e))
worker.start()
def edit_profile(self, pid):
worker = APIWorker("api_profiles.php", "GET", params={"id": pid},
user_id=self.user_id, session=self.session)
worker.finished.connect(lambda r: self.open_editor(r))
worker.start()
def open_editor(self, profile):
if isinstance(profile, dict):
dlg = ProfileEditDialog(self.user_id, self.session, profile, getattr(self, 'groups', []))
if dlg.exec():
self.load_profiles()
def new_profile(self):
dlg = ProfileEditDialog(self.user_id, self.session, groups=getattr(self, 'groups', []))
if dlg.exec():
self.load_profiles()
def delete_profile(self, pid):
if QMessageBox.question(self, "Löschen", "Profil wirklich löschen?") == QMessageBox.StandardButton.Yes:
worker = APIWorker("api_profiles.php", "DELETE", params={"id": pid},
user_id=self.user_id, session=self.session)
worker.finished.connect(lambda r: self.load_profiles())
worker.start()
def logout(self):
APIWorker("api_logout.php", "POST", user_id=self.user_id, session=self.session).start()
self.close()
def main():
app = QApplication(sys.argv)
login = LoginDialog()
if login.exec() == QDialog.DialogCode.Accepted:
win = MainWindow(login.user_id, login.session)
win.show()
sys.exit(app.exec())
else:
sys.exit(0)
if __name__ == "__main__":
main()