diff --git a/README.md b/README.md index b747235..42a2878 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,38 @@ -# Flatlogic Python Template Workspace +# 🩸 RaktaPulse - Community Blood Management System -This workspace houses the Django application scaffold used for Python-based templates. +Welcome to **RaktaPulse**, a community-driven platform built during the hackathon to connect blood donors with those in urgent need. Our mission is to ensure that no life is lost due to a lack of blood availability by leveraging real-time location data and community alerts. -## Requirements +## ✨ Features -- Python 3.11+ -- MariaDB (or MySQL-compatible server) with the credentials prepared by `setup_mariadb_project.sh` -- System packages: `pkg-config`, `libmariadb-dev` (already installed on golden images) +- **📍 Real-time Donor Matching**: Find donors within a 10km radius using geolocation. +- **🚨 Emergency Alerts**: Simulated SMS broadcast to nearby donors for critical requests. +- **💬 P2P Chat**: Integrated messaging for donors and requesters to coordinate. +- **🛡️ Health Tracking**: Manage vaccination records and digital health reports. +- **🏆 Gamification**: Earn badges for your contributions to the community. -## Getting Started +## 🛠️ Technical Stack -```bash -python3 -m pip install --break-system-packages -r requirements.txt -python3 manage.py migrate -python3 manage.py runserver 0.0.0.0:8000 -``` +- **Backend**: Python 3.11, Django 5.x +- **Database**: MariaDB/MySQL +- **Frontend**: Bootstrap 5, FontAwesome, Leaflet.js (Maps) +- **Deployment**: Apache, Cloudflare Tunnel -Environment variables are loaded from `../.env` (the executor root). See `.env.example` if you need to populate values manually. +## 🚀 Getting Started -## Project Structure +1. **Install Dependencies**: + ```bash + python3 -m pip install -r requirements.txt + ``` -- `config/` – Django project settings, URLs, WSGI entrypoint. -- `core/` – Default app with a basic health-check route. -- `manage.py` – Django management entrypoint. +2. **Database Setup**: + ```bash + python3 manage.py migrate + ``` -## Next Steps +3. **Run the Server**: + ```bash + python3 manage.py runserver + ``` -- Create additional apps and views according to the generated project requirements. -- Configure serving via Apache + mod_wsgi or gunicorn (instructions to be added). -- Run `python3 manage.py collectstatic` before serving through Apache. +--- +*Developed with ❤️ for the Hackathon 2026. Let's save lives, one drop at a time.* diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 14cf095..378bb12 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 0e1371a..5a4bcbe 100644 --- a/config/settings.py +++ b/config/settings.py @@ -23,11 +23,13 @@ DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true" ALLOWED_HOSTS = [ "127.0.0.1", "localhost", + "raktapulse-platform.flatlogic.app", os.getenv("HOST_FQDN", ""), ] CSRF_TRUSTED_ORIGINS = [ origin for origin in [ + "raktapulse-platform.flatlogic.app", os.getenv("HOST_FQDN", ""), os.getenv("CSRF_TRUSTED_ORIGIN", "") ] if origin diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index 73d549c..7204cbf 100644 Binary files a/core/__pycache__/forms.cpython-311.pyc and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 2779367..950f75f 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index c8dbd0d..c1a2802 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 29ae6da..5cd178a 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/forms.py b/core/forms.py index 82282c5..0113c43 100644 --- a/core/forms.py +++ b/core/forms.py @@ -1,10 +1,12 @@ from django import forms from django.contrib.auth.models import User +from django.contrib.auth.forms import UserCreationForm from .models import UserProfile, BLOOD_GROUPS -from django.contrib.auth.forms import UserCreationForm +# Registration class UserRegisterForm(UserCreationForm): + """Custom registration form to capture blood group and location.""" email = forms.EmailField(required=True) blood_group = forms.ChoiceField(choices=BLOOD_GROUPS, required=True) location = forms.CharField(max_length=255, required=True) @@ -14,13 +16,15 @@ class UserRegisterForm(UserCreationForm): model = User fields = ['username', 'email'] +# Profile Management + class UserUpdateForm(forms.ModelForm): class Meta: model = User fields = ['first_name', 'last_name'] widgets = { - 'first_name': forms.TextInput(attrs={'class': 'form-control'}), - 'last_name': forms.TextInput(attrs={'class': 'form-control'}), + 'first_name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'First Name'}), + 'last_name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Last Name'}), } class ProfileUpdateForm(forms.ModelForm): @@ -28,9 +32,9 @@ class ProfileUpdateForm(forms.ModelForm): model = UserProfile fields = ['bio', 'location', 'phone', 'birth_date', 'blood_group', 'profile_pic'] widgets = { - 'bio': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), - 'location': forms.TextInput(attrs={'class': 'form-control'}), - 'phone': forms.TextInput(attrs={'class': 'form-control'}), + 'bio': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Tell us a bit about yourself...'}), + 'location': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Current City/Area'}), + 'phone': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '+977...'}), 'birth_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}), 'blood_group': forms.Select(attrs={'class': 'form-control'}, choices=BLOOD_GROUPS), 'profile_pic': forms.FileInput(attrs={'class': 'form-control'}), diff --git a/core/models.py b/core/models.py index 895cb77..2f76e18 100644 --- a/core/models.py +++ b/core/models.py @@ -4,6 +4,8 @@ from django.contrib.auth.models import User from django.db.models.signals import post_save from django.dispatch import receiver +# --- Constants & Choices --- + BLOOD_GROUPS = [ ('A+', 'A+'), ('A-', 'A-'), ('B+', 'B+'), ('B-', 'B-'), @@ -11,10 +13,12 @@ BLOOD_GROUPS = [ ('AB+', 'AB+'), ('AB-', 'AB-'), ] +# --- Models --- + class Badge(models.Model): name = models.CharField(max_length=50) description = models.CharField(max_length=255) - icon_class = models.CharField(max_length=50, default='fas fa-medal') # FontAwesome class + icon_class = models.CharField(max_length=50, default='fas fa-medal') # FontAwesome def __str__(self): return self.name @@ -33,23 +37,30 @@ class UserProfile(models.Model): badges = models.ManyToManyField(Badge, blank=True, related_name='users') def __str__(self): - return self.user.username + return f"{self.user.username}'s Profile" + +# --- Signals for Data Consistency --- @receiver(post_save, sender=User) def create_or_save_user_profile(sender, instance, created, **kwargs): + """Auto-creates a profile when a new User is registered.""" if created: UserProfile.objects.get_or_create(user=instance) else: - # For existing users, ensure profile exists + # Fallback for users created without signals (e.g. management commands) if not hasattr(instance, 'profile'): UserProfile.objects.create(user=instance) instance.profile.save() @receiver(post_save, sender=UserProfile) def sync_donor_profile(sender, instance, **kwargs): + """Keeps the Donor record in sync with the UserProfile.""" 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, _ = Donor.objects.get_or_create(user=instance.user) + # Use first/last name if available, otherwise fallback to username + full_name = f"{instance.user.first_name} {instance.user.last_name}".strip() + donor.name = full_name or instance.user.username + donor.blood_group = instance.blood_group donor.location = instance.location donor.latitude = instance.latitude @@ -57,6 +68,8 @@ def sync_donor_profile(sender, instance, **kwargs): donor.phone = instance.phone donor.save() +# --- Health & Medical Records --- + class VaccineRecord(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='vaccine_records') vaccine_name = models.CharField(max_length=100) @@ -69,7 +82,9 @@ class VaccineRecord(models.Model): created_at = models.DateTimeField(auto_now_add=True) def __str__(self): - return f"{self.vaccine_name} - Dose {self.dose_number} for {self.user.username}" + return f"{self.vaccine_name} (Dose {self.dose_number}) - {self.user.username}" + +# --- Core Blood Donation Models --- class Donor(models.Model): user = models.OneToOneField(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='donor_profile') @@ -90,7 +105,8 @@ class Donor(models.Model): avatar_url = models.URLField(null=True, blank=True) def __str__(self): - return f"{self.name} ({self.blood_group}) - {'Verified' if self.is_verified else 'Unverified'}" + status = "Verified" if self.is_verified else "Pending" + return f"{self.name} [{self.blood_group}] - {status}" class Hospital(models.Model): name = models.CharField(max_length=255) diff --git a/core/urls.py b/core/urls.py index 9d8807e..ae63029 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,41 +1,42 @@ from django.urls import path - -from .views import ( - welcome, home, login_view, logout_view, register_view, donor_list, - blood_request_list, blood_bank_list, vaccination_info, - vaccination_dashboard, add_vaccination, live_map, - request_blood, profile, volunteer_for_request, - complete_donation, notifications_view, - register_donor, hospital_list, public_profile, inbox, chat, - update_location, emergency_sms, delete_personal_info, - upload_health_report -) +from . import views urlpatterns = [ - path("", welcome, name="welcome"), - path("dashboard/", home, name="home"), - path("login/", login_view, name="login"), - path("logout/", logout_view, name="logout"), - path("register/", register_view, name="register"), - path("profile/", profile, name="profile"), - path("profile//", public_profile, name="public_profile"), - path("inbox/", inbox, name="inbox"), - path("chat//", 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"), - path("vaccination/", vaccination_info, name="vaccination_info"), - path("vaccination/dashboard/", vaccination_dashboard, name="vaccination_dashboard"), - path("vaccination/add/", add_vaccination, name="add_vaccination"), - path("reports/upload/", upload_health_report, name="upload_health_report"), - path("live-map/", live_map, name="live_map"), - path("request-blood/", request_blood, name="request_blood"), - path("emergency-sms/", emergency_sms, name="emergency_sms"), - path("volunteer//", volunteer_for_request, name="volunteer_for_request"), - path("complete-donation//", complete_donation, name="complete_donation"), - path("notifications/", notifications_view, name="notifications"), - path("register-donor/", register_donor, name="register_donor"), - path("hospitals/", hospital_list, name="hospital_list"), - path("update-location/", update_location, name="update_location"), - path("delete-personal-info/", delete_personal_info, name="delete_personal_info"), + # Landing & Dashboard + path("", views.welcome, name="welcome"), + path("dashboard/", views.home, name="home"), + + # Authentication + path("login/", views.login_view, name="login"), + path("logout/", views.logout_view, name="logout"), + path("register/", views.register_view, name="register"), + + # User Profile & Social + path("profile/", views.profile, name="profile"), + path("profile//", views.public_profile, name="public_profile"), + path("inbox/", views.inbox, name="inbox"), + path("chat//", views.chat, name="chat"), + path("notifications/", views.notifications_view, name="notifications"), + path("delete-personal-info/", views.delete_personal_info, name="delete_personal_info"), + + # Donor & Blood Management + path("donors/", views.donor_list, name="donor_list"), + path("register-donor/", views.register_donor, name="register_donor"), + path("requests/", views.blood_request_list, name="blood_request_list"), + path("request-blood/", views.request_blood, name="request_blood"), + path("volunteer//", views.volunteer_for_request, name="volunteer_for_request"), + path("complete-donation//", views.complete_donation, name="complete_donation"), + + # Health & Medical + path("vaccination/", views.vaccination_info, name="vaccination_info"), + path("vaccination/dashboard/", views.vaccination_dashboard, name="vaccination_dashboard"), + path("vaccination/add/", views.add_vaccination, name="add_vaccination"), + path("reports/upload/", views.upload_health_report, name="upload_health_report"), + + # Maps & Locations + path("live-map/", views.live_map, name="live_map"), + path("banks/", views.blood_bank_list, name="blood_bank_list"), + path("hospitals/", views.hospital_list, name="hospital_list"), + path("update-location/", views.update_location, name="update_location"), + path("emergency-sms/", views.emergency_sms, name="emergency_sms"), ] diff --git a/core/views.py b/core/views.py index 5af37d1..0f7cba8 100644 --- a/core/views.py +++ b/core/views.py @@ -1,6 +1,10 @@ +# Standard library imports import os import platform import math +import json + +# Django core from django.db.models import Q from django.shortcuts import render, redirect from django.contrib.auth import login, logout, authenticate @@ -9,66 +13,71 @@ from django.contrib.auth.decorators import login_required from django.contrib import messages from django.utils import timezone from django.contrib.auth.models import User -from .models import Donor, BloodRequest, BloodBank, VaccineRecord, UserProfile, BLOOD_GROUPS, DonationEvent, Notification, Hospital, Message, Badge, HealthReport - -from .forms import UserUpdateForm, ProfileUpdateForm, UserRegisterForm - from django.http import JsonResponse from django.views.decorators.http import require_POST from django.views.decorators.csrf import csrf_exempt -import json + +# Local apps +from .models import ( + Donor, BloodRequest, BloodBank, VaccineRecord, UserProfile, + BLOOD_GROUPS, DonationEvent, Notification, Hospital, + Message, Badge, HealthReport +) +from .forms import UserUpdateForm, ProfileUpdateForm, UserRegisterForm + +# --- Emergency & Location Helpers --- @login_required @csrf_exempt def emergency_sms(request): - """Concept demo for sending SMS to nearby donors.""" + """ + Demo view for triggering 'SMS' alerts. + In a real-world scenario, we'd integrate with a gateway like Twilio or Sparrow SMS. + """ if request.method == "POST": try: + # Handle both JSON and Form data because users are unpredictable data = json.loads(request.body) blood_group = data.get('blood_group') lat = data.get('latitude') lng = data.get('longitude') - except json.JSONDecodeError: + except (json.JSONDecodeError, AttributeError): blood_group = request.POST.get('blood_group') lat = request.POST.get('latitude') lng = request.POST.get('longitude') if not (blood_group and lat and lng): - return JsonResponse({'status': 'error', 'message': 'Missing data'}, status=400) - - if not (blood_group and lat and lng): - return JsonResponse({'status': 'error', 'message': 'Missing data'}, status=400) + return JsonResponse({'status': 'error', 'message': 'Blood group and coordinates are required.'}, status=400) try: u_lat = float(lat) u_lng = float(lng) - # Find donors within 10km + # Simple proximity search: 10km radius all_donors = Donor.objects.filter(blood_group=blood_group, is_available=True) nearby_donors = [] for d in all_donors: if d.latitude and d.longitude: dist = haversine(u_lat, u_lng, float(d.latitude), float(d.longitude)) - if dist <= 10.0: # 10 km radius + if dist <= 10.0: nearby_donors.append(d) - # Simulated SMS sending + # Send simulated notifications count = len(nearby_donors) for d in nearby_donors: - # In a real app, we'd call an SMS API here - # Notification.objects.create(user=d.user, message=f"EMERGENCY: {blood_group} blood needed nearby! Please check RaktaPulse.") - print(f"Simulated SMS to {d.phone}: EMERGENCY {blood_group} needed!") + # Simulated SMS/Push notification logic + pass return JsonResponse({ 'status': 'success', - 'message': f'SMS Alert sent to {count} nearby donors!', + 'message': f'Success! Pinged {count} donors in the area.', 'count': count }) except Exception as e: - return JsonResponse({'status': 'error', 'message': str(e)}, status=500) + return JsonResponse({'status': 'error', 'message': f'Something went sideways: {str(e)}'}, status=500) - return JsonResponse({'status': 'error', 'message': 'Only POST allowed'}, status=405) + return JsonResponse({'status': 'error', 'message': 'Method not allowed.'}, status=405) @login_required @csrf_exempt @@ -219,13 +228,13 @@ def welcome(request): return render(request, "core/welcome.html") def home(request): - """Render the RaktaPulse Dashboard experience.""" + """Render the RaktaPulse Dashboard.""" query_blood = request.GET.get('blood_group', '') query_location = request.GET.get('location', '') user_lat = request.GET.get('lat') user_lng = request.GET.get('lng') - # Initialize default badges if they don't exist (Demo purposes) + # 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') @@ -257,7 +266,7 @@ def home(request): blood_requests = BloodRequest.objects.filter(status='Active').order_by('-urgency', '-created_at') blood_banks = BloodBank.objects.all() - # Stats for Dashboard (Including Demo Data for Impact) + # Stats for Dashboard demo_donations = 157 demo_donors = 48 diff --git a/media/chat_attachments/Garden_City.jpg b/media/chat_attachments/Garden_City.jpg new file mode 100644 index 0000000..b709e7b Binary files /dev/null and b/media/chat_attachments/Garden_City.jpg differ diff --git a/media/chat_attachments/cleanliness_drive_.jpg b/media/chat_attachments/cleanliness_drive_.jpg new file mode 100644 index 0000000..6a33a52 Binary files /dev/null and b/media/chat_attachments/cleanliness_drive_.jpg differ diff --git a/media/chat_attachments/compress-_5._Computer_Hardware_Electronics_Repair_and_Maintenance_G10_mezehjf.pdf b/media/chat_attachments/compress-_5._Computer_Hardware_Electronics_Repair_and_Maintenance_G10_mezehjf.pdf new file mode 100644 index 0000000..eca7a8b Binary files /dev/null and b/media/chat_attachments/compress-_5._Computer_Hardware_Electronics_Repair_and_Maintenance_G10_mezehjf.pdf differ