| """ |
| Database models for ICH Screening Application with user authentication and privacy |
| """ |
| import os |
| from datetime import datetime |
| from zoneinfo import ZoneInfo |
| from flask_sqlalchemy import SQLAlchemy |
| from flask_login import UserMixin |
| from werkzeug.security import generate_password_hash, check_password_hash |
| import secrets |
|
|
| db = SQLAlchemy() |
| IST = ZoneInfo("Asia/Kolkata") |
|
|
| def now_ist() -> datetime: |
| return datetime.now(IST).replace(tzinfo=None) |
|
|
|
|
| class User(UserMixin, db.Model): |
| """User account model for authentication""" |
| __tablename__ = 'users' |
| |
| id = db.Column(db.Integer, primary_key=True) |
| username = db.Column(db.String(80), unique=True, nullable=False, index=True) |
| email = db.Column(db.String(120), unique=True, nullable=False, index=True) |
| password_hash = db.Column(db.String(255), nullable=False) |
| full_name = db.Column(db.String(120)) |
| created_at = db.Column(db.DateTime, default=now_ist, nullable=False) |
| updated_at = db.Column(db.DateTime, default=now_ist, onupdate=now_ist) |
| is_active = db.Column(db.Boolean, default=True, nullable=False) |
| |
| |
| screening_uploads = db.relationship('ScreeningUpload', backref='user', lazy=True, cascade='all, delete-orphan') |
| screening_reports = db.relationship('ScreeningReport', backref='user', lazy=True, cascade='all, delete-orphan') |
| |
| def set_password(self, password): |
| """Hash and set the user's password""" |
| self.password_hash = generate_password_hash(password, method='pbkdf2:sha256') |
| |
| def check_password(self, password): |
| """Verify password against stored hash""" |
| return check_password_hash(self.password_hash, password) |
| |
| def __repr__(self): |
| return f'<User {self.username}>' |
|
|
|
|
| class ScreeningUpload(db.Model): |
| """Track uploaded DICOM files with user ownership""" |
| __tablename__ = 'screening_uploads' |
| |
| id = db.Column(db.Integer, primary_key=True) |
| user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True) |
| file_name = db.Column(db.String(255), nullable=False) |
| original_filename = db.Column(db.String(255), nullable=False) |
| file_size = db.Column(db.Integer) |
| file_path = db.Column(db.String(500), nullable=False) |
| upload_timestamp = db.Column(db.DateTime, default=now_ist, nullable=False, index=True) |
| processing_status = db.Column(db.String(20), default='pending') |
| processing_error = db.Column(db.Text) |
| |
| |
| reports = db.relationship('ScreeningReport', backref='upload', lazy=True, cascade='all, delete-orphan') |
| |
| def __repr__(self): |
| return f'<ScreeningUpload {self.id} - user {self.user_id}>' |
|
|
|
|
| class ScreeningReport(db.Model): |
| """Store screening results with full user isolation""" |
| __tablename__ = 'screening_reports' |
| |
| id = db.Column(db.Integer, primary_key=True) |
| user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True) |
| upload_id = db.Column(db.Integer, db.ForeignKey('screening_uploads.id'), nullable=False, index=True) |
| image_id = db.Column(db.String(100), nullable=False) |
| |
| |
| screening_outcome = db.Column(db.String(100)) |
| raw_probability = db.Column(db.Float) |
| calibrated_probability = db.Column(db.Float) |
| confidence_band = db.Column(db.String(50)) |
| decision_threshold = db.Column(db.Float) |
| |
| |
| triage_action = db.Column(db.String(100)) |
| urgency = db.Column(db.String(50)) |
| llm_summary = db.Column(db.Text) |
| |
| |
| true_label = db.Column(db.String(100)) |
| |
| |
| report_json_path = db.Column(db.String(500)) |
| gradcam_image_path = db.Column(db.String(500)) |
| report_payload = db.Column(db.Text) |
| |
| |
| generated_at = db.Column(db.DateTime, default=now_ist, nullable=False, index=True) |
| created_at = db.Column(db.DateTime, default=now_ist, nullable=False) |
| |
| def __repr__(self): |
| return f'<ScreeningReport {self.id} - user {self.user_id} - {self.image_id}>' |
|
|
|
|
| class AuditLog(db.Model): |
| """Audit trail for security and compliance""" |
| __tablename__ = 'audit_logs' |
| |
| id = db.Column(db.Integer, primary_key=True) |
| user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True, index=True) |
| action = db.Column(db.String(100), nullable=False) |
| resource_type = db.Column(db.String(50)) |
| resource_id = db.Column(db.String(255)) |
| details = db.Column(db.Text) |
| ip_address = db.Column(db.String(45)) |
| timestamp = db.Column(db.DateTime, default=now_ist, nullable=False, index=True) |
| status = db.Column(db.String(20), default='success') |
| |
| def __repr__(self): |
| return f'<AuditLog {self.action} - user {self.user_id} - {self.timestamp}>' |
|
|