Autosave: 20260301-130550

This commit is contained in:
Flatlogic Bot 2026-03-01 13:05:50 +00:00
parent 03ebc3a83e
commit 1c8a1cd56f
13 changed files with 139 additions and 113 deletions

View File

@ -76,7 +76,13 @@
</a>
{% endif %}
{% if user.is_authenticated and user.donor_profile and req.user != user %}
<a href="{% url 'volunteer_for_request' req.id %}" class="btn btn-sm btn-outline-danger px-3 rounded-pill">Volunteer</a>
{% if can_volunteer %}
<a href="{% url 'volunteer_for_request' req.id %}" class="btn btn-sm btn-outline-danger px-3 rounded-pill">Volunteer</a>
{% else %}
<button class="btn btn-sm btn-outline-secondary px-3 rounded-pill" disabled title="Eligible to volunteer in {{ days_until_eligible }} days">
Ineligible ({{ days_until_eligible }}d)
</button>
{% endif %}
{% endif %}
<a href="tel:{{ req.contact_number }}" class="btn btn-sm btn-danger px-3 rounded-pill">Call</a>
</div>

View File

@ -115,13 +115,28 @@
<span class="rounded-circle bg-success" style="width: 8px; height: 8px;"></span> Online
</small>
</div>
<div class="ms-auto d-flex gap-2">
<div class="ms-auto d-flex gap-2 align-items-center">
<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 class="dropdown">
<button class="btn btn-link text-secondary p-0" data-bs-toggle="dropdown">
<i class="bi bi-three-dots-vertical fs-5"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow border-0">
<li>
<form action="{% url 'delete_messages' other_user.username %}" method="POST" onsubmit="return confirm('Are you sure you want to delete all messages in this conversation?');">
{% csrf_token %}
<button type="submit" class="dropdown-item text-danger d-flex align-items-center gap-2">
<i class="bi bi-trash"></i> Delete Conversation
</button>
</form>
</li>
</ul>
</div>
</div>
</div>

View File

@ -84,7 +84,7 @@
</div>
<div>
<div class="fw-bold">{{ donation.donor_user.username }}</div>
<div class="text-muted small">Verified Hero</div>
<div class="text-muted small">Verified Champion</div>
</div>
</div>
</td>

View File

@ -78,8 +78,8 @@
</div>
<div class="d-flex align-items-center gap-4">
<div class="text-end d-none d-sm-block">
<span class="badge {% if donor.is_available %}bg-success{% else %}bg-secondary{% endif %} bg-opacity-10 text-{% if donor.is_available %}success{% else %}secondary{% endif %} mb-1">
{% if donor.is_available %}Available{% else %}Unavailable{% endif %}
<span class="badge {% if donor.on_break %}bg-warning{% elif donor.is_available %}bg-success{% else %}bg-secondary{% endif %} bg-opacity-10 text-{% if donor.on_break %}warning{% elif donor.is_available %}success{% else %}secondary{% endif %} mb-1">
{% if donor.on_break %}On Break ({{ donor.days_remaining }}d){% elif donor.is_available %}Available{% else %}Unavailable{% endif %}
</span>
<p class="mb-0 text-muted extra-small" style="font-size: 0.7rem;">Phone: {{ donor.phone }}</p>
</div>

View File

