Autosave: 20260301-130550
This commit is contained in:
parent
03ebc3a83e
commit
1c8a1cd56f
Binary file not shown.
Binary file not shown.
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user