Spaces:
Sleeping
Sleeping
Commit ·
718f018
0
Parent(s):
All files added
Browse files- .gitignore +38 -0
- api/__init__.py +0 -0
- api/admin.py +23 -0
- api/apps.py +5 -0
- api/authentication.py +75 -0
- api/email_utils.py +131 -0
- api/migrations/0001_initial.py +43 -0
- api/migrations/0002_userprofile_address_userprofile_full_name_and_more.py +28 -0
- api/migrations/__init__.py +0 -0
- api/ml_engine.py +69 -0
- api/models.py +47 -0
- api/serializers.py +54 -0
- api/storage.py +15 -0
- api/templates/email/test_result.html +150 -0
- api/urls.py +19 -0
- api/views.py +157 -0
- manage.py +22 -0
- model/efficientnet_b0.h5 +3 -0
- requirements.txt +117 -0
- respirex_backend/__init__.py +0 -0
- respirex_backend/asgi.py +16 -0
- respirex_backend/settings.py +134 -0
- respirex_backend/urls.py +7 -0
- respirex_backend/wsgi.py +16 -0
.gitignore
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python / Django
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# Virtual Environment (Very Important)
|
| 7 |
+
venv/
|
| 8 |
+
env/
|
| 9 |
+
.venv/
|
| 10 |
+
|
| 11 |
+
# Environment Variables (Security)
|
| 12 |
+
.env
|
| 13 |
+
.env.local
|
| 14 |
+
.env.example
|
| 15 |
+
|
| 16 |
+
# Database (If using SQLite)
|
| 17 |
+
db.sqlite3
|
| 18 |
+
db.sqlite3-journal
|
| 19 |
+
|
| 20 |
+
# Local Settings
|
| 21 |
+
*/settings/local.py
|
| 22 |
+
|
| 23 |
+
# Static and Media files (User uploads should not be in git)
|
| 24 |
+
media/
|
| 25 |
+
static/
|
| 26 |
+
staticfiles/
|
| 27 |
+
|
| 28 |
+
# VS Code / Editor settings
|
| 29 |
+
.vscode/
|
| 30 |
+
.idea/
|
| 31 |
+
*.swp
|
| 32 |
+
*.swo
|
| 33 |
+
|
| 34 |
+
# Mac System Files
|
| 35 |
+
.DS_Store
|
| 36 |
+
|
| 37 |
+
# Logs
|
| 38 |
+
*.log
|
api/__init__.py
ADDED
|
File without changes
|
api/admin.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.contrib import admin
|
| 2 |
+
from .models import UserProfile, TestResult
|
| 3 |
+
|
| 4 |
+
# Register the UserProfile model so you can change roles
|
| 5 |
+
@admin.register(UserProfile)
|
| 6 |
+
class UserProfileAdmin(admin.ModelAdmin):
|
| 7 |
+
# Columns to show in the list
|
| 8 |
+
list_display = ('user', 'email_display', 'role', 'full_name', 'phone')
|
| 9 |
+
# Filters on the right sidebar
|
| 10 |
+
list_filter = ('role', 'state')
|
| 11 |
+
# Search box functionality
|
| 12 |
+
search_fields = ('user__username', 'user__email', 'full_name')
|
| 13 |
+
|
| 14 |
+
# Helper to show email from the related User model
|
| 15 |
+
def email_display(self, obj):
|
| 16 |
+
return obj.user.email
|
| 17 |
+
email_display.short_description = 'Email'
|
| 18 |
+
|
| 19 |
+
# Register the TestResult model to see patient scans
|
| 20 |
+
@admin.register(TestResult)
|
| 21 |
+
class TestResultAdmin(admin.ModelAdmin):
|
| 22 |
+
list_display = ('patient', 'result', 'confidence_score', 'date_tested')
|
| 23 |
+
list_filter = ('result', 'risk_level', 'date_tested')
|
api/apps.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.apps import AppConfig
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class ApiConfig(AppConfig):
|
| 5 |
+
name = 'api'
|
api/authentication.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from rest_framework import authentication, exceptions
|
| 2 |
+
from supabase import create_client, Client
|
| 3 |
+
from django.conf import settings
|
| 4 |
+
from django.contrib.auth.models import User
|
| 5 |
+
from .models import UserProfile
|
| 6 |
+
|
| 7 |
+
# 1. DRF Authentication Class (Keep this, it's good practice)
|
| 8 |
+
class SupabaseAuthentication(authentication.BaseAuthentication):
|
| 9 |
+
def authenticate(self, request):
|
| 10 |
+
auth_header = request.headers.get('Authorization')
|
| 11 |
+
if not auth_header:
|
| 12 |
+
return None
|
| 13 |
+
|
| 14 |
+
try:
|
| 15 |
+
token = auth_header.split(' ')[1]
|
| 16 |
+
supabase: Client = create_client(settings.SUPABASE_URL, settings.SUPABASE_KEY)
|
| 17 |
+
|
| 18 |
+
user_data = supabase.auth.get_user(token)
|
| 19 |
+
if not user_data:
|
| 20 |
+
raise exceptions.AuthenticationFailed('Invalid token')
|
| 21 |
+
|
| 22 |
+
uid = user_data.user.id
|
| 23 |
+
email = user_data.user.email
|
| 24 |
+
|
| 25 |
+
user, created = User.objects.get_or_create(username=uid, defaults={'email': email})
|
| 26 |
+
return (user, None)
|
| 27 |
+
|
| 28 |
+
except Exception as e:
|
| 29 |
+
raise exceptions.AuthenticationFailed(f'Authentication failed: {str(e)}')
|
| 30 |
+
|
| 31 |
+
# 2. MISSING FUNCTIONS (Add these to fix the ImportError)
|
| 32 |
+
|
| 33 |
+
def authenticate_user(email, password):
|
| 34 |
+
"""
|
| 35 |
+
Logs in the user via Supabase and returns the access token.
|
| 36 |
+
Used by the 'login' view.
|
| 37 |
+
"""
|
| 38 |
+
supabase: Client = create_client(settings.SUPABASE_URL, settings.SUPABASE_KEY)
|
| 39 |
+
try:
|
| 40 |
+
response = supabase.auth.sign_in_with_password({
|
| 41 |
+
"email": email,
|
| 42 |
+
"password": password
|
| 43 |
+
})
|
| 44 |
+
# Return the access token string
|
| 45 |
+
return response.session.access_token
|
| 46 |
+
except Exception as e:
|
| 47 |
+
print(f"❌ Login failed: {e}")
|
| 48 |
+
return None
|
| 49 |
+
|
| 50 |
+
def get_user_from_token(request):
|
| 51 |
+
"""
|
| 52 |
+
Manually extracts the user from the request headers.
|
| 53 |
+
Used by 'patient_dashboard' and 'upload_xray'.
|
| 54 |
+
"""
|
| 55 |
+
auth_header = request.headers.get('Authorization')
|
| 56 |
+
if not auth_header:
|
| 57 |
+
return None
|
| 58 |
+
|
| 59 |
+
try:
|
| 60 |
+
token = auth_header.split(' ')[1]
|
| 61 |
+
supabase: Client = create_client(settings.SUPABASE_URL, settings.SUPABASE_KEY)
|
| 62 |
+
|
| 63 |
+
user_data = supabase.auth.get_user(token)
|
| 64 |
+
if not user_data:
|
| 65 |
+
return None
|
| 66 |
+
|
| 67 |
+
# Sync with Django User model (required for Foreign Keys in TestResult)
|
| 68 |
+
uid = user_data.user.id
|
| 69 |
+
email = user_data.user.email
|
| 70 |
+
user, _ = User.objects.get_or_create(username=uid, defaults={'email': email})
|
| 71 |
+
|
| 72 |
+
return user
|
| 73 |
+
except Exception as e:
|
| 74 |
+
print(f"❌ Token extraction failed: {e}")
|
| 75 |
+
return None
|
api/email_utils.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import base64
|
| 3 |
+
from sendgrid import SendGridAPIClient
|
| 4 |
+
from sendgrid.helpers.mail import (
|
| 5 |
+
Mail, Attachment, FileContent, FileName, FileType, Disposition
|
| 6 |
+
)
|
| 7 |
+
from django.conf import settings
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
|
| 10 |
+
# --- CONFIGURATION ---
|
| 11 |
+
# 1. HARDCODE YOUR KEY HERE FOR TESTING (Remove before deploying to GitHub)
|
| 12 |
+
SENDGRID_API_KEY = os.environ.get('SENDGRID_API_KEY') # <--- PASTE YOUR KEY HERE inside quotes
|
| 13 |
+
SENDER_EMAIL = "gamingyash54@gmail.com" # <--- MUST match the Single Sender you verified
|
| 14 |
+
# ---------------------
|
| 15 |
+
|
| 16 |
+
def send_html_email(subject, recipient_list, html_content, pdf_buffer=None, filename="Report.pdf"):
|
| 17 |
+
"""
|
| 18 |
+
Sends an email using SendGrid API (Bypasses Gmail SMTP).
|
| 19 |
+
"""
|
| 20 |
+
# 1. Create the email object
|
| 21 |
+
message = Mail(
|
| 22 |
+
from_email=SENDER_EMAIL,
|
| 23 |
+
to_emails=recipient_list,
|
| 24 |
+
subject=subject,
|
| 25 |
+
html_content=html_content
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# 2. Attach the PDF if it exists
|
| 29 |
+
if pdf_buffer:
|
| 30 |
+
# SendGrid requires the file to be encoded in Base64 string
|
| 31 |
+
encoded_file = base64.b64encode(pdf_buffer.getvalue()).decode()
|
| 32 |
+
|
| 33 |
+
attachment = Attachment(
|
| 34 |
+
FileContent(encoded_file),
|
| 35 |
+
FileName(filename),
|
| 36 |
+
FileType('application/pdf'),
|
| 37 |
+
Disposition('attachment')
|
| 38 |
+
)
|
| 39 |
+
message.attachment = attachment
|
| 40 |
+
|
| 41 |
+
# 3. Send via API
|
| 42 |
+
try:
|
| 43 |
+
print(f"🚀 Sending email via SendGrid to {recipient_list}...")
|
| 44 |
+
|
| 45 |
+
sg = SendGridAPIClient(SENDGRID_API_KEY)
|
| 46 |
+
response = sg.send(message)
|
| 47 |
+
|
| 48 |
+
print(f"✅ SendGrid Status: {response.status_code}")
|
| 49 |
+
|
| 50 |
+
if response.status_code in [200, 201, 202]:
|
| 51 |
+
print("SUCCESS: Email sent!")
|
| 52 |
+
else:
|
| 53 |
+
print(f"WARNING: Unexpected status code {response.status_code}")
|
| 54 |
+
|
| 55 |
+
except Exception as e:
|
| 56 |
+
print(f"❌ SendGrid Failed: {str(e)}")
|
| 57 |
+
if hasattr(e, 'body'):
|
| 58 |
+
print(f"Error Body: {e.body}")
|
| 59 |
+
raise e # Re-raise to alert the frontend
|
| 60 |
+
|
| 61 |
+
def get_medical_email_template(patient_name, test_date, risk_level, confidence):
|
| 62 |
+
"""
|
| 63 |
+
Returns the HTML email body.
|
| 64 |
+
"""
|
| 65 |
+
# Define colors
|
| 66 |
+
if risk_level in ["High", "Medium"]:
|
| 67 |
+
color = "#e11d48" # Red
|
| 68 |
+
icon = "⚠️"
|
| 69 |
+
else:
|
| 70 |
+
color = "#059669" # Green
|
| 71 |
+
icon = "✅"
|
| 72 |
+
|
| 73 |
+
dashboard_link = "https://respirex.vercel.app" # Change to your Vercel URL later
|
| 74 |
+
|
| 75 |
+
return f"""
|
| 76 |
+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; border: 1px solid #eee; border-radius: 8px;">
|
| 77 |
+
<div style="background-color: #0f172a; color: white; padding: 20px; text-align: center;">
|
| 78 |
+
<h2 style="margin:0;">RespireX Report</h2>
|
| 79 |
+
</div>
|
| 80 |
+
<div style="padding: 30px;">
|
| 81 |
+
<h3>Hello {patient_name},</h3>
|
| 82 |
+
<p>Your analysis from {test_date} is complete.</p>
|
| 83 |
+
|
| 84 |
+
<div style="background: #f8fafc; padding: 15px; border-radius: 6px; margin: 20px 0;">
|
| 85 |
+
<p><strong>Result:</strong> <span style="color: {color}; font-weight: bold;">{icon} {risk_level} Risk</span></p>
|
| 86 |
+
<p><strong>AI Confidence:</strong> {confidence}%</p>
|
| 87 |
+
</div>
|
| 88 |
+
|
| 89 |
+
<p>Please find the detailed PDF report attached.</p>
|
| 90 |
+
|
| 91 |
+
<div style="text-align: center; margin-top: 30px;">
|
| 92 |
+
<a href="{dashboard_link}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: bold;">Login to Dashboard</a>
|
| 93 |
+
</div>
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
"""
|
| 97 |
+
|
| 98 |
+
def send_test_result_email(user_email, prediction):
|
| 99 |
+
"""
|
| 100 |
+
Orchestrates sending the test result email.
|
| 101 |
+
Handles both dict (future) and tuple (current ml_engine) formats.
|
| 102 |
+
"""
|
| 103 |
+
# 1. Extract data safely
|
| 104 |
+
if isinstance(prediction, dict):
|
| 105 |
+
label = prediction.get('label', 'Analysis Complete')
|
| 106 |
+
confidence = prediction.get('confidence', 0)
|
| 107 |
+
# Estimate risk if missing
|
| 108 |
+
risk_level = "High" if label == "Positive" else "Low"
|
| 109 |
+
elif isinstance(prediction, (tuple, list)) and len(prediction) >= 3:
|
| 110 |
+
# Handle tuple from ml_engine.py: (result, confidence, risk_level)
|
| 111 |
+
label = prediction[0]
|
| 112 |
+
confidence = prediction[1]
|
| 113 |
+
risk_level = prediction[2]
|
| 114 |
+
else:
|
| 115 |
+
label = "Unknown"
|
| 116 |
+
confidence = 0
|
| 117 |
+
risk_level = "Low"
|
| 118 |
+
|
| 119 |
+
# 2. Prepare Email
|
| 120 |
+
subject = f"RespireX Result: {label}"
|
| 121 |
+
date_str = datetime.now().strftime("%B %d, %Y")
|
| 122 |
+
|
| 123 |
+
html_content = get_medical_email_template(
|
| 124 |
+
patient_name="Valued Patient",
|
| 125 |
+
test_date=date_str,
|
| 126 |
+
risk_level=risk_level,
|
| 127 |
+
confidence=confidence
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
# 3. Send
|
| 131 |
+
send_html_email(subject, [user_email], html_content)
|
api/migrations/0001_initial.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 6.0 on 2025-12-31 19:56
|
| 2 |
+
|
| 3 |
+
import django.db.models.deletion
|
| 4 |
+
from django.conf import settings
|
| 5 |
+
from django.db import migrations, models
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class Migration(migrations.Migration):
|
| 9 |
+
|
| 10 |
+
initial = True
|
| 11 |
+
|
| 12 |
+
dependencies = [
|
| 13 |
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
operations = [
|
| 17 |
+
migrations.CreateModel(
|
| 18 |
+
name='UserProfile',
|
| 19 |
+
fields=[
|
| 20 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 21 |
+
('role', models.CharField(choices=[('patient', 'Patient'), ('doctor', 'Doctor')], max_length=10)),
|
| 22 |
+
('state', models.CharField(blank=True, max_length=100)),
|
| 23 |
+
('city', models.CharField(blank=True, max_length=100)),
|
| 24 |
+
('license_number', models.CharField(blank=True, max_length=50, null=True)),
|
| 25 |
+
('age', models.IntegerField(blank=True, null=True)),
|
| 26 |
+
('gender', models.CharField(blank=True, max_length=20)),
|
| 27 |
+
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
|
| 28 |
+
],
|
| 29 |
+
),
|
| 30 |
+
migrations.CreateModel(
|
| 31 |
+
name='TestResult',
|
| 32 |
+
fields=[
|
| 33 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 34 |
+
('xray_image_url', models.URLField()),
|
| 35 |
+
('date_tested', models.DateTimeField(auto_now_add=True)),
|
| 36 |
+
('result', models.CharField(choices=[('Positive', 'Positive'), ('Negative', 'Negative')], max_length=20)),
|
| 37 |
+
('confidence_score', models.FloatField()),
|
| 38 |
+
('risk_level', models.CharField(choices=[('High', 'High'), ('Medium', 'Medium'), ('Low', 'Low')], max_length=20)),
|
| 39 |
+
('symptoms_data', models.JSONField(default=dict)),
|
| 40 |
+
('patient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='test_results', to='api.userprofile')),
|
| 41 |
+
],
|
| 42 |
+
),
|
| 43 |
+
]
|
api/migrations/0002_userprofile_address_userprofile_full_name_and_more.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 6.0 on 2026-01-01 09:38
|
| 2 |
+
|
| 3 |
+
from django.db import migrations, models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
dependencies = [
|
| 9 |
+
('api', '0001_initial'),
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
operations = [
|
| 13 |
+
migrations.AddField(
|
| 14 |
+
model_name='userprofile',
|
| 15 |
+
name='address',
|
| 16 |
+
field=models.TextField(blank=True),
|
| 17 |
+
),
|
| 18 |
+
migrations.AddField(
|
| 19 |
+
model_name='userprofile',
|
| 20 |
+
name='full_name',
|
| 21 |
+
field=models.CharField(blank=True, max_length=200),
|
| 22 |
+
),
|
| 23 |
+
migrations.AddField(
|
| 24 |
+
model_name='userprofile',
|
| 25 |
+
name='phone',
|
| 26 |
+
field=models.CharField(blank=True, max_length=20),
|
| 27 |
+
),
|
| 28 |
+
]
|
api/migrations/__init__.py
ADDED
|
File without changes
|
api/ml_engine.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import numpy as np
|
| 3 |
+
import tensorflow as tf
|
| 4 |
+
from PIL import Image
|
| 5 |
+
from django.conf import settings
|
| 6 |
+
|
| 7 |
+
# Define the path to the model
|
| 8 |
+
MODEL_PATH = os.path.join(settings.BASE_DIR, 'model', 'efficientnet_b0.h5')
|
| 9 |
+
model = None
|
| 10 |
+
|
| 11 |
+
# Load the model once when the server starts
|
| 12 |
+
try:
|
| 13 |
+
model = tf.keras.models.load_model(MODEL_PATH)
|
| 14 |
+
print("✅ ML Model Loaded Successfully")
|
| 15 |
+
except Exception as e:
|
| 16 |
+
print(f"⚠️ ML Model not found or error loading: {e}. Using dummy mode.")
|
| 17 |
+
|
| 18 |
+
def predict_xray(image_file):
|
| 19 |
+
"""
|
| 20 |
+
Predicts if an X-ray image is Positive (Tuberculosis) or Negative (Normal).
|
| 21 |
+
"""
|
| 22 |
+
if not model:
|
| 23 |
+
# Dummy fallback if model didn't load
|
| 24 |
+
return "Negative", 85.5, "Low"
|
| 25 |
+
|
| 26 |
+
try:
|
| 27 |
+
# 1. Load and Preprocess Image
|
| 28 |
+
# Convert to RGB to ensure 3 channels
|
| 29 |
+
img = Image.open(image_file).convert('RGB')
|
| 30 |
+
|
| 31 |
+
# Resize to 128x128 (Must match your training notebook size)
|
| 32 |
+
img = img.resize((128, 128))
|
| 33 |
+
|
| 34 |
+
# Convert to array and add batch dimension
|
| 35 |
+
img_array = tf.keras.preprocessing.image.img_to_array(img)
|
| 36 |
+
img_array = np.expand_dims(img_array, axis=0)
|
| 37 |
+
|
| 38 |
+
# CRITICAL FIX: Normalize pixel values to 0-1 range
|
| 39 |
+
img_array = img_array / 255.0
|
| 40 |
+
|
| 41 |
+
# 2. Make Prediction
|
| 42 |
+
# Expected output shape: [[prob_normal, prob_tb]]
|
| 43 |
+
prediction = model.predict(img_array)
|
| 44 |
+
|
| 45 |
+
# Get the class with the highest probability
|
| 46 |
+
# Class 0 = Normal, Class 1 = Tuberculosis
|
| 47 |
+
class_idx = np.argmax(prediction, axis=1)[0]
|
| 48 |
+
confidence = float(np.max(prediction))
|
| 49 |
+
|
| 50 |
+
# 3. Interpret Results
|
| 51 |
+
if class_idx == 1:
|
| 52 |
+
result = "Positive" # Tuberculosis detected
|
| 53 |
+
|
| 54 |
+
if confidence > 0.8:
|
| 55 |
+
risk_level = "High"
|
| 56 |
+
elif confidence >= 0.5:
|
| 57 |
+
risk_level = "Medium"
|
| 58 |
+
else:
|
| 59 |
+
risk_level = "Low"
|
| 60 |
+
else:
|
| 61 |
+
result = "Negative" # Normal
|
| 62 |
+
risk_level = "Low"
|
| 63 |
+
|
| 64 |
+
return (result, round(confidence * 100, 2), risk_level)
|
| 65 |
+
|
| 66 |
+
except Exception as e:
|
| 67 |
+
print(f"❌ Error processing image: {e}")
|
| 68 |
+
# Return error state or safe default
|
| 69 |
+
return "Error", 0.0, "Low"
|
api/models.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.db import models
|
| 2 |
+
from django.contrib.auth.models import User
|
| 3 |
+
|
| 4 |
+
class UserProfile(models.Model):
|
| 5 |
+
ROLE_CHOICES = (('patient', 'Patient'), ('doctor', 'Doctor'))
|
| 6 |
+
|
| 7 |
+
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
|
| 8 |
+
role = models.CharField(max_length=10, choices=ROLE_CHOICES)
|
| 9 |
+
|
| 10 |
+
# Common fields
|
| 11 |
+
# --- NEW FIELDS ADDED HERE ---
|
| 12 |
+
full_name = models.CharField(max_length=200, blank=True)
|
| 13 |
+
phone = models.CharField(max_length=20, blank=True)
|
| 14 |
+
address = models.TextField(blank=True)
|
| 15 |
+
# -----------------------------
|
| 16 |
+
|
| 17 |
+
state = models.CharField(max_length=100, blank=True)
|
| 18 |
+
city = models.CharField(max_length=100, blank=True)
|
| 19 |
+
|
| 20 |
+
# Doctor specific
|
| 21 |
+
license_number = models.CharField(max_length=50, blank=True, null=True)
|
| 22 |
+
|
| 23 |
+
# Patient specific
|
| 24 |
+
age = models.IntegerField(null=True, blank=True)
|
| 25 |
+
gender = models.CharField(max_length=20, blank=True)
|
| 26 |
+
|
| 27 |
+
def __str__(self):
|
| 28 |
+
return f"{self.user.username} - {self.role}"
|
| 29 |
+
|
| 30 |
+
class TestResult(models.Model):
|
| 31 |
+
RESULT_CHOICES = (('Positive', 'Positive'), ('Negative', 'Negative'))
|
| 32 |
+
RISK_CHOICES = (('High', 'High'), ('Medium', 'Medium'), ('Low', 'Low'))
|
| 33 |
+
|
| 34 |
+
patient = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name='test_results')
|
| 35 |
+
xray_image_url = models.URLField()
|
| 36 |
+
date_tested = models.DateTimeField(auto_now_add=True)
|
| 37 |
+
|
| 38 |
+
# Prediction Data
|
| 39 |
+
result = models.CharField(max_length=20, choices=RESULT_CHOICES)
|
| 40 |
+
confidence_score = models.FloatField()
|
| 41 |
+
risk_level = models.CharField(max_length=20, choices=RISK_CHOICES)
|
| 42 |
+
|
| 43 |
+
# Storing symptoms as a JSON object for flexibility
|
| 44 |
+
symptoms_data = models.JSONField(default=dict)
|
| 45 |
+
|
| 46 |
+
def __str__(self):
|
| 47 |
+
return f"{self.patient.user.username} - {self.result} ({self.date_tested})"
|
api/serializers.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from rest_framework import serializers
|
| 2 |
+
from .models import UserProfile, TestResult
|
| 3 |
+
from django.contrib.auth.models import User
|
| 4 |
+
|
| 5 |
+
class UserProfileSerializer(serializers.ModelSerializer):
|
| 6 |
+
email = serializers.EmailField(source='user.email', read_only=True)
|
| 7 |
+
|
| 8 |
+
class Meta:
|
| 9 |
+
model = UserProfile
|
| 10 |
+
# Add the new fields to the serializer
|
| 11 |
+
fields = ['id', 'email', 'full_name', 'phone', 'address', 'role', 'state', 'city', 'age', 'gender', 'license_number']
|
| 12 |
+
|
| 13 |
+
class TestResultSerializer(serializers.ModelSerializer):
|
| 14 |
+
# Use full_name if available, otherwise fallback to email
|
| 15 |
+
patient_name = serializers.SerializerMethodField()
|
| 16 |
+
|
| 17 |
+
# Fetch extra patient details
|
| 18 |
+
email = serializers.CharField(source='patient.user.email', read_only=True)
|
| 19 |
+
|
| 20 |
+
state = serializers.CharField(source='patient.state', read_only=True)
|
| 21 |
+
city = serializers.CharField(source='patient.city', read_only=True)
|
| 22 |
+
age = serializers.IntegerField(source='patient.age', read_only=True)
|
| 23 |
+
gender = serializers.CharField(source='patient.gender', read_only=True)
|
| 24 |
+
phone = serializers.CharField(source='patient.phone', read_only=True)
|
| 25 |
+
|
| 26 |
+
# OVERRIDE the model field to dynamically calculate risk level for ALL records (old and new)
|
| 27 |
+
risk_level = serializers.SerializerMethodField()
|
| 28 |
+
|
| 29 |
+
class Meta:
|
| 30 |
+
model = TestResult
|
| 31 |
+
fields = '__all__'
|
| 32 |
+
|
| 33 |
+
def get_patient_name(self, obj):
|
| 34 |
+
# Return full_name if it exists, else return email
|
| 35 |
+
if obj.patient.full_name:
|
| 36 |
+
return obj.patient.full_name
|
| 37 |
+
return obj.patient.user.email
|
| 38 |
+
|
| 39 |
+
def get_risk_level(self, obj):
|
| 40 |
+
"""
|
| 41 |
+
Dynamically calculates Risk Level based on Confidence Score.
|
| 42 |
+
This ensures 'Medium' is returned for 50-80% even if the DB says 'Low'.
|
| 43 |
+
"""
|
| 44 |
+
if obj.result == 'Negative':
|
| 45 |
+
return 'Low'
|
| 46 |
+
|
| 47 |
+
# Positive Case Logic
|
| 48 |
+
# obj.confidence_score is stored as 0-100 float
|
| 49 |
+
if obj.confidence_score > 80:
|
| 50 |
+
return 'High'
|
| 51 |
+
elif obj.confidence_score >= 50:
|
| 52 |
+
return 'Medium'
|
| 53 |
+
else:
|
| 54 |
+
return 'Low'
|
api/storage.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from supabase import create_client
|
| 2 |
+
from django.conf import settings
|
| 3 |
+
import uuid
|
| 4 |
+
|
| 5 |
+
def upload_to_supabase(image_file):
|
| 6 |
+
supabase = create_client(settings.SUPABASE_URL, settings.SUPABASE_KEY)
|
| 7 |
+
filename = f"{uuid.uuid4()}.{image_file.name.split('.')[-1]}"
|
| 8 |
+
file_content = image_file.read()
|
| 9 |
+
|
| 10 |
+
supabase.storage.from_("xrays").upload(
|
| 11 |
+
file=file_content,
|
| 12 |
+
path=filename,
|
| 13 |
+
file_options={"content-type": image_file.content_type}
|
| 14 |
+
)
|
| 15 |
+
return supabase.storage.from_("xrays").get_public_url(filename)
|
api/templates/email/test_result.html
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>RespireX Test Results</title>
|
| 7 |
+
</head>
|
| 8 |
+
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f0f4f8;">
|
| 9 |
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #f0f4f8; padding: 40px 20px;">
|
| 10 |
+
<tr>
|
| 11 |
+
<td align="center">
|
| 12 |
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" style="max-width: 600px; background-color: #ffffff; border-radius: 16px; overflow: hidden; box-shadow: 0 10px 25px rgba(0,0,0,0.1);">
|
| 13 |
+
|
| 14 |
+
<!-- Header -->
|
| 15 |
+
<tr>
|
| 16 |
+
<td style="background: linear-gradient(135deg, #3b82f6 0%, #06b6d4 100%); padding: 40px 30px; text-align: center;">
|
| 17 |
+
<h1 style="margin: 0; color: #ffffff; font-size: 32px; font-weight: 700; letter-spacing: -0.5px;">RespireX</h1>
|
| 18 |
+
<p style="margin: 10px 0 0; color: rgba(255,255,255,0.9); font-size: 14px; font-weight: 500;">AI-Powered Tuberculosis Detection</p>
|
| 19 |
+
</td>
|
| 20 |
+
</tr>
|
| 21 |
+
|
| 22 |
+
<!-- Result Banner -->
|
| 23 |
+
<tr>
|
| 24 |
+
<td style="padding: 0;">
|
| 25 |
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: {{ banner_bg }}; padding: 24px 30px; border-left: 6px solid {{ banner_border }};">
|
| 26 |
+
<tr>
|
| 27 |
+
<td width="80" style="vertical-align: middle; text-align: center;">
|
| 28 |
+
<div style="width: 50px; height: 50px; background-color: {{ icon_bg }}; border-radius: 50%; margin: 0 auto; line-height: 50px; text-align: center;">
|
| 29 |
+
<span style="font-size: 28px;">{{ icon }}</span>
|
| 30 |
+
</div>
|
| 31 |
+
</td>
|
| 32 |
+
<td style="vertical-align: middle; padding-left: 20px;">
|
| 33 |
+
<h2 style="margin: 0; color: #1f2937; font-size: 24px; font-weight: 700;">{{ result }} Result</h2>
|
| 34 |
+
<p style="margin: 5px 0 0; color: #4b5563; font-size: 14px;">Test completed on {{ test_date }}</p>
|
| 35 |
+
</td>
|
| 36 |
+
</tr>
|
| 37 |
+
</table>
|
| 38 |
+
</td>
|
| 39 |
+
</tr>
|
| 40 |
+
|
| 41 |
+
<!-- Greeting -->
|
| 42 |
+
<tr>
|
| 43 |
+
<td style="padding: 30px 30px 20px;">
|
| 44 |
+
<p style="margin: 0; color: #1f2937; font-size: 16px; line-height: 1.6;">Dear <strong>{{ patient_name }}</strong>,</p>
|
| 45 |
+
<p style="margin: 15px 0 0; color: #4b5563; font-size: 15px; line-height: 1.7;">{{ message }}</p>
|
| 46 |
+
</td>
|
| 47 |
+
</tr>
|
| 48 |
+
|
| 49 |
+
<!-- Test Details -->
|
| 50 |
+
<tr>
|
| 51 |
+
<td style="padding: 20px 30px;">
|
| 52 |
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #f9fafb; border-radius: 12px; overflow: hidden;">
|
| 53 |
+
<tr>
|
| 54 |
+
<td style="padding: 20px;">
|
| 55 |
+
<h3 style="margin: 0 0 15px; color: #1f2937; font-size: 18px; font-weight: 600;">Test Summary</h3>
|
| 56 |
+
|
| 57 |
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin-bottom: 12px;">
|
| 58 |
+
<tr>
|
| 59 |
+
<td style="padding: 8px 0;"><span style="color: #6b7280; font-size: 14px;">Result:</span></td>
|
| 60 |
+
<td align="right" style="padding: 8px 0;">
|
| 61 |
+
<span style="background-color: {{ result_badge_bg }}; color: {{ result_badge_color }}; padding: 4px 12px; border-radius: 20px; font-size: 14px; font-weight: 600;">{{ result }}</span>
|
| 62 |
+
</td>
|
| 63 |
+
</tr>
|
| 64 |
+
</table>
|
| 65 |
+
|
| 66 |
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin-bottom: 12px;">
|
| 67 |
+
<tr>
|
| 68 |
+
<td style="padding: 8px 0;"><span style="color: #6b7280; font-size: 14px;">Confidence Score:</span></td>
|
| 69 |
+
<td align="right" style="padding: 8px 0;">
|
| 70 |
+
<span style="color: #1f2937; font-size: 18px; font-weight: 700;">{{ confidence }}%</span>
|
| 71 |
+
</td>
|
| 72 |
+
</tr>
|
| 73 |
+
</table>
|
| 74 |
+
|
| 75 |
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
| 76 |
+
<tr>
|
| 77 |
+
<td style="padding: 8px 0;"><span style="color: #6b7280; font-size: 14px;">Risk Level:</span></td>
|
| 78 |
+
<td align="right" style="padding: 8px 0;">
|
| 79 |
+
<span style="background-color: {{ risk_badge_bg }}; color: {{ risk_badge_color }}; padding: 4px 12px; border-radius: 20px; font-size: 14px; font-weight: 600;">{{ risk_level }}</span>
|
| 80 |
+
</td>
|
| 81 |
+
</tr>
|
| 82 |
+
</table>
|
| 83 |
+
</td>
|
| 84 |
+
</tr>
|
| 85 |
+
</table>
|
| 86 |
+
</td>
|
| 87 |
+
</tr>
|
| 88 |
+
|
| 89 |
+
<!-- Next Steps -->
|
| 90 |
+
<tr>
|
| 91 |
+
<td style="padding: 20px 30px;">
|
| 92 |
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: {{ recommendation_bg }}; border-left: 4px solid {{ recommendation_border }}; padding: 20px; border-radius: 8px;">
|
| 93 |
+
<tr>
|
| 94 |
+
<td>
|
| 95 |
+
<h4 style="margin: 0 0 12px; color: #1f2937; font-size: 16px; font-weight: 600;">{{ recommendation_title }}</h4>
|
| 96 |
+
{{ recommendations|safe }}
|
| 97 |
+
</td>
|
| 98 |
+
</tr>
|
| 99 |
+
</table>
|
| 100 |
+
</td>
|
| 101 |
+
</tr>
|
| 102 |
+
|
| 103 |
+
<!-- Attachment Notice -->
|
| 104 |
+
<tr>
|
| 105 |
+
<td style="padding: 20px 30px;">
|
| 106 |
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #f3f4f6; border-radius: 8px; padding: 16px;">
|
| 107 |
+
<tr>
|
| 108 |
+
<td width="40" style="vertical-align: middle;"><span style="font-size: 24px;">📎</span></td>
|
| 109 |
+
<td style="vertical-align: middle; padding-left: 12px;">
|
| 110 |
+
<p style="margin: 0; color: #1f2937; font-size: 14px; font-weight: 600;">Detailed Report Attached</p>
|
| 111 |
+
<p style="margin: 4px 0 0; color: #6b7280; font-size: 13px;">RespireX_Report_{{ test_id }}.pdf</p>
|
| 112 |
+
</td>
|
| 113 |
+
</tr>
|
| 114 |
+
</table>
|
| 115 |
+
</td>
|
| 116 |
+
</tr>
|
| 117 |
+
|
| 118 |
+
<!-- Disclaimer -->
|
| 119 |
+
<tr>
|
| 120 |
+
<td style="padding: 20px 30px 30px;">
|
| 121 |
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #fef3c7; border-radius: 8px; padding: 16px; border-left: 4px solid #f59e0b;">
|
| 122 |
+
<tr>
|
| 123 |
+
<td>
|
| 124 |
+
<p style="margin: 0; color: #92400e; font-size: 13px; line-height: 1.6;">
|
| 125 |
+
<strong>⚠️ Important Disclaimer:</strong> This is an AI-powered screening tool and does NOT replace professional medical diagnosis. Please consult with a qualified healthcare provider for proper diagnosis and treatment.
|
| 126 |
+
</p>
|
| 127 |
+
</td>
|
| 128 |
+
</tr>
|
| 129 |
+
</table>
|
| 130 |
+
</td>
|
| 131 |
+
</tr>
|
| 132 |
+
|
| 133 |
+
<!-- Footer -->
|
| 134 |
+
<tr>
|
| 135 |
+
<td style="background-color: #1f2937; padding: 30px; text-align: center;">
|
| 136 |
+
<p style="margin: 0 0 8px; color: #d1d5db; font-size: 14px; font-weight: 600;">RespireX - AI-Powered Healthcare</p>
|
| 137 |
+
<p style="margin: 0 0 15px; color: #9ca3af; font-size: 12px;">Making healthcare accessible for everyone</p>
|
| 138 |
+
<div style="border-top: 1px solid #374151; padding-top: 15px; margin-top: 15px;">
|
| 139 |
+
<p style="margin: 0; color: #9ca3af; font-size: 11px;">© 2025 RespireX. All rights reserved. | By Team BitBash</p>
|
| 140 |
+
<p style="margin: 8px 0 0; color: #6b7280; font-size: 11px;">Part of Atmanirbhar Bharat Mission</p>
|
| 141 |
+
</div>
|
| 142 |
+
</td>
|
| 143 |
+
</tr>
|
| 144 |
+
|
| 145 |
+
</table>
|
| 146 |
+
</td>
|
| 147 |
+
</tr>
|
| 148 |
+
</table>
|
| 149 |
+
</body>
|
| 150 |
+
</html>
|
api/urls.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.urls import path
|
| 2 |
+
from . import views
|
| 3 |
+
|
| 4 |
+
urlpatterns = [
|
| 5 |
+
# ─── Auth ───
|
| 6 |
+
path('api/signup/', views.signup, name='signup'),
|
| 7 |
+
path('api/login/', views.login, name='login'),
|
| 8 |
+
|
| 9 |
+
# ─── Patient ───
|
| 10 |
+
path('api/patient/dashboard/', views.patient_dashboard, name='patient-dashboard'),
|
| 11 |
+
path('api/patient/upload-xray/', views.upload_xray, name='upload-xray'),
|
| 12 |
+
path('api/patient/symptom-test/', views.symptom_test, name='symptom-test'),
|
| 13 |
+
|
| 14 |
+
# ─── Doctor (Direct Access — no auth in prototype) ───
|
| 15 |
+
path('api/doctor/dashboard/', views.doctor_dashboard, name='doctor-dashboard'),
|
| 16 |
+
|
| 17 |
+
# ─── REMOVED for prototype ───
|
| 18 |
+
# path('api/report/...', ...) ← all report/PDF routes removed
|
| 19 |
+
]
|
api/views.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.shortcuts import render
|
| 2 |
+
from django.http import JsonResponse
|
| 3 |
+
from rest_framework.decorators import api_view, parser_classes
|
| 4 |
+
from rest_framework.parsers import MultiPartParser
|
| 5 |
+
from rest_framework.response import Response
|
| 6 |
+
from rest_framework import status
|
| 7 |
+
|
| 8 |
+
from .models import UserProfile, TestResult
|
| 9 |
+
from .serializers import UserProfileSerializer, TestResultSerializer
|
| 10 |
+
from .ml_engine import predict_xray as predict
|
| 11 |
+
from .email_utils import send_test_result_email
|
| 12 |
+
from .authentication import authenticate_user, get_user_from_token
|
| 13 |
+
# pdf_generator import REMOVED for prototype
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
import uuid
|
| 17 |
+
from datetime import datetime
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# ─── AUTH ENDPOINTS ─────────────────────────────────────────────
|
| 21 |
+
|
| 22 |
+
@api_view(['POST'])
|
| 23 |
+
def signup(request):
|
| 24 |
+
"""Patient / Doctor signup"""
|
| 25 |
+
serializer = UserProfileSerializer(data=request.data)
|
| 26 |
+
if serializer.is_valid():
|
| 27 |
+
serializer.save()
|
| 28 |
+
return Response({"message": "Signup successful", "user": serializer.data}, status=status.HTTP_201_CREATED)
|
| 29 |
+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@api_view(['POST'])
|
| 33 |
+
def login(request):
|
| 34 |
+
"""Patient login (Doctor gets direct access — no login needed)"""
|
| 35 |
+
email = request.data.get('email')
|
| 36 |
+
password = request.data.get('password')
|
| 37 |
+
|
| 38 |
+
token = authenticate_user(email, password)
|
| 39 |
+
if token:
|
| 40 |
+
return Response({"token": token, "message": "Login successful"}, status=status.HTTP_200_OK)
|
| 41 |
+
return Response({"error": "Invalid credentials"}, status=status.HTTP_401_UNAUTHORIZED)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# ─── PATIENT ENDPOINTS ──────────────────────────────────────────
|
| 45 |
+
|
| 46 |
+
@api_view(['GET'])
|
| 47 |
+
def patient_dashboard(request):
|
| 48 |
+
"""Patient home — returns their latest test result (if any)"""
|
| 49 |
+
user = get_user_from_token(request)
|
| 50 |
+
if not user:
|
| 51 |
+
return Response({"error": "Unauthorized"}, status=status.HTTP_401_UNAUTHORIZED)
|
| 52 |
+
|
| 53 |
+
latest = TestResult.objects.filter(patient=user).order_by('-created_at').first()
|
| 54 |
+
if latest:
|
| 55 |
+
return Response(TestResultSerializer(latest).data)
|
| 56 |
+
return Response({"message": "No tests yet"})
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@api_view(['POST'], )
|
| 60 |
+
@parser_classes([MultiPartParser])
|
| 61 |
+
def upload_xray(request):
|
| 62 |
+
"""Receive chest X-ray image, run ML prediction, save result"""
|
| 63 |
+
user = get_user_from_token(request)
|
| 64 |
+
if not user:
|
| 65 |
+
return Response({"error": "Unauthorized"}, status=status.HTTP_401_UNAUTHORIZED)
|
| 66 |
+
|
| 67 |
+
image = request.FILES.get('xray')
|
| 68 |
+
if not image:
|
| 69 |
+
return Response({"error": "No image provided"}, status=status.HTTP_400_BAD_REQUEST)
|
| 70 |
+
|
| 71 |
+
# Save image
|
| 72 |
+
filename = f"{uuid.uuid4()}_{image.name}"
|
| 73 |
+
upload_path = os.path.join('uploads', filename)
|
| 74 |
+
with open(upload_path, 'wb') as f:
|
| 75 |
+
for chunk in image.chunks():
|
| 76 |
+
f.write(chunk)
|
| 77 |
+
|
| 78 |
+
# ML prediction
|
| 79 |
+
prediction = predict(upload_path) # returns dict like { "label": "Normal", "confidence": 0.92 }
|
| 80 |
+
|
| 81 |
+
# Save test result
|
| 82 |
+
result = TestResult.objects.create(
|
| 83 |
+
patient=user,
|
| 84 |
+
image_path=upload_path,
|
| 85 |
+
label=prediction['label'],
|
| 86 |
+
confidence=prediction['confidence'],
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
# Optionally send email (no PDF attachment in prototype)
|
| 90 |
+
try:
|
| 91 |
+
send_test_result_email(user.email, prediction)
|
| 92 |
+
except Exception:
|
| 93 |
+
pass # non-blocking
|
| 94 |
+
|
| 95 |
+
return Response({
|
| 96 |
+
"id": result.id,
|
| 97 |
+
"label": prediction['label'],
|
| 98 |
+
"confidence": prediction['confidence'],
|
| 99 |
+
"created_at": result.created_at.isoformat(),
|
| 100 |
+
}, status=status.HTTP_201_CREATED)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
@api_view(['POST'])
|
| 104 |
+
def symptom_test(request):
|
| 105 |
+
"""Simple symptom quiz — returns a basic risk score"""
|
| 106 |
+
answers = request.data.get('answers', [])
|
| 107 |
+
# Lightweight scoring (no ML model needed)
|
| 108 |
+
score = sum(answers) # assumes 0/1 answers
|
| 109 |
+
total = len(answers) if answers else 1
|
| 110 |
+
|
| 111 |
+
if score / total >= 0.6:
|
| 112 |
+
risk = "High"
|
| 113 |
+
elif score / total >= 0.3:
|
| 114 |
+
risk = "Moderate"
|
| 115 |
+
else:
|
| 116 |
+
risk = "Low"
|
| 117 |
+
|
| 118 |
+
return Response({"risk_level": risk, "score": score, "total": total})
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
# ─── DOCTOR ENDPOINTS (Direct Access — no auth required for prototype) ───
|
| 122 |
+
|
| 123 |
+
@api_view(['GET'])
|
| 124 |
+
def doctor_dashboard(request):
|
| 125 |
+
"""
|
| 126 |
+
Doctor direct-access endpoint.
|
| 127 |
+
Returns dummy test data for prototype.
|
| 128 |
+
No authentication required.
|
| 129 |
+
"""
|
| 130 |
+
dummy_tests = [
|
| 131 |
+
{
|
| 132 |
+
"id": 1,
|
| 133 |
+
"test_name": "Test 1",
|
| 134 |
+
"patient_name": "Rahul Sharma",
|
| 135 |
+
"patient_email": "rahul.sharma@email.com",
|
| 136 |
+
"label": "Pneumonia",
|
| 137 |
+
"confidence": 0.87,
|
| 138 |
+
"date": "2026-01-28",
|
| 139 |
+
"status": "Pending Review",
|
| 140 |
+
},
|
| 141 |
+
{
|
| 142 |
+
"id": 2,
|
| 143 |
+
"test_name": "Test 2",
|
| 144 |
+
"patient_name": "Priya Mehta",
|
| 145 |
+
"patient_email": "priya.mehta@email.com",
|
| 146 |
+
"label": "Normal",
|
| 147 |
+
"confidence": 0.94,
|
| 148 |
+
"date": "2026-01-30",
|
| 149 |
+
"status": "Pending Review",
|
| 150 |
+
},
|
| 151 |
+
]
|
| 152 |
+
return Response({"tests": dummy_tests}, status=status.HTTP_200_OK)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
# ─── REPORT GENERATION — REMOVED FOR PROTOTYPE ─────────────────
|
| 156 |
+
# All /api/report/* endpoints and pdf_generator usage have been removed.
|
| 157 |
+
# In the full version, this section handled PDF generation via pdf_generator.py.
|
manage.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""Django's command-line utility for administrative tasks."""
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def main():
|
| 8 |
+
"""Run administrative tasks."""
|
| 9 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'respirex_backend.settings')
|
| 10 |
+
try:
|
| 11 |
+
from django.core.management import execute_from_command_line
|
| 12 |
+
except ImportError as exc:
|
| 13 |
+
raise ImportError(
|
| 14 |
+
"Couldn't import Django. Are you sure it's installed and "
|
| 15 |
+
"available on your PYTHONPATH environment variable? Did you "
|
| 16 |
+
"forget to activate a virtual environment?"
|
| 17 |
+
) from exc
|
| 18 |
+
execute_from_command_line(sys.argv)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
if __name__ == '__main__':
|
| 22 |
+
main()
|
model/efficientnet_b0.h5
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c3e49b621fb0eb04d4a20920949f6cd56def85795bbb5fd8c08425e17acd9208
|
| 3 |
+
size 78245936
|
requirements.txt
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
absl-py==2.3.1
|
| 2 |
+
annotated-types==0.7.0
|
| 3 |
+
anyio==4.12.1
|
| 4 |
+
arabic-reshaper==3.0.0
|
| 5 |
+
asgiref==3.11.0
|
| 6 |
+
asn1crypto==1.5.1
|
| 7 |
+
astunparse==1.6.3
|
| 8 |
+
boto3==1.42.34
|
| 9 |
+
botocore==1.42.34
|
| 10 |
+
cachetools==6.2.5
|
| 11 |
+
certifi==2026.1.4
|
| 12 |
+
cffi==2.0.0
|
| 13 |
+
charset-normalizer==3.4.4
|
| 14 |
+
click==8.3.1
|
| 15 |
+
contourpy==1.3.3
|
| 16 |
+
cryptography==46.0.3
|
| 17 |
+
cssselect2==0.8.0
|
| 18 |
+
cycler==0.12.1
|
| 19 |
+
deprecation==2.1.0
|
| 20 |
+
dj-database-url==3.1.0
|
| 21 |
+
Django==5.2.10
|
| 22 |
+
django-cors-headers==4.9.0
|
| 23 |
+
django-ses==4.6.0
|
| 24 |
+
djangorestframework==3.16.1
|
| 25 |
+
flatbuffers==25.12.19
|
| 26 |
+
fonttools==4.61.1
|
| 27 |
+
freetype-py==2.5.1
|
| 28 |
+
fsspec==2026.1.0
|
| 29 |
+
gast==0.7.0
|
| 30 |
+
google-pasta==0.2.0
|
| 31 |
+
grpcio==1.76.0
|
| 32 |
+
gunicorn==24.1.1
|
| 33 |
+
h11==0.16.0
|
| 34 |
+
h2==4.3.0
|
| 35 |
+
h5py==3.15.1
|
| 36 |
+
hpack==4.1.0
|
| 37 |
+
html5lib==1.1
|
| 38 |
+
httpcore==1.0.9
|
| 39 |
+
httpx==0.28.1
|
| 40 |
+
hyperframe==6.1.0
|
| 41 |
+
idna==3.11
|
| 42 |
+
jmespath==1.1.0
|
| 43 |
+
keras==3.13.1
|
| 44 |
+
kiwisolver==1.4.9
|
| 45 |
+
libclang==18.1.1
|
| 46 |
+
lxml==6.0.2
|
| 47 |
+
Markdown==3.10.1
|
| 48 |
+
markdown-it-py==4.0.0
|
| 49 |
+
MarkupSafe==3.0.3
|
| 50 |
+
matplotlib==3.10.8
|
| 51 |
+
mdurl==0.1.2
|
| 52 |
+
ml_dtypes==0.5.4
|
| 53 |
+
mmh3==5.2.0
|
| 54 |
+
multidict==6.7.0
|
| 55 |
+
namex==0.1.0
|
| 56 |
+
numpy==2.4.1
|
| 57 |
+
opt_einsum==3.4.0
|
| 58 |
+
optree==0.18.0
|
| 59 |
+
oscrypto==1.3.0
|
| 60 |
+
packaging==26.0
|
| 61 |
+
pillow==12.1.0
|
| 62 |
+
postgrest==2.27.2
|
| 63 |
+
propcache==0.4.1
|
| 64 |
+
protobuf==6.33.4
|
| 65 |
+
psycopg2-binary==2.9.11
|
| 66 |
+
pycairo==1.29.0
|
| 67 |
+
pycparser==3.0
|
| 68 |
+
pydantic==2.12.5
|
| 69 |
+
pydantic_core==2.41.5
|
| 70 |
+
Pygments==2.19.2
|
| 71 |
+
pyHanko==0.32.0
|
| 72 |
+
pyhanko-certvalidator==0.29.0
|
| 73 |
+
pyiceberg==0.10.0
|
| 74 |
+
PyJWT==2.10.1
|
| 75 |
+
pyparsing==3.3.2
|
| 76 |
+
pypdf==6.6.1
|
| 77 |
+
pyroaring==1.0.3
|
| 78 |
+
python-bidi==0.6.7
|
| 79 |
+
python-dateutil==2.9.0.post0
|
| 80 |
+
python-dotenv==1.2.1
|
| 81 |
+
python-http-client==3.3.7
|
| 82 |
+
PyYAML==6.0.3
|
| 83 |
+
realtime==2.27.2
|
| 84 |
+
reportlab==4.4.9
|
| 85 |
+
requests==2.32.5
|
| 86 |
+
rich==14.3.1
|
| 87 |
+
rlPyCairo==0.4.0
|
| 88 |
+
s3transfer==0.16.0
|
| 89 |
+
sendgrid==6.12.5
|
| 90 |
+
six==1.17.0
|
| 91 |
+
sortedcontainers==2.4.0
|
| 92 |
+
sqlparse==0.5.5
|
| 93 |
+
storage3==2.27.2
|
| 94 |
+
StrEnum==0.4.15
|
| 95 |
+
strictyaml==1.7.3
|
| 96 |
+
supabase==2.27.2
|
| 97 |
+
supabase-auth==2.27.2
|
| 98 |
+
supabase-functions==2.27.2
|
| 99 |
+
svglib==1.6.0
|
| 100 |
+
tenacity==9.1.2
|
| 101 |
+
tensorboard==2.20.0
|
| 102 |
+
tensorboard-data-server==0.7.2
|
| 103 |
+
tensorflow==2.20.0
|
| 104 |
+
termcolor==3.3.0
|
| 105 |
+
tinycss2==1.5.1
|
| 106 |
+
typing-inspection==0.4.2
|
| 107 |
+
typing_extensions==4.15.0
|
| 108 |
+
tzlocal==5.3.1
|
| 109 |
+
uritools==6.0.1
|
| 110 |
+
urllib3==2.6.3
|
| 111 |
+
webencodings==0.5.1
|
| 112 |
+
websockets==15.0.1
|
| 113 |
+
Werkzeug==3.1.5
|
| 114 |
+
whitenoise==6.11.0
|
| 115 |
+
wrapt==2.0.1
|
| 116 |
+
xhtml2pdf==0.2.17
|
| 117 |
+
yarl==1.22.0
|
respirex_backend/__init__.py
ADDED
|
File without changes
|
respirex_backend/asgi.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ASGI config for respirex_backend project.
|
| 3 |
+
|
| 4 |
+
It exposes the ASGI callable as a module-level variable named ``application``.
|
| 5 |
+
|
| 6 |
+
For more information on this file, see
|
| 7 |
+
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
from django.core.asgi import get_asgi_application
|
| 13 |
+
|
| 14 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'respirex_backend.settings')
|
| 15 |
+
|
| 16 |
+
application = get_asgi_application()
|
respirex_backend/settings.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import dj_database_url
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
| 9 |
+
|
| 10 |
+
SECRET_KEY = os.getenv('SECRET_KEY', 'unsafe-secret-key')
|
| 11 |
+
DEBUG = os.getenv('DEBUG', 'False') == 'True'
|
| 12 |
+
|
| 13 |
+
ALLOWED_HOSTS = ['*']
|
| 14 |
+
|
| 15 |
+
# Application definition
|
| 16 |
+
INSTALLED_APPS = [
|
| 17 |
+
'django.contrib.admin',
|
| 18 |
+
'django.contrib.auth',
|
| 19 |
+
'django.contrib.contenttypes',
|
| 20 |
+
'django.contrib.sessions',
|
| 21 |
+
'django.contrib.messages',
|
| 22 |
+
'django.contrib.staticfiles',
|
| 23 |
+
|
| 24 |
+
# Third party
|
| 25 |
+
'rest_framework',
|
| 26 |
+
'corsheaders',
|
| 27 |
+
|
| 28 |
+
# Local
|
| 29 |
+
'api',
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
MIDDLEWARE = [
|
| 33 |
+
'corsheaders.middleware.CorsMiddleware', # Must be at top
|
| 34 |
+
'django.middleware.security.SecurityMiddleware',
|
| 35 |
+
"whitenoise.middleware.WhiteNoiseMiddleware",
|
| 36 |
+
'django.contrib.sessions.middleware.SessionMiddleware',
|
| 37 |
+
'django.middleware.common.CommonMiddleware',
|
| 38 |
+
'django.middleware.csrf.CsrfViewMiddleware',
|
| 39 |
+
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
| 40 |
+
'django.contrib.messages.middleware.MessageMiddleware',
|
| 41 |
+
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
ROOT_URLCONF = 'respirex_backend.urls'
|
| 45 |
+
|
| 46 |
+
TEMPLATES = [
|
| 47 |
+
{
|
| 48 |
+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
| 49 |
+
'DIRS': [],
|
| 50 |
+
'APP_DIRS': True,
|
| 51 |
+
'OPTIONS': {
|
| 52 |
+
'context_processors': [
|
| 53 |
+
'django.template.context_processors.debug',
|
| 54 |
+
'django.template.context_processors.request',
|
| 55 |
+
'django.contrib.auth.context_processors.auth',
|
| 56 |
+
'django.contrib.messages.context_processors.messages',
|
| 57 |
+
],
|
| 58 |
+
},
|
| 59 |
+
},
|
| 60 |
+
]
|
| 61 |
+
|
| 62 |
+
WSGI_APPLICATION = 'respirex_backend.wsgi.application'
|
| 63 |
+
|
| 64 |
+
# Database
|
| 65 |
+
DATABASES = {
|
| 66 |
+
'default': dj_database_url.config(
|
| 67 |
+
default='sqlite:///db.sqlite3',
|
| 68 |
+
conn_max_age=600
|
| 69 |
+
)
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
# Password validation
|
| 73 |
+
AUTH_PASSWORD_VALIDATORS = [
|
| 74 |
+
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
|
| 75 |
+
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
|
| 76 |
+
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
|
| 77 |
+
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
|
| 78 |
+
]
|
| 79 |
+
|
| 80 |
+
LANGUAGE_CODE = 'en-us'
|
| 81 |
+
TIME_ZONE = 'UTC'
|
| 82 |
+
USE_I18N = True
|
| 83 |
+
USE_TZ = True
|
| 84 |
+
|
| 85 |
+
STATIC_URL = 'static/'
|
| 86 |
+
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
| 87 |
+
|
| 88 |
+
# CORS Config (Allow React Frontend)
|
| 89 |
+
CORS_ALLOW_ALL_ORIGINS = True # For development
|
| 90 |
+
# CORS_ALLOWED_ORIGINS = ["http://localhost:5173"] # Use this for production
|
| 91 |
+
|
| 92 |
+
# Supabase Settings
|
| 93 |
+
SUPABASE_URL = os.getenv('SUPABASE_URL')
|
| 94 |
+
SUPABASE_KEY = os.getenv('SUPABASE_KEY')
|
| 95 |
+
|
| 96 |
+
# DRF Config
|
| 97 |
+
REST_FRAMEWORK = {
|
| 98 |
+
'DEFAULT_AUTHENTICATION_CLASSES': [
|
| 99 |
+
'api.authentication.SupabaseAuthentication',
|
| 100 |
+
],
|
| 101 |
+
'DEFAULT_PERMISSION_CLASSES': [
|
| 102 |
+
'rest_framework.permissions.IsAuthenticated',
|
| 103 |
+
]
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
from corsheaders.defaults import default_headers
|
| 107 |
+
|
| 108 |
+
CORS_ALLOW_HEADERS = list(default_headers) + [
|
| 109 |
+
'authorization',
|
| 110 |
+
'x-csrftoken',
|
| 111 |
+
'x-requested-with',
|
| 112 |
+
]
|
| 113 |
+
ALLOWED_HOSTS = ['*'] # You can replace '*' with your specific Render URL later
|
| 114 |
+
STATIC_URL = 'static/'
|
| 115 |
+
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
|
| 116 |
+
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
| 117 |
+
CORS_ALLOW_ALL_ORIGINS = True # For simplicity. Or list your frontend Render URL here later.
|
| 118 |
+
|
| 119 |
+
# HUGGING FACE SETTINGS
|
| 120 |
+
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
| 121 |
+
CSRF_TRUSTED_ORIGINS = ['https://*.hf.space']
|
| 122 |
+
|
| 123 |
+
# Email Configuration
|
| 124 |
+
EMAIL_BACKEND = 'django_ses.SESBackend'
|
| 125 |
+
AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
|
| 126 |
+
AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY')
|
| 127 |
+
AWS_SES_REGION_NAME = os.getenv('AWS_SES_REGION', 'us-east-1')
|
| 128 |
+
AWS_SES_REGION_ENDPOINT = f'email.{AWS_SES_REGION_NAME}.amazonaws.com'
|
| 129 |
+
|
| 130 |
+
# Fallback to console for development
|
| 131 |
+
if DEBUG:
|
| 132 |
+
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
| 133 |
+
|
| 134 |
+
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'noreply@respirex.health')
|
respirex_backend/urls.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.contrib import admin
|
| 2 |
+
from django.urls import path, include
|
| 3 |
+
|
| 4 |
+
urlpatterns = [
|
| 5 |
+
path('admin/', admin.site.urls),
|
| 6 |
+
path('api/', include('api.urls')),
|
| 7 |
+
]
|
respirex_backend/wsgi.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
WSGI config for respirex_backend project.
|
| 3 |
+
|
| 4 |
+
It exposes the WSGI callable as a module-level variable named ``application``.
|
| 5 |
+
|
| 6 |
+
For more information on this file, see
|
| 7 |
+
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
from django.core.wsgi import get_wsgi_application
|
| 13 |
+
|
| 14 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'respirex_backend.settings')
|
| 15 |
+
|
| 16 |
+
application = get_wsgi_application()
|