Autosave: 20260219-045105

This commit is contained in:
Flatlogic Bot 2026-02-19 04:51:05 +00:00
parent e38c53b0c9
commit fd84fa729b
27 changed files with 947 additions and 86 deletions

View File

@ -15,14 +15,10 @@ class UserRegisterForm(UserCreationForm):
fields = ['username', 'email']
class UserUpdateForm(forms.ModelForm):
email = forms.EmailField()
class Meta:
model = User
fields = ['username', 'email', 'first_name', 'last_name']
fields = ['first_name', 'last_name']
widgets = {
'username': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
}
@ -30,11 +26,12 @@ class UserUpdateForm(forms.ModelForm):
class ProfileUpdateForm(forms.ModelForm):
class Meta:
model = UserProfile
fields = ['bio', 'location', 'phone', 'blood_group', 'profile_pic']
fields = ['bio', 'location', 'phone', 'birth_date', 'blood_group', 'profile_pic']
widgets = {
'bio': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'location': forms.TextInput(attrs={'class': 'form-control'}),
'phone': forms.TextInput(attrs={'class': 'form-control'}),
'birth_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'blood_group': forms.Select(attrs={'class': 'form-control'}, choices=BLOOD_GROUPS),
'profile_pic': forms.FileInput(attrs={'class': 'form-control'}),
}

View File

