RaktaPulse

This commit is contained in:
Flatlogic Bot 2026-02-18 09:21:22 +00:00
parent dba5db4b7d
commit c0bb59aeba
20 changed files with 639 additions and 10 deletions

View File

@ -86,6 +86,7 @@ TEMPLATES = [
'django.contrib.messages.context_processors.messages',
# IMPORTANT: do not remove injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp
'core.context_processors.project_context',
'core.context_processors.unread_messages',
],
},
},

View File

@ -1,5 +1,13 @@
import os
import time
from .models import Message
def unread_messages(request):
if request.user.is_authenticated:
return {
'unread_messages_count': Message.objects.filter(receiver=request.user, is_read=False).count()
}
return {'unread_messages_count': 0}
def project_context(request):
"""

View File

@ -0,0 +1,27 @@
# Generated by Django 5.2.7 on 2026-02-18 07:37
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0013_userprofile_profile_pic'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Message',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField()),
('timestamp', models.DateTimeField(auto_now_add=True)),
('is_read', models.BooleanField(default=False)),
('receiver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to=settings.AUTH_USER_MODEL)),
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -148,3 +148,13 @@ class Notification(models.Model):
def __str__(self):
return f"Notification for {self.user.username}: {self.message[:20]}..."
class Message(models.Model):
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()
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}"

View File

@ -275,8 +275,8 @@
<li class="{% if request.resolver_match.url_name == 'home' %}active{% endif %}"><a href="{% url 'home' %}"><i class="bi bi-grid-1x2-fill"></i> {% trans "Dashboard" %}</a></li>
<li class="{% if request.resolver_match.url_name == 'donor_list' %}active{% endif %}"><a href="{% url 'donor_list' %}"><i class="bi bi-people-fill"></i> {% trans "Donors" %}</a></li>
<li class="{% if request.resolver_match.url_name == 'blood_request_list' %}active{% endif %}"><a href="{% url 'blood_request_list' %}"><i class="bi bi-megaphone-fill"></i> {% trans "Blood Requests" %}</a></li>
<li class="{% if request.resolver_match.url_name == 'blood_bank_list' %}active{% endif %}"><a href="{% url 'blood_bank_list' %}"><i class="bi bi-hospital-fill"></i> {% trans "Blood Banks" %}</a></li>
<li class="{% if request.resolver_match.url_name == 'hospital_list' %}active{% endif %}"><a href="{% url 'hospital_list' %}"><i class="bi bi-building-heart"></i> {% trans "Hospitals" %}</a></li>
<li class="{% if request.resolver_match.url_name == 'blood_bank_list' %}active{% endif %}"><a href="{% url 'blood_bank_list' %}"><i class="bi bi-droplet-half"></i> {% trans "Blood Banks" %}</a></li>
<li class="{% if request.resolver_match.url_name == 'hospital_list' %}active{% endif %}"><a href="{% url 'hospital_list' %}"><i class="bi bi-hospital-fill"></i> {% trans "Hospitals" %}</a></li>
<li class="{% if request.resolver_match.url_name == 'live_map' %}active{% endif %}"><a href="{% url 'live_map' %}"><i class="bi bi-map text-danger"></i> {% trans "Live Alerts" %}</a></li>
<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 %}
@ -343,6 +343,16 @@
{{ user.notifications.count }}
</span>
</a>
<!-- Messages Inbox -->
<a href="{% url 'inbox' %}" class="text-decoration-none me-2 position-relative">
<i class="bi bi-chat-dots-fill fs-5 text-secondary"></i>
{% if unread_messages_count > 0 %}
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" style="font-size: 0.6rem;">
{{ unread_messages_count }}
</span>
{% endif %}
</a>
<!-- Profile Dropdown -->
<div class="dropdown">

View File

@ -52,6 +52,11 @@
<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>
<div class="d-flex gap-2">
{% if req.user %}
<a href="{% url 'chat' req.user.username %}" class="btn btn-sm btn-outline-secondary px-2 rounded-pill" title="Message Requester">
<i class="bi bi-chat-dots-fill"></i>
</a>
{% endif %}
<a href="{% url 'volunteer_for_request' req.id %}" class="btn btn-sm btn-outline-danger px-3 rounded-pill">Volunteer</a>
<a href="tel:{{ req.contact_number }}" class="btn btn-sm btn-danger px-3 rounded-pill">Call</a>
</div>

View File

@ -0,0 +1,353 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}Chat with {{ other_user.username }} - RaktaPulse{% endblock %}
{% block head %}
<style>
.chat-container {
height: calc(100vh - 250px);
display: flex;
flex-direction: column;
}
.chat-messages {
flex-grow: 1;
overflow-y: auto;
padding: 20px;
background: #f8f9fa;
border-radius: 12px;
margin-bottom: 20px;
display: flex;
flex-direction: column;
gap: 15px;
}
.message {
max-width: 75%;
padding: 12px 16px;
border-radius: 16px;
position: relative;
font-size: 0.95rem;
line-height: 1.4;
}
.message-sent {
align-self: flex-end;
background-color: var(--pulse-red);
color: white;
border-bottom-right-radius: 4px;
}
.message-received {
align-self: flex-start;
background-color: white;
color: var(--text-primary);
border-bottom-left-radius: 4px;
border: 1px solid var(--border-color);
}
.message-time {
font-size: 0.7rem;
opacity: 0.7;
margin-top: 4px;
display: block;
}
#videoGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
background: #000;
border-radius: 12px;
overflow: hidden;
position: relative;
}
.video-wrapper {
position: relative;
aspect-ratio: 4/3;
background: #222;
}
video {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-label {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(0,0,0,0.5);
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.8rem;
}
.call-controls {
display: flex;
justify-content: center;
gap: 15px;
padding: 15px;
background: rgba(0,0,0,0.8);
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
}
</style>
{% endblock %}
{% block content %}
<div class="container py-4">
<div class="glass-card chat-container">
<div class="d-flex align-items-center gap-3 pb-3 mb-3 border-bottom">
<a href="{% url 'inbox' %}" class="btn btn-link text-secondary p-0">
<i class="bi bi-arrow-left fs-4"></i>
</a>
<div class="rounded-circle overflow-hidden border border-danger-subtle" style="width: 45px; height: 45px;">
{% if other_user.profile.profile_pic %}
<img src="{{ other_user.profile.profile_pic.url }}" alt="{{ other_user.username }}" class="w-100 h-100 object-fit-cover">
{% else %}
<div class="bg-danger bg-opacity-10 w-100 h-100 d-flex align-items-center justify-content-center">
<i class="bi bi-person-fill text-danger"></i>
</div>
{% endif %}
</div>
<div>
<h6 class="fw-bold mb-0">
<a href="{% url 'public_profile' other_user.username %}" class="text-decoration-none text-dark">
{{ other_user.first_name }} {{ other_user.last_name|default:other_user.username }}
</a>
</h6>
<small class="text-success d-flex align-items-center gap-1">
<span class="rounded-circle bg-success" style="width: 8px; height: 8px;"></span> Online
</small>
</div>
<div class="ms-auto d-flex gap-2">
<button id="startAudioCall" class="btn btn-outline-secondary rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;" title="Audio Call">
<i class="bi bi-telephone-fill"></i>
</button>
<button id="startVideoCall" class="btn btn-outline-danger rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;" title="Video Call">
<i class="bi bi-camera-video-fill"></i>
</button>
</div>
</div>
<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 }}
<span class="message-time {% if msg.sender == user %}text-white-50{% else %}text-secondary{% endif %}">
{{ msg.timestamp|date:"g:i a" }}
</span>
</div>
{% empty %}
<div class="text-center my-auto text-secondary">
<p class="mb-0">No messages yet. Say hi!</p>
</div>
{% endfor %}
</div>
<form method="post" class="d-flex gap-2">
{% 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>
</form>
</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">
<div class="modal-content bg-dark border-0 overflow-hidden rounded-4">
<div class="modal-body p-0">
<div id="videoGrid">
<div class="video-wrapper" id="localVideoWrapper">
<video id="localVideo" autoplay muted playsinline></video>
<div class="video-label">{% trans "You" %}</div>
</div>
<div class="video-wrapper" id="remoteVideoWrapper" style="display: none;">
<video id="remoteVideo" autoplay playsinline></video>
<div class="video-label" id="remoteLabel">{{ other_user.username }}</div>
</div>
</div>
<div class="call-controls">
<button id="toggleMic" class="btn btn-outline-light rounded-circle d-flex align-items-center justify-content-center" style="width: 45px; height: 45px;"><i class="bi bi-mic-fill"></i></button>
<button id="toggleVideo" class="btn btn-outline-light rounded-circle d-flex align-items-center justify-content-center" style="width: 45px; height: 45px;"><i class="bi bi-camera-video-fill"></i></button>
<button id="endCall" class="btn btn-danger rounded-pill px-4 fw-bold">{% trans "End Call" %}</button>
</div>
</div>
</div>
</div>
</div>
<!-- Incoming Call UI -->
<div id="incomingCallUI" class="position-fixed top-0 start-50 translate-middle-x mt-4 glass-card p-3 shadow-lg border-danger animate__animated animate__fadeInDown" style="display: none; z-index: 9999; min-width: 320px; border: 2px solid var(--pulse-red) !important;">
<div class="d-flex align-items-center gap-3">
<div class="bg-danger bg-opacity-10 p-2 rounded-circle">
<i class="bi bi-telephone-inbound-fill text-danger fs-4"></i>
</div>
<div class="flex-grow-1">
<h6 class="mb-0 fw-bold">{% trans "Incoming Call" %}</h6>
<small class="text-secondary">{{ other_user.username }} {% trans "is calling..." %}</small>
</div>
<div class="d-flex gap-2">
<button id="rejectCall" class="btn btn-light rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;"><i class="bi bi-x-lg"></i></button>
<button id="acceptCall" class="btn btn-danger rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;"><i class="bi bi-check-lg"></i></button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
<script>
// Chat UI logic
const chatMessages = document.getElementById('chatMessages');
chatMessages.scrollTop = chatMessages.scrollHeight;
// Video/Audio Call Logic
const MY_USERNAME = "{{ user.username }}";
const OTHER_USERNAME = "{{ other_user.username }}";
const PEER_ID_PREFIX = "raktapulse_";
// Sanitize username for PeerJS ID (only alphanumeric, -, _)
const sanitizeId = (id) => id.replace(/[^a-zA-Z0-9-_]/g, '_');
// Initialize PeerJS
let peer = new Peer(PEER_ID_PREFIX + sanitizeId(MY_USERNAME), {
debug: 1
});
let localStream;
let currentCall;
const callModal = new bootstrap.Modal(document.getElementById('callModal'));
const incomingCallUI = document.getElementById('incomingCallUI');
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');
const remoteVideoWrapper = document.getElementById('remoteVideoWrapper');
// Handle Peer Open
peer.on('open', (id) => {
console.log('My peer ID is: ' + id);
});
// Handle incoming calls
peer.on('call', (call) => {
console.log('Incoming call from: ' + call.peer);
// Only accept if it's the other user we are chatting with
if (call.peer === PEER_ID_PREFIX + sanitizeId(OTHER_USERNAME)) {
currentCall = call;
incomingCallUI.style.display = 'block';
// Auto-hide incoming call after 30 seconds if not answered
setTimeout(() => {
if (incomingCallUI.style.display === 'block') {
incomingCallUI.style.display = 'none';
}
}, 30000);
}
});
// Start Call Function
async function startCall(videoEnabled = true) {
try {
localStream = await navigator.mediaDevices.getUserMedia({
video: videoEnabled,
audio: true
});
localVideo.srcObject = localStream;
callModal.show();
const call = peer.call(PEER_ID_PREFIX + sanitizeId(OTHER_USERNAME), localStream);
handleCall(call);
} catch (err) {
console.error('Failed to get local stream', err);
alert('Could not access camera or microphone. Please ensure you have given permission.');
}
}
function handleCall(call) {
currentCall = call;
call.on('stream', (remoteStream) => {
console.log('Received remote stream');
remoteVideo.srcObject = remoteStream;
remoteVideoWrapper.style.display = 'block';
});
call.on('close', () => {
endCall();
});
call.on('error', (err) => {
console.error('Call error:', err);
endCall();
});
}
// Button Listeners
document.getElementById('startVideoCall').addEventListener('click', () => startCall(true));
document.getElementById('startAudioCall').addEventListener('click', () => startCall(false));
document.getElementById('acceptCall').addEventListener('click', async () => {
incomingCallUI.style.display = 'none';
try {
localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
localVideo.srcObject = localStream;
callModal.show();
currentCall.answer(localStream);
handleCall(currentCall);
} catch (err) {
console.error('Failed to get local stream', err);
currentCall.close();
alert('Could not access camera or microphone.');
}
});
document.getElementById('rejectCall').addEventListener('click', () => {
incomingCallUI.style.display = 'none';
if (currentCall) currentCall.close();
});
document.getElementById('endCall').addEventListener('click', () => {
endCall();
});
function endCall() {
if (currentCall) currentCall.close();
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
}
callModal.hide();
remoteVideoWrapper.style.display = 'none';
localVideo.srcObject = null;
remoteVideo.srcObject = null;
}
// Toggle Mic/Video
document.getElementById('toggleMic').addEventListener('click', function() {
if (!localStream) return;
const audioTrack = localStream.getAudioTracks()[0];
if (audioTrack) {
audioTrack.enabled = !audioTrack.enabled;
this.innerHTML = audioTrack.enabled ? '<i class="bi bi-mic-fill"></i>' : '<i class="bi bi-mic-mute-fill"></i>';
this.classList.toggle('btn-outline-light');
this.classList.toggle('btn-danger');
}
});
document.getElementById('toggleVideo').addEventListener('click', function() {
if (!localStream) return;
const videoTrack = localStream.getVideoTracks()[0];
if (videoTrack) {
videoTrack.enabled = !videoTrack.enabled;
this.innerHTML = videoTrack.enabled ? '<i class="bi bi-camera-video-fill"></i>' : '<i class="bi bi-camera-video-off-fill"></i>';
this.classList.toggle('btn-outline-light');
this.classList.toggle('btn-danger');
}
});
// Handle cleanup on page unload
window.addEventListener('beforeunload', () => {
endCall();
peer.destroy();
});
</script>
{% endblock %}

View File

@ -79,7 +79,17 @@
</span>
<p class="mb-0 text-muted extra-small" style="font-size: 0.7rem;">Phone: {{ donor.phone }}</p>
</div>
<a href="tel:{{ donor.phone }}" class="btn btn-outline-danger btn-sm px-3 rounded-pill">Contact Now</a>
<div class="d-flex gap-2">
{% if donor.user %}
<a href="{% url 'public_profile' donor.user.username %}" class="btn btn-outline-secondary btn-sm rounded-pill" title="View Profile">
<i class="bi bi-person-circle"></i>
</a>
<a href="{% url 'chat' donor.user.username %}" class="btn btn-outline-danger btn-sm rounded-pill" title="Send Message">
<i class="bi bi-chat-dots-fill"></i>
</a>
{% endif %}
<a href="tel:{{ donor.phone }}" class="btn btn-danger btn-sm px-3 rounded-pill">Call</a>
</div>
</div>
</div>
{% endfor %}

View File

@ -50,7 +50,7 @@
</p>
{% endif %}
<a href="{% url 'request_blood' %}?hospital={{ hospital.name|urlencode }}" class="btn btn-outline-danger btn-sm w-100 rounded-pill">Request Blood Here</a>
<a href="{% url 'request_blood' %}?hospital={{ hospital.name|urlencode }}" class="btn btn-outline-danger btn-sm w-100 rounded-pill"><i class="bi bi-plus-circle me-1"></i> Request Blood Here</a>
</div>
</div>
</div>

View File

@ -0,0 +1,54 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}Inbox - RaktaPulse{% endblock %}
{% block content %}
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Messages</h2>
</div>
<div class="glass-card p-0 overflow-hidden">
{% if conversations %}
<div class="list-group list-group-flush">
{% for conv in conversations %}
<a href="{% url 'chat' conv.user.username %}" class="list-group-item list-group-item-action p-4 border-0 border-bottom d-flex align-items-center gap-3">
<div class="rounded-circle overflow-hidden border border-danger-subtle flex-shrink-0" style="width: 60px; height: 60px;">
{% if conv.user.profile.profile_pic %}
<img src="{{ conv.user.profile.profile_pic.url }}" alt="{{ conv.user.username }}" class="w-100 h-100 object-fit-cover">
{% else %}
<div class="bg-danger bg-opacity-10 w-100 h-100 d-flex align-items-center justify-content-center">
<i class="bi bi-person-fill text-danger fs-4"></i>
</div>
{% endif %}
</div>
<div class="flex-grow-1 overflow-hidden">
<div class="d-flex justify-content-between align-items-center mb-1">
<h6 class="fw-bold mb-0 text-dark">{{ conv.user.first_name }} {{ conv.user.last_name|default:conv.user.username }}</h6>
<small class="text-secondary">{{ conv.last_message.timestamp|date:"M d, g:i a" }}</small>
</div>
<p class="text-secondary mb-0 text-truncate {% if not conv.last_message.is_read and conv.last_message.receiver == user %}fw-bold text-dark{% endif %}">
{% if conv.last_message.sender == user %}You: {% endif %}
{{ conv.last_message.content }}
</p>
</div>
{% if not conv.last_message.is_read and conv.last_message.receiver == user %}
<div class="bg-danger rounded-circle" style="width: 10px; height: 10px;"></div>
{% endif %}
</a>
{% endfor %}
</div>
{% else %}
<div class="p-5 text-center">
<div class="bg-light rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 80px; height: 80px;">
<i class="bi bi-chat-dots text-secondary fs-1"></i>
</div>
<h5 class="text-dark">No messages yet</h5>
<p class="text-secondary">Start a conversation with a donor or requester!</p>
<a href="{% url 'donor_list' %}" class="btn btn-danger px-4 mt-2">Find Donors</a>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -257,7 +257,14 @@
</span>
<p class="mb-0 text-muted extra-small" style="font-size: 0.7rem;">Last Donated: {{ donor.last_donation_date|default:"Never" }}</p>
</div>
<a href="tel:{{ donor.phone }}" class="btn btn-outline-danger btn-sm px-3 rounded-pill">Contact</a>
<div class="d-flex gap-2">
{% if donor.user %}
<a href="{% url 'chat' donor.user.username %}" class="btn btn-outline-danger btn-sm rounded-pill" title="Message">
<i class="bi bi-chat-dots-fill"></i>
</a>
{% endif %}
<a href="tel:{{ donor.phone }}" class="btn btn-danger btn-sm px-3 rounded-pill">Call</a>
</div>
</div>
</div>
{% empty %}
@ -312,7 +319,14 @@
<p class="text-secondary extra-small mb-2"><i class="bi bi-hospital me-1"></i> {{ req.hospital }}</p>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted" style="font-size: 0.7rem;">{{ req.created_at|timesince }} ago</small>
<a href="tel:{{ req.contact_number }}" class="btn btn-link text-danger p-0 text-decoration-none small">Help Now →</a>
<div class="d-flex gap-2 align-items-center">
{% if req.user %}
<a href="{% url 'chat' req.user.username %}" class="text-secondary" title="Message">
<i class="bi bi-chat-dots-fill"></i>
</a>
{% endif %}
<a href="tel:{{ req.contact_number }}" class="btn btn-link text-danger p-0 text-decoration-none small">Help Now →</a>
</div>
</div>
</div>
{% empty %}

View File

@ -0,0 +1,72 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{{ profile_user.username }}'s Profile - RaktaPulse{% endblock %}
{% block content %}
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="glass-card">
<div class="text-center mb-4">
<div class="rounded-circle overflow-hidden border border-danger-subtle d-inline-block p-1 mb-3" style="width: 150px; height: 150px;">
{% if profile.profile_pic %}
<img src="{{ profile.profile_pic.url }}" alt="{{ profile_user.username }}" class="w-100 h-100 object-fit-cover rounded-circle">
{% else %}
<div class="bg-danger bg-opacity-10 w-100 h-100 d-flex align-items-center justify-content-center rounded-circle">
<i class="bi bi-person-fill text-danger" style="font-size: 5rem;"></i>
</div>
{% endif %}
</div>
<h2 class="fw-bold mb-0">{{ profile_user.first_name }} {{ profile_user.last_name }}</h2>
<p class="text-secondary mb-3">@{{ profile_user.username }}</p>
{% if donor %}
<div class="d-flex justify-content-center gap-2 mb-4">
<span class="badge bg-danger fs-5 px-3">{{ donor.blood_group }}</span>
{% if donor.is_available %}
<span class="badge bg-success d-flex align-items-center"><i class="bi bi-check-circle-fill me-1"></i> Available</span>
{% else %}
<span class="badge bg-secondary d-flex align-items-center"><i class="bi bi-clock-fill me-1"></i> Not Available</span>
{% endif %}
</div>
{% endif %}
</div>
<div class="row g-4 mb-4">
<div class="col-sm-6">
<div class="p-3 bg-light rounded-3 h-100">
<h6 class="text-secondary fw-bold small text-uppercase mb-2">Location</h6>
<p class="mb-0"><i class="bi bi-geo-alt-fill text-danger me-2"></i>{{ profile.location|default:"Not specified" }}</p>
</div>
</div>
<div class="col-sm-6">
<div class="p-3 bg-light rounded-3 h-100">
<h6 class="text-secondary fw-bold small text-uppercase mb-2">Member Since</h6>
<p class="mb-0"><i class="bi bi-calendar-event-fill text-danger me-2"></i>{{ profile_user.date_joined|date:"F Y" }}</p>
</div>
</div>
</div>
<div class="mb-4">
<h5 class="fw-bold mb-3">About</h5>
<div class="p-3 border rounded-3 bg-light-subtle">
{{ profile.bio|default:"No bio provided."|linebreaks }}
</div>
</div>
<div class="d-grid gap-2">
{% if user.is_authenticated and user != profile_user %}
<a href="{% url 'chat' profile_user.username %}" class="btn btn-danger py-2">
<i class="bi bi-chat-dots-fill me-2"></i> Send Message
</a>
{% endif %}
<a href="{% url 'donor_list' %}" class="btn btn-outline-secondary py-2">
<i class="bi bi-arrow-left me-2"></i> Back to Donors
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -6,7 +6,7 @@ from .views import (
vaccination_dashboard, add_vaccination, live_map,
request_blood, profile, volunteer_for_request,
complete_donation, notifications_view,
register_donor, hospital_list
register_donor, hospital_list, public_profile, inbox, chat
)
urlpatterns = [
@ -15,6 +15,9 @@ urlpatterns = [
path("logout/", logout_view, name="logout"),
path("register/", register_view, name="register"),
path("profile/", profile, name="profile"),
path("profile/<str:username>/", public_profile, name="public_profile"),
path("inbox/", inbox, name="inbox"),
path("chat/<str:username>/", chat, name="chat"),
path("donors/", donor_list, name="donor_list"),
path("requests/", blood_request_list, name="blood_request_list"),
path("banks/", blood_bank_list, name="blood_bank_list"),

View File

@ -1,14 +1,16 @@
import os
import platform
import math
from django.db import models
from django.db.models import Q
from django.shortcuts import render, redirect
from django.contrib.auth import login, logout, authenticate
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.utils import timezone
from .models import Donor, BloodRequest, BloodBank, VaccineRecord, UserProfile, BLOOD_GROUPS, DonationEvent, Notification, Hospital
from django.contrib.auth.models import User
from .models import Donor, BloodRequest, BloodBank, VaccineRecord, UserProfile, BLOOD_GROUPS, DonationEvent, Notification, Hospital, Message
from .forms import UserUpdateForm, ProfileUpdateForm, UserRegisterForm
def hospital_list(request):
@ -171,7 +173,7 @@ def home(request):
if request.user.is_authenticated:
# Get active involvements (where user is donor or requester)
involved_events = DonationEvent.objects.filter(
(models.Q(donor_user=request.user) | models.Q(request__user=request.user)),
(Q(donor_user=request.user) | Q(request__user=request.user)),
is_completed=False
)
context["involved_events"] = involved_events
@ -434,3 +436,63 @@ def register_donor(request):
messages.error(request, "Please fill in all required fields.")
return render(request, 'core/register_donor.html', {'blood_groups': [g[0] for g in BLOOD_GROUPS]})
def public_profile(request, username):
user = User.objects.get(username=username)
profile = user.profile
donor_profile = getattr(user, 'donor_profile', None)
context = {
'profile_user': user,
'profile': profile,
'donor': donor_profile,
}
return render(request, 'core/public_profile.html', context)
@login_required
def inbox(request):
# Get all users the current user has messaged or received messages from
sent_to = Message.objects.filter(sender=request.user).values_list('receiver', flat=True)
received_from = Message.objects.filter(receiver=request.user).values_list('sender', flat=True)
user_ids = set(list(sent_to) + list(received_from))
users = User.objects.filter(id__in=user_ids)
# Get last message for each conversation
conversations = []
for user in users:
last_message = Message.objects.filter(
(Q(sender=request.user) & Q(receiver=user)) |
(Q(sender=user) & Q(receiver=request.user))
).order_by('-timestamp').first()
conversations.append({
'user': user,
'last_message': last_message
})
conversations.sort(key=lambda x: x['last_message'].timestamp, reverse=True)
return render(request, 'core/inbox.html', {'conversations': conversations})
@login_required
def chat(request, username):
other_user = User.objects.get(username=username)
if request.method == "POST":
content = request.POST.get('content')
if content:
Message.objects.create(
sender=request.user,
receiver=other_user,
content=content
)
return redirect('chat', username=username)
messages = Message.objects.filter(
(Q(sender=request.user) & Q(receiver=other_user)) |
(Q(sender=other_user) & Q(receiver=request.user))
).order_by('timestamp')
# Mark as read
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})