@ -13,30 +13,40 @@
{% 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 class="list-group-item p-0 border-0 border-bottom d-flex align-items-center">
<a href="{% url 'chat' conv.user.username %}" class="list-group-item-action p-4 d-flex align-items-center gap-3 flex-grow-1 text-decoration-none">
<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>
<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 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>
<div class="pe-4">
<form action="{% url 'delete_messages' conv.user.username %}" method="POST" onsubmit="return confirm('Delete conversation with {{ conv.user.username }}?');">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-outline-light text-secondary border-0 rounded-circle p-2" title="Delete conversation">
<i class="bi bi-trash fs-5"></i>
</button>
</form>
</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>
</div>
{% endfor %}
</div>
{% else %}

View File

@ -111,50 +111,6 @@
border-radius: 10px;
}
.hero-section {
background: linear-gradient(135deg, #1a1a1a, #2d1b1b);
border: 1px solid rgba(255, 215, 0, 0.3);
border-radius: 16px;
padding: 12px 16px;
margin-bottom: 20px;
position: relative;
}
.hero-item {
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 8px 12px;
margin-right: 10px;
border: 1px solid rgba(255, 215, 0, 0.1);
display: inline-flex;
align-items: center;
gap: 10px;
min-width: 180px;
}
.live-pulse {
width: 8px;
height: 8px;
background: #FF4D4D;
border-radius: 50%;
display: inline-block;
margin-right: 5px;
box-shadow: 0 0 0 rgba(255, 77, 77, 0.4);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(255, 77, 77, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(255, 77, 77, 0); }
100% { box-shadow: 0 0 0 0 rgba(255, 77, 77, 0); }
}
.heroes-scroll {
display: flex;
overflow-x: auto;
padding: 5px 0;
scrollbar-width: none;
}
.heroes-scroll::-webkit-scrollbar {
display: none;
}
.filter-btn {
background: #ffffff;
border: 1px solid var(--border-color);
@ -292,7 +248,6 @@
<div class="col-lg-8">
<div class="glass-card mb-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="mb-0 brand-font">Hero Community</h4>
<div class="d-flex gap-2">
<button class="filter-btn active" id="btn-list">List View</button>
<button class="filter-btn" id="btn-map">Map View</button>
@ -300,35 +255,6 @@
</div>
</div>
{% if recent_heroes %}
<div class="hero-section">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-white small fw-bold">
<span class="live-pulse"></span>
Live: Today's Heroes (Last 24h)
</span>
<span class="badge bg-warning text-dark extra-small">{{ recent_heroes.count }} Active</span>
</div>
<div class="heroes-scroll">
{% for hero in recent_heroes %}
<div class="hero-item">
{% if hero.donor.user and hero.donor.user.profile.profile_pic %}
<img src="{{ hero.donor.user.profile.profile_pic.url }}" class="rounded-circle" style="width: 30px; height: 30px; object-fit: cover;">
{% else %}
<div class="rounded-circle bg-danger text-white d-flex align-items-center justify-content-center" style="width: 30px; height: 30px; font-size: 0.7rem;">
{{ hero.donor.blood_group }}
</div>
{% endif %}
<div class="overflow-hidden">
<p class="text-white mb-0 small fw-bold text-truncate" style="max-width: 100px;">{{ hero.donor.name }}</p>
<p class="text-warning extra-small mb-0" style="font-size: 0.65rem;">Saved Life {{ hero.date|timesince }} ago</p>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<div id="map-container" style="display: none; height: 400px; border-radius: 12px; margin-bottom: 20px; overflow: hidden; border: 1px solid var(--border-color);">
<div id="map" style="height: 100%;"></div>
</div>

View File

@ -44,7 +44,7 @@
<table class="table table-hover align-middle mb-0">
<thead class="bg-light text-muted small text-uppercase">
<tr>
<th class="ps-4 border-0 py-3">Hero</th>
<th class="ps-4 border-0 py-3">Champion</th>
<th class="border-0 py-3">Impact</th>
<th class="border-0 py-3">Location</th>
<th class="pe-4 border-0 py-3 text-end">Date</th>

View File

@ -174,12 +174,18 @@
</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.');">
<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.');" class="mb-2">
{% csrf_token %}
<button type="submit" class="btn btn-outline-danger btn-sm">
<button type="submit" class="btn btn-outline-danger btn-sm w-100 mb-2">
Clear Personal Information
</button>
</form>
<form action="{% url 'delete_account' %}" method="POST" onsubmit="return confirm('WARNING: Are you sure you want to delete your account? This will permanently remove all your data, including messages and donation history. This action cannot be undone.');">
{% csrf_token %}
<button type="submit" class="btn btn-danger btn-sm w-100">
<i class="bi bi-trash-fill me-1"></i>Delete My Account Permanently
</button>
</form>
</div>
</div>
</div>

View File

@ -11,7 +11,7 @@
<i class="bi bi-person-plus-fill text-white fs-1"></i>
</div>
<h2 class="fw-bold text-danger brand-font mb-2">Join RaktaPulse</h2>
<p class="text-secondary">Be a hero. Start saving lives today.</p>
<p class="text-secondary">Join our mission. Start saving lives today.</p>
</div>
<form method="post" class="needs-validation">

View File

@ -18,6 +18,8 @@ urlpatterns = [
path("chat/<str:username>/", views.chat, name="chat"),
path("notifications/", views.notifications_view, name="notifications"),
path("delete-personal-info/", views.delete_personal_info, name="delete_personal_info"),
path("delete-messages/<str:username>/", views.delete_messages, name="delete_messages"),
path("delete-account/", views.delete_account, name="delete_account"),
# Donor & Blood Management
path("donors/", views.donor_list, name="donor_list"),

View File

@ -172,14 +172,40 @@ def delete_personal_info(request):
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()
# If they have a donor profile, clear that too (or keep it but mark unavailable)
if hasattr(user, 'donor_profile'):
donor = user.donor_profile
donor.phone = ""
donor.location = ""
donor.save()
messages.success(request, "Your non-essential personal information has been cleared.")
return redirect('profile')
@login_required
@require_POST
def delete_messages(request, username):
"""Delete all messages between the current user and another user."""
other_user = User.objects.get(username=username)
Message.objects.filter(
(Q(sender=request.user) & Q(receiver=other_user)) |
(Q(sender=other_user) & Q(receiver=request.user))
).delete()
messages.success(request, f"Conversation with {username} has been deleted.")
return redirect('inbox')
@login_required
@require_POST
def delete_account(request):
"""Delete the user's account and all associated data."""
user = request.user
logout(request) # Logout before deleting to clear session
user.delete()
messages.success(request, "Your account and all associated data have been permanently deleted.")
return redirect('welcome')
def haversine(lat1, lon1, lat2, lon2):
# Radius of the Earth in km
R = 6371.0
@ -244,7 +270,7 @@ def home(request):
# Ensure default badges exist
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='Community Champion', 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()
@ -285,9 +311,9 @@ def home(request):
actual_completed = DonationEvent.objects.filter(is_completed=True).count()
completed_donations = actual_completed + demo_donations
# Find Recent Heroes (Last 24 Hours)
# Find Recent Contributions (Last 24 Hours)
last_24_hours = timezone.now() - timezone.timedelta(hours=24)
recent_heroes = DonationEvent.objects.filter(
recent_contributions = DonationEvent.objects.filter(
is_completed=True,
date__gte=last_24_hours
).select_related('donor').order_by('-date')
@ -327,7 +353,7 @@ def home(request):
"blood_banks": blood_banks,
"blood_groups": [g[0] for g in BLOOD_GROUPS],
"stats": stats,
"recent_heroes": recent_heroes,
"recent_contributions": recent_contributions,
"project_name": "RaktaPulse",
"current_time": timezone.now(),
"myths_vs_facts": myths_vs_facts,
@ -378,6 +404,15 @@ def donor_list(request):
else:
donor_list_data.sort(key=lambda x: (-x.is_verified, x.name))
# Check 90-day availability
for d in donor_list_data:
if d.last_donation_date:
days_since = (timezone.now().date() - d.last_donation_date).days
if days_since < 90:
d.is_available = False
d.on_break = True
d.days_remaining = 90 - days_since
context = {
'donors': donor_list_data,
'blood_groups': [g[0] for g in BLOOD_GROUPS],
@ -391,9 +426,22 @@ def blood_request_list(request):
requests = requests.filter(status=status)
requests = requests.order_by('-created_at')
can_volunteer = True
days_until_eligible = 0
if request.user.is_authenticated:
donor_profile = getattr(request.user, 'donor_profile', None)
if donor_profile and donor_profile.last_donation_date:
days_since = (timezone.now().date() - donor_profile.last_donation_date).days
if days_since < 90:
can_volunteer = False
days_until_eligible = 90 - days_since
context = {
'requests': requests,
'current_status': status,
'can_volunteer': can_volunteer,
'days_until_eligible': days_until_eligible,
}
return render(request, 'core/blood_request_list.html', context)
@ -604,6 +652,14 @@ def volunteer_for_request(request, request_id):
messages.error(request, "You need to be registered as a donor to volunteer.")
return redirect('donor_list')
# Check for 3-month (90 days) restriction
if donor_profile.last_donation_date:
days_since_last_donation = (timezone.now().date() - donor_profile.last_donation_date).days
if days_since_last_donation < 90:
remaining_days = 90 - days_since_last_donation
messages.error(request, f"For your safety, you must wait 3 months (90 days) between donations. You can volunteer again in {remaining_days} days.")
return redirect('blood_request_list')
# Prevent requester from volunteering for their own request
if blood_request.user == request.user:
messages.error(request, "You cannot volunteer for your own blood request.")
@ -642,6 +698,11 @@ def complete_donation(request, event_id):
event.is_completed = True
event.save()
# Update donor's last donation date
if event.donor:
event.donor.last_donation_date = timezone.now().date()
event.donor.save()
# Award Badges Logic
donor_profile = event.donor_user.profile
completed_count = DonationEvent.objects.filter(donor_user=event.donor_user, is_completed=True).count()
@ -652,7 +713,7 @@ def complete_donation(request, event_id):
donor_profile.badges.add(badge)
if completed_count >= 5:
badge = Badge.objects.filter(name='Community Hero').first()
badge = Badge.objects.filter(name='Community Champion').first()
if badge:
donor_profile.badges.add(badge)