@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2026-02-18 13:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0014_message'),
]
operations = [
migrations.AddField(
model_name='userprofile',
name='last_location_update',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='userprofile',
name='latitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
migrations.AddField(
model_name='userprofile',
name='longitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 5.2.7 on 2026-02-18 14:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0015_userprofile_last_location_update_and_more'),
]
operations = [
migrations.CreateModel(
name='Badge',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('description', models.CharField(max_length=255)),
('icon_class', models.CharField(default='fas fa-medal', max_length=50)),
],
),
migrations.AddField(
model_name='userprofile',
name='badges',
field=models.ManyToManyField(blank=True, related_name='users', to='core.badge'),
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 5.2.7 on 2026-02-18 15:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0016_badge_userprofile_badges'),
]
operations = [
migrations.AddField(
model_name='message',
name='attachment',
field=models.FileField(blank=True, null=True, upload_to='chat_attachments/'),
),
migrations.AddField(
model_name='message',
name='message_type',
field=models.CharField(choices=[('text', 'Text'), ('image', 'Image'), ('video', 'Video'), ('file', 'File'), ('sticker', 'Sticker')], default='text', max_length=10),
),
migrations.AddField(
model_name='message',
name='sticker_id',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='message',
name='content',
field=models.TextField(blank=True, null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2026-02-19 04:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0017_message_attachment_message_message_type_and_more'),
]
operations = [
migrations.AddField(
model_name='bloodrequest',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='blood_requests/'),
),
]

View File

@ -0,0 +1,32 @@
# Generated by Django 5.2.7 on 2026-02-19 04:45
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0018_bloodrequest_image'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='HealthReport',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('hospital_name', models.CharField(blank=True, max_length=255, null=True)),
('report_file', models.FileField(upload_to='health_reports/')),
('description', models.TextField(blank=True, null=True)),
('report_date', models.DateField(default=django.utils.timezone.now)),
('next_test_date', models.DateField(blank=True, null=True)),
('allow_notifications', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='health_reports', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -11,14 +11,26 @@ BLOOD_GROUPS = [
('AB+', 'AB+'), ('AB-', 'AB-'),
]
class Badge(models.Model):
name = models.CharField(max_length=50)
description = models.CharField(max_length=255)
icon_class = models.CharField(max_length=50, default='fas fa-medal') # FontAwesome class
def __str__(self):
return self.name
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
bio = models.TextField(max_length=500, blank=True)
location = models.CharField(max_length=100, blank=True)
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
last_location_update = models.DateTimeField(null=True, blank=True)
birth_date = models.DateField(null=True, blank=True)
phone = models.CharField(max_length=20, blank=True)
blood_group = models.CharField(max_length=5, choices=BLOOD_GROUPS, blank=True)
profile_pic = models.ImageField(upload_to='profile_pics', blank=True, null=True)
badges = models.ManyToManyField(Badge, blank=True, related_name='users')
def __str__(self):
return self.user.username
@ -40,6 +52,8 @@ def sync_donor_profile(sender, instance, **kwargs):
donor.name = f"{instance.user.first_name} {instance.user.last_name}".strip() or instance.user.username
donor.blood_group = instance.blood_group
donor.location = instance.location
donor.latitude = instance.latitude
donor.longitude = instance.longitude
donor.phone = instance.phone
donor.save()
@ -103,6 +117,7 @@ class BloodRequest(models.Model):
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
contact_number = models.CharField(max_length=20)
image = models.ImageField(upload_to='blood_requests/', null=True, blank=True)
required_date = models.DateField(default=timezone.now)
status = models.CharField(max_length=20, default='Active')
created_at = models.DateTimeField(auto_now_add=True)
@ -150,11 +165,35 @@ class Notification(models.Model):
return f"Notification for {self.user.username}: {self.message[:20]}..."
class Message(models.Model):
MESSAGE_TYPES = [
('text', 'Text'),
('image', 'Image'),
('video', 'Video'),
('file', 'File'),
('sticker', 'Sticker'),
]
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_messages')
receiver = models.ForeignKey(User, on_delete=models.CASCADE, related_name='received_messages')
content = models.TextField()
content = models.TextField(blank=True, null=True)
attachment = models.FileField(upload_to='chat_attachments/', blank=True, null=True)
message_type = models.CharField(max_length=10, choices=MESSAGE_TYPES, default='text')
sticker_id = models.CharField(max_length=50, blank=True, null=True)
timestamp = models.DateTimeField(auto_now_add=True)
is_read = models.BooleanField(default=False)
def __str__(self):
return f"From {self.sender.username} to {self.receiver.username} at {self.timestamp}"
class HealthReport(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='health_reports')
title = models.CharField(max_length=200)
hospital_name = models.CharField(max_length=255, blank=True, null=True)
report_file = models.FileField(upload_to='health_reports/')
description = models.TextField(blank=True, null=True)
report_date = models.DateField(default=timezone.now)
next_test_date = models.DateField(blank=True, null=True)
allow_notifications = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Report: {self.title} for {self.user.username}"

View File

@ -281,6 +281,7 @@
<li class="{% if request.resolver_match.url_name == 'vaccination_info' %}active{% endif %}"><a href="{% url 'vaccination_info' %}"><i class="bi bi-shield-check"></i> {% trans "Vaccination" %}</a></li>
{% if user.is_authenticated %}
<li class="{% if 'vaccination_dashboard' in request.resolver_match.url_name or 'add_vaccination' in request.resolver_match.url_name %}active{% endif %}"><a href="{% url 'vaccination_dashboard' %}"><i class="bi bi-journal-check"></i> {% trans "My Records" %}</a></li>
<li class="{% if 'health_report' in request.resolver_match.url_name %}active{% endif %}"><a href="{% url 'health_report_list' %}"><i class="bi bi-file-medical-fill text-danger"></i> {% trans "Health Reports" %}</a></li>
{% endif %}
<li><a href="/admin/"><i class="bi bi-gear-fill"></i> {% trans "Settings" %}</a></li>
</ul>
@ -511,5 +512,46 @@
</script>
{% block scripts %}{% endblock %}
{% if user.is_authenticated %}
<script>
(function() {
function sendLocation(position) {
const lat = position.coords.latitude;
const lng = position.coords.longitude;
// Update local display if it exists
const locText = document.getElementById('location-text');
if (locText && locText.innerText === "Detect Location") {
locText.innerText = lat.toFixed(4) + ", " + lng.toFixed(4);
}
fetch("{% url 'update_location' %}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": "{{ csrf_token }}"
},
body: JSON.stringify({
latitude: lat,
longitude: lng
})
}).catch(err => console.debug("Location update deferred"));
}
if ("geolocation" in navigator) {
// Live tracking
navigator.geolocation.watchPosition(sendLocation,
(err) => console.debug("Position update skipped"),
{
enableHighAccuracy: true,
maximumAge: 60000, // 1 minute
timeout: 30000
}
);
}
})();
</script>
{% endif %}
</body>
</html>

View File

@ -47,7 +47,14 @@
</div>
</div>
<h5 class="mb-1 fw-bold text-dark">{{ req.patient_name }}</h5>
<p class="text-secondary small mb-3"><i class="bi bi-hospital me-1"></i> {{ req.hospital }}</p>
<p class="text-secondary small mb-2"><i class="bi bi-hospital me-1"></i> {{ req.hospital }}</p>
{% if req.image %}
<div class="mb-3">
<img src="{{ req.image.url }}" class="img-fluid rounded border shadow-sm" alt="Patient/Prescription" style="max-height: 150px; width: 100%; object-fit: cover; cursor: pointer;" onclick="window.open(this.src)">
</div>
{% endif %}
<div class="mt-auto pt-3 border-top">
<div class="d-flex justify-content-between align-items-center">
<span class="small text-muted"><i class="bi bi-clock me-1"></i> {{ req.created_at|timesince }} ago</span>

View File

@ -128,7 +128,28 @@
<div class="chat-messages" id="chatMessages">
{% for msg in chat_messages %}
<div class="message {% if msg.sender == user %}message-sent{% else %}message-received{% endif %}">
{{ msg.content }}
{% if msg.message_type == 'text' %}
{{ msg.content }}
{% elif msg.message_type == 'image' %}
<img src="{{ msg.attachment.url }}" class="img-fluid rounded-3 mb-2" style="max-height: 300px; cursor: pointer;" onclick="window.open(this.src)">
{% if msg.content %}<p class="mb-0">{{ msg.content }}</p>{% endif %}
{% elif msg.message_type == 'video' %}
<video src="{{ msg.attachment.url }}" controls class="rounded-3 mb-2 w-100" style="max-height: 300px;"></video>
{% if msg.content %}<p class="mb-0">{{ msg.content }}</p>{% endif %}
{% elif msg.message_type == 'file' %}
<div class="d-flex align-items-center gap-2 p-2 bg-black bg-opacity-10 rounded-3">
<i class="bi bi-file-earmark-arrow-down-fill fs-4"></i>
<div class="overflow-hidden">
<a href="{{ msg.attachment.url }}" target="_blank" class="text-reset text-decoration-none d-block text-truncate small fw-bold">
{{ msg.attachment.name|cut:"chat_attachments/" }}
</a>
<small class="opacity-75">{{ msg.attachment.size|filesizeformat }}</small>
</div>
</div>
{% if msg.content %}<p class="mt-2 mb-0">{{ msg.content }}</p>{% endif %}
{% elif msg.message_type == 'sticker' %}
<div class="display-4">{{ msg.sticker_id }}</div>
{% endif %}
<span class="message-time {% if msg.sender == user %}text-white-50{% else %}text-secondary{% endif %}">
{{ msg.timestamp|date:"g:i a" }}
</span>
@ -140,16 +161,86 @@
{% endfor %}
</div>
<form method="post" class="d-flex gap-2">
<form method="post" enctype="multipart/form-data" class="d-flex flex-column gap-2" id="messageForm">
{% csrf_token %}
<input type="text" name="content" class="form-control rounded-pill px-4" placeholder="Type your message..." autocomplete="off" required>
<button type="submit" class="btn btn-danger rounded-circle d-flex align-items-center justify-content-center" style="width: 45px; height: 45px;">
<i class="bi bi-send-fill"></i>
</button>
<div id="attachmentPreview" class="p-2 border rounded-3 bg-light d-none align-items-center gap-2">
<div id="previewContent" class="flex-grow-1 small text-truncate"></div>
<button type="button" class="btn btn-sm btn-outline-danger border-0 rounded-circle" onclick="clearAttachment()">
<i class="bi bi-x"></i>
</button>
</div>
<div class="d-flex gap-2 align-items-center">
<div class="dropdown">
<button type="button" class="btn btn-outline-secondary rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-plus-lg"></i>
</button>
<ul class="dropdown-menu shadow border-0 p-2">
<li><a class="dropdown-item rounded-2 d-flex align-items-center gap-2" href="#" onclick="document.getElementById('fileInput').click()">
<i class="bi bi-file-earmark-plus text-primary"></i> Document
</a></li>
<li><a class="dropdown-item rounded-2 d-flex align-items-center gap-2" href="#" onclick="openCamera()">
<i class="bi bi-camera text-danger"></i> Camera
</a></li>
<li><a class="dropdown-item rounded-2 d-flex align-items-center gap-2" href="#" onclick="document.getElementById('fileInput').setAttribute('accept', 'image/*,video/*'); document.getElementById('fileInput').click()">
<i class="bi bi-images text-success"></i> Photos & Videos
</a></li>
</ul>
</div>
<button type="button" class="btn btn-outline-secondary rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-emoji-smile"></i>
</button>
<div class="dropdown-menu p-3 shadow border-0" style="width: 300px;">
<h6 class="dropdown-header px-0 mb-2">Select Sticker</h6>
<div class="d-grid gap-2 text-center" id="stickerGrid" style="grid-template-columns: repeat(4, 1fr);">
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('🩸')">🩸</div>
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('💉')">💉</div>
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('❤️')">❤️</div>
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('🩹')">🩹</div>
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('🩺')">🩺</div>
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('🏥')">🏥</div>
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('🚑')">🚑</div>
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('🆘')">🆘</div>
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('⭐')"></div>
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('🏆')">🏆</div>
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('🙌')">🙌</div>
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('🙏')">🙏</div>
</div>
</div>
<input type="text" name="content" id="messageInput" class="form-control rounded-pill px-4" placeholder="Type your message..." autocomplete="off">
<input type="file" name="attachment" id="fileInput" class="d-none" onchange="handleFileSelect(this)">
<input type="hidden" name="sticker_id" id="stickerInput">
<button type="submit" class="btn btn-danger rounded-circle d-flex align-items-center justify-content-center" style="width: 45px; height: 45px;">
<i class="bi bi-send-fill"></i>
</button>
</div>
</form>
</div>
</div>
<!-- Camera Modal -->
<div class="modal fade" id="cameraModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content bg-dark border-0 overflow-hidden">
<div class="modal-body p-0 position-relative">
<video id="cameraVideo" autoplay playsinline class="w-100 bg-black"></video>
<canvas id="cameraCanvas" class="d-none"></canvas>
<div class="position-absolute bottom-0 start-0 end-0 p-4 d-flex justify-content-center gap-3">
<button type="button" class="btn btn-light rounded-circle p-3 shadow" onclick="capturePhoto()">
<i class="bi bi-camera-fill fs-4"></i>
</button>
<button type="button" class="btn btn-outline-light rounded-circle p-3" data-bs-dismiss="modal">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Call Modal -->
<div class="modal fade" id="callModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
@ -228,11 +319,42 @@
{ url: 'stun:stun4.l.google.com:19302' },
]
},
debug: 1
debug: 3
});
let localStream;
let currentCall;
// Helper to get local stream with fallback
async function getLocalStream(videoRequested) {
const constraints = {
audio: true,
video: videoRequested ? {
width: { ideal: 640 },
height: { ideal: 480 },
facingMode: "user"
} : false
};
try {
console.log('Attempting to get media with constraints:', constraints);
return await navigator.mediaDevices.getUserMedia(constraints);
} catch (err) {
console.warn('Initial media request failed, trying fallback...', err);
if (videoRequested) {
// If video failed, try audio only
try {
console.log('Falling back to audio only...');
return await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
} catch (audioErr) {
console.error('Audio fallback also failed', audioErr);
throw audioErr;
}
}
throw err;
}
}
const callModal = new bootstrap.Modal(document.getElementById('callModal'));
const incomingCallUI = document.getElementById('incomingCallUI');
@ -264,7 +386,6 @@
peer.on('call', (call) => {
console.log('Incoming call from: ' + call.peer);
if (call.peer === PEER_ID_PREFIX + sanitizeId(OTHER_USERNAME)) {
// If already in a call, busy logic could go here
currentCall = call;
incomingCallUI.style.display = 'block';
playRingtone('incoming');
@ -280,12 +401,14 @@
// Start Call Function
async function startCall(videoEnabled = true) {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
alert('Your browser does not support video/audio calls or is not using a secure connection (HTTPS).');
return;
}
try {
callStatus.innerText = "Requesting permissions...";
localStream = await navigator.mediaDevices.getUserMedia({
video: videoEnabled,
audio: true
});
localStream = await getLocalStream(videoEnabled);
localVideo.srcObject = localStream;
callModal.show();
@ -296,7 +419,10 @@
handleCall(call);
} catch (err) {
console.error('Failed to get local stream', err);
alert('Could not access camera or microphone.');
let msg = 'Could not access camera or microphone. Please check your permissions and hardware.';
if (err.name === 'NotAllowedError') msg = 'Permission denied. Please allow camera/microphone access in your browser settings.';
if (err.name === 'NotFoundError') msg = 'No camera or microphone found on your device.';
alert(msg);
}
}
@ -328,10 +454,8 @@
stopRingtones();
try {
callStatus.innerText = "Answering...";
localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
// Try to get video if possible, but fallback to audio
localStream = await getLocalStream(true);
localVideo.srcObject = localStream;
callModal.show();
currentCall.answer(localStream);
@ -388,10 +512,86 @@
}
});
// Messaging Enhancements
const attachmentPreview = document.getElementById('attachmentPreview');
const previewContent = document.getElementById('previewContent');
const cameraModal = new bootstrap.Modal(document.getElementById('cameraModal'));
const cameraVideo = document.getElementById('cameraVideo');
const cameraCanvas = document.getElementById('cameraCanvas');
let cameraStream = null;
window.handleFileSelect = function(input) {
if (input.files && input.files[0]) {
const file = input.files[0];
previewContent.innerText = `Selected: ${file.name} (${(file.size / 1024).toFixed(1)} KB)`;
attachmentPreview.classList.remove('d-none');
attachmentPreview.classList.add('d-flex');
}
}
window.clearAttachment = function() {
document.getElementById('fileInput').value = '';
attachmentPreview.classList.remove('d-flex');
attachmentPreview.classList.add('d-none');
}
window.openCamera = async function() {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
alert('Your browser does not support camera access or is not using HTTPS.');
return;
}
try {
cameraStream = await getLocalStream(true);
cameraVideo.srcObject = cameraStream;
cameraModal.show();
} catch (err) {
console.error('Camera error:', err);
let msg = 'Could not access camera.';
if (err.name === 'NotAllowedError') msg = 'Camera permission denied.';
if (err.name === 'NotFoundError') msg = 'No camera found on your device.';
alert(msg);
}
}
document.getElementById('cameraModal').addEventListener('hidden.bs.modal', () => {
if (cameraStream) {
cameraStream.getTracks().forEach(track => track.stop());
cameraStream = null;
}
});
window.capturePhoto = function() {
const context = cameraCanvas.getContext('2d');
cameraCanvas.width = cameraVideo.videoWidth;
cameraCanvas.height = cameraVideo.videoHeight;
context.drawImage(cameraVideo, 0, 0);
cameraCanvas.toBlob((blob) => {
const file = new File([blob], "camera_capture.jpg", { type: "image/jpeg" });
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
document.getElementById('fileInput').files = dataTransfer.files;
previewContent.innerText = "Captured photo ready to send";
attachmentPreview.classList.remove('d-none');
attachmentPreview.classList.add('d-flex');
cameraModal.hide();
}, 'image/jpeg');
}
window.sendSticker = function(sticker) {
document.getElementById('stickerInput').value = sticker;
document.getElementById('messageForm').submit();
}
// Handle cleanup on page unload
window.addEventListener('beforeunload', () => {
endCall();
peer.destroy();
if (cameraStream) {
cameraStream.getTracks().forEach(track => track.stop());
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,80 @@
{% extends "base.html" %}
{% load static %}
{% block title %}My Health Records - RaktaPulse{% endblock %}
{% block content %}
<div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-5">
<div>
<h1 style="font-weight: 800; color: var(--text-primary);">My Health Records</h1>
<p class="text-muted">Manage your hospital reports and medical history.</p>
</div>
<a href="{% url 'upload_health_report' %}" class="btn btn-primary px-4 py-2" style="border-radius: 10px; font-weight: 700; background-color: var(--pulse-red); border: none;">
<i class="fas fa-plus me-2"></i> Add New Report
</a>
</div>
{% if reports %}
<div class="row">
{% for report in reports %}
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100 shadow-sm border-0" style="border-radius: 15px; overflow: hidden; transition: all 0.3s ease;">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-start mb-3">
<div class="icon-box" style="width: 50px; height: 50px; background: rgba(230, 57, 70, 0.1); border-radius: 12px; display: flex; align-items: center; justify-content: center; color: var(--pulse-red);">
<i class="fas fa-file-medical fa-lg"></i>
</div>
{% if report.allow_notifications %}
<span class="badge bg-success" style="border-radius: 6px;">
<i class="fas fa-bell me-1"></i> Notifications On
</span>
{% endif %}
</div>
<h5 style="font-weight: 700; margin-bottom: 5px;">{{ report.title }}</h5>
<p class="text-muted small mb-3">
<i class="fas fa-hospital me-1"></i> {{ report.hospital_name|default:"Not Specified" }}
</p>
<div class="mb-3">
<div class="d-flex justify-content-between small text-muted mb-1">
<span>Test Date:</span>
<span class="fw-bold">{{ report.report_date }}</span>
</div>
{% if report.next_test_date %}
<div class="d-flex justify-content-between small text-muted">
<span>Next Test:</span>
<span class="text-primary fw-bold">{{ report.next_test_date }}</span>
</div>
{% endif %}
</div>
{% if report.description %}
<p class="small text-secondary mb-4">{{ report.description|truncatechars:80 }}</p>
{% endif %}
<div class="d-grid">
<a href="{{ report.report_file.url }}" class="btn btn-outline-dark" target="_blank" style="border-radius: 8px; font-weight: 600;">
<i class="fas fa-eye me-2"></i> View Report
</a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5">
<div class="mb-4">
<i class="fas fa-folder-open fa-5x text-muted opacity-50"></i>
</div>
<h3>No health records found</h3>
<p class="text-muted">Start by uploading your first medical report today.</p>
<a href="{% url 'upload_health_report' %}" class="btn btn-primary mt-3 px-5 py-2" style="border-radius: 10px; font-weight: 700; background-color: var(--pulse-red); border: none;">
Upload Now
</a>
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -112,11 +112,23 @@
{% block content %}
<div class="container-fluid p-0">
<!-- Welcome Header -->
<div class="row mb-4">
<div class="col-12">
<div class="row mb-4 align-items-center">
<div class="col-md-7">
<h2 class="brand-font mb-1">{% trans "RaktaPulse Community Dashboard" %}</h2>
<p class="text-secondary">{% trans "Overview of blood donation activity and requirements in your area." %}</p>
<p class="text-secondary mb-0">{% trans "Overview of blood donation activity and requirements in your area." %}</p>
</div>
{% if user.is_authenticated and user_badges %}
<div class="col-md-5 text-md-end mt-3 mt-md-0">
<div class="d-flex flex-wrap gap-2 justify-content-md-end">
{% for badge in user_badges %}
<div class="badge-item d-flex align-items-center gap-2 px-3 py-2 bg-white rounded-pill border shadow-sm" title="{{ badge.description }}">
<i class="{{ badge.icon_class }} text-warning"></i>
<span class="small fw-bold text-dark">{{ badge.name }}</span>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% if involved_events %}
@ -143,42 +155,51 @@
</div>
{% endif %}
<!-- Quick Stats -->
<!-- Quick Stats & Impact Counter -->
<div class="row g-4 mb-5">
<div class="col-xl-3 col-md-6">
<div class="col-xl-2 col-md-4 col-6">
<a href="{% url 'donor_list' %}" class="stat-card">
<div class="icon-box bg-danger bg-opacity-10 text-danger">
<i class="bi bi-people"></i>
</div>
<div class="stat-value">{{ stats.total_donors }}</div>
<div class="stat-label">{% trans "Registered Donors" %}</div>
<div class="stat-label">{% trans "Donors" %}</div>
</a>
</div>
<div class="col-xl-3 col-md-6">
<div class="col-xl-2 col-md-4 col-6">
<a href="{% url 'blood_request_list' %}?status=Active" class="stat-card">
<div class="icon-box bg-warning bg-opacity-10 text-warning">
<i class="bi bi-activity"></i>
</div>
<div class="stat-value">{{ stats.active_requests }}</div>
<div class="stat-label">{% trans "Active Requests" %}</div>
<div class="stat-label">{% trans "Requests" %}</div>
</a>
</div>
<div class="col-xl-3 col-md-6">
<div class="col-xl-2 col-md-4 col-6">
<div class="stat-card">
<div class="icon-box bg-success bg-opacity-10 text-success">
<i class="bi bi-check2-circle"></i>
</div>
<div class="stat-value">{{ stats.completed_donations }}</div>
<div class="stat-label">{% trans "Donations" %}</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-6">
<div class="stat-card bg-danger bg-opacity-10 border-danger">
<div class="icon-box bg-danger text-white">
<i class="bi bi-heart-fill"></i>
</div>
<div class="stat-value text-danger">{{ stats.lives_saved }}</div>
<div class="stat-label text-danger">{% trans "Lives Saved" %}</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="stat-card">
<div class="icon-box bg-primary bg-opacity-10 text-primary">
<i class="bi bi-droplet"></i>
</div>
<div class="stat-value">{{ stats.total_stock }} <small class="fs-6 fw-normal">/ {{ stats.total_capacity }}</small></div>
<div class="stat-label">{% trans "Inventory vs Capacity" %}</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="stat-card">
<div class="icon-box bg-success bg-opacity-10 text-success">
<i class="bi bi-shield-check"></i>
</div>
<div class="stat-value">{{ stats.vaccinated_percentage }}%</div>
<div class="stat-label">{% trans "Vaccinated Donors" %}</div>
<div class="stat-label">{% trans "Stock Level" %}</div>
</div>
</div>
</div>
@ -308,6 +329,12 @@
Urgent Requests
<span class="badge bg-danger rounded-pill px-2" style="font-size: 0.6rem;">HOT</span>
</h5>
<!-- Emergency SMS Concept Button -->
<button onclick="sendEmergencyAlert()" class="btn btn-danger w-100 mb-4 py-2 rounded-pill shadow-sm">
<i class="bi bi-broadcast me-2"></i>{% trans "Send Emergency SMS Alert" %}
</button>
<div class="request-feed">
{% for req in blood_requests %}
<div class="mb-4 border-bottom border-light pb-3">
@ -367,36 +394,22 @@
</div>
</div>
<!-- Blood & Vaccine Facts Section -->
<!-- Myths vs Facts Section -->
<div class="row g-4 mt-2 mb-5">
<div class="col-12">
<div class="glass-card">
<h4 class="brand-font mb-4"><i class="bi bi-info-circle text-danger me-2"></i>Blood & Vaccine Facts</h4>
<div class="glass-card bg-light bg-opacity-10 border-danger">
<h4 class="brand-font mb-4 text-center"><i class="bi bi-question-diamond text-danger me-2"></i>{% trans "Top 5 Myths vs. Facts about Blood Donation" %}</h4>
<div class="row g-4">
<div class="col-md-3">
<div class="p-3 border border-light rounded-4 h-100 bg-light bg-opacity-10">
<h6 class="fw-bold text-danger"><i class="bi bi-droplet-fill me-2"></i>Lifesaver</h6>
<p class="small text-secondary mb-0">A single blood donation can save up to three lives. Your contribution has a massive impact on the community.</p>
</div>
</div>
<div class="col-md-3">
<div class="p-3 border border-light rounded-4 h-100 bg-light bg-opacity-10">
<h6 class="fw-bold text-danger"><i class="bi bi-shield-check me-2"></i>Vaccines & Donation</h6>
<p class="small text-secondary mb-0">Most vaccines (like COVID-19 or Flu) don't stop you from donating if you feel well. Stay protected and keep saving lives!</p>
</div>
</div>
<div class="col-md-3">
<div class="p-3 border border-light rounded-4 h-100 bg-light bg-opacity-10">
<h6 class="fw-bold text-danger"><i class="bi bi-award me-2"></i>Universal Type</h6>
<p class="small text-secondary mb-0">O-Negative is the universal donor type. It's vital for emergency rooms and trauma centers during critical hours.</p>
</div>
</div>
<div class="col-md-3">
<div class="p-3 border border-light rounded-4 h-100 bg-light bg-opacity-10">
<h6 class="fw-bold text-danger"><i class="bi bi-lightning-charge me-2"></i>Quick Recovery</h6>
<p class="small text-secondary mb-0">Your body replaces blood plasma within 24-48 hours. Just stay hydrated and have a light snack after your donation!</p>
{% for item in myths_vs_facts %}
<div class="col-md-4 col-lg-2 {% if forloop.first %}offset-lg-1{% endif %}">
<div class="myth-fact-card p-3 h-100 bg-white rounded-4 shadow-sm border-bottom border-danger border-3">
<div class="text-danger small fw-bold mb-2">{% trans "MYTH" %}</div>
<p class="small text-muted mb-3 italic">"{{ item.myth }}"</p>
<div class="text-success small fw-bold mb-1">{% trans "FACT" %}</div>
<p class="extra-small text-dark mb-0">{{ item.fact }}</p>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
@ -472,5 +485,47 @@
.bindPopup("<b>{{ donor.name }}</b><br>{{ donor.blood_group }} - {{ donor.location }}");
{% endfor %}
}
function sendEmergencyAlert() {
if (!confirm("This will simulate sending an emergency SMS to all nearby donors of a specific blood group. Continue?")) return;
const bloodGroup = prompt("Enter the required blood group (e.g., A+, O-):", "O+");
if (!bloodGroup) return;
if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition(function(position) {
const data = {
blood_group: bloodGroup,
latitude: position.coords.latitude,
longitude: position.coords.longitude
};
fetch('/emergency-sms/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.status === 'success') {
alert(result.message);
} else {
alert("Error: " + result.message);
}
})
.catch(error => {
console.error('Error:', error);
alert("Concept Demo: SMS API call simulated for " + bloodGroup + " donors nearby.");
});
}, function(error) {
alert("Error getting location: " + error.message);
});
} else {
alert("Geolocation is not supported.");
}
}
</script>
{% endblock %}

