mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 18:23:42 +00:00
feat: SQLAlchemy models for users, cases, reports, audit
11 models across 4 files matching the MariaDB schema: - user.py: User, RefreshToken, InvitationLink, AllowedDomain - case.py: Case, CaseICDCode - report.py: WeeklyReport, YearlySummary - audit.py: ImportLog, AuditLog, Notification All CHECK constraints, indexes (incl. prefix index), foreign keys, and server defaults match the SQL DDL specification exactly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5d57b1f349
commit
e7befe78b6
5 changed files with 703 additions and 0 deletions
|
|
@ -0,0 +1,24 @@
|
|||
"""SQLAlchemy models for the DAK Zweitmeinungs-Portal."""
|
||||
|
||||
from app.models.user import AllowedDomain, InvitationLink, RefreshToken, User
|
||||
from app.models.case import Case, CaseICDCode
|
||||
from app.models.report import WeeklyReport, YearlySummary
|
||||
from app.models.audit import AuditLog, ImportLog, Notification
|
||||
|
||||
__all__ = [
|
||||
# User & Auth
|
||||
"User",
|
||||
"RefreshToken",
|
||||
"InvitationLink",
|
||||
"AllowedDomain",
|
||||
# Cases
|
||||
"Case",
|
||||
"CaseICDCode",
|
||||
# Reports
|
||||
"WeeklyReport",
|
||||
"YearlySummary",
|
||||
# Logging & Notifications
|
||||
"ImportLog",
|
||||
"AuditLog",
|
||||
"Notification",
|
||||
]
|
||||
130
backend/app/models/audit.py
Normal file
130
backend/app/models/audit.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
"""Import log, audit log, and notification models."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
CheckConstraint,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
String,
|
||||
Text,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.dialects.mysql import JSON
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class ImportLog(Base):
|
||||
__tablename__ = "import_log"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
filename: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
import_type: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
cases_imported: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, server_default="0"
|
||||
)
|
||||
cases_skipped: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, server_default="0"
|
||||
)
|
||||
cases_updated: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, server_default="0"
|
||||
)
|
||||
errors: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
details: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
|
||||
imported_by: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey("users.id"), nullable=True
|
||||
)
|
||||
imported_at: Mapped[dt.datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=func.now()
|
||||
)
|
||||
|
||||
# Relationships
|
||||
imported_by_user: Mapped[Optional["User"]] = relationship(
|
||||
foreign_keys=[imported_by]
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
CheckConstraint(
|
||||
"import_type IN ('csv_crm','icd_xlsx','historical_excel','excel_sync')",
|
||||
name="chk_imp_type",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class AuditLog(Base):
|
||||
__tablename__ = "audit_log"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey("users.id"), nullable=True
|
||||
)
|
||||
action: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
entity_type: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
|
||||
entity_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
||||
old_values: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
|
||||
new_values: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
|
||||
ip_address: Mapped[Optional[str]] = mapped_column(String(45), nullable=True)
|
||||
user_agent: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
created_at: Mapped[dt.datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=func.now()
|
||||
)
|
||||
|
||||
# Relationships
|
||||
user: Mapped[Optional["User"]] = relationship(foreign_keys=[user_id])
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_user", "user_id"),
|
||||
Index("idx_entity", "entity_type", "entity_id"),
|
||||
Index("idx_created", "created_at"),
|
||||
)
|
||||
|
||||
|
||||
class Notification(Base):
|
||||
__tablename__ = "notifications"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
recipient_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("users.id"), nullable=False
|
||||
)
|
||||
notification_type: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
title: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
message: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
related_entity_type: Mapped[Optional[str]] = mapped_column(
|
||||
String(50), nullable=True
|
||||
)
|
||||
related_entity_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, nullable=True
|
||||
)
|
||||
is_read: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, server_default="0"
|
||||
)
|
||||
email_sent: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, server_default="0"
|
||||
)
|
||||
email_sent_at: Mapped[Optional[dt.datetime]] = mapped_column(
|
||||
DateTime, nullable=True
|
||||
)
|
||||
created_at: Mapped[dt.datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=func.now()
|
||||
)
|
||||
|
||||
# Relationships
|
||||
recipient: Mapped["User"] = relationship(foreign_keys=[recipient_id])
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_recipient", "recipient_id", "is_read"),
|
||||
CheckConstraint(
|
||||
"notification_type IN ("
|
||||
"'new_cases_uploaded','icd_entered','icd_uploaded',"
|
||||
"'report_ready','coding_completed')",
|
||||
name="chk_notif",
|
||||
),
|
||||
)
|
||||
198
backend/app/models/case.py
Normal file
198
backend/app/models/case.py
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
"""Case and ICD code models."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
CheckConstraint,
|
||||
Date,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
SmallInteger,
|
||||
String,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
func,
|
||||
text,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.schema import FetchedValue
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class Case(Base):
|
||||
__tablename__ = "cases"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
fall_id: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
||||
crm_ticket_id: Mapped[Optional[str]] = mapped_column(String(20), nullable=True)
|
||||
jahr: Mapped[int] = mapped_column(SmallInteger, nullable=False)
|
||||
kw: Mapped[int] = mapped_column(SmallInteger, nullable=False)
|
||||
datum: Mapped[dt.date] = mapped_column(Date, nullable=False)
|
||||
anrede: Mapped[Optional[str]] = mapped_column(String(20), nullable=True)
|
||||
vorname: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
||||
nachname: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
geburtsdatum: Mapped[Optional[dt.date]] = mapped_column(Date, nullable=True)
|
||||
kvnr: Mapped[Optional[str]] = mapped_column(String(20), nullable=True)
|
||||
versicherung: Mapped[str] = mapped_column(
|
||||
String(50), nullable=False, server_default="DAK"
|
||||
)
|
||||
icd: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
fallgruppe: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
strasse: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
plz: Mapped[Optional[str]] = mapped_column(String(10), nullable=True)
|
||||
ort: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
||||
email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
ansprechpartner: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)
|
||||
telefonnummer: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
|
||||
mobiltelefon: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
|
||||
email2: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
telefon2: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
|
||||
unterlagen: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, server_default="0"
|
||||
)
|
||||
unterlagen_verschickt: Mapped[Optional[dt.date]] = mapped_column(
|
||||
Date, nullable=True
|
||||
)
|
||||
erhalten: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True)
|
||||
unterlagen_erhalten: Mapped[Optional[dt.date]] = mapped_column(
|
||||
Date, nullable=True
|
||||
)
|
||||
unterlagen_an_gutachter: Mapped[Optional[dt.date]] = mapped_column(
|
||||
Date, nullable=True
|
||||
)
|
||||
gutachten: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, server_default="0"
|
||||
)
|
||||
gutachter: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
||||
gutachten_erstellt: Mapped[Optional[dt.date]] = mapped_column(
|
||||
Date, nullable=True
|
||||
)
|
||||
gutachten_versendet: Mapped[Optional[dt.date]] = mapped_column(
|
||||
Date, nullable=True
|
||||
)
|
||||
schweigepflicht: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, server_default="0"
|
||||
)
|
||||
ablehnung: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, server_default="0"
|
||||
)
|
||||
abbruch: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, server_default="0"
|
||||
)
|
||||
abbruch_datum: Mapped[Optional[dt.date]] = mapped_column(Date, nullable=True)
|
||||
gutachten_typ: Mapped[Optional[str]] = mapped_column(String(20), nullable=True)
|
||||
therapieaenderung: Mapped[Optional[str]] = mapped_column(String(5), nullable=True)
|
||||
ta_diagnosekorrektur: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, server_default="0"
|
||||
)
|
||||
ta_unterversorgung: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, server_default="0"
|
||||
)
|
||||
ta_uebertherapie: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, server_default="0"
|
||||
)
|
||||
kurzbeschreibung: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
fragestellung: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
kommentar: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
sonstiges: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
abgerechnet: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, server_default="0"
|
||||
)
|
||||
abrechnung_datum: Mapped[Optional[dt.date]] = mapped_column(Date, nullable=True)
|
||||
import_source: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
imported_at: Mapped[dt.datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=func.now()
|
||||
)
|
||||
updated_at: Mapped[dt.datetime] = mapped_column(
|
||||
DateTime,
|
||||
nullable=False,
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
server_onupdate=FetchedValue(),
|
||||
)
|
||||
updated_by: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey("users.id"), nullable=True
|
||||
)
|
||||
icd_entered_by: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey("users.id"), nullable=True
|
||||
)
|
||||
icd_entered_at: Mapped[Optional[dt.datetime]] = mapped_column(
|
||||
DateTime, nullable=True
|
||||
)
|
||||
coding_completed_by: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey("users.id"), nullable=True
|
||||
)
|
||||
coding_completed_at: Mapped[Optional[dt.datetime]] = mapped_column(
|
||||
DateTime, nullable=True
|
||||
)
|
||||
|
||||
# Relationships
|
||||
icd_codes: Mapped[list[CaseICDCode]] = relationship(
|
||||
back_populates="case", cascade="all, delete-orphan"
|
||||
)
|
||||
updated_by_user: Mapped[Optional["User"]] = relationship(
|
||||
foreign_keys=[updated_by]
|
||||
)
|
||||
icd_entered_by_user: Mapped[Optional["User"]] = relationship(
|
||||
foreign_keys=[icd_entered_by]
|
||||
)
|
||||
coding_completed_by_user: Mapped[Optional["User"]] = relationship(
|
||||
foreign_keys=[coding_completed_by]
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("fall_id", name="uk_fall_id"),
|
||||
Index("idx_jahr_kw", "jahr", "kw"),
|
||||
Index("idx_kvnr", "kvnr"),
|
||||
Index("idx_fallgruppe", "fallgruppe"),
|
||||
Index("idx_datum", "datum"),
|
||||
Index("idx_nachname_vorname", "nachname", "vorname"),
|
||||
# Note: idx_pending_icd uses a prefix length icd(20) which SQLAlchemy
|
||||
# does not support natively. We use text() for the prefix column.
|
||||
Index("idx_pending_icd", "jahr", "kw", "fallgruppe", text("icd(20)")),
|
||||
Index("idx_pending_coding", "gutachten", "gutachten_typ"),
|
||||
CheckConstraint(
|
||||
"fallgruppe IN ('onko','kardio','intensiv','galle','sd')",
|
||||
name="chk_fallgruppe",
|
||||
),
|
||||
CheckConstraint(
|
||||
"gutachten_typ IS NULL OR gutachten_typ IN ('Bestätigung','Alternative')",
|
||||
name="chk_gutachten_typ",
|
||||
),
|
||||
CheckConstraint(
|
||||
"therapieaenderung IS NULL OR therapieaenderung IN ('Ja','Nein')",
|
||||
name="chk_ta",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class CaseICDCode(Base):
|
||||
__tablename__ = "case_icd_codes"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
case_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("cases.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
icd_code: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
icd_hauptgruppe: Mapped[Optional[str]] = mapped_column(String(10), nullable=True)
|
||||
created_at: Mapped[dt.datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=func.now()
|
||||
)
|
||||
|
||||
# Relationships
|
||||
case: Mapped[Case] = relationship(back_populates="icd_codes")
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_case", "case_id"),
|
||||
Index("idx_code", "icd_code"),
|
||||
Index("idx_haupt", "icd_hauptgruppe"),
|
||||
)
|
||||
189
backend/app/models/report.py
Normal file
189
backend/app/models/report.py
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
"""Weekly report and yearly summary models."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import (
|
||||
Date,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
SmallInteger,
|
||||
String,
|
||||
UniqueConstraint,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.dialects.mysql import JSON
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class WeeklyReport(Base):
|
||||
__tablename__ = "weekly_reports"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
jahr: Mapped[int] = mapped_column(SmallInteger, nullable=False)
|
||||
kw: Mapped[int] = mapped_column(SmallInteger, nullable=False)
|
||||
report_date: Mapped[dt.date] = mapped_column(Date, nullable=False)
|
||||
report_file_path: Mapped[Optional[str]] = mapped_column(
|
||||
String(500), nullable=True
|
||||
)
|
||||
report_data: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
|
||||
generated_by: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey("users.id"), nullable=True
|
||||
)
|
||||
generated_at: Mapped[dt.datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=func.now()
|
||||
)
|
||||
|
||||
# Relationships
|
||||
generated_by_user: Mapped[Optional["User"]] = relationship(
|
||||
foreign_keys=[generated_by]
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("jahr", "kw", name="uk_jahr_kw"),
|
||||
)
|
||||
|
||||
|
||||
class YearlySummary(Base):
|
||||
__tablename__ = "yearly_summary"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
jahr: Mapped[int] = mapped_column(SmallInteger, nullable=False)
|
||||
kw: Mapped[int] = mapped_column(SmallInteger, nullable=False)
|
||||
|
||||
# Overall counts
|
||||
erstberatungen: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
ablehnungen: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
unterlagen: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
keine_rueckmeldung: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
gutachten_gesamt: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
gutachten_alternative: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
gutachten_bestaetigung: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
|
||||
# Per-Fallgruppe counts: onko
|
||||
onko_anzahl: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
onko_gutachten: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
onko_keine_rm: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
|
||||
# Per-Fallgruppe counts: kardio
|
||||
kardio_anzahl: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
kardio_gutachten: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
kardio_keine_rm: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
|
||||
# Per-Fallgruppe counts: intensiv
|
||||
intensiv_anzahl: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
intensiv_gutachten: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
intensiv_keine_rm: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
|
||||
# Per-Fallgruppe counts: galle
|
||||
galle_anzahl: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
galle_gutachten: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
galle_keine_rm: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
|
||||
# Per-Fallgruppe counts: sd
|
||||
sd_anzahl: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
sd_gutachten: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
sd_keine_rm: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
|
||||
# Gutachten-Typ per Fallgruppe
|
||||
onko_alternative: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
onko_bestaetigung: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
kardio_alternative: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
kardio_bestaetigung: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
intensiv_alternative: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
intensiv_bestaetigung: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
galle_alternative: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
galle_bestaetigung: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
sd_alternative: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
sd_bestaetigung: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
|
||||
# Therapieaenderung counts
|
||||
ta_ja: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
ta_nein: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
ta_diagnosekorrektur: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
ta_unterversorgung: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
ta_uebertherapie: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, server_default="0"
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("jahr", "kw", name="uk_jahr_kw"),
|
||||
)
|
||||
162
backend/app/models/user.py
Normal file
162
backend/app/models/user.py
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
"""User, authentication, and access control models."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
CheckConstraint,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
String,
|
||||
UniqueConstraint,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.schema import FetchedValue
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
username: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
email: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
role: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, server_default="dak_mitarbeiter"
|
||||
)
|
||||
mfa_secret: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
mfa_enabled: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, server_default="0"
|
||||
)
|
||||
is_active: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, server_default="1"
|
||||
)
|
||||
must_change_password: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, server_default="0"
|
||||
)
|
||||
last_login: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
failed_login_attempts: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, server_default="0"
|
||||
)
|
||||
locked_until: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=func.now()
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
nullable=False,
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
server_onupdate=FetchedValue(),
|
||||
)
|
||||
|
||||
# Relationships
|
||||
refresh_tokens: Mapped[list[RefreshToken]] = relationship(
|
||||
back_populates="user", cascade="all, delete-orphan"
|
||||
)
|
||||
invitations_created: Mapped[list[InvitationLink]] = relationship(
|
||||
foreign_keys="InvitationLink.created_by", back_populates="creator"
|
||||
)
|
||||
invitations_used: Mapped[list[InvitationLink]] = relationship(
|
||||
foreign_keys="InvitationLink.used_by", back_populates="used_by_user"
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("username", name="uk_username"),
|
||||
UniqueConstraint("email", name="uk_email"),
|
||||
CheckConstraint(
|
||||
"role IN ('admin', 'dak_mitarbeiter')", name="chk_role"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class RefreshToken(Base):
|
||||
__tablename__ = "refresh_tokens"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
token_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
expires_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||
revoked: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, server_default="0"
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=func.now()
|
||||
)
|
||||
|
||||
# Relationships
|
||||
user: Mapped[User] = relationship(back_populates="refresh_tokens")
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_user", "user_id"),
|
||||
Index("idx_token", "token_hash"),
|
||||
)
|
||||
|
||||
|
||||
class InvitationLink(Base):
|
||||
__tablename__ = "invitation_links"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
token: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
role: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, server_default="dak_mitarbeiter"
|
||||
)
|
||||
created_by: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey("users.id"), nullable=True
|
||||
)
|
||||
expires_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||
used_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
used_by: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey("users.id"), nullable=True
|
||||
)
|
||||
is_active: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, server_default="1"
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=func.now()
|
||||
)
|
||||
|
||||
# Relationships
|
||||
creator: Mapped[Optional[User]] = relationship(
|
||||
foreign_keys=[created_by], back_populates="invitations_created"
|
||||
)
|
||||
used_by_user: Mapped[Optional[User]] = relationship(
|
||||
foreign_keys=[used_by], back_populates="invitations_used"
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("token", name="uk_token"),
|
||||
)
|
||||
|
||||
|
||||
class AllowedDomain(Base):
|
||||
__tablename__ = "allowed_domains"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
domain: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
role: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, server_default="dak_mitarbeiter"
|
||||
)
|
||||
is_active: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, server_default="1"
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=func.now()
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("domain", name="uk_domain"),
|
||||
)
|
||||
Loading…
Reference in a new issue