Autosave: 20260218-073239
This commit is contained in:
parent
800e9db512
commit
dba5db4b7d
Binary file not shown.
Binary file not shown.
@ -193,3 +193,6 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
LOGIN_URL = '/login/'
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = BASE_DIR / 'media'
|
||||
|
||||
@ -28,3 +28,4 @@ urlpatterns = [
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -2,6 +2,18 @@ from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from .models import UserProfile, BLOOD_GROUPS
|
||||
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
|
||||
class UserRegisterForm(UserCreationForm):
|
||||
email = forms.EmailField(required=True)
|
||||
blood_group = forms.ChoiceField(choices=BLOOD_GROUPS, required=True)
|
||||
location = forms.CharField(max_length=255, required=True)
|
||||
phone = forms.CharField(max_length=20, required=True)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username', 'email']
|
||||
|
||||
class UserUpdateForm(forms.ModelForm):
|
||||
email = forms.EmailField()
|
||||
|
||||
@ -18,10 +30,11 @@ class UserUpdateForm(forms.ModelForm):
|
||||
class ProfileUpdateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = ['bio', 'location', 'phone', 'blood_group']
|
||||
fields = ['bio', 'location', 'phone', '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'}),
|
||||
'blood_group': forms.Select(attrs={'class': 'form-control'}, choices=BLOOD_GROUPS),
|
||||
'profile_pic': forms.FileInput(attrs={'class': 'form-control'}),
|
||||
}
|
||||
|
||||
0
core/management/__init__.py
Normal file
0
core/management/__init__.py
Normal file
BIN
core/management/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
core/management/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
0
core/management/commands/__init__.py
Normal file
0
core/management/commands/__init__.py
Normal file
BIN
core/management/commands/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
core/management/commands/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
46
core/management/commands/cleanup_requests.py
Normal file
46
core/management/commands/cleanup_requests.py
Normal file
@ -0,0 +1,46 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
from core.models import BloodRequest
|
||||
from datetime import timedelta
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Deletes old blood requests based on urgency and status'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
now = timezone.now()
|
||||
|
||||
# Define thresholds (days)
|
||||
thresholds = {
|
||||
'CRITICAL': 3,
|
||||
'URGENT': 7,
|
||||
'NORMAL': 15,
|
||||
}
|
||||
|
||||
deleted_count = 0
|
||||
|
||||
# 1. Delete by Urgency
|
||||
for urgency, days in thresholds.items():
|
||||
cutoff = now - timedelta(days=days)
|
||||
old_requests = BloodRequest.objects.filter(
|
||||
urgency=urgency,
|
||||
created_at__lt=cutoff
|
||||
)
|
||||
count = old_requests.count()
|
||||
if count > 0:
|
||||
old_requests.delete()
|
||||
self.stdout.write(self.style.SUCCESS(f'Deleted {count} {urgency} requests older than {days} days.'))
|
||||
deleted_count += count
|
||||
|
||||
# 2. Delete non-Active requests older than 7 days
|
||||
cutoff_inactive = now - timedelta(days=7)
|
||||
inactive_requests = BloodRequest.objects.exclude(status='Active').filter(created_at__lt=cutoff_inactive)
|
||||
count_inactive = inactive_requests.count()
|
||||
if count_inactive > 0:
|
||||
inactive_requests.delete()
|
||||
self.stdout.write(self.style.SUCCESS(f'Deleted {count_inactive} non-active requests older than 7 days.'))
|
||||
deleted_count += count_inactive
|
||||
|
||||
if deleted_count == 0:
|
||||
self.stdout.write(self.style.SUCCESS('No old requests to delete.'))
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS(f'Cleanup complete. Total deleted: {deleted_count}'))
|
||||
47
core/management/commands/seed_hospitals.py
Normal file
47
core/management/commands/seed_hospitals.py
Normal file
@ -0,0 +1,47 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from core.models import Hospital
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Seed the database with Kathmandu hospitals and coordinates'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
hospitals = [
|
||||
{"name": "Bir Hospital", "location": "Kanti Path, Kathmandu", "phone": "01-4221988", "lat": 27.7058, "lng": 85.3135},
|
||||
{"name": "Kathmandu Valley Hospital", "location": "Bagh darbar marg, Kathmandu", "phone": "01-4255330", "lat": 27.7015, "lng": 85.3115},
|
||||
{"name": "Civil Service Hospital of Nepal", "location": "Minbhawan marg, Kathmandu", "phone": "01-4793000", "website": "https://csh.gov.np/ne", "lat": 27.6841, "lng": 85.3385},
|
||||
{"name": "Venus Hospital", "location": "Devkota Sadak, Kathmandu", "phone": "01-4475120", "lat": 27.6945, "lng": 85.3415},
|
||||
{"name": "Vayodha Hospital", "location": "Balkhu, Kathmandu", "phone": "01-4281666", "website": "https://www.vayodhahospitals.com/", "lat": 27.6855, "lng": 85.2935},
|
||||
{"name": "Grande City Hospital", "location": "Kanti path, Kathmandu", "phone": "01-4163500", "website": "http://grandecityhospital.com/", "lat": 27.7065, "lng": 85.3135},
|
||||
{"name": "Teaching Hospital", "location": "Maharajgunj, Kathmandu", "phone": "01-4412303", "website": "http://iom.edu.np/", "lat": 27.7351, "lng": 85.3315},
|
||||
{"name": "Kathmandu Hospital", "location": "Tripureshwor marg, Kathmandu", "phone": "01-4229656", "lat": 27.6935, "lng": 85.3145},
|
||||
{"name": "Kathmandu Neuro & General Hospital", "location": "Bagh Durbar marg, Kathmandu", "phone": "01-5327735", "lat": 27.7018, "lng": 85.3118},
|
||||
{"name": "Teku Hospital", "location": "Teku, Kathmandu", "phone": "01-4253396", "website": "http://www.stidh.gov.np/", "lat": 27.6961, "lng": 85.3025},
|
||||
{"name": "Nepal Eye Hospital", "location": "Tripureswor, Kathmandu", "phone": "01-4260813", "website": "https://www.nepaleyehospital.org/", "lat": 27.6940, "lng": 85.3140},
|
||||
{"name": "Everest Hospital Pvt. Ltd.", "location": "Kathmandu", "phone": "01-4793024", "website": "http://everesthospital.org.np/", "lat": 27.6925, "lng": 85.3345},
|
||||
{"name": "Om Hospital & Research Center", "location": "Chabil, Kathmandu", "phone": "01-4476225", "website": "https://omhospitalnepal.com/", "lat": 27.7175, "lng": 85.3485},
|
||||
{"name": "Annapurna Neuro Hospital", "location": "Maitighar mandala", "phone": "01-4256656", "website": "https://www.annapurnahospitals.com", "lat": 27.6945, "lng": 85.3215},
|
||||
{"name": "Norvic International Hospital", "location": "Kathmandu", "phone": "01-5970032", "website": "https://www.norvichospital.com/", "lat": 27.6897, "lng": 85.3235},
|
||||
{"name": "Blue Cross Hospital", "location": "Tripura marga, Kathmandu", "phone": "01-4262027", "lat": 27.6948, "lng": 85.3148},
|
||||
{"name": "Paropakar Maternity and Womens’ Hospital", "location": "Thapathali, Kathmandu", "phone": "01-4261363", "lat": 27.6915, "lng": 85.3215},
|
||||
{"name": "ERA INTERNATIONAL HOSPITAL Pvt. Ltd.", "location": "Kathmandu", "phone": "01-4352447", "website": "https://www.era-hospital.com/", "lat": 27.7215, "lng": 85.3015},
|
||||
{"name": "Birendra Military Hospital", "location": "Kathmandu", "phone": "01-4271941", "website": "https://birendrahospital.nepalarmy.mil.np/", "lat": 27.7085, "lng": 85.2815},
|
||||
{"name": "Capital Hospital", "location": "Kathmandu", "phone": "01-4168222", "lat": 27.7045, "lng": 85.3215},
|
||||
{"name": "Valley Maternity Nursing Home", "location": "Kathmandu", "phone": "01-4420224", "lat": 27.7125, "lng": 85.3245},
|
||||
{"name": "Medicare National Hospital & Research Center", "location": "Ring road, kathmandu", "phone": "01-4467067", "website": "http://medicarehosp.com/", "lat": 27.7145, "lng": 85.3415},
|
||||
{"name": "CIWEC Hospital Pvt. Ltd.", "location": "Kathmandu", "phone": "01-4435232", "lat": 27.7165, "lng": 85.3215},
|
||||
{"name": "Nepal-Bharat Maitri Hospital", "location": "Dakshinha murti marga, Kathmandu", "phone": "01-5241288", "lat": 27.7185, "lng": 85.3515},
|
||||
{"name": "Kantipur Hospital", "location": "Shriganesh Marg, Tripureshwor", "phone": "01-4111", "lat": 27.6938, "lng": 85.3142},
|
||||
]
|
||||
|
||||
for h_data in hospitals:
|
||||
Hospital.objects.update_or_create(
|
||||
name=h_data['name'],
|
||||
defaults={
|
||||
'location': h_data['location'],
|
||||
'phone': h_data['phone'],
|
||||
'website': h_data.get('website', ''),
|
||||
'latitude': h_data.get('lat'),
|
||||
'longitude': h_data.get('lng'),
|
||||
}
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f'Successfully updated {len(hospitals)} hospitals with coordinates.'))
|
||||
16
core/migrations/0010_delete_feedback.py
Normal file
16
core/migrations/0010_delete_feedback.py
Normal file
@ -0,0 +1,16 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-18 05:57
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0009_bloodrequest_user_donor_user_donationevent_feedback_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='Feedback',
|
||||
),
|
||||
]
|
||||
23
core/migrations/0011_hospital.py
Normal file
23
core/migrations/0011_hospital.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-18 06:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0010_delete_feedback'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Hospital',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('location', models.CharField(max_length=255)),
|
||||
('phone', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('website', models.URLField(blank=True, null=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
23
core/migrations/0012_hospital_latitude_hospital_longitude.py
Normal file
23
core/migrations/0012_hospital_latitude_hospital_longitude.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-18 06:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0011_hospital'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='hospital',
|
||||
name='latitude',
|
||||
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='hospital',
|
||||
name='longitude',
|
||||
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
|
||||
),
|
||||
]
|
||||
18
core/migrations/0013_userprofile_profile_pic.py
Normal file
18
core/migrations/0013_userprofile_profile_pic.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-18 07:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0012_hospital_latitude_hospital_longitude'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='profile_pic',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='profile_pics'),
|
||||
),
|
||||
]
|
||||
BIN
core/migrations/__pycache__/0010_delete_feedback.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0010_delete_feedback.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/migrations/__pycache__/0011_hospital.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0011_hospital.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -18,6 +18,7 @@ class UserProfile(models.Model):
|
||||
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)
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
@ -32,6 +33,16 @@ def create_or_save_user_profile(sender, instance, created, **kwargs):
|
||||
UserProfile.objects.create(user=instance)
|
||||
instance.profile.save()
|
||||
|
||||
@receiver(post_save, sender=UserProfile)
|
||||
def sync_donor_profile(sender, instance, **kwargs):
|
||||
if instance.blood_group:
|
||||
donor, created = Donor.objects.get_or_create(user=instance.user)
|
||||
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.phone = instance.phone
|
||||
donor.save()
|
||||
|
||||
class VaccineRecord(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='vaccine_records')
|
||||
vaccine_name = models.CharField(max_length=100)
|
||||
@ -66,6 +77,17 @@ class Donor(models.Model):
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.blood_group}) - {'Verified' if self.is_verified else 'Unverified'}"
|
||||
|
||||
class Hospital(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
location = models.CharField(max_length=255)
|
||||
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)
|
||||
phone = models.CharField(max_length=100, null=True, blank=True)
|
||||
website = models.URLField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class BloodRequest(models.Model):
|
||||
URGENCY_LEVELS = [
|
||||
('CRITICAL', 'Critical'),
|
||||
@ -126,12 +148,3 @@ class Notification(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return f"Notification for {self.user.username}: {self.message[:20]}..."
|
||||
|
||||
class Feedback(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='feedbacks')
|
||||
content = models.TextField()
|
||||
rating = models.PositiveIntegerField(default=5)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Feedback from {self.user.username} - {self.rating} stars"
|
||||
|
||||
@ -218,6 +218,10 @@
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.dropdown-toggle.no-caret::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sos-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -272,11 +276,10 @@
|
||||
<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 == '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>
|
||||
<li class="{% if request.resolver_match.url_name == 'submit_feedback' %}active{% endif %}"><a href="{% url 'submit_feedback' %}"><i class="bi bi-chat-left-heart-fill"></i> {% trans "Feedback" %}</a></li>
|
||||
{% if user.is_authenticated %}
|
||||
<li class="{% if request.resolver_match.url_name == 'profile' %}active{% endif %}"><a href="{% url 'profile' %}"><i class="bi bi-person-bounding-box"></i> {% trans "My Profile" %}</a></li>
|
||||
<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>
|
||||
{% endif %}
|
||||
<li><a href="/admin/"><i class="bi bi-gear-fill"></i> {% trans "Settings" %}</a></li>
|
||||
@ -340,13 +343,43 @@
|
||||
{{ user.notifications.count }}
|
||||
</span>
|
||||
</a>
|
||||
<a href="{% url 'profile' %}" class="text-decoration-none d-flex align-items-center gap-2">
|
||||
<div class="bg-danger bg-opacity-10 rounded-circle p-1 d-flex align-items-center justify-content-center" style="width: 32px; height: 32px;">
|
||||
<i class="bi bi-person-fill text-danger"></i>
|
||||
</div>
|
||||
<span class="d-none d-sm-inline fw-bold text-dark">{{ user.username }}</span>
|
||||
</a>
|
||||
<a href="{% url 'logout' %}" class="btn btn-outline-danger btn-sm">Logout</a>
|
||||
|
||||
<!-- Profile Dropdown -->
|
||||
<div class="dropdown">
|
||||
<a href="#" class="text-decoration-none d-flex align-items-center gap-2 dropdown-toggle no-caret" id="profileDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<div class="rounded-circle overflow-hidden border border-danger-subtle d-flex align-items-center justify-content-center" style="width: 35px; height: 35px;">
|
||||
{% if user.profile.profile_pic %}
|
||||
<img src="{{ user.profile.profile_pic.url }}" alt="{{ 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>
|
||||
<span class="d-none d-sm-inline fw-bold text-dark">{{ user.username }}</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow border-0 mt-2" aria-labelledby="profileDropdown">
|
||||
<li>
|
||||
<a class="dropdown-item d-flex align-items-center gap-2 py-2" href="{% url 'profile' %}">
|
||||
<i class="bi bi-person-circle text-danger"></i>
|
||||
<span>{% trans "My Profile" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item d-flex align-items-center gap-2 py-2" href="{% url 'profile' %}">
|
||||
<i class="bi bi-pencil-square text-danger"></i>
|
||||
<span>{% trans "Change Name" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider opacity-50"></li>
|
||||
<li>
|
||||
<a class="dropdown-item d-flex align-items-center gap-2 py-2 text-danger" href="{% url 'logout' %}">
|
||||
<i class="bi bi-box-arrow-right"></i>
|
||||
<span>{% trans "Logout" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
|
||||
@ -45,8 +45,17 @@
|
||||
{% for donor in donors %}
|
||||
<div class="donor-row d-flex align-items-center justify-content-between flex-wrap gap-3 p-3 mb-3 border rounded bg-light">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="bg-danger bg-opacity-10 text-danger rounded p-2 fw-bold" style="width: 45px; height: 45px; display: flex; align-items: center; justify-content: center;">
|
||||
{{ donor.blood_group }}
|
||||
<div class="position-relative">
|
||||
{% if donor.user and donor.user.profile.profile_pic %}
|
||||
<img src="{{ donor.user.profile.profile_pic.url }}" alt="{{ donor.name }}" class="rounded-circle" style="width: 50px; height: 50px; object-fit: cover;">
|
||||
<span class="position-absolute bottom-0 end-0 badge rounded-pill bg-danger border border-white" style="font-size: 0.65rem; padding: 0.25rem 0.45rem;">
|
||||
{{ donor.blood_group }}
|
||||
</span>
|
||||
{% else %}
|
||||
<div class="bg-danger bg-opacity-10 text-danger rounded p-2 fw-bold" style="width: 45px; height: 45px; display: flex; align-items: center; justify-content: center;">
|
||||
{{ donor.blood_group }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-0 fw-bold text-dark">
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
{% extends 'core/base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="glass-card p-4">
|
||||
<h2 class="mb-4">{% trans "Share Your Feedback" %}</h2>
|
||||
<p class="text-muted">{% trans "We value your experience. Please let us know how we can improve." %}</p>
|
||||
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="rating" class="form-label">{% trans "Rating" %}</label>
|
||||
<select name="rating" id="rating" class="form-select">
|
||||
<option value="5">⭐⭐⭐⭐⭐ (Excellent)</option>
|
||||
<option value="4">⭐⭐⭐⭐ (Good)</option>
|
||||
<option value="3">⭐⭐⭐ (Average)</option>
|
||||
<option value="2">⭐⭐ (Poor)</option>
|
||||
<option value="1">⭐ (Very Poor)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="content" class="form-label">{% trans "Your Feedback" %}</label>
|
||||
<textarea name="content" id="content" rows="5" class="form-control" placeholder="{% trans 'Tell us about your experience...' %}" required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">{% trans "Submit Feedback" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
92
core/templates/core/hospital_list.html
Normal file
92
core/templates/core/hospital_list.html
Normal file
@ -0,0 +1,92 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Hospitals - RaktaPulse{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid p-0">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<h2 class="brand-font mb-1">Hospitals in Kathmandu</h2>
|
||||
<p class="text-secondary">A comprehensive list of public and private hospitals for blood requests and emergency care.</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-md-end d-flex align-items-center justify-content-md-end">
|
||||
<button id="nearMeBtn" class="btn btn-glass btn-sm rounded-pill">
|
||||
<i class="bi bi-geo-fill me-1"></i> Nearest First
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
{% for hospital in hospitals %}
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="glass-card h-100 d-flex flex-column">
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<h4 class="brand-font mb-1 text-dark">{{ hospital.name }}</h4>
|
||||
{% if hospital.distance %}
|
||||
<span class="badge bg-danger-soft text-danger rounded-pill small">
|
||||
{{ hospital.distance|floatformat:1 }} km away
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="text-secondary small mb-0"><i class="bi bi-geo-alt me-1"></i> {{ hospital.location }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto">
|
||||
{% if hospital.phone %}
|
||||
<p class="mb-2">
|
||||
<a href="tel:{{ hospital.phone }}" class="text-decoration-none text-danger fw-bold">
|
||||
<i class="bi bi-telephone me-2"></i> {{ hospital.phone }}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if hospital.website %}
|
||||
<p class="mb-3">
|
||||
<a href="{{ hospital.website }}" target="_blank" class="text-decoration-none text-primary small">
|
||||
<i class="bi bi-globe me-2"></i> Visit Website
|
||||
</a>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="bi bi-hospital fs-1 text-secondary opacity-25"></i>
|
||||
<p class="text-secondary mt-3">No hospitals registered in the system.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('nearMeBtn').addEventListener('click', function() {
|
||||
if (navigator.geolocation) {
|
||||
this.innerHTML = '<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span> Locating...';
|
||||
navigator.geolocation.getCurrentPosition(function(position) {
|
||||
const lat = position.coords.latitude;
|
||||
const lng = position.coords.longitude;
|
||||
window.location.href = `?lat=${lat}&lng=${lng}`;
|
||||
}, function(error) {
|
||||
console.error("Error getting location: ", error);
|
||||
alert("Could not get your location. Please ensure location services are enabled.");
|
||||
document.getElementById('nearMeBtn').innerHTML = '<i class="bi bi-geo-fill me-1"></i> Nearest First';
|
||||
});
|
||||
} else {
|
||||
alert("Geolocation is not supported by this browser.");
|
||||
}
|
||||
});
|
||||
|
||||
// Automatically check if we should show distance
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.has('lat') && urlParams.has('lng')) {
|
||||
document.getElementById('nearMeBtn').classList.add('btn-danger');
|
||||
document.getElementById('nearMeBtn').classList.remove('btn-glass');
|
||||
document.getElementById('nearMeBtn').innerHTML = '<i class="bi bi-check-circle-fill me-1"></i> Sorted by Proximity';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -224,8 +224,18 @@
|
||||
{% for donor in donors %}
|
||||
<div class="donor-row d-flex align-items-center justify-content-between flex-wrap gap-3">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="blood-group-pill">{{ donor.blood_group }}</div>
|
||||
<div class="position-relative">
|
||||
{% if donor.user and donor.user.profile.profile_pic %}
|
||||
<img src="{{ donor.user.profile.profile_pic.url }}" alt="{{ donor.name }}" class="rounded-circle" style="width: 45px; height: 45px; object-fit: cover;">
|
||||
<span class="position-absolute bottom-0 end-0 badge rounded-pill bg-danger border border-white" style="font-size: 0.6rem; padding: 0.2rem 0.4rem;">
|
||||
{{ donor.blood_group }}
|
||||
</span>
|
||||
{% else %}
|
||||
<div class="blood-group-pill">{{ donor.blood_group }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<h6 class="mb-0 fw-bold text-dark">
|
||||
{{ donor.name }}
|
||||
{% if donor.is_verified %}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
{% extends 'core/base.html' %}
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@ -9,16 +9,20 @@
|
||||
<div class="col-lg-8">
|
||||
<div class="glass-card">
|
||||
<div class="d-flex align-items-center mb-4">
|
||||
<div class="bg-danger rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 60px; height: 60px;">
|
||||
<i class="bi bi-person-fill text-white fs-2"></i>
|
||||
</div>
|
||||
{% if user.profile.profile_pic %}
|
||||
<img src="{{ user.profile.profile_pic.url }}" alt="{{ user.username }}" class="rounded-circle me-3 object-fit-cover" style="width: 64px; height: 64px;">
|
||||
{% else %}
|
||||
<div class="bg-danger rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 60px; height: 60px;">
|
||||
<i class="bi bi-person-fill text-white fs-2"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<h2 class="mb-0 fw-bold">{{ user.username }}</h2>
|
||||
<p class="text-secondary mb-0">Member since {{ user.date_joined|date:"M d, Y" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<h5 class="fw-bold mb-3 border-bottom pb-2">Account Information</h5>
|
||||
<div class="row g-3 mb-4">
|
||||
@ -59,6 +63,10 @@
|
||||
<label class="form-label small fw-bold">Location</label>
|
||||
{{ p_form.location }}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label small fw-bold">Profile Picture</label>
|
||||
{{ p_form.profile_pic }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
{% extends 'core/base.html' %}
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold text-secondary">Hospital Name *</label>
|
||||
<input type="text" name="hospital" class="form-control rounded-3" required>
|
||||
<input type="text" name="hospital" class="form-control rounded-3" value="{{ selected_hospital }}" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold text-secondary">Urgency *</label>
|
||||
|
||||
@ -5,8 +5,8 @@ from .views import (
|
||||
blood_request_list, blood_bank_list, vaccination_info,
|
||||
vaccination_dashboard, add_vaccination, live_map,
|
||||
request_blood, profile, volunteer_for_request,
|
||||
complete_donation, submit_feedback, notifications_view,
|
||||
register_donor
|
||||
complete_donation, notifications_view,
|
||||
register_donor, hospital_list
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@ -25,7 +25,7 @@ urlpatterns = [
|
||||
path("request-blood/", request_blood, name="request_blood"),
|
||||
path("volunteer/<int:request_id>/", volunteer_for_request, name="volunteer_for_request"),
|
||||
path("complete-donation/<int:event_id>/", complete_donation, name="complete_donation"),
|
||||
path("feedback/", submit_feedback, name="submit_feedback"),
|
||||
path("notifications/", notifications_view, name="notifications"),
|
||||
path("register-donor/", register_donor, name="register_donor"),
|
||||
path("hospitals/", hospital_list, name="hospital_list"),
|
||||
]
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import os
|
||||
import platform
|
||||
import math
|
||||
from django.db import models
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib.auth import login, logout, authenticate
|
||||
@ -7,9 +8,32 @@ 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, Feedback
|
||||
from .forms import UserUpdateForm, ProfileUpdateForm
|
||||
import math
|
||||
from .models import Donor, BloodRequest, BloodBank, VaccineRecord, UserProfile, BLOOD_GROUPS, DonationEvent, Notification, Hospital
|
||||
from .forms import UserUpdateForm, ProfileUpdateForm, UserRegisterForm
|
||||
|
||||
def hospital_list(request):
|
||||
user_lat = request.GET.get('lat')
|
||||
user_lng = request.GET.get('lng')
|
||||
|
||||
hospitals = Hospital.objects.all()
|
||||
hospital_list_data = list(hospitals)
|
||||
|
||||
if user_lat and user_lng:
|
||||
try:
|
||||
u_lat = float(user_lat)
|
||||
u_lng = float(user_lng)
|
||||
for h in hospital_list_data:
|
||||
if h.latitude and h.longitude:
|
||||
h.distance = haversine(u_lat, u_lng, float(h.latitude), float(h.longitude))
|
||||
else:
|
||||
h.distance = 999999
|
||||
hospital_list_data.sort(key=lambda x: x.distance)
|
||||
except ValueError:
|
||||
hospital_list_data.sort(key=lambda x: x.name)
|
||||
else:
|
||||
hospital_list_data.sort(key=lambda x: x.name)
|
||||
|
||||
return render(request, 'core/hospital_list.html', {'hospitals': hospital_list_data})
|
||||
|
||||
@login_required
|
||||
def profile(request):
|
||||
@ -18,7 +42,7 @@ def profile(request):
|
||||
|
||||
if request.method == 'POST':
|
||||
u_form = UserUpdateForm(request.POST, instance=request.user)
|
||||
p_form = ProfileUpdateForm(request.POST, instance=profile)
|
||||
p_form = ProfileUpdateForm(request.POST, request.FILES, instance=profile)
|
||||
if u_form.is_valid() and p_form.is_valid():
|
||||
u_form.save()
|
||||
p_form.save()
|
||||
@ -67,13 +91,20 @@ def logout_view(request):
|
||||
|
||||
def register_view(request):
|
||||
if request.method == "POST":
|
||||
form = UserCreationForm(request.POST)
|
||||
form = UserRegisterForm(request.POST)
|
||||
if form.is_valid():
|
||||
user = form.save()
|
||||
profile = user.profile
|
||||
profile.blood_group = form.cleaned_data.get('blood_group')
|
||||
profile.location = form.cleaned_data.get('location')
|
||||
profile.phone = form.cleaned_data.get('phone')
|
||||
profile.save()
|
||||
|
||||
login(request, user)
|
||||
messages.success(request, f"Welcome to RaktaPulse, {user.username}! You are now a registered donor.")
|
||||
return redirect("home")
|
||||
else:
|
||||
form = UserCreationForm()
|
||||
form = UserRegisterForm()
|
||||
return render(request, "core/register.html", {"form": form})
|
||||
|
||||
def home(request):
|
||||
@ -276,6 +307,7 @@ def request_blood(request):
|
||||
context = {
|
||||
'blood_groups': [g[0] for g in BLOOD_GROUPS],
|
||||
'urgency_levels': BloodRequest.URGENCY_LEVELS,
|
||||
'selected_hospital': request.GET.get('hospital', ''),
|
||||
}
|
||||
return render(request, 'core/request_blood.html', context)
|
||||
|
||||
@ -355,12 +387,12 @@ def complete_donation(request, event_id):
|
||||
# Notify both
|
||||
Notification.objects.create(
|
||||
user=event.donor_user,
|
||||
message=f"Thank you for your donation to {event.request.patient_name}! Your feedback is valuable to us."
|
||||
message=f"Thank you for your donation to {event.request.patient_name}!"
|
||||
)
|
||||
if event.request.user:
|
||||
Notification.objects.create(
|
||||
user=event.request.user,
|
||||
message=f"We hope the donation for {event.request.patient_name} went well. Please share your feedback!"
|
||||
message=f"We hope the donation for {event.request.patient_name} went well."
|
||||
)
|
||||
|
||||
messages.success(request, "Donation marked as completed. Thank you!")
|
||||
@ -369,21 +401,6 @@ def complete_donation(request, event_id):
|
||||
|
||||
return redirect('home')
|
||||
|
||||
@login_required
|
||||
def submit_feedback(request):
|
||||
if request.method == "POST":
|
||||
content = request.POST.get('content')
|
||||
rating = request.POST.get('rating')
|
||||
if content:
|
||||
Feedback.objects.create(
|
||||
user=request.user,
|
||||
content=content,
|
||||
rating=rating if rating else 5
|
||||
)
|
||||
messages.success(request, "Thank you for your feedback!")
|
||||
return redirect('home')
|
||||
return render(request, 'core/feedback.html')
|
||||
|
||||
@login_required
|
||||
def notifications_view(request):
|
||||
notifications = Notification.objects.filter(user=request.user).order_by('-created_at')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user