View File

@ -27,12 +27,12 @@
<h5 class="fw-bold mb-3 border-bottom pb-2">Account Information</h5>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label small fw-bold">Username</label>
{{ u_form.username }}
<label class="form-label small fw-bold text-muted">Username (Permanent)</label>
<input type="text" class="form-control bg-light border-0" value="{{ user.username }}" readonly>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold">Email Address</label>
{{ u_form.email }}
<label class="form-label small fw-bold text-muted">Email Address (Permanent)</label>
<input type="text" class="form-control bg-light border-0" value="{{ user.email }}" readonly>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold">First Name</label>
@ -60,21 +60,40 @@
{{ p_form.phone }}
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Birth Date</label>
{{ p_form.birth_date }}
</div>
<div class="col-md-6">
<label class="form-label small fw-bold">Location</label>
{{ p_form.location }}
</div>
<div class="col-12">
<div class="col-md-6">
<label class="form-label small fw-bold">Profile Picture</label>
{{ p_form.profile_pic }}
</div>
</div>
<div class="d-grid">
<div class="d-grid mb-5">
<button type="submit" class="btn btn-danger py-2 fw-bold">
<i class="bi bi-check-circle me-2"></i>Update Profile
</button>
</div>
</form>
<div class="mt-5 border-top pt-4">
<h5 class="text-danger fw-bold mb-3">
<i class="bi bi-exclamation-triangle-fill me-2"></i>Danger Zone
</h5>
<div class="p-3 border border-danger border-opacity-25 rounded bg-danger bg-opacity-10">
<p class="small mb-3">Clearing personal info will remove your bio, location, phone number, and profile picture. Your account and donation history will remain intact.</p>
<form action="{% url 'delete_personal_info' %}" method="POST" onsubmit="return confirm('Are you sure you want to clear your personal information? This cannot be undone.');">
{% csrf_token %}
<button type="submit" class="btn btn-outline-danger btn-sm">
Clear Personal Information
</button>
</form>
</div>
</div>
</div>
</div>
</div>

