CineDev commited on
Commit
718f018
·
0 Parent(s):

All files added

Browse files
.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()