View File

@ -16,7 +16,7 @@
<p class="text-secondary">Fill in the details to alert nearby donors.</p>
</div>
<form method="POST" id="requestForm">
<form method="POST" id="requestForm" enctype="multipart/form-data">
{% csrf_token %}
<div class="row g-3">
<div class="col-md-6">
@ -52,6 +52,11 @@
<label class="form-label small fw-bold text-secondary">Contact Number *</label>
<input type="text" name="contact_number" class="form-control rounded-3" required>
</div>
<div class="col-12">
<label class="form-label small fw-bold text-secondary">Upload Photo (Optional)</label>
<input type="file" name="image" class="form-control rounded-3" accept="image/*">
<div class="form-text small">Include a prescription or patient photo for verification.</div>
</div>
<!-- Hidden coordinates -->
<input type="hidden" name="latitude" id="id_latitude">

View File

@ -0,0 +1,72 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Upload Health Report - RaktaPulse{% endblock %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow-sm border-0" style="border-radius: 20px;">
<div class="card-body p-5">
<h2 class="text-center mb-4" style="font-weight: 700;">Upload Medical Report</h2>
<p class="text-muted text-center mb-5">Save your hospital reports and health records securely.</p>
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
<div class="mb-4">
<label class="form-label" style="font-weight: 600;">Report Title *</label>
<input type="text" name="title" class="form-control" placeholder="e.g., Annual Blood Test" required style="border-radius: 10px; padding: 12px;">
</div>
<div class="mb-4">
<label class="form-label" style="font-weight: 600;">Hospital/Clinic Name</label>
<input type="text" name="hospital_name" class="form-control" placeholder="e.g., City General Hospital" style="border-radius: 10px; padding: 12px;">
</div>
<div class="row">
<div class="col-md-6 mb-4">
<label class="form-label" style="font-weight: 600;">Report Date *</label>
<input type="date" name="report_date" class="form-control" required style="border-radius: 10px; padding: 12px;">
</div>
<div class="col-md-6 mb-4">
<label class="form-label" style="font-weight: 600;">Follow-up Test Date</label>
<input type="date" name="next_test_date" class="form-control" style="border-radius: 10px; padding: 12px;">
<small class="text-muted">When should you take the next test?</small>
</div>
</div>
<div class="mb-4">
<label class="form-label" style="font-weight: 600;">Upload File (PDF/Image) *</label>
<input type="file" name="report_file" class="form-control" required style="border-radius: 10px; padding: 12px;">
</div>
<div class="mb-4">
<label class="form-label" style="font-weight: 600;">Description/Notes</label>
<textarea name="description" class="form-control" rows="3" placeholder="Any specific notes from the doctor..." style="border-radius: 10px; padding: 12px;"></textarea>
</div>
<div class="mb-4">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="allow_notifications" id="allowNotifications">
<label class="form-check-label" for="allowNotifications" style="font-weight: 600;">
Enable Follow-up Notifications
</label>
</div>
<p class="text-muted small">We'll notify you when it's time for your next test based on the date set above.</p>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary py-3" style="border-radius: 10px; font-weight: 700; background-color: var(--pulse-red); border: none;">
<i class="fas fa-upload me-2"></i> Save Report
</button>
<a href="{% url 'health_report_list' %}" class="btn btn-light py-3" style="border-radius: 10px; font-weight: 600;">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -6,7 +6,9 @@ from .views import (
vaccination_dashboard, add_vaccination, live_map,
request_blood, profile, volunteer_for_request,
complete_donation, notifications_view,
register_donor, hospital_list, public_profile, inbox, chat
register_donor, hospital_list, public_profile, inbox, chat,
update_location, emergency_sms, delete_personal_info,
health_report_list, upload_health_report
)
urlpatterns = [
@ -25,11 +27,16 @@ urlpatterns = [
path("vaccination/", vaccination_info, name="vaccination_info"),
path("vaccination/dashboard/", vaccination_dashboard, name="vaccination_dashboard"),
path("vaccination/add/", add_vaccination, name="add_vaccination"),
path("reports/", health_report_list, name="health_report_list"),
path("reports/upload/", upload_health_report, name="upload_health_report"),
path("live-map/", live_map, name="live_map"),
path("request-blood/", request_blood, name="request_blood"),
path("emergency-sms/", emergency_sms, name="emergency_sms"),
path("volunteer/<int:request_id>/", volunteer_for_request, name="volunteer_for_request"),
path("complete-donation/<int:event_id>/", complete_donation, name="complete_donation"),
path("notifications/", notifications_view, name="notifications"),
path("register-donor/", register_donor, name="register_donor"),
path("hospitals/", hospital_list, name="hospital_list"),
path("update-location/", update_location, name="update_location"),
path("delete-personal-info/", delete_personal_info, name="delete_personal_info"),
]

View File

@ -9,10 +9,87 @@ from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.utils import timezone
from django.contrib.auth.models import User
from .models import Donor, BloodRequest, BloodBank, VaccineRecord, UserProfile, BLOOD_GROUPS, DonationEvent, Notification, Hospital, Message
from .models import Donor, BloodRequest, BloodBank, VaccineRecord, UserProfile, BLOOD_GROUPS, DonationEvent, Notification, Hospital, Message, Badge, HealthReport
from .forms import UserUpdateForm, ProfileUpdateForm, UserRegisterForm
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
import json
@login_required
@csrf_exempt
def emergency_sms(request):
"""Concept demo for sending SMS to nearby donors."""
if request.method == "POST":
try:
data = json.loads(request.body)
blood_group = data.get('blood_group')
lat = data.get('latitude')
lng = data.get('longitude')
except json.JSONDecodeError:
blood_group = request.POST.get('blood_group')
lat = request.POST.get('latitude')
lng = request.POST.get('longitude')
if not (blood_group and lat and lng):
return JsonResponse({'status': 'error', 'message': 'Missing data'}, status=400)
if not (blood_group and lat and lng):
return JsonResponse({'status': 'error', 'message': 'Missing data'}, status=400)
try:
u_lat = float(lat)
u_lng = float(lng)
# Find donors within 10km
all_donors = Donor.objects.filter(blood_group=blood_group, is_available=True)
nearby_donors = []
for d in all_donors:
if d.latitude and d.longitude:
dist = haversine(u_lat, u_lng, float(d.latitude), float(d.longitude))
if dist <= 10.0: # 10 km radius
nearby_donors.append(d)
# Simulated SMS sending
count = len(nearby_donors)
for d in nearby_donors:
# In a real app, we'd call an SMS API here
# Notification.objects.create(user=d.user, message=f"EMERGENCY: {blood_group} blood needed nearby! Please check RaktaPulse.")
print(f"Simulated SMS to {d.phone}: EMERGENCY {blood_group} needed!")
return JsonResponse({
'status': 'success',
'message': f'SMS Alert sent to {count} nearby donors!',
'count': count
})
except Exception as e:
return JsonResponse({'status': 'error', 'message': str(e)}, status=500)
return JsonResponse({'status': 'error', 'message': 'Only POST allowed'}, status=405)
@login_required
@csrf_exempt
@require_POST
def update_location(request):
try:
data = json.loads(request.body)
lat = data.get('latitude')
lng = data.get('longitude')
if lat and lng:
profile = request.user.profile
profile.latitude = lat
profile.longitude = lng
profile.last_location_update = timezone.now()
profile.save()
return JsonResponse({'status': 'success'})
except Exception as e:
return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
return JsonResponse({'status': 'error', 'message': 'Invalid data'}, status=400)
def hospital_list(request):
user_lat = request.GET.get('lat')
user_lng = request.GET.get('lng')
@ -61,6 +138,32 @@ def profile(request):
return render(request, 'core/profile.html', context)
@login_required
@require_POST
def delete_personal_info(request):
"""View to clear non-essential personal information."""
user = request.user
profile = user.profile
# Clear User fields
user.first_name = ""
user.last_name = ""
user.save()
# Clear Profile fields
profile.bio = ""
profile.location = ""
profile.phone = ""
profile.birth_date = None
profile.profile_pic = None
# We keep blood_group as it's often essential for the app's functionality (blood donation)
# but we can clear it if the user really wants to.
# For now, let's just clear the "soft" personal info.
profile.save()
messages.success(request, "Your non-essential personal information has been cleared.")
return redirect('profile')
def haversine(lat1, lon1, lat2, lon2):
# Radius of the Earth in km
R = 6371.0
@ -122,6 +225,12 @@ def home(request):
user_lat = request.GET.get('lat')
user_lng = request.GET.get('lng')
# Initialize default badges if they don't exist (Demo purposes)
if Badge.objects.count() == 0:
Badge.objects.create(name='First-Time Donor', description='Completed your first donation!', icon_class='fas fa-award')
Badge.objects.create(name='Community Hero', description='Completed 5 donations!', icon_class='fas fa-medal')
Badge.objects.create(name='Life Saver', description='Completed 10+ donations!', icon_class='fas fa-heart')
donors = Donor.objects.all()
if query_blood:
donors = donors.filter(blood_group=query_blood)
@ -148,9 +257,15 @@ def home(request):
blood_requests = BloodRequest.objects.filter(status='Active').order_by('-urgency', '-created_at')
blood_banks = BloodBank.objects.all()
# Stats for Dashboard
# Stats for Dashboard (Including Demo Data for Impact)
demo_donations = 157
demo_donors = 48
actual_completed = DonationEvent.objects.filter(is_completed=True).count()
completed_donations = actual_completed + demo_donations
stats = {
"total_donors": Donor.objects.count(),
"total_donors": Donor.objects.count() + demo_donors,
"active_requests": BloodRequest.objects.filter(status='Active').count(),
"total_stock": sum([
bb.stock_a_plus + bb.stock_a_minus + bb.stock_b_plus + bb.stock_b_minus +
@ -158,7 +273,9 @@ def home(request):
for bb in blood_banks
]),
"total_capacity": sum([bb.total_capacity * 8 for bb in blood_banks]), # 8 blood types
"vaccinated_percentage": 0
"vaccinated_percentage": 0,
"completed_donations": completed_donations,
"lives_saved": completed_donations * 3
}
total_d = stats["total_donors"]
@ -166,6 +283,14 @@ def home(request):
vaccinated_count = Donor.objects.filter(vaccination_status__icontains='Fully').count()
stats["vaccinated_percentage"] = int((vaccinated_count / total_d) * 100)
myths_vs_facts = [
{"myth": "Donating blood is painful.", "fact": "You only feel a quick pinch, like a mosquito bite."},
{"myth": "I'm too old to donate.", "fact": "There is no upper age limit as long as you're healthy."},
{"myth": "It takes all day to donate.", "fact": "The actual donation takes about 10 minutes, the whole process is under an hour."},
{"myth": "Giving blood makes you weak.", "fact": "Your body replaces fluids within 24 hours and cells within weeks."},
{"myth": "I can't donate because I have high BP.", "fact": "As long as it's within 180/100 at the time of donation, you're fine."},
]
context = {
"donors": donor_list_data[:8],
"blood_requests": blood_requests[:6],
@ -174,15 +299,17 @@ def home(request):
"stats": stats,
"project_name": "RaktaPulse",
"current_time": timezone.now(),
"myths_vs_facts": myths_vs_facts,
}
if request.user.is_authenticated:
# Get active involvements (where user is donor or requester)
# Get active involvements
involved_events = DonationEvent.objects.filter(
(Q(donor_user=request.user) | Q(request__user=request.user)),
is_completed=False
)
context["involved_events"] = involved_events
context["user_badges"] = request.user.profile.badges.all()
return render(request, "core/index.html", context)
@ -292,6 +419,7 @@ def request_blood(request):
urgency = request.POST.get('urgency')
hospital = request.POST.get('hospital')
contact_number = request.POST.get('contact_number')
image = request.FILES.get('image')
latitude = request.POST.get('latitude')
longitude = request.POST.get('longitude')
@ -304,6 +432,7 @@ def request_blood(request):
urgency=urgency,
hospital=hospital,
contact_number=contact_number,
image=image,
latitude=latitude if latitude else None,
longitude=longitude if longitude else None
)
@ -387,15 +516,34 @@ def volunteer_for_request(request, request_id):
@login_required
def complete_donation(request, event_id):
event = DonationEvent.objects.get(id=event_id)
# Only the requester or the donor can mark as complete (for simplicity, letting both)
# Only the requester or the donor can mark as complete
if request.user == event.donor_user or (event.request.user and request.user == event.request.user):
event.is_completed = True
event.save()
# Award Badges Logic
donor_profile = event.donor_user.profile
completed_count = DonationEvent.objects.filter(donor_user=event.donor_user, is_completed=True).count()
if completed_count >= 1:
badge = Badge.objects.filter(name='First-Time Donor').first()
if badge:
donor_profile.badges.add(badge)
if completed_count >= 5:
badge = Badge.objects.filter(name='Community Hero').first()
if badge:
donor_profile.badges.add(badge)
if completed_count >= 10:
badge = Badge.objects.filter(name='Life Saver').first()
if badge:
donor_profile.badges.add(badge)
# Notify both
Notification.objects.create(
user=event.donor_user,
message=f"Thank you for your donation to {event.request.patient_name}!"
message=f"Thank you for your donation to {event.request.patient_name}! You've earned recognition for your impact."
)
if event.request.user:
Notification.objects.create(
@ -485,11 +633,29 @@ def chat(request, username):
other_user = User.objects.get(username=username)
if request.method == "POST":
content = request.POST.get('content')
if content:
attachment = request.FILES.get('attachment')
sticker_id = request.POST.get('sticker_id')
msg_type = 'text'
if sticker_id:
msg_type = 'sticker'
elif attachment:
content_type = attachment.content_type
if content_type.startswith('image/'):
msg_type = 'image'
elif content_type.startswith('video/'):
msg_type = 'video'
else:
msg_type = 'file'
if content or attachment or sticker_id:
Message.objects.create(
sender=request.user,
receiver=other_user,
content=content
content=content,
attachment=attachment,
message_type=msg_type,
sticker_id=sticker_id
)
return redirect('chat', username=username)
@ -502,3 +668,37 @@ def chat(request, username):
Message.objects.filter(sender=other_user, receiver=request.user, is_read=False).update(is_read=True)
return render(request, 'core/chat.html', {'other_user': other_user, 'chat_messages': messages})
@login_required
def health_report_list(request):
reports = HealthReport.objects.filter(user=request.user).order_by('-report_date')
return render(request, 'core/health_report_list.html', {'reports': reports})
@login_required
def upload_health_report(request):
if request.method == "POST":
title = request.POST.get('title')
hospital_name = request.POST.get('hospital_name')
report_file = request.FILES.get('report_file')
description = request.POST.get('description')
report_date = request.POST.get('report_date')
next_test_date = request.POST.get('next_test_date')
allow_notifications = request.POST.get('allow_notifications') == 'on'
if title and report_file and report_date:
HealthReport.objects.create(
user=request.user,
title=title,
hospital_name=hospital_name,
report_file=report_file,
description=description,
report_date=report_date,
next_test_date=next_test_date if next_test_date else None,
allow_notifications=allow_notifications
)
messages.success(request, "Health report uploaded successfully!")
return redirect('health_report_list')
else:
messages.error(request, "Please fill in all required fields.")
return render(request, 'core/upload_health_report.html')

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB