commit 21ef5153eea9f2aa4ee3c4b9e42e5ae416acf5aa Author: Flatlogic Bot Date: Wed Mar 11 07:41:51 2026 +0000 Initial import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e427ff3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +*/node_modules/ +*/build/ diff --git a/.perm_test_apache b/.perm_test_apache new file mode 100644 index 0000000..e69de29 diff --git a/.perm_test_exec b/.perm_test_exec new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..42a2878 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# 🩸 RaktaPulse - Community Blood Management System + +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. + +## ✨ Features + +- **📍 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. + +## 🛠️ Technical Stack + +- **Backend**: Python 3.11, Django 5.x +- **Database**: MariaDB/MySQL +- **Frontend**: Bootstrap 5, FontAwesome, Leaflet.js (Maps) +- **Deployment**: Apache, Cloudflare Tunnel + +## 🚀 Getting Started + +1. **Install Dependencies**: + ```bash + python3 -m pip install -r requirements.txt + ``` + +2. **Database Setup**: + ```bash + python3 manage.py migrate + ``` + +3. **Run the Server**: + ```bash + python3 manage.py runserver + ``` + +--- +*Developed with ❤️ for the Hackathon 2026. Let's save lives, one drop at a time.* diff --git a/ai/__init__.py b/ai/__init__.py new file mode 100644 index 0000000..37a7b09 --- /dev/null +++ b/ai/__init__.py @@ -0,0 +1,3 @@ +"""Helpers for interacting with the Flatlogic AI proxy from Django code.""" + +from .local_ai_api import LocalAIApi, create_response, request, decode_json_from_response # noqa: F401 diff --git a/ai/local_ai_api.py b/ai/local_ai_api.py new file mode 100644 index 0000000..bcff732 --- /dev/null +++ b/ai/local_ai_api.py @@ -0,0 +1,420 @@ +""" +LocalAIApi — lightweight Python client for the Flatlogic AI proxy. + +Usage (inside the Django workspace): + + from ai.local_ai_api import LocalAIApi + + response = LocalAIApi.create_response({ + "input": [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Summarise this text in two sentences."}, + ], + "text": {"format": {"type": "json_object"}}, + }) + + if response.get("success"): + data = LocalAIApi.decode_json_from_response(response) + # ... + +# Typical successful payload (truncated): +# { +# "id": "resp_xxx", +# "status": "completed", +# "output": [ +# {"type": "reasoning", "summary": []}, +# {"type": "message", "content": [{"type": "output_text", "text": "Your final answer here."}]} +# ], +# "usage": { "input_tokens": 123, "output_tokens": 456 } +# } + +The helper automatically injects the project UUID header and falls back to +reading executor/.env if environment variables are missing. +""" + +from __future__ import annotations + +import json +import os +import time +import ssl +from typing import Any, Dict, Iterable, Optional +from urllib import error as urlerror +from urllib import request as urlrequest + +__all__ = [ + "LocalAIApi", + "create_response", + "request", + "fetch_status", + "await_response", + "extract_text", + "decode_json_from_response", +] + + +_CONFIG_CACHE: Optional[Dict[str, Any]] = None + + +class LocalAIApi: + """Static helpers mirroring the PHP implementation.""" + + @staticmethod + def create_response(params: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + return create_response(params, options or {}) + + @staticmethod + def request(path: Optional[str] = None, payload: Optional[Dict[str, Any]] = None, + options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + return request(path, payload or {}, options or {}) + + @staticmethod + def extract_text(response: Dict[str, Any]) -> str: + return extract_text(response) + + @staticmethod + def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]: + return decode_json_from_response(response) + + +def create_response(params: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Signature compatible with the OpenAI Responses API.""" + options = options or {} + payload = dict(params) + + if not isinstance(payload.get("input"), list) or not payload["input"]: + return { + "success": False, + "error": "input_missing", + "message": 'Parameter "input" is required and must be a non-empty list.', + } + + cfg = _config() + if not payload.get("model"): + payload["model"] = cfg["default_model"] + + initial = request(options.get("path"), payload, options) + if not initial.get("success"): + return initial + + data = initial.get("data") + if isinstance(data, dict) and "ai_request_id" in data: + ai_request_id = data["ai_request_id"] + poll_timeout = int(options.get("poll_timeout", 300)) + poll_interval = int(options.get("poll_interval", 5)) + return await_response(ai_request_id, { + "interval": poll_interval, + "timeout": poll_timeout, + "headers": options.get("headers"), + "timeout_per_call": options.get("timeout"), + }) + + return initial + + +def request(path: Optional[str], payload: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Perform a raw request to the AI proxy.""" + cfg = _config() + options = options or {} + + resolved_path = path or options.get("path") or cfg["responses_path"] + if not resolved_path: + return { + "success": False, + "error": "project_id_missing", + "message": "PROJECT_ID is not defined; cannot resolve AI proxy endpoint.", + } + + project_uuid = cfg["project_uuid"] + if not project_uuid: + return { + "success": False, + "error": "project_uuid_missing", + "message": "PROJECT_UUID is not defined; aborting AI request.", + } + + if "project_uuid" not in payload and project_uuid: + payload["project_uuid"] = project_uuid + + url = _build_url(resolved_path, cfg["base_url"]) + opt_timeout = options.get("timeout") + timeout = int(cfg["timeout"] if opt_timeout is None else opt_timeout) + verify_tls = options.get("verify_tls", cfg["verify_tls"]) + + headers: Dict[str, str] = { + "Content-Type": "application/json", + "Accept": "application/json", + cfg["project_header"]: project_uuid, + } + extra_headers = options.get("headers") + if isinstance(extra_headers, Iterable): + for header in extra_headers: + if isinstance(header, str) and ":" in header: + name, value = header.split(":", 1) + headers[name.strip()] = value.strip() + + body = json.dumps(payload, ensure_ascii=False).encode("utf-8") + return _http_request(url, "POST", body, headers, timeout, verify_tls) + + +def fetch_status(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Fetch status for a queued AI request.""" + cfg = _config() + options = options or {} + + project_uuid = cfg["project_uuid"] + if not project_uuid: + return { + "success": False, + "error": "project_uuid_missing", + "message": "PROJECT_UUID is not defined; aborting status check.", + } + + status_path = _resolve_status_path(ai_request_id, cfg) + url = _build_url(status_path, cfg["base_url"]) + + opt_timeout = options.get("timeout") + timeout = int(cfg["timeout"] if opt_timeout is None else opt_timeout) + verify_tls = options.get("verify_tls", cfg["verify_tls"]) + + headers: Dict[str, str] = { + "Accept": "application/json", + cfg["project_header"]: project_uuid, + } + extra_headers = options.get("headers") + if isinstance(extra_headers, Iterable): + for header in extra_headers: + if isinstance(header, str) and ":" in header: + name, value = header.split(":", 1) + headers[name.strip()] = value.strip() + + return _http_request(url, "GET", None, headers, timeout, verify_tls) + + +def await_response(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Poll status endpoint until the request is complete or timed out.""" + options = options or {} + timeout = int(options.get("timeout", 300)) + interval = int(options.get("interval", 5)) + if interval <= 0: + interval = 5 + per_call_timeout = options.get("timeout_per_call") + + deadline = time.time() + max(timeout, interval) + + while True: + status_resp = fetch_status(ai_request_id, { + "headers": options.get("headers"), + "timeout": per_call_timeout, + "verify_tls": options.get("verify_tls"), + }) + if status_resp.get("success"): + data = status_resp.get("data") or {} + if isinstance(data, dict): + status_value = data.get("status") + if status_value == "success": + return { + "success": True, + "status": 200, + "data": data.get("response", data), + } + if status_value == "failed": + return { + "success": False, + "status": 500, + "error": str(data.get("error") or "AI request failed"), + "data": data, + } + else: + return status_resp + + if time.time() >= deadline: + return { + "success": False, + "error": "timeout", + "message": "Timed out waiting for AI response.", + } + time.sleep(interval) + + +def extract_text(response: Dict[str, Any]) -> str: + """Public helper to extract plain text from a Responses payload.""" + return _extract_text(response) + + +def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """Attempt to decode JSON emitted by the model (handles markdown fences).""" + text = _extract_text(response) + if text == "": + return None + + try: + decoded = json.loads(text) + if isinstance(decoded, dict): + return decoded + except json.JSONDecodeError: + pass + + stripped = text.strip() + if stripped.startswith("```json"): + stripped = stripped[7:] + if stripped.endswith("```"): + stripped = stripped[:-3] + stripped = stripped.strip() + if stripped and stripped != text: + try: + decoded = json.loads(stripped) + if isinstance(decoded, dict): + return decoded + except json.JSONDecodeError: + return None + return None + + +def _extract_text(response: Dict[str, Any]) -> str: + payload = response.get("data") if response.get("success") else response.get("response") + if isinstance(payload, dict): + output = payload.get("output") + if isinstance(output, list): + combined = "" + for item in output: + content = item.get("content") if isinstance(item, dict) else None + if isinstance(content, list): + for block in content: + if isinstance(block, dict) and block.get("type") == "output_text" and block.get("text"): + combined += str(block["text"]) + if combined: + return combined + choices = payload.get("choices") + if isinstance(choices, list) and choices: + message = choices[0].get("message") + if isinstance(message, dict) and message.get("content"): + return str(message["content"]) + if isinstance(payload, str): + return payload + return "" + + +def _config() -> Dict[str, Any]: + global _CONFIG_CACHE # noqa: PLW0603 + if _CONFIG_CACHE is not None: + return _CONFIG_CACHE + + _ensure_env_loaded() + + base_url = os.getenv("AI_PROXY_BASE_URL", "https://flatlogic.com") + project_id = os.getenv("PROJECT_ID") or None + responses_path = os.getenv("AI_RESPONSES_PATH") + if not responses_path and project_id: + responses_path = f"/projects/{project_id}/ai-request" + + _CONFIG_CACHE = { + "base_url": base_url, + "responses_path": responses_path, + "project_id": project_id, + "project_uuid": os.getenv("PROJECT_UUID"), + "project_header": os.getenv("AI_PROJECT_HEADER", "project-uuid"), + "default_model": os.getenv("AI_DEFAULT_MODEL", "gpt-5-mini"), + "timeout": int(os.getenv("AI_TIMEOUT", "30")), + "verify_tls": os.getenv("AI_VERIFY_TLS", "true").lower() not in {"0", "false", "no"}, + } + return _CONFIG_CACHE + + +def _build_url(path: str, base_url: str) -> str: + trimmed = path.strip() + if trimmed.startswith("http://") or trimmed.startswith("https://"): + return trimmed + if trimmed.startswith("/"): + return f"{base_url}{trimmed}" + return f"{base_url}/{trimmed}" + + +def _resolve_status_path(ai_request_id: Any, cfg: Dict[str, Any]) -> str: + base_path = (cfg.get("responses_path") or "").rstrip("/") + if not base_path: + return f"/ai-request/{ai_request_id}/status" + if not base_path.endswith("/ai-request"): + base_path = f"{base_path}/ai-request" + return f"{base_path}/{ai_request_id}/status" + + +def _http_request(url: str, method: str, body: Optional[bytes], headers: Dict[str, str], + timeout: int, verify_tls: bool) -> Dict[str, Any]: + """ + Shared HTTP helper for GET/POST requests. + """ + req = urlrequest.Request(url, data=body, method=method.upper()) + for name, value in headers.items(): + req.add_header(name, value) + + context = None + if not verify_tls: + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + try: + with urlrequest.urlopen(req, timeout=timeout, context=context) as resp: + status = resp.getcode() + response_body = resp.read().decode("utf-8", errors="replace") + except urlerror.HTTPError as exc: + status = exc.getcode() + response_body = exc.read().decode("utf-8", errors="replace") + except Exception as exc: # pylint: disable=broad-except + return { + "success": False, + "error": "request_failed", + "message": str(exc), + } + + decoded = None + if response_body: + try: + decoded = json.loads(response_body) + except json.JSONDecodeError: + decoded = None + + if 200 <= status < 300: + return { + "success": True, + "status": status, + "data": decoded if decoded is not None else response_body, + } + + error_message = "AI proxy request failed" + if isinstance(decoded, dict): + error_message = decoded.get("error") or decoded.get("message") or error_message + elif response_body: + error_message = response_body + + return { + "success": False, + "status": status, + "error": error_message, + "response": decoded if decoded is not None else response_body, + } + + +def _ensure_env_loaded() -> None: + """Populate os.environ from executor/.env if variables are missing.""" + if os.getenv("PROJECT_UUID") and os.getenv("PROJECT_ID"): + return + + env_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".env")) + if not os.path.exists(env_path): + return + + try: + with open(env_path, "r", encoding="utf-8") as handle: + for line in handle: + stripped = line.strip() + if not stripped or stripped.startswith("#") or "=" not in stripped: + continue + key, value = stripped.split("=", 1) + key = key.strip() + value = value.strip().strip('\'"') + if key and not os.getenv(key): + os.environ[key] = value + except OSError: + pass diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..abb9e5e Binary files /dev/null and b/config/__pycache__/__init__.cpython-311.pyc differ diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc new file mode 100644 index 0000000..378bb12 Binary files /dev/null and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc new file mode 100644 index 0000000..d1a5bd9 Binary files /dev/null and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc new file mode 100644 index 0000000..dc8fc90 Binary files /dev/null and b/config/__pycache__/wsgi.cpython-311.pyc differ diff --git a/config/asgi.py b/config/asgi.py new file mode 100644 index 0000000..ed7c431 --- /dev/null +++ b/config/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for config project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + +application = get_asgi_application() diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..5a4bcbe --- /dev/null +++ b/config/settings.py @@ -0,0 +1,201 @@ +""" +Django settings for config project. + +Generated by 'django-admin startproject' using Django 5.2.7. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.2/ref/settings/ +""" + +from pathlib import Path +import os +from dotenv import load_dotenv + +BASE_DIR = Path(__file__).resolve().parent.parent +load_dotenv(BASE_DIR.parent / ".env") + +SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "change-me") +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 +] +CSRF_TRUSTED_ORIGINS = [ + f"https://{host}" if not host.startswith(("http://", "https://")) else host + for host in CSRF_TRUSTED_ORIGINS +] + +# Cookies must always be HTTPS-only; SameSite=Lax keeps CSRF working behind the proxy. +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True +SESSION_COOKIE_SAMESITE = "None" +CSRF_COOKIE_SAMESITE = "None" + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'core', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + # Disable X-Frame-Options middleware to allow Flatlogic preview iframes. + # 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +X_FRAME_OPTIONS = 'ALLOWALL' + +ROOT_URLCONF = 'config.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + # IMPORTANT: do not remove – injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp + 'core.context_processors.project_context', + 'core.context_processors.unread_messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'config.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': os.getenv('DB_NAME', ''), + 'USER': os.getenv('DB_USER', ''), + 'PASSWORD': os.getenv('DB_PASS', ''), + 'HOST': os.getenv('DB_HOST', '127.0.0.1'), + 'PORT': os.getenv('DB_PORT', '3306'), + 'OPTIONS': { + 'charset': 'utf8mb4', + }, + }, +} + + +# Password validation +# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + +LANGUAGES = [ + ('en', 'English'), + ('ne', 'Nepali'), +] + +LOCALE_PATHS = [ + BASE_DIR / 'locale', +] + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.2/howto/static-files/ + +STATIC_URL = 'static/' +# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS. +STATIC_ROOT = BASE_DIR / 'staticfiles' + +STATICFILES_DIRS = [ + BASE_DIR / 'static', + BASE_DIR / 'assets', + BASE_DIR / 'node_modules', +] + +# Email +EMAIL_BACKEND = os.getenv( + "EMAIL_BACKEND", + "django.core.mail.backends.smtp.EmailBackend" +) +EMAIL_HOST = os.getenv("EMAIL_HOST", "127.0.0.1") +EMAIL_PORT = int(os.getenv("EMAIL_PORT", "587")) +EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "") +EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "") +EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "true").lower() == "true" +EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", "false").lower() == "true" +DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "no-reply@example.com") +CONTACT_EMAIL_TO = [ + item.strip() + for item in os.getenv("CONTACT_EMAIL_TO", DEFAULT_FROM_EMAIL).split(",") + if item.strip() +] + +# When both TLS and SSL flags are enabled, prefer SSL explicitly +if EMAIL_USE_SSL: + EMAIL_USE_TLS = False +# Default primary key field type +# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +LOGIN_URL = '/login/' +LOGIN_REDIRECT_URL = '/' + +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' diff --git a/config/urls.py b/config/urls.py new file mode 100644 index 0000000..4321ed9 --- /dev/null +++ b/config/urls.py @@ -0,0 +1,35 @@ +""" +URL configuration for config project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import include, path +from django.conf import settings +from django.conf.urls.static import static + +urlpatterns = [ + path("admin/", admin.site.urls), + path("i18n/", include("django.conf.urls.i18n")), + path("", include("core.urls")), +] + +admin.site.site_header = "Blood Bank Management System" +admin.site.site_title = "Admin Portal" +admin.site.index_title = "Welcome to Blood Bank Management Portal" + +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) diff --git a/config/wsgi.py b/config/wsgi.py new file mode 100644 index 0000000..e2fbd58 --- /dev/null +++ b/config/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for config project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + +application = get_wsgi_application() diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/__pycache__/__init__.cpython-311.pyc b/core/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..8e9fc3d Binary files /dev/null and b/core/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc new file mode 100644 index 0000000..42ebf60 Binary files /dev/null and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/apps.cpython-311.pyc b/core/__pycache__/apps.cpython-311.pyc new file mode 100644 index 0000000..1c28907 Binary files /dev/null and b/core/__pycache__/apps.cpython-311.pyc differ diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc new file mode 100644 index 0000000..db70f1c Binary files /dev/null and b/core/__pycache__/context_processors.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000..7204cbf Binary files /dev/null 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 new file mode 100644 index 0000000..cd00605 Binary files /dev/null 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 new file mode 100644 index 0000000..198b087 Binary files /dev/null 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 new file mode 100644 index 0000000..aeea159 Binary files /dev/null and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py new file mode 100644 index 0000000..9e3ca05 --- /dev/null +++ b/core/admin.py @@ -0,0 +1,90 @@ +from django.contrib import admin +import csv +from django.http import HttpResponse +from .models import ( + Donor, BloodRequest, BloodBank, VaccineRecord, + DonationEvent, Hospital, UserProfile, Badge, + Notification, Message, HealthReport +) + +def export_as_csv(self, request, queryset): + meta = self.model._meta + field_names = [field.name for field in meta.fields] + + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = f'attachment; filename={meta}.csv' + writer = csv.writer(response) + + writer.writerow(field_names) + for obj in queryset: + writer.writerow([getattr(obj, field) for field in field_names]) + + return response + +export_as_csv.short_description = "Export Selected to CSV" + +@admin.register(UserProfile) +class UserProfileAdmin(admin.ModelAdmin): + list_display = ('user', 'blood_group', 'location', 'phone') + list_filter = ('blood_group',) + search_fields = ('user__username', 'phone', 'location') + actions = [export_as_csv] + +@admin.register(Badge) +class BadgeAdmin(admin.ModelAdmin): + list_display = ('name', 'description') + +@admin.register(VaccineRecord) +class VaccineRecordAdmin(admin.ModelAdmin): + list_display = ('vaccine_name', 'user', 'dose_number', 'date_taken', 'location') + list_filter = ('vaccine_name', 'date_taken') + search_fields = ('vaccine_name', 'user__username', 'location') + actions = [export_as_csv] + +@admin.register(Donor) +class DonorAdmin(admin.ModelAdmin): + list_display = ('name', 'blood_group', 'location', 'is_available', 'is_verified') + list_filter = ('blood_group', 'is_available', 'is_verified') + search_fields = ('name', 'location', 'phone') + actions = [export_as_csv] + +@admin.register(Hospital) +class HospitalAdmin(admin.ModelAdmin): + list_display = ('name', 'location', 'phone') + search_fields = ('name', 'location') + +@admin.register(BloodRequest) +class BloodRequestAdmin(admin.ModelAdmin): + list_display = ('patient_name', 'blood_group', 'urgency', 'status', 'hospital', 'created_at') + list_filter = ('blood_group', 'urgency', 'status') + search_fields = ('patient_name', 'hospital') + actions = [export_as_csv] + +@admin.register(BloodBank) +class BloodBankAdmin(admin.ModelAdmin): + list_display = ('name', 'location', 'stock_a_plus', 'stock_b_plus', 'stock_o_plus', 'stock_ab_plus') + search_fields = ('name', 'location') + actions = [export_as_csv] + +@admin.register(DonationEvent) +class DonationEventAdmin(admin.ModelAdmin): + list_display = ('donor', 'request', 'date', 'is_completed') + list_filter = ('is_completed', 'date') + search_fields = ('donor__name', 'request__patient_name') + actions = [export_as_csv] + +@admin.register(Notification) +class NotificationAdmin(admin.ModelAdmin): + list_display = ('user', 'message', 'is_read', 'created_at') + list_filter = ('is_read', 'created_at') + +@admin.register(Message) +class MessageAdmin(admin.ModelAdmin): + list_display = ('sender', 'receiver', 'message_type', 'timestamp', 'is_read') + list_filter = ('message_type', 'is_read', 'timestamp') + +@admin.register(HealthReport) +class HealthReportAdmin(admin.ModelAdmin): + list_display = ('title', 'user', 'hospital_name', 'report_date') + list_filter = ('report_date',) + search_fields = ('title', 'user__username', 'hospital_name') diff --git a/core/apps.py b/core/apps.py new file mode 100644 index 0000000..8115ae6 --- /dev/null +++ b/core/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'core' diff --git a/core/context_processors.py b/core/context_processors.py new file mode 100644 index 0000000..4bae31f --- /dev/null +++ b/core/context_processors.py @@ -0,0 +1,21 @@ +import os +import time +from .models import Message + +def unread_messages(request): + if request.user.is_authenticated: + return { + 'unread_messages_count': Message.objects.filter(receiver=request.user, is_read=False).count() + } + return {'unread_messages_count': 0} + +def project_context(request): + """ + Adds project-specific environment variables to the template context globally. + """ + return { + "project_description": os.getenv("PROJECT_DESCRIPTION", ""), + "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + # Used for cache-busting static assets + "deployment_timestamp": int(time.time()), + } diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..0113c43 --- /dev/null +++ b/core/forms.py @@ -0,0 +1,41 @@ +from django import forms +from django.contrib.auth.models import User +from django.contrib.auth.forms import UserCreationForm +from .models import UserProfile, BLOOD_GROUPS + +# 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) + phone = forms.CharField(max_length=20, required=True) + + class Meta: + 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', 'placeholder': 'First Name'}), + 'last_name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Last Name'}), + } + +class ProfileUpdateForm(forms.ModelForm): + class Meta: + model = UserProfile + fields = ['bio', 'location', 'phone', 'birth_date', 'blood_group', 'profile_pic'] + widgets = { + '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/management/__init__.py b/core/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/__pycache__/__init__.cpython-311.pyc b/core/management/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..80f2921 Binary files /dev/null and b/core/management/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/management/commands/__init__.py b/core/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/commands/__pycache__/__init__.cpython-311.pyc b/core/management/commands/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..822ff85 Binary files /dev/null and b/core/management/commands/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/management/commands/__pycache__/cleanup_requests.cpython-311.pyc b/core/management/commands/__pycache__/cleanup_requests.cpython-311.pyc new file mode 100644 index 0000000..f75a249 Binary files /dev/null and b/core/management/commands/__pycache__/cleanup_requests.cpython-311.pyc differ diff --git a/core/management/commands/__pycache__/seed_hospitals.cpython-311.pyc b/core/management/commands/__pycache__/seed_hospitals.cpython-311.pyc new file mode 100644 index 0000000..b243ce4 Binary files /dev/null and b/core/management/commands/__pycache__/seed_hospitals.cpython-311.pyc differ diff --git a/core/management/commands/cleanup_requests.py b/core/management/commands/cleanup_requests.py new file mode 100644 index 0000000..3221ec5 --- /dev/null +++ b/core/management/commands/cleanup_requests.py @@ -0,0 +1,61 @@ +from django.core.management.base import BaseCommand +from django.utils import timezone +from django.db.models import Q +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 and non-Accepted requests older than 7 days + cutoff_inactive = now - timedelta(days=7) + inactive_requests = BloodRequest.objects.exclude( + Q(status='Active') | Q(status='Accepted') + ).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/non-accepted requests older than 7 days.')) + deleted_count += count_inactive + + # 3. For Accepted requests, we keep them for 30 days after acceptance for history + cutoff_accepted = now - timedelta(days=30) + old_accepted = BloodRequest.objects.filter( + status='Accepted', + accepted_at__lt=cutoff_accepted + ) + count_accepted = old_accepted.count() + if count_accepted > 0: + old_accepted.delete() + self.stdout.write(self.style.SUCCESS(f'Deleted {count_accepted} accepted requests older than 30 days.')) + deleted_count += count_accepted + + 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}')) diff --git a/core/management/commands/seed_hospitals.py b/core/management/commands/seed_hospitals.py new file mode 100644 index 0000000..5600d30 --- /dev/null +++ b/core/management/commands/seed_hospitals.py @@ -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.')) diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..2555f26 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,61 @@ +# Generated by Django 5.2.7 on 2026-02-17 13:38 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='BloodBank', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('location', models.CharField(max_length=255)), + ('stock_a_plus', models.IntegerField(default=0)), + ('stock_a_minus', models.IntegerField(default=0)), + ('stock_b_plus', models.IntegerField(default=0)), + ('stock_b_minus', models.IntegerField(default=0)), + ('stock_o_plus', models.IntegerField(default=0)), + ('stock_o_minus', models.IntegerField(default=0)), + ('stock_ab_plus', models.IntegerField(default=0)), + ('stock_ab_minus', models.IntegerField(default=0)), + ], + ), + migrations.CreateModel( + name='BloodRequest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('patient_name', models.CharField(max_length=100)), + ('blood_group', models.CharField(choices=[('A+', 'A+'), ('A-', 'A-'), ('B+', 'B+'), ('B-', 'B-'), ('O+', 'O+'), ('O-', 'O-'), ('AB+', 'AB+'), ('AB-', 'AB-')], max_length=5)), + ('location', models.CharField(max_length=255)), + ('urgency', models.CharField(choices=[('CRITICAL', 'Critical'), ('URGENT', 'Urgent'), ('NORMAL', 'Normal')], default='NORMAL', max_length=10)), + ('hospital', models.CharField(max_length=255)), + ('contact_number', models.CharField(max_length=20)), + ('required_date', models.DateField(default=django.utils.timezone.now)), + ('status', models.CharField(default='Active', max_length=20)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='Donor', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('blood_group', models.CharField(choices=[('A+', 'A+'), ('A-', 'A-'), ('B+', 'B+'), ('B-', 'B-'), ('O+', 'O+'), ('O-', 'O-'), ('AB+', 'AB+'), ('AB-', 'AB-')], max_length=5)), + ('location', models.CharField(max_length=255)), + ('latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)), + ('longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)), + ('phone', models.CharField(max_length=20)), + ('is_available', models.BooleanField(default=True)), + ('last_donation_date', models.DateField(blank=True, null=True)), + ('vaccination_status', models.CharField(default='Unknown', max_length=100)), + ], + ), + ] diff --git a/core/migrations/0002_bloodbank_contact_number_bloodbank_is_24_7_and_more.py b/core/migrations/0002_bloodbank_contact_number_bloodbank_is_24_7_and_more.py new file mode 100644 index 0000000..8ce3046 --- /dev/null +++ b/core/migrations/0002_bloodbank_contact_number_bloodbank_is_24_7_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 5.2.7 on 2026-02-17 13:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='bloodbank', + name='contact_number', + field=models.CharField(blank=True, max_length=20, null=True), + ), + migrations.AddField( + model_name='bloodbank', + name='is_24_7', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='donor', + name='avatar_url', + field=models.URLField(blank=True, null=True), + ), + migrations.AddField( + model_name='donor', + name='email', + field=models.EmailField(blank=True, max_length=254, null=True), + ), + migrations.AddField( + model_name='donor', + name='last_vaccination_date', + field=models.DateField(blank=True, null=True), + ), + ] diff --git a/core/migrations/0003_donor_citizenship_no_donor_district_and_more.py b/core/migrations/0003_donor_citizenship_no_donor_district_and_more.py new file mode 100644 index 0000000..7d91b5a --- /dev/null +++ b/core/migrations/0003_donor_citizenship_no_donor_district_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.7 on 2026-02-17 13:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_bloodbank_contact_number_bloodbank_is_24_7_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='donor', + name='citizenship_no', + field=models.CharField(blank=True, max_length=50, null=True), + ), + migrations.AddField( + model_name='donor', + name='district', + field=models.CharField(default='Kathmandu', max_length=100), + ), + migrations.AddField( + model_name='donor', + name='is_verified', + field=models.BooleanField(default=False), + ), + ] diff --git a/core/migrations/0004_vaccinerecord.py b/core/migrations/0004_vaccinerecord.py new file mode 100644 index 0000000..71a2d25 --- /dev/null +++ b/core/migrations/0004_vaccinerecord.py @@ -0,0 +1,30 @@ +# Generated by Django 5.2.7 on 2026-02-17 14:37 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0003_donor_citizenship_no_donor_district_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='VaccineRecord', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('vaccine_name', models.CharField(max_length=100)), + ('dose_number', models.PositiveIntegerField(default=1)), + ('date_taken', models.DateField()), + ('location', models.CharField(max_length=255)), + ('center_name', models.CharField(blank=True, max_length=255, null=True)), + ('notes', models.TextField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vaccine_records', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/core/migrations/0005_bloodbank_latitude_bloodbank_longitude.py b/core/migrations/0005_bloodbank_latitude_bloodbank_longitude.py new file mode 100644 index 0000000..a1a60e1 --- /dev/null +++ b/core/migrations/0005_bloodbank_latitude_bloodbank_longitude.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2026-02-17 15:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_vaccinerecord'), + ] + + operations = [ + migrations.AddField( + model_name='bloodbank', + name='latitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), + ), + migrations.AddField( + model_name='bloodbank', + name='longitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), + ), + ] diff --git a/core/migrations/0006_bloodrequest_latitude_bloodrequest_longitude.py b/core/migrations/0006_bloodrequest_latitude_bloodrequest_longitude.py new file mode 100644 index 0000000..dc1ede4 --- /dev/null +++ b/core/migrations/0006_bloodrequest_latitude_bloodrequest_longitude.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2026-02-17 15:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_bloodbank_latitude_bloodbank_longitude'), + ] + + operations = [ + migrations.AddField( + model_name='bloodrequest', + name='latitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), + ), + migrations.AddField( + model_name='bloodrequest', + name='longitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), + ), + ] diff --git a/core/migrations/0007_bloodbank_total_capacity.py b/core/migrations/0007_bloodbank_total_capacity.py new file mode 100644 index 0000000..e08250a --- /dev/null +++ b/core/migrations/0007_bloodbank_total_capacity.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-02-17 16:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_bloodrequest_latitude_bloodrequest_longitude'), + ] + + operations = [ + migrations.AddField( + model_name='bloodbank', + name='total_capacity', + field=models.IntegerField(default=1000), + ), + ] diff --git a/core/migrations/0008_userprofile.py b/core/migrations/0008_userprofile.py new file mode 100644 index 0000000..b0208e9 --- /dev/null +++ b/core/migrations/0008_userprofile.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.7 on 2026-02-17 16:13 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_bloodbank_total_capacity'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='UserProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('bio', models.TextField(blank=True, max_length=500)), + ('location', models.CharField(blank=True, max_length=100)), + ('birth_date', models.DateField(blank=True, null=True)), + ('phone', models.CharField(blank=True, max_length=20)), + ('blood_group', models.CharField(blank=True, choices=[('A+', 'A+'), ('A-', 'A-'), ('B+', 'B+'), ('B-', 'B-'), ('O+', 'O+'), ('O-', 'O-'), ('AB+', 'AB+'), ('AB-', 'AB-')], max_length=5)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/core/migrations/0009_bloodrequest_user_donor_user_donationevent_feedback_and_more.py b/core/migrations/0009_bloodrequest_user_donor_user_donationevent_feedback_and_more.py new file mode 100644 index 0000000..f9e4101 --- /dev/null +++ b/core/migrations/0009_bloodrequest_user_donor_user_donationevent_feedback_and_more.py @@ -0,0 +1,58 @@ +# Generated by Django 5.2.7 on 2026-02-18 05:21 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0008_userprofile'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='bloodrequest', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='blood_requests', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='donor', + name='user', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='donor_profile', to=settings.AUTH_USER_MODEL), + ), + migrations.CreateModel( + name='DonationEvent', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateTimeField(default=django.utils.timezone.now)), + ('is_completed', models.BooleanField(default=False)), + ('donor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='donations', to='core.donor')), + ('donor_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='my_donations', to=settings.AUTH_USER_MODEL)), + ('request', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='donations', to='core.bloodrequest')), + ], + ), + migrations.CreateModel( + name='Feedback', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', models.TextField()), + ('rating', models.PositiveIntegerField(default=5)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Notification', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('message', models.TextField()), + ('is_read', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/core/migrations/0010_delete_feedback.py b/core/migrations/0010_delete_feedback.py new file mode 100644 index 0000000..3756f65 --- /dev/null +++ b/core/migrations/0010_delete_feedback.py @@ -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', + ), + ] diff --git a/core/migrations/0011_hospital.py b/core/migrations/0011_hospital.py new file mode 100644 index 0000000..be6635e --- /dev/null +++ b/core/migrations/0011_hospital.py @@ -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)), + ], + ), + ] diff --git a/core/migrations/0012_hospital_latitude_hospital_longitude.py b/core/migrations/0012_hospital_latitude_hospital_longitude.py new file mode 100644 index 0000000..ba98870 --- /dev/null +++ b/core/migrations/0012_hospital_latitude_hospital_longitude.py @@ -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), + ), + ] diff --git a/core/migrations/0013_userprofile_profile_pic.py b/core/migrations/0013_userprofile_profile_pic.py new file mode 100644 index 0000000..095f467 --- /dev/null +++ b/core/migrations/0013_userprofile_profile_pic.py @@ -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'), + ), + ] diff --git a/core/migrations/0014_message.py b/core/migrations/0014_message.py new file mode 100644 index 0000000..8fd2829 --- /dev/null +++ b/core/migrations/0014_message.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2.7 on 2026-02-18 07:37 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0013_userprofile_profile_pic'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Message', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', models.TextField()), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('is_read', models.BooleanField(default=False)), + ('receiver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to=settings.AUTH_USER_MODEL)), + ('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/core/migrations/0015_userprofile_last_location_update_and_more.py b/core/migrations/0015_userprofile_last_location_update_and_more.py new file mode 100644 index 0000000..3de02e8 --- /dev/null +++ b/core/migrations/0015_userprofile_last_location_update_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.7 on 2026-02-18 13:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0014_message'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='last_location_update', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='userprofile', + name='latitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), + ), + migrations.AddField( + model_name='userprofile', + name='longitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), + ), + ] diff --git a/core/migrations/0016_badge_userprofile_badges.py b/core/migrations/0016_badge_userprofile_badges.py new file mode 100644 index 0000000..fc3cb71 --- /dev/null +++ b/core/migrations/0016_badge_userprofile_badges.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2.7 on 2026-02-18 14:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0015_userprofile_last_location_update_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Badge', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50)), + ('description', models.CharField(max_length=255)), + ('icon_class', models.CharField(default='fas fa-medal', max_length=50)), + ], + ), + migrations.AddField( + model_name='userprofile', + name='badges', + field=models.ManyToManyField(blank=True, related_name='users', to='core.badge'), + ), + ] diff --git a/core/migrations/0017_message_attachment_message_message_type_and_more.py b/core/migrations/0017_message_attachment_message_message_type_and_more.py new file mode 100644 index 0000000..4489cab --- /dev/null +++ b/core/migrations/0017_message_attachment_message_message_type_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.7 on 2026-02-18 15:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0016_badge_userprofile_badges'), + ] + + operations = [ + migrations.AddField( + model_name='message', + name='attachment', + field=models.FileField(blank=True, null=True, upload_to='chat_attachments/'), + ), + migrations.AddField( + model_name='message', + name='message_type', + field=models.CharField(choices=[('text', 'Text'), ('image', 'Image'), ('video', 'Video'), ('file', 'File'), ('sticker', 'Sticker')], default='text', max_length=10), + ), + migrations.AddField( + model_name='message', + name='sticker_id', + field=models.CharField(blank=True, max_length=50, null=True), + ), + migrations.AlterField( + model_name='message', + name='content', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/core/migrations/0018_bloodrequest_image.py b/core/migrations/0018_bloodrequest_image.py new file mode 100644 index 0000000..deb4e44 --- /dev/null +++ b/core/migrations/0018_bloodrequest_image.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-02-19 04:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0017_message_attachment_message_message_type_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='bloodrequest', + name='image', + field=models.ImageField(blank=True, null=True, upload_to='blood_requests/'), + ), + ] diff --git a/core/migrations/0019_healthreport.py b/core/migrations/0019_healthreport.py new file mode 100644 index 0000000..7ec9bd0 --- /dev/null +++ b/core/migrations/0019_healthreport.py @@ -0,0 +1,32 @@ +# Generated by Django 5.2.7 on 2026-02-19 04:45 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0018_bloodrequest_image'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='HealthReport', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('hospital_name', models.CharField(blank=True, max_length=255, null=True)), + ('report_file', models.FileField(upload_to='health_reports/')), + ('description', models.TextField(blank=True, null=True)), + ('report_date', models.DateField(default=django.utils.timezone.now)), + ('next_test_date', models.DateField(blank=True, null=True)), + ('allow_notifications', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='health_reports', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/core/migrations/0020_vaccinerecord_photo.py b/core/migrations/0020_vaccinerecord_photo.py new file mode 100644 index 0000000..4a27b87 --- /dev/null +++ b/core/migrations/0020_vaccinerecord_photo.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-02-19 05:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0019_healthreport'), + ] + + operations = [ + migrations.AddField( + model_name='vaccinerecord', + name='photo', + field=models.ImageField(blank=True, null=True, upload_to='vaccine_photos/'), + ), + ] diff --git a/core/migrations/0021_bloodrequest_accepted_at.py b/core/migrations/0021_bloodrequest_accepted_at.py new file mode 100644 index 0000000..9d522e4 --- /dev/null +++ b/core/migrations/0021_bloodrequest_accepted_at.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-02-27 13:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0020_vaccinerecord_photo'), + ] + + operations = [ + migrations.AddField( + model_name='bloodrequest', + name='accepted_at', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/core/migrations/__init__.py b/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..c162e57 Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_bloodbank_contact_number_bloodbank_is_24_7_and_more.cpython-311.pyc b/core/migrations/__pycache__/0002_bloodbank_contact_number_bloodbank_is_24_7_and_more.cpython-311.pyc new file mode 100644 index 0000000..b20e5ab Binary files /dev/null and b/core/migrations/__pycache__/0002_bloodbank_contact_number_bloodbank_is_24_7_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0003_donor_citizenship_no_donor_district_and_more.cpython-311.pyc b/core/migrations/__pycache__/0003_donor_citizenship_no_donor_district_and_more.cpython-311.pyc new file mode 100644 index 0000000..05ff597 Binary files /dev/null and b/core/migrations/__pycache__/0003_donor_citizenship_no_donor_district_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0004_vaccinerecord.cpython-311.pyc b/core/migrations/__pycache__/0004_vaccinerecord.cpython-311.pyc new file mode 100644 index 0000000..2dabec1 Binary files /dev/null and b/core/migrations/__pycache__/0004_vaccinerecord.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0005_bloodbank_latitude_bloodbank_longitude.cpython-311.pyc b/core/migrations/__pycache__/0005_bloodbank_latitude_bloodbank_longitude.cpython-311.pyc new file mode 100644 index 0000000..e1a0ddd Binary files /dev/null and b/core/migrations/__pycache__/0005_bloodbank_latitude_bloodbank_longitude.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0006_bloodrequest_latitude_bloodrequest_longitude.cpython-311.pyc b/core/migrations/__pycache__/0006_bloodrequest_latitude_bloodrequest_longitude.cpython-311.pyc new file mode 100644 index 0000000..144b2a2 Binary files /dev/null and b/core/migrations/__pycache__/0006_bloodrequest_latitude_bloodrequest_longitude.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0007_bloodbank_total_capacity.cpython-311.pyc b/core/migrations/__pycache__/0007_bloodbank_total_capacity.cpython-311.pyc new file mode 100644 index 0000000..120d712 Binary files /dev/null and b/core/migrations/__pycache__/0007_bloodbank_total_capacity.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0008_userprofile.cpython-311.pyc b/core/migrations/__pycache__/0008_userprofile.cpython-311.pyc new file mode 100644 index 0000000..6c0e3a7 Binary files /dev/null and b/core/migrations/__pycache__/0008_userprofile.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0009_bloodrequest_user_donor_user_donationevent_feedback_and_more.cpython-311.pyc b/core/migrations/__pycache__/0009_bloodrequest_user_donor_user_donationevent_feedback_and_more.cpython-311.pyc new file mode 100644 index 0000000..0431d83 Binary files /dev/null and b/core/migrations/__pycache__/0009_bloodrequest_user_donor_user_donationevent_feedback_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0010_delete_feedback.cpython-311.pyc b/core/migrations/__pycache__/0010_delete_feedback.cpython-311.pyc new file mode 100644 index 0000000..adbdc32 Binary files /dev/null and b/core/migrations/__pycache__/0010_delete_feedback.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0011_hospital.cpython-311.pyc b/core/migrations/__pycache__/0011_hospital.cpython-311.pyc new file mode 100644 index 0000000..bab95c1 Binary files /dev/null and b/core/migrations/__pycache__/0011_hospital.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0012_hospital_latitude_hospital_longitude.cpython-311.pyc b/core/migrations/__pycache__/0012_hospital_latitude_hospital_longitude.cpython-311.pyc new file mode 100644 index 0000000..6039d83 Binary files /dev/null and b/core/migrations/__pycache__/0012_hospital_latitude_hospital_longitude.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0013_userprofile_profile_pic.cpython-311.pyc b/core/migrations/__pycache__/0013_userprofile_profile_pic.cpython-311.pyc new file mode 100644 index 0000000..6041900 Binary files /dev/null and b/core/migrations/__pycache__/0013_userprofile_profile_pic.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0014_message.cpython-311.pyc b/core/migrations/__pycache__/0014_message.cpython-311.pyc new file mode 100644 index 0000000..7665b75 Binary files /dev/null and b/core/migrations/__pycache__/0014_message.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0015_userprofile_last_location_update_and_more.cpython-311.pyc b/core/migrations/__pycache__/0015_userprofile_last_location_update_and_more.cpython-311.pyc new file mode 100644 index 0000000..09409a5 Binary files /dev/null and b/core/migrations/__pycache__/0015_userprofile_last_location_update_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0016_badge_userprofile_badges.cpython-311.pyc b/core/migrations/__pycache__/0016_badge_userprofile_badges.cpython-311.pyc new file mode 100644 index 0000000..83b2863 Binary files /dev/null and b/core/migrations/__pycache__/0016_badge_userprofile_badges.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0017_message_attachment_message_message_type_and_more.cpython-311.pyc b/core/migrations/__pycache__/0017_message_attachment_message_message_type_and_more.cpython-311.pyc new file mode 100644 index 0000000..1c00a37 Binary files /dev/null and b/core/migrations/__pycache__/0017_message_attachment_message_message_type_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0018_bloodrequest_image.cpython-311.pyc b/core/migrations/__pycache__/0018_bloodrequest_image.cpython-311.pyc new file mode 100644 index 0000000..89cd3cb Binary files /dev/null and b/core/migrations/__pycache__/0018_bloodrequest_image.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0019_healthreport.cpython-311.pyc b/core/migrations/__pycache__/0019_healthreport.cpython-311.pyc new file mode 100644 index 0000000..b85f3ca Binary files /dev/null and b/core/migrations/__pycache__/0019_healthreport.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0020_vaccinerecord_photo.cpython-311.pyc b/core/migrations/__pycache__/0020_vaccinerecord_photo.cpython-311.pyc new file mode 100644 index 0000000..4080729 Binary files /dev/null and b/core/migrations/__pycache__/0020_vaccinerecord_photo.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0021_bloodrequest_accepted_at.cpython-311.pyc b/core/migrations/__pycache__/0021_bloodrequest_accepted_at.cpython-311.pyc new file mode 100644 index 0000000..f83dd3d Binary files /dev/null and b/core/migrations/__pycache__/0021_bloodrequest_accepted_at.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/__init__.cpython-311.pyc b/core/migrations/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..c33b24d Binary files /dev/null and b/core/migrations/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py new file mode 100644 index 0000000..d03c633 --- /dev/null +++ b/core/models.py @@ -0,0 +1,217 @@ +from django.db import models +from django.utils import timezone +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-'), + ('O+', 'O+'), ('O-', 'O-'), + ('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 + + def __str__(self): + return self.name + +class UserProfile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') + bio = models.TextField(max_length=500, blank=True) + location = models.CharField(max_length=100, blank=True) + 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) + last_location_update = models.DateTimeField(null=True, blank=True) + 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) + badges = models.ManyToManyField(Badge, blank=True, related_name='users') + + def __str__(self): + 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: + # 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, _ = 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 + donor.longitude = instance.longitude + 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) + dose_number = models.PositiveIntegerField(default=1) + date_taken = models.DateField() + location = models.CharField(max_length=255) + center_name = models.CharField(max_length=255, null=True, blank=True) + notes = models.TextField(blank=True, null=True) + photo = models.ImageField(upload_to='vaccine_photos/', null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + 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') + name = models.CharField(max_length=100) + blood_group = models.CharField(max_length=5, choices=BLOOD_GROUPS) + district = models.CharField(max_length=100, default='Kathmandu') + 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=20) + email = models.EmailField(null=True, blank=True) + is_available = models.BooleanField(default=True) + is_verified = models.BooleanField(default=False) + citizenship_no = models.CharField(max_length=50, null=True, blank=True) + last_donation_date = models.DateField(null=True, blank=True) + vaccination_status = models.CharField(max_length=100, default='Unknown') + last_vaccination_date = models.DateField(null=True, blank=True) + avatar_url = models.URLField(null=True, blank=True) + + def __str__(self): + 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) + 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'), + ('URGENT', 'Urgent'), + ('NORMAL', 'Normal'), + ] + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blood_requests', null=True, blank=True) + patient_name = models.CharField(max_length=100) + blood_group = models.CharField(max_length=5, choices=BLOOD_GROUPS) + location = models.CharField(max_length=255) + urgency = models.CharField(max_length=10, choices=URGENCY_LEVELS, default='NORMAL') + hospital = 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) + contact_number = models.CharField(max_length=20) + image = models.ImageField(upload_to='blood_requests/', null=True, blank=True) + required_date = models.DateField(default=timezone.now) + status = models.CharField(max_length=20, default='Active') + accepted_at = models.DateTimeField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.blood_group} for {self.patient_name}" + +class BloodBank(models.Model): + name = models.CharField(max_length=100) + 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) + contact_number = models.CharField(max_length=20, null=True, blank=True) + is_24_7 = models.BooleanField(default=True) + stock_a_plus = models.IntegerField(default=0) + stock_a_minus = models.IntegerField(default=0) + stock_b_plus = models.IntegerField(default=0) + stock_b_minus = models.IntegerField(default=0) + stock_o_plus = models.IntegerField(default=0) + stock_o_minus = models.IntegerField(default=0) + stock_ab_plus = models.IntegerField(default=0) + stock_ab_minus = models.IntegerField(default=0) + total_capacity = models.IntegerField(default=1000) + + def __str__(self): + return self.name + +class DonationEvent(models.Model): + donor = models.ForeignKey(Donor, on_delete=models.CASCADE, related_name='donations') + request = models.ForeignKey(BloodRequest, on_delete=models.CASCADE, related_name='donations') + donor_user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='my_donations') + date = models.DateTimeField(default=timezone.now) + is_completed = models.BooleanField(default=False) + + def __str__(self): + return f"Donation by {self.donor.name} for {self.request.patient_name}" + +class Notification(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notifications') + message = models.TextField() + is_read = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Notification for {self.user.username}: {self.message[:20]}..." + +class Message(models.Model): + MESSAGE_TYPES = [ + ('text', 'Text'), + ('image', 'Image'), + ('video', 'Video'), + ('file', 'File'), + ('sticker', 'Sticker'), + ] + sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_messages') + receiver = models.ForeignKey(User, on_delete=models.CASCADE, related_name='received_messages') + content = models.TextField(blank=True, null=True) + attachment = models.FileField(upload_to='chat_attachments/', blank=True, null=True) + message_type = models.CharField(max_length=10, choices=MESSAGE_TYPES, default='text') + sticker_id = models.CharField(max_length=50, blank=True, null=True) + timestamp = models.DateTimeField(auto_now_add=True) + is_read = models.BooleanField(default=False) + + def __str__(self): + return f"From {self.sender.username} to {self.receiver.username} at {self.timestamp}" + +class HealthReport(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='health_reports') + title = models.CharField(max_length=200) + hospital_name = models.CharField(max_length=255, blank=True, null=True) + report_file = models.FileField(upload_to='health_reports/') + description = models.TextField(blank=True, null=True) + report_date = models.DateField(default=timezone.now) + next_test_date = models.DateField(blank=True, null=True) + allow_notifications = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Report: {self.title} for {self.user.username}" diff --git a/core/templates/base.html b/core/templates/base.html new file mode 100644 index 0000000..0a323df --- /dev/null +++ b/core/templates/base.html @@ -0,0 +1,575 @@ +{% load i18n %} + + + + + + {% block title %}RaktaPulse Dashboard{% endblock %} + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+ + +
+ +
+ + + +
+ + Detect Location +
+
+ + {{ current_time|date:"M d" }} +
+ {% if user.is_authenticated %} + + {% else %} +
+ Login + Register +
+ {% endif %} +
+
+ +
+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + {% block content %}{% endblock %} +
+
+
+ + + + + + + +
+
+ SOS + 1115 +
+
+ + + + {% block scripts %}{% endblock %} + + {% if user.is_authenticated %} + + {% endif %} + + diff --git a/core/templates/core/add_vaccination.html b/core/templates/core/add_vaccination.html new file mode 100644 index 0000000..36d0a77 --- /dev/null +++ b/core/templates/core/add_vaccination.html @@ -0,0 +1,79 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}Add Vaccination Record - {{ project_name }}{% endblock %} + +{% block content %} +
+
+
+
+
+
+ +
+

Add Vaccine Record

+

Keep your immunization data accurate and up to date.

+
+ +
+ {% csrf_token %} + +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + Cancel +
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/article_detail.html b/core/templates/core/article_detail.html new file mode 100644 index 0000000..8820990 --- /dev/null +++ b/core/templates/core/article_detail.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} + +{% block title %}{{ article.title }}{% endblock %} + +{% block content %} +
+

{{ article.title }}

+

Published on {{ article.created_at|date:"F d, Y" }}

+
+
+ {{ article.content|safe }} +
+
+{% endblock %} diff --git a/core/templates/core/blood_bank_list.html b/core/templates/core/blood_bank_list.html new file mode 100644 index 0000000..646ce28 --- /dev/null +++ b/core/templates/core/blood_bank_list.html @@ -0,0 +1,165 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}Blood Banks - RaktaPulse{% endblock %} + +{% block content %} +
+
+
+

Blood Banks

+

Official blood repositories and their current inventory levels.

+
+
+ + Reset + +
+
+ +
+ {% for bank in banks %} +
+
+
+

{{ bank.name }}

+ {% if bank.is_24_7 %} + 24/7 Available + {% endif %} +
+ + {% if bank.distance and bank.distance < 1000 %} +
+ + {{ bank.distance|floatformat:1 }} km away from you + +
+ {% endif %} + +

{{ bank.location }}

+

+ + {{ bank.contact_number }} + +

+ +
Inventory Levels (Max {{ bank.total_capacity }} units/type)
+
+
+
+
A+
+
{{ bank.stock_a_plus }}
+
+
+
+
+
+
+
+
A-
+
{{ bank.stock_a_minus }}
+
+
+
+
+
+
+
+
B+
+
{{ bank.stock_b_plus }}
+
+
+
+
+
+
+
+
B-
+
{{ bank.stock_b_minus }}
+
+
+
+
+
+
+
+
O+
+
{{ bank.stock_o_plus }}
+
+
+
+
+
+
+
+
O-
+
{{ bank.stock_o_minus }}
+
+
+
+
+
+
+
+
AB+
+
{{ bank.stock_ab_plus }}
+
+
+
+
+
+
+
+
AB-
+
{{ bank.stock_ab_minus }}
+
+
+
+
+
+
+ + Contact Bank +
+
+ {% empty %} +
+ +

No blood banks registered in the system.

+
+ {% endfor %} +
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/core/templates/core/blood_request_list.html b/core/templates/core/blood_request_list.html new file mode 100644 index 0000000..f76c069 --- /dev/null +++ b/core/templates/core/blood_request_list.html @@ -0,0 +1,131 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}Blood Requests - RaktaPulse{% endblock %} + +{% block content %} + + +
+
+
+
+

{% if current_status %}{{ current_status }} {% endif %}Blood Requests

+

{% if current_status == 'Active' %}Current urgent requirements for blood.{% else %}History of blood requests in our community.{% endif %}

+
+ Post a Request +
+
+ +
+
+ {% for req in requests %} +
+
+
+
+ + {% if req.urgency == 'CRITICAL' %} + + {% elif req.urgency == 'URGENT' %} + + {% else %} + + {% endif %} + {{ req.urgency }} + + {% if req.status == 'Active' %} + Active + {% else %} + {{ req.status }} + {% endif %} +
+
+ {{ req.blood_group }} +
+
+
{{ req.patient_name }}
+

{{ req.hospital }}

+ + {% with acceptance=req.donations.first %} + {% if acceptance %} +
+

+ + Accepted by: {{ acceptance.donor.name }} +

+
+ {% endif %} + {% endwith %} + + {% if req.image %} +
+ Patient/Prescription +
+ {% endif %} + +
+
+ {{ req.created_at|timesince }} ago +
+ {% if req.user %} + + + + {% endif %} + {% if user.is_authenticated and user.donor_profile and req.user != user %} + {% if req.can_volunteer %} + Volunteer + {% else %} + + {% endif %} + {% endif %} + Call +
+
+
+
+
+ {% empty %} +
+ +

No active blood requests at the moment.

+
+ {% endfor %} +
+
+
+{% endblock %} diff --git a/core/templates/core/chat.html b/core/templates/core/chat.html new file mode 100644 index 0000000..e868a41 --- /dev/null +++ b/core/templates/core/chat.html @@ -0,0 +1,612 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block title %}Chat with {{ other_user.username }} - RaktaPulse{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+
+
+ + + +
+ {% if other_user.profile.profile_pic %} + {{ other_user.username }} + {% else %} +
+ +
+ {% endif %} +
+
+
+ + {{ other_user.first_name }} {{ other_user.last_name|default:other_user.username }} + +
+ + Online + +
+
+ + + +
+
+ +
+ {% for msg in chat_messages %} +
+ {% if msg.message_type == 'text' %} + {{ msg.content }} + {% elif msg.message_type == 'image' %} + + {% if msg.content %}

{{ msg.content }}

{% endif %} + {% elif msg.message_type == 'video' %} + + {% if msg.content %}

{{ msg.content }}

{% endif %} + {% elif msg.message_type == 'file' %} +
+ +
+ + {{ msg.attachment.name|cut:"chat_attachments/" }} + + {{ msg.attachment.size|filesizeformat }} +
+
+ {% if msg.content %}

{{ msg.content }}

{% endif %} + {% elif msg.message_type == 'sticker' %} +
{{ msg.sticker_id }}
+ {% endif %} + + {{ msg.timestamp|date:"g:i a" }} + +
+ {% empty %} +
+

No messages yet. Say hi!

+
+ {% endfor %} +
+ +
+ {% csrf_token %} +
+
+ +
+ +
+ + + + + + + + + + +
+
+
+
+ + + + + + + + + + + + + +{% endblock %} + +{% block scripts %} + + +{% endblock %} diff --git a/core/templates/core/donation_history.html b/core/templates/core/donation_history.html new file mode 100644 index 0000000..9756377 --- /dev/null +++ b/core/templates/core/donation_history.html @@ -0,0 +1,150 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+
+

{{ title }}

+

Explore the complete log of blood donations within the RaktaPulse community.

+
+
+
+ Total Impact + {{ donations.count }} Donations +
+ + Export + +
+
+ + +
+
+
+
+ + +
+
+ +
+ + + + +
+
+
+ +
+
+
+
+ +
+
+
+
+
Detailed Logs
+
+
+ Showing all completed events +
+
+
+
+
+ + + + + + + + + + + + + {% for donation in donations %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
DonorBlood GroupRecipient/PatientLocationDateStatus
+
+
+ +
+
+
{{ donation.donor_user.username }}
+
Verified Champion
+
+
+
+ {{ donation.request.blood_group }} + +
{{ donation.request.patient_name }}
+
+
+ {{ donation.request.hospital }} +
+
{{ donation.request.location }}
+
+
{{ donation.date|date:"M d, Y" }}
+
{{ donation.date|time:"H:i" }}
+
+ + Completed + +
+
+ +
+
No donation history available yet.
+

When donations are completed, they will appear here.

+
+
+
+
+ + +
+ + +{% endblock %} diff --git a/core/templates/core/donor_list.html b/core/templates/core/donor_list.html new file mode 100644 index 0000000..b7caea6 --- /dev/null +++ b/core/templates/core/donor_list.html @@ -0,0 +1,130 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}Donors - RaktaPulse{% endblock %} + +{% block content %} +
+
+
+

Blood Donors

+

Find and connect with blood donors in your community.

+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ Reset +
+ + +
+ +
+ {% for donor in donors %} +
+
+
+ {% if donor.user and donor.user.profile.profile_pic %} + {{ donor.name }} + + {{ donor.blood_group }} + + {% else %} +
+ {{ donor.blood_group }} +
+ {% endif %} +
+
+
+ {{ donor.name }} + {% if donor.is_verified %} + + {% endif %} + {% if donor.distance and donor.distance < 1000 %} + + {{ donor.distance|floatformat:1 }} km away + + {% endif %} +
+

{{ donor.location }}, {{ donor.district }}

+
+
+
+
+ + {% if donor.on_break %}On Break ({{ donor.days_remaining }}d){% elif donor.is_available %}Available{% else %}Unavailable{% endif %} + +

Phone: {{ donor.phone }}

+
+
+ {% if donor.user %} + + + + + + + {% endif %} + Call +
+
+
+ {% endfor %} +
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/core/templates/core/hospital_list.html b/core/templates/core/hospital_list.html new file mode 100644 index 0000000..edf2eb5 --- /dev/null +++ b/core/templates/core/hospital_list.html @@ -0,0 +1,92 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}Hospitals - RaktaPulse{% endblock %} + +{% block content %} +
+
+
+

Hospitals in Kathmandu

+

A comprehensive list of public and private hospitals for blood requests and emergency care.

+
+
+ +
+
+ +
+ {% for hospital in hospitals %} +
+
+
+
+

{{ hospital.name }}

+ {% if hospital.distance %} + + {{ hospital.distance|floatformat:1 }} km away + + {% endif %} +
+

{{ hospital.location }}

+
+ +
+ {% if hospital.phone %} +

+ + {{ hospital.phone }} + +

+ {% endif %} + + {% if hospital.website %} +

+ + Visit Website + +

+ {% endif %} + + Request Blood Here +
+
+
+ {% empty %} +
+ +

No hospitals registered in the system.

+
+ {% endfor %} +
+
+ + +{% endblock %} diff --git a/core/templates/core/inbox.html b/core/templates/core/inbox.html new file mode 100644 index 0000000..6de5e22 --- /dev/null +++ b/core/templates/core/inbox.html @@ -0,0 +1,64 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block title %}Inbox - RaktaPulse{% endblock %} + +{% block content %} +
+
+

Messages

+
+ + +
+{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html new file mode 100644 index 0000000..1fc249b --- /dev/null +++ b/core/templates/core/index.html @@ -0,0 +1,684 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} + +{% block title %}{% trans "RaktaPulse Dashboard" %} - {% trans "Lifeline of the Community" %}{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+ +
+
+
+

{% trans "RaktaPulse Community Dashboard" %}

+ + + SYSTEM ONLINE + +
+

{% trans "Overview of blood donation activity and requirements in your area." %}

+
+ {% if user.is_authenticated and user_badges %} +
+
+ {% for badge in user_badges %} +
+ + {{ badge.name }} +
+ {% endfor %} +
+
+ {% endif %} +
+ + {% if involved_events %} + +
+
+
+
{% trans "Action Required: Donations in Progress" %}
+
+ {% for event in involved_events %} +
+
+

{{ event.donor.name }} is helping {{ event.request.patient_name }}

+
+ {{ event.date|date:"M d, Y" }} + {% trans "Mark Completed" %} +
+
+
+ {% endfor %} +
+
+
+
+ {% endif %} + + + + +
+ +
+
+
+
+ + + +
+
+ + + +
+
+ +
+
+ +
+
+ +
+ + +
+ +
+
+ {% for donor in donors %} +
+
+
+ {% if donor.user and donor.user.profile.profile_pic %} + {{ donor.name }} + + {{ donor.blood_group }} + + {% else %} +
{{ donor.blood_group }}
+ {% endif %} +
+
+ +
+ {{ donor.name }} + {% if donor.is_verified %} + + {% endif %} + {% if donor.distance and donor.distance < 1000 %} + + {{ donor.distance|floatformat:1 }} km + + {% endif %} +
+

{{ donor.location }}, {{ donor.district }}

+
+
+
+
+ + {% if donor.is_available %}Available{% else %}Unavailable{% endif %} + +

Last Donated: {{ donor.last_donation_date|default:"Never" }}

+
+
+ {% if donor.user %} + + + + {% endif %} + Call +
+
+
+ {% empty %} +
+ +

No donors match your search criteria.

+
+ {% endfor %} +
+
+
+ + +
+
+
+

Vaccination & Eligibility

+

We prioritize donors who are fully vaccinated to ensure maximum safety for recipients.

+
+
+ Community Immunity Level + {{ stats.vaccinated_percentage }}% +
+
+
+
+
+ Update Status +
+
+ +
+
+
+ + +
+

{% trans "Top 5 Myths vs. Facts about Blood Donation" %}

+
+ {% for item in myths_vs_facts %} +
+
+
{% trans "MYTH" %}
+

"{{ item.myth }}"

+
{% trans "FACT" %}
+

{{ item.fact }}

+
+
+ {% endfor %} +
+
+
+ + +
+ +
+
Urgency Distribution
+
+ +
+
+ + +
+
+ Urgent Requests + HOT +
+ + + {% if user.is_staff %} + + {% endif %} + +
+ {% for req in blood_requests %} +
+
+ + {% if req.urgency == 'CRITICAL' %} + + {% elif req.urgency == 'URGENT' %} + + {% else %} + + {% endif %} + {{ req.urgency }} + + {{ req.blood_group }} +
+
{{ req.patient_name }}
+

{{ req.hospital }}

+ + {% with acceptance=req.donations.first %} + {% if acceptance %} +
+

+ + Accepted by: {{ acceptance.donor.name }} +

+
+ {% endif %} + {% endwith %} +
+ {{ req.created_at|timesince }} ago +
+ {% if req.user %} + + + + {% endif %} + Help Now → +
+
+
+ {% empty %} +

No active requests found.

+ {% endfor %} +
+
+ + +
+
Blood Bank Inventory
+
+ {% for bank in blood_banks %} +
+
+ {{ bank.name }} + 24/7 Available +
+
+ A+: {{ bank.stock_a_plus }} + A-: {{ bank.stock_a_minus }} + B+: {{ bank.stock_b_plus }} + B-: {{ bank.stock_b_minus }} + O+: {{ bank.stock_o_plus }} + O-: {{ bank.stock_o_minus }} + AB+: {{ bank.stock_ab_plus }} + AB-: {{ bank.stock_ab_minus }} +
+
+ {% empty %} +

No blood banks registered.

+ {% endfor %} +
+ {% if user.is_staff %} + Manage Banks + {% endif %} +
+ + +
+
Lifesaver Tip
+

Donating once can save up to three lives. Make sure to stay hydrated and rest before your appointment!

+
+
+
+
+{% endblock %} + +{% block scripts %} + + +{% endblock %} diff --git a/core/templates/core/live_map.html b/core/templates/core/live_map.html new file mode 100644 index 0000000..3d556cf --- /dev/null +++ b/core/templates/core/live_map.html @@ -0,0 +1,157 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+
+

Live Alert Map

+ LIVE UPDATES +
+ +
+
+
+
+
+ +
+
+
+
+
Recent Alerts
+
+
+
+ + + + + + + + + + + + + {% for req in requests %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
PatientBlood GroupHospitalUrgencyStatus / Accepted ByAction
{{ req.patient_name }}{{ req.blood_group }}{{ req.hospital }} + {% if req.urgency == 'CRITICAL' %} + + CRITICAL + + {% elif req.urgency == 'URGENT' %} + + URGENT + + {% else %} + + NORMAL + + {% endif %} + + {% with acceptance=req.donations.first %} + {% if acceptance %} + + {{ acceptance.donor.name }} + + {% else %} + Awaiting Help + {% endif %} + {% endwith %} + + +
No active alerts found.
+
+
+
+
+
+
+ + + + +{% endblock %} diff --git a/core/templates/core/lives_saved.html b/core/templates/core/lives_saved.html new file mode 100644 index 0000000..089f410 --- /dev/null +++ b/core/templates/core/lives_saved.html @@ -0,0 +1,91 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+
+

{{ title }}

+

Every single donation has the potential to save up to three lives. Together, we are building a safer community.

+
+
+ +
+
+
+
+
+ +
+

{{ total_impact }}

+

Total Lives Saved

+
+
+
+
+
+
+
+ +
+

{{ total_donations }}

+

Successful Donations

+
+
+
+
+ +
+
+

Recent Life-Saving Events

+
+
+
+ + + + + + + + + + + {% for donation in donations %} + + + + + + + {% empty %} + + + + {% endfor %} + +
ChampionImpactLocationDate
+
{{ donation.donor_user.username }}
+
Verified Donor
+
+ + 3 Lives Saved + + +
{{ donation.request.location }}
+
+
{{ donation.date|date:"M d, Y" }}
+
+

No recent events to display.

+
+
+
+
+ + +
+{% endblock %} diff --git a/core/templates/core/login.html b/core/templates/core/login.html new file mode 100644 index 0000000..e08b46a --- /dev/null +++ b/core/templates/core/login.html @@ -0,0 +1,100 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}Login - RaktaPulse{% endblock %} + +{% block content %} +
+
+
+
+
+ +
+

{% trans "System Login" %}

+

{% trans "Secure access for registered members" %}

+
+ + {% if form.non_field_errors %} +
+ {% for error in form.non_field_errors %} +
+ + {{ error }} +
+ {% endfor %} +
+ {% endif %} + +
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.errors %} +
+ {% for error in field.errors %}{{ error }}{% endfor %} +
+ {% endif %} +
+ {% endfor %} + +
+
+ + +
+ Forgot password? +
+ + +
+ +
+

Don't have an account? Create one now

+
+ + Back to Home + +
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/notifications.html b/core/templates/core/notifications.html new file mode 100644 index 0000000..e5c80c5 --- /dev/null +++ b/core/templates/core/notifications.html @@ -0,0 +1,35 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block content %} +
+
+
+
+
+

{% trans "Notifications" %}

+ {{ notifications.count }} +
+ + {% if notifications %} +
+ {% for note in notifications %} +
+
+

{{ note.message }}

+ {{ note.created_at|timesince }} {% trans "ago" %} +
+
+ {% endfor %} +
+ {% else %} +
+ +

{% trans "No notifications yet." %}

+
+ {% endif %} +
+
+
+
+{% endblock %} diff --git a/core/templates/core/profile.html b/core/templates/core/profile.html new file mode 100644 index 0000000..82c6acd --- /dev/null +++ b/core/templates/core/profile.html @@ -0,0 +1,195 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}User Profile - RaktaPulse{% endblock %} + +{% block content %} +
+
+
+
+
+ {% if user.profile.profile_pic %} + {{ user.username }} + {% else %} +
+ +
+ {% endif %} +
+

{{ user.username }}

+

Member since {{ user.date_joined|date:"M d, Y" }}

+
+
+ +
+ {% csrf_token %} +
Account Information
+
+
+ + +
+
+ + +
+
+ + {{ u_form.first_name }} +
+
+ + {{ u_form.last_name }} +
+
+ +
Profile Details
+
+
+ + {{ p_form.bio }} +
Write a short bio about yourself.
+
+
+ + {{ p_form.blood_group }} +
+
+ + {{ p_form.phone }} +
+
+ + {{ p_form.birth_date }} +
+
+ + {{ p_form.location }} +
+
+ + {{ p_form.profile_pic }} +
+
+ +
+ +
+
+ +
+ Transaction & Operation History +
+ +
+
Donor Record (People You've Helped)
+
+
+
+
{{ donations_made.count }}
+
Total Volunteers
+
+
+
{{ completed_donations_count }}
+
Successful Donations
+
+
+
+ + {% if donations_made %} +
+ {% for donation in donations_made %} +
+
+
+ Helped {{ donation.request.patient_name }} +
{{ donation.request.hospital }} | {{ donation.date|date:"M d, Y" }}
+
+
+ {% if donation.is_completed %} + Completed + {% else %} + Pending + {% endif %} +
+
+
+ {% endfor %} +
+ {% else %} +
+

You haven't volunteered for any requests yet.

+
+ {% endif %} +
+ +
+
Request History (Who Helped You)
+ {% if requests_made %} +
+ {% for br in requests_made %} +
+
+
+ {{ br.blood_group }} for {{ br.patient_name }} +
{{ br.created_at|date:"M d, Y" }} | Status: {{ br.status }}
+ +
+

Volunteers:

+ {% with br.donations.all as donations %} + {% if donations %} +
+ {% for d in donations %} + + {{ d.donor_user.username }} + + {% endfor %} +
+ {% else %} + No volunteers yet + {% endif %} + {% endwith %} +
+
+ + {{ br.status }} + +
+
+ {% endfor %} +
+ {% else %} +
+

You haven't made any blood requests yet.

+
+ {% endif %} +
+ +
+
+ Danger Zone +
+
+

Clearing personal info will remove your bio, location, phone number, and profile picture. Your account and donation history will remain intact.

+
+ {% csrf_token %} + +
+
+ {% csrf_token %} + +
+
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/public_profile.html b/core/templates/core/public_profile.html new file mode 100644 index 0000000..7087ed5 --- /dev/null +++ b/core/templates/core/public_profile.html @@ -0,0 +1,113 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block title %}{{ profile_user.username }}'s Profile - RaktaPulse{% endblock %} + +{% block content %} +
+
+
+
+
+
+ {% if profile.profile_pic %} + {{ profile_user.username }} + {% else %} +
+ +
+ {% endif %} +
+

{{ profile_user.first_name }} {{ profile_user.last_name }}

+

@{{ profile_user.username }}

+ + {% if donor %} +
+ {{ donor.blood_group }} + {% if donor.is_available %} + Available + {% else %} + Not Available + {% endif %} +
+ {% endif %} +
+ +
+
+
+
Location
+

{{ profile.location|default:"Not specified" }}

+
+
+
+
+
Member Since
+

{{ profile_user.date_joined|date:"F Y" }}

+
+
+
+ +
+
About
+
+ {{ profile.bio|default:"No bio provided."|linebreaks }} +
+
+ +
+
Impact & Reliability
+
+
+
+
{{ donations_made.count }}
+
Volunteered
+
+
+
+
+
{{ completed_donations_count }}
+
Successful
+
+
+
+
+ + {% if donations_made %} +
+
Recent Donations
+
+ {% for donation in donations_made|slice:":5" %} +
+
+
+
Donated to {{ donation.request.patient_name }}
+
{{ donation.request.hospital }} | {{ donation.date|date:"M d, Y" }}
+
+ {% if donation.is_completed %} + Verified + {% else %} + Pending + {% endif %} +
+
+ {% endfor %} +
+
+ {% endif %} + +
+ {% if user.is_authenticated and user != profile_user %} + + Send Message + + {% endif %} + + Back to Donors + +
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/register.html b/core/templates/core/register.html new file mode 100644 index 0000000..55cdf87 --- /dev/null +++ b/core/templates/core/register.html @@ -0,0 +1,90 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}Create Account - RaktaPulse{% endblock %} + +{% block content %} +
+
+
+
+
+ +
+

{% trans "Create Account" %}

+

{% trans "Join the next-gen management system" %}

+
+ +
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.help_text %} +
{{ field.help_text|safe }}
+ {% endif %} + {% if field.errors %} +
+ {% for error in field.errors %}{{ error }}{% endfor %} +
+ {% endif %} +
+ {% endfor %} + + +
+ +
+

Already have an account? Log in here

+
+ + Back to Home + +
+
+
+
+ + +{% endblock %} diff --git a/core/templates/core/register_donor.html b/core/templates/core/register_donor.html new file mode 100644 index 0000000..375f8d2 --- /dev/null +++ b/core/templates/core/register_donor.html @@ -0,0 +1,37 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block content %} +
+
+
+
+

{% trans "Become a Blood Donor" %}

+

{% trans "Join our community of lifesavers. Your donation can save lives." %}

+ +
+ {% csrf_token %} +
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/request_blood.html b/core/templates/core/request_blood.html new file mode 100644 index 0000000..0c42861 --- /dev/null +++ b/core/templates/core/request_blood.html @@ -0,0 +1,104 @@ +{% extends "base.html" %} + +{% block title %}Post Blood Request - RaktaPulse{% endblock %} + +{% block content %} +
+
+
+
+
+
+
+ +
+

Post a Blood Request

+

Fill in the details to alert nearby donors.

+
+ +
+ {% csrf_token %} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
Include a prescription or patient photo for verification.
+
+ + + + +
+ +
+ + Fetching your precise location for the live map... +
+ +
+ + Cancel +
+
+
+
+
+
+
+ + +{% endblock %} diff --git a/core/templates/core/upload_health_report.html b/core/templates/core/upload_health_report.html new file mode 100644 index 0000000..20146eb --- /dev/null +++ b/core/templates/core/upload_health_report.html @@ -0,0 +1,72 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}Upload Health Report - RaktaPulse{% endblock %} + +{% block content %} +
+
+
+
+
+

Upload Medical Report

+

Save your hospital reports and health records securely.

+ +
+ {% csrf_token %} + +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + + When should you take the next test? +
+
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+

We'll notify you when it's time for your next test based on the date set above.

+
+ +
+ + Cancel +
+
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/vaccination_dashboard.html b/core/templates/core/vaccination_dashboard.html new file mode 100644 index 0000000..eaea071 --- /dev/null +++ b/core/templates/core/vaccination_dashboard.html @@ -0,0 +1,228 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}My Records - {{ project_name }}{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+
+
+

Health & Records

+

Manage your vaccinations and hospital reports in one place.

+
+ +
+ + {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} + + + +
+ +
+
+ {% for record in records %} +
+
+
+
{{ record.vaccine_name }}
+ Dose {{ record.dose_number }} +
+ +
+ {{ record.date_taken|date:"M d, Y" }} + {{ record.center_name|default:record.location }} +
+ + {% if record.photo %} + + {% endif %} + +
+

+ Location: {{ record.location }} +

+ {% if record.notes %} +

+ {{ record.notes|truncatechars:100 }} +

+ {% endif %} +
+
+
+ {% empty %} +
+
+ +

No vaccination records

+

Keep track of your immunizations.

+ Add Record +
+
+ {% endfor %} +
+
+ + +
+
+ {% for report in reports %} +
+
+
+
{{ report.title }}
+ + + +
+ +

{{ report.hospital_name }}

+ +
+ {{ report.report_date|date:"M d, Y" }} + {% if report.next_test_date %} + Next: {{ report.next_test_date|date:"M d, Y" }} + {% endif %} +
+ + {% if report.description %} +

{{ report.description|truncatechars:120 }}

+ {% endif %} + +
+ + + {% if report.allow_notifications %}Notifications On{% else %}Notifications Off{% endif %} + + {{ report.created_at|timesince }} ago +
+
+
+ {% empty %} +
+
+ +

No hospital reports

+

Upload your medical reports for safekeeping and reminders.

+ Upload First Report +
+
+ {% endfor %} +
+
+
+
+ + +{% endblock %} diff --git a/core/templates/core/vaccination_info.html b/core/templates/core/vaccination_info.html new file mode 100644 index 0000000..72f18e1 --- /dev/null +++ b/core/templates/core/vaccination_info.html @@ -0,0 +1,73 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}Vaccination Status - RaktaPulse{% endblock %} + +{% block content %} +
+
+
+

Vaccination Overview

+

Monitoring community immunity and donor eligibility.

+
+
+ +
+
+
+

Why Vaccination Matters?

+

Ensuring our donors are vaccinated is crucial for the safety of both the donors and the recipients. Fully vaccinated individuals contribute to a safer blood supply chain.

+ +
+
Community Progress
+
+ Fully Vaccinated Donors + {{ stats.percentage }}% +
+
+
{{ stats.percentage }}%
+
+

{{ stats.vaccinated_count }} out of {{ stats.total_donors }} registered donors are fully vaccinated.

+
+ +
Eligibility Criteria
+
    +
  • Must be fully vaccinated for priority donation.
  • +
  • At least 14 days must have passed since the last dose.
  • +
  • Must be in good health on the day of donation.
  • +
+ +
Interesting Vaccination Facts
+
+
+
+
Ancient Roots
+

The word "vaccine" comes from the Latin vacca (cow), honoring the first smallpox vaccine derived from cowpox.

+
+
+
+
+
Immune Memory
+

Vaccines create "memory cells" that allow your body to recognize and fight pathogens years after the initial shot.

+
+
+
+
+
+ +
+
+
Update Your Status
+

Are you a donor? Keep your vaccination records up to date to maintain your verified status.

+ +
+ +
+
Contact Support
+

Have questions about vaccination and donation? Our medical team is here to help.

+ Email Support +
+
+
+
+{% endblock %} diff --git a/core/templates/core/welcome.html b/core/templates/core/welcome.html new file mode 100644 index 0000000..f6a1f89 --- /dev/null +++ b/core/templates/core/welcome.html @@ -0,0 +1,297 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}Welcome to RaktaPulse{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ + + + +
+
+
+
+
+ Next-Gen Blood Management +
+

Streamlining Life-Saving Connections.

+

A high-efficiency platform for real-time blood donor matching, hospital coordination, and emergency response management.

+ +
+
+
+ +
+
+
+
+
+ + +
+
+
+
+

Engineered for Impact

+

Our core modules provide the tools needed for rapid response and efficient data management.

+
+
+
+
+
+ +

Smart Matching

+

AI-driven geolocation algorithm to find the most compatible donors in the shortest time.

+
+
+
+
+ +

Live Inventory

+

Real-time tracking of blood stock levels across all partner hospitals and blood banks.

+
+
+
+
+ +

Secure Channels

+

End-to-end encrypted communication for donor privacy and secure medical data handling.

+
+
+
+
+
+ + +
+
+
+
+
10.4k
+
Verified Donors
+
+
+
85%
+
Response Rate
+
+
+
120+
+
Tech Partners
+
+
+
+
+ + +
+
+
+

Ready to Integrate?

+

Join the network of tech-driven healthcare providers saving lives every day.

+ Create Your Account +
+
+
+ + +
+
+
+
+ RaktaPulse © 2026. v2.1.0-stable +
+
+
+ + + +
+
+
+
+
+
+{% endblock %} diff --git a/core/tests.py b/core/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/core/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/core/urls.py b/core/urls.py new file mode 100644 index 0000000..bb63c67 --- /dev/null +++ b/core/urls.py @@ -0,0 +1,46 @@ +from django.urls import path +from . import views + +urlpatterns = [ + # 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"), + path("delete-messages//", 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"), + 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"), + path("donation-history/", views.donation_history, name="donation_history"), + path("lives-saved/", views.lives_saved, name="lives_saved"), +] diff --git a/core/views.py b/core/views.py new file mode 100644 index 0000000..833f1a6 --- /dev/null +++ b/core/views.py @@ -0,0 +1,933 @@ +# 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 +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 django.contrib.auth.models import User +from django.http import JsonResponse +from django.views.decorators.http import require_POST +from django.views.decorators.csrf import csrf_exempt + +# Local apps +from .models import ( + Donor, BloodRequest, BloodBank, VaccineRecord, UserProfile, + BLOOD_GROUPS, DonationEvent, Notification, Hospital, + Message, Badge, HealthReport +) +from .forms import UserUpdateForm, ProfileUpdateForm, UserRegisterForm + +# --- Medical Compatibility Constants --- + +COMPATIBILITY_MATRIX = { + 'O-': ['O-', 'O+', 'A-', 'A+', 'B-', 'B+', 'AB-', 'AB+'], # Universal Donor + 'O+': ['O+', 'A+', 'B+', 'AB+'], + 'A-': ['A-', 'A+', 'AB-', 'AB+'], + 'A+': ['A+', 'AB+'], + 'B-': ['B-', 'B+', 'AB-', 'AB+'], + 'B+': ['B+', 'AB+'], + 'AB-': ['AB-', 'AB+'], + 'AB+': ['AB+'], +} + +# --- Emergency & Location Helpers --- + +@login_required +@csrf_exempt +def emergency_sms(request): + """ + 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, 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': 'Blood group and coordinates are required.'}, status=400) + + try: + u_lat = float(lat) + u_lng = float(lng) + + # 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: + nearby_donors.append(d) + + # Send simulated notifications + count = len(nearby_donors) + for d in nearby_donors: + # Simulated SMS/Push notification logic + pass + + return JsonResponse({ + 'status': 'success', + 'message': f'Success! Pinged {count} donors in the area.', + 'count': count + }) + except Exception as e: + return JsonResponse({'status': 'error', 'message': f'Something went sideways: {str(e)}'}, status=500) + + return JsonResponse({'status': 'error', 'message': 'Method not allowed.'}, status=405) + +@login_required +@csrf_exempt +@require_POST +def update_location(request): + try: + data = json.loads(request.body) + lat = data.get('latitude') + lng = data.get('longitude') + + if lat and lng: + profile = request.user.profile + profile.latitude = lat + profile.longitude = lng + profile.last_location_update = timezone.now() + profile.save() + return JsonResponse({'status': 'success'}) + except Exception as e: + return JsonResponse({'status': 'error', 'message': str(e)}, status=400) + return JsonResponse({'status': 'error', 'message': 'Invalid data'}, status=400) + +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): + # Ensure user has a profile + profile, created = UserProfile.objects.get_or_create(user=request.user) + + if request.method == 'POST': + u_form = UserUpdateForm(request.POST, instance=request.user) + 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() + messages.success(request, f'Your account has been updated!') + return redirect('profile') + else: + u_form = UserUpdateForm(instance=request.user) + p_form = ProfileUpdateForm(instance=profile) + + # Fetch History + requests_made = BloodRequest.objects.filter(user=request.user).order_by('-created_at') + donations_made = DonationEvent.objects.filter(donor_user=request.user).select_related('request').order_by('-date') + + context = { + 'u_form': u_form, + 'p_form': p_form, + 'requests_made': requests_made, + 'donations_made': donations_made, + 'completed_donations_count': donations_made.filter(is_completed=True).count() + } + + return render(request, 'core/profile.html', context) + +@login_required +@require_POST +def delete_personal_info(request): + """View to clear non-essential personal information.""" + user = request.user + profile = user.profile + + # Clear User fields + user.first_name = "" + user.last_name = "" + user.save() + + # Clear Profile fields + profile.bio = "" + profile.location = "" + profile.phone = "" + profile.birth_date = None + profile.profile_pic = None + 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 + + dlat = math.radians(lat2 - lat1) + dlon = math.radians(lon2 - lon1) + + a = math.sin(dlat / 2)**2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon / 2)**2 + c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) + + return R * c + +def login_view(request): + if request.method == "POST": + form = AuthenticationForm(request, data=request.POST) + if form.is_valid(): + user = form.get_user() + login(request, user) + messages.success(request, f"Welcome back, {user.username}!") + return redirect("home") + else: + messages.error(request, "Invalid username or password. Please try again.") + else: + form = AuthenticationForm() + return render(request, "core/login.html", {"form": form}) + +def logout_view(request): + logout(request) + return redirect("home") + +def register_view(request): + if request.method == "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 = UserRegisterForm() + return render(request, "core/register.html", {"form": form}) + +def welcome(request): + """Render a beautiful animated welcome page.""" + if request.user.is_authenticated: + return redirect('home') + return render(request, "core/welcome.html") + +def home(request): + """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') + + # 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 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() + if query_blood: + donors = donors.filter(blood_group=query_blood) + if query_location: + donors = donors.filter(location__icontains=query_location) + + donor_list_data = list(donors) + + if user_lat and user_lng: + try: + u_lat = float(user_lat) + u_lng = float(user_lng) + for d in donor_list_data: + if d.latitude and d.longitude: + d.distance = haversine(u_lat, u_lng, float(d.latitude), float(d.longitude)) + else: + d.distance = 999999 # Very far + donor_list_data.sort(key=lambda x: x.distance) + except ValueError: + donor_list_data.sort(key=lambda x: (-x.is_available, x.name)) + else: + donor_list_data.sort(key=lambda x: (-x.is_available, x.name)) + + three_days_ago = timezone.now() - timezone.timedelta(days=3) + blood_requests = BloodRequest.objects.filter( + Q(status='Active') | + Q(status='Accepted', accepted_at__gte=three_days_ago) + ).order_by('-urgency', '-created_at') + + blood_banks = BloodBank.objects.all() + + # Stats for Dashboard + demo_donations = 157 + demo_donors = 48 + + actual_completed = DonationEvent.objects.filter(is_completed=True).count() + completed_donations = actual_completed + demo_donations + + # Find Recent Contributions (Last 24 Hours) + last_24_hours = timezone.now() - timezone.timedelta(hours=24) + recent_contributions = DonationEvent.objects.filter( + is_completed=True, + date__gte=last_24_hours + ).select_related('donor').order_by('-date') + + stats = { + "total_donors": Donor.objects.count() + demo_donors, + "active_requests": BloodRequest.objects.filter( + Q(status='Active') | Q(status='Accepted', accepted_at__gte=three_days_ago) + ).count(), + "total_stock": sum([ + bb.stock_a_plus + bb.stock_a_minus + bb.stock_b_plus + bb.stock_b_minus + + bb.stock_o_plus + bb.stock_o_minus + bb.stock_ab_plus + bb.stock_ab_minus + for bb in blood_banks + ]), + "total_capacity": sum([bb.total_capacity * 8 for bb in blood_banks]), # 8 blood types + "vaccinated_percentage": 0, + "completed_donations": completed_donations, + "lives_saved": completed_donations * 3 + } + + total_d = stats["total_donors"] + if total_d > 0: + vaccinated_count = Donor.objects.filter(vaccination_status__icontains='Fully').count() + stats["vaccinated_percentage"] = int((vaccinated_count / total_d) * 100) + + myths_vs_facts = [ + {"myth": "Donating blood is painful.", "fact": "You only feel a quick pinch, like a mosquito bite."}, + {"myth": "I'm too old to donate.", "fact": "There is no upper age limit as long as you're healthy."}, + {"myth": "It takes all day to donate.", "fact": "The actual donation takes about 10 minutes, the whole process is under an hour."}, + {"myth": "Giving blood makes you weak.", "fact": "Your body replaces fluids within 24 hours and cells within weeks."}, + {"myth": "I can't donate because I have high BP.", "fact": "As long as it's within 180/100 at the time of donation, you're fine."}, + ] + + # Urgency Distribution for Pie Chart + urgency_counts = { + 'CRITICAL': blood_requests.filter(urgency='CRITICAL').count(), + 'URGENT': blood_requests.filter(urgency='URGENT').count(), + 'NORMAL': blood_requests.filter(urgency='NORMAL').count(), + } + + context = { + "donors": donor_list_data[:15], # Increased count for scrollability + "blood_requests": blood_requests[:6], + "blood_banks": blood_banks, + "blood_groups": [g[0] for g in BLOOD_GROUPS], + "stats": stats, + "recent_contributions": recent_contributions, + "project_name": "RaktaPulse", + "current_time": timezone.now(), + "myths_vs_facts": myths_vs_facts, + "urgency_counts": json.dumps(urgency_counts), + } + + if request.user.is_authenticated: + # Get active involvements + involved_events = DonationEvent.objects.filter( + (Q(donor_user=request.user) | Q(request__user=request.user)), + is_completed=False + ) + context["involved_events"] = involved_events + context["user_badges"] = request.user.profile.badges.all() + context["unread_notifications_count"] = request.user.notifications.filter(is_read=False).count() + context["unread_messages_count"] = Message.objects.filter(receiver=request.user, is_read=False).count() + + return render(request, "core/index.html", context) + +def donor_list(request): + query = request.GET.get('q', '') + blood_group = request.GET.get('blood_group', '') + district = request.GET.get('district', '') + user_lat = request.GET.get('lat') + user_lng = request.GET.get('lng') + + donors = Donor.objects.all() + if query: + donors = donors.filter(Q(name__icontains=query) | Q(location__icontains=query)) + if blood_group: + donors = donors.filter(blood_group=blood_group) + if district: + donors = donors.filter(district__icontains=district) + + donor_list_data = list(donors) + + if user_lat and user_lng: + try: + u_lat = float(user_lat) + u_lng = float(user_lng) + for d in donor_list_data: + if d.latitude and d.longitude: + d.distance = haversine(u_lat, u_lng, float(d.latitude), float(d.longitude)) + else: + d.distance = 999999 + donor_list_data.sort(key=lambda x: x.distance) + except ValueError: + donor_list_data.sort(key=lambda x: (-x.is_verified, x.name)) + 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], + } + return render(request, 'core/donor_list.html', context) + +def blood_request_list(request): + status = request.GET.get('status', '') + requests = BloodRequest.objects.all() + if status: + requests = requests.filter(status=status) + + requests = requests.order_by('-created_at') + + donor_profile = None + if request.user.is_authenticated: + donor_profile = getattr(request.user, 'donor_profile', None) + + for req in requests: + req.can_volunteer = True + req.ineligibility_reason = "" + + if not donor_profile: + req.can_volunteer = False + req.ineligibility_reason = "Register as donor" + else: + # Check medical compatibility + donor_group = donor_profile.blood_group + compatible_groups = COMPATIBILITY_MATRIX.get(donor_group, []) + + if req.blood_group not in compatible_groups: + req.can_volunteer = False + req.ineligibility_reason = "Incompatible" + + # Check 90 days limit + if donor_profile.last_donation_date: + days_since = (timezone.now().date() - donor_profile.last_donation_date).days + if days_since < 90: + req.can_volunteer = False + days_left = 90 - days_since + req.ineligibility_reason = f"Wait {days_left}d" + req.days_until_eligible = days_left + + context = { + 'requests': requests, + 'current_status': status, + } + return render(request, 'core/blood_request_list.html', context) + +def blood_bank_list(request): + user_lat = request.GET.get('lat') + user_lng = request.GET.get('lng') + + banks = BloodBank.objects.all() + bank_list_data = list(banks) + + if user_lat and user_lng: + try: + u_lat = float(user_lat) + u_lng = float(user_lng) + for b in bank_list_data: + if b.latitude and b.longitude: + b.distance = haversine(u_lat, u_lng, float(b.latitude), float(b.longitude)) + else: + b.distance = 999999 + bank_list_data.sort(key=lambda x: x.distance) + except ValueError: + pass + + context = { + 'banks': bank_list_data, + } + return render(request, 'core/blood_bank_list.html', context) + +def vaccination_info(request): + stats = { + "total_donors": Donor.objects.count(), + "vaccinated_count": Donor.objects.filter(vaccination_status__icontains='Fully').count(), + } + if stats["total_donors"] > 0: + stats["percentage"] = int((stats["vaccinated_count"] / stats["total_donors"]) * 100) + else: + stats["percentage"] = 0 + + return render(request, 'core/vaccination_info.html', {'stats': stats}) + +def live_map(request): + """View to display live alerts/requests on a map.""" + three_days_ago = timezone.now() - timezone.timedelta(days=3) + active_requests = BloodRequest.objects.filter( + Q(status='Active') | + Q(status='Accepted', accepted_at__gte=three_days_ago) + ).order_by('-created_at') + + # Also include blood banks and donors optionally if we want a full map + # But focusing on alerts as requested. + context = { + 'requests': active_requests, + 'title': 'Live Alert Map', + } + return render(request, 'core/live_map.html', context) + +def request_blood(request): + """View to create a new blood request with geolocation.""" + if request.method == "POST": + patient_name = request.POST.get('patient_name') + blood_group = request.POST.get('blood_group') + location = request.POST.get('location') + urgency = request.POST.get('urgency') + hospital = request.POST.get('hospital') + contact_number = request.POST.get('contact_number') + image = request.FILES.get('image') + latitude = request.POST.get('latitude') + longitude = request.POST.get('longitude') + + if patient_name and blood_group and hospital and contact_number: + BloodRequest.objects.create( + user=request.user if request.user.is_authenticated else None, + patient_name=patient_name, + blood_group=blood_group, + location=location, + urgency=urgency, + hospital=hospital, + contact_number=contact_number, + image=image, + latitude=latitude if latitude else None, + longitude=longitude if longitude else None + ) + messages.success(request, "Blood request posted successfully! Help is on the way.") + return redirect('blood_request_list') + else: + messages.error(request, "Please fill in all required fields.") + + 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) + +import csv +from django.http import HttpResponse + +@login_required +def donation_history(request): + """View to display the full history of completed donations with filtering and export.""" + completed_donations = DonationEvent.objects.filter(is_completed=True).select_related('donor_user', 'request').order_by('-date') + + # Filtering + blood_group = request.GET.get('blood_group') + location = request.GET.get('location') + export = request.GET.get('export') + + if blood_group: + completed_donations = completed_donations.filter(request__blood_group=blood_group) + if location: + completed_donations = completed_donations.filter(request__location__icontains=location) + + # Export to CSV + if export == 'csv': + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename="donation_history.csv"' + + writer = csv.writer(response) + writer.writerow(['Donor', 'Blood Group', 'Patient', 'Location', 'Hospital', 'Date']) + + for donation in completed_donations: + writer.writerow([ + donation.donor_user.username if donation.donor_user else donation.donor.name, + donation.request.blood_group, + donation.request.patient_name, + donation.request.location, + donation.request.hospital, + donation.date.strftime('%Y-%m-%d %H:%M') + ]) + return response + + context = { + 'donations': completed_donations, + 'title': 'Donation History', + 'blood_groups': [g[0] for g in BLOOD_GROUPS], + 'current_filters': { + 'blood_group': blood_group, + 'location': location + } + } + return render(request, 'core/donation_history.html', context) + +@login_required +def lives_saved(request): + """View to display the impact and lives saved through donations.""" + completed_donations = DonationEvent.objects.filter(is_completed=True).select_related('donor_user', 'request').order_by('-date') + + total_donations = completed_donations.count() + # Demo data from home view to keep consistency + demo_donations = 157 + total_impact = (total_donations + demo_donations) * 3 + + context = { + 'donations': completed_donations[:10], # Show recent impact + 'total_donations': total_donations + demo_donations, + 'total_impact': total_impact, + 'title': 'Lives Saved & Community Impact', + } + return render(request, 'core/lives_saved.html', context) + +@login_required +@login_required +def vaccination_dashboard(request): + records = VaccineRecord.objects.filter(user=request.user).order_by('-date_taken') + reports = HealthReport.objects.filter(user=request.user).order_by('-report_date') + context = { + 'records': records, + 'reports': reports, + 'project_name': "RaktaPulse", + } + return render(request, 'core/vaccination_dashboard.html', context) + +@login_required +def add_vaccination(request): + if request.method == "POST": + vaccine_name = request.POST.get('vaccine_name') + dose_number = request.POST.get('dose_number') + date_taken = request.POST.get('date_taken') + location = request.POST.get('location') + center_name = request.POST.get('center_name') + notes = request.POST.get('notes') + photo = request.FILES.get('photo') + + if vaccine_name and dose_number and date_taken: + VaccineRecord.objects.create( + user=request.user, + vaccine_name=vaccine_name, + dose_number=dose_number, + date_taken=date_taken, + location=location, + center_name=center_name, + notes=notes, + photo=photo + ) + messages.success(request, "Vaccination record added successfully!") + return redirect('vaccination_dashboard') + else: + messages.error(request, "Please fill in all required fields.") + + return render(request, 'core/add_vaccination.html') + +@login_required +def volunteer_for_request(request, request_id): + blood_request = BloodRequest.objects.get(id=request_id) + donor_profile = getattr(request.user, 'donor_profile', None) + + if not donor_profile: + 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.") + return redirect('blood_request_list') + + # Check for medical compatibility + donor_group = donor_profile.blood_group + compatible_groups = COMPATIBILITY_MATRIX.get(donor_group, []) + + if blood_request.blood_group not in compatible_groups: + messages.error(request, f"Your blood group ({donor_group}) is not medically compatible with {blood_request.blood_group}. Only compatible donors can volunteer.") + return redirect('blood_request_list') + + # Check if already volunteered + if DonationEvent.objects.filter(donor=donor_profile, request=blood_request).exists(): + messages.warning(request, "You have already volunteered for this request.") + else: + DonationEvent.objects.create( + donor=donor_profile, + request=blood_request, + donor_user=request.user + ) + # Update request status and timestamp + blood_request.status = 'Accepted' + blood_request.accepted_at = timezone.now() + blood_request.save() + + messages.success(request, "Thank you for volunteering! The requester has been notified.") + + # Notify the requester + if blood_request.user: + Notification.objects.create( + user=blood_request.user, + message=f"Donor {request.user.username} has volunteered to help {blood_request.patient_name}!" + ) + + return redirect('blood_request_list') + +@login_required +def complete_donation(request, event_id): + event = DonationEvent.objects.get(id=event_id) + # Only the requester or the donor can mark as complete + if request.user == event.donor_user or (event.request.user and request.user == event.request.user): + 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() + + if completed_count >= 1: + badge = Badge.objects.filter(name='First-Time Donor').first() + if badge: + donor_profile.badges.add(badge) + + if completed_count >= 5: + badge = Badge.objects.filter(name='Community Champion').first() + if badge: + donor_profile.badges.add(badge) + + if completed_count >= 10: + badge = Badge.objects.filter(name='Life Saver').first() + if badge: + donor_profile.badges.add(badge) + + # Notify both + Notification.objects.create( + user=event.donor_user, + message=f"Thank you for your donation to {event.request.patient_name}! You've earned recognition for your impact." + ) + if event.request.user: + Notification.objects.create( + user=event.request.user, + message=f"We hope the donation for {event.request.patient_name} went well." + ) + + messages.success(request, "Donation marked as completed. Thank you!") + else: + messages.error(request, "You are not authorized to complete this event.") + + return redirect('home') + +@login_required +def notifications_view(request): + notifications = Notification.objects.filter(user=request.user).order_by('-created_at') + # Mark as read when viewed + notifications.filter(is_read=False).update(is_read=True) + return render(request, 'core/notifications.html', {'notifications': notifications}) + +@login_required +def register_donor(request): + if hasattr(request.user, 'donor_profile'): + messages.info(request, "You are already registered as a donor.") + return redirect('profile') + + if request.method == "POST": + blood_group = request.POST.get('blood_group') + location = request.POST.get('location') + phone = request.POST.get('phone') + + if blood_group and phone: + Donor.objects.create( + user=request.user, + name=request.user.username, + blood_group=blood_group, + location=location, + phone=phone, + is_available=True + ) + messages.success(request, "Congratulations! You are now a registered donor.") + return redirect('donor_list') + else: + messages.error(request, "Please fill in all required fields.") + + return render(request, 'core/register_donor.html', {'blood_groups': [g[0] for g in BLOOD_GROUPS]}) + +def public_profile(request, username): + user = User.objects.get(username=username) + profile = user.profile + donor_profile = getattr(user, 'donor_profile', None) + + # Fetch History + donations_made = DonationEvent.objects.filter(donor_user=user).select_related('request').order_by('-date') + completed_donations_count = donations_made.filter(is_completed=True).count() + + context = { + 'profile_user': user, + 'profile': profile, + 'donor': donor_profile, + 'donations_made': donations_made, + 'completed_donations_count': completed_donations_count + } + return render(request, 'core/public_profile.html', context) + +@login_required +def inbox(request): + # Get all users the current user has messaged or received messages from + sent_to = Message.objects.filter(sender=request.user).values_list('receiver', flat=True) + received_from = Message.objects.filter(receiver=request.user).values_list('sender', flat=True) + user_ids = set(list(sent_to) + list(received_from)) + + users = User.objects.filter(id__in=user_ids) + + # Get last message for each conversation + conversations = [] + for user in users: + last_message = Message.objects.filter( + (Q(sender=request.user) & Q(receiver=user)) | + (Q(sender=user) & Q(receiver=request.user)) + ).order_by('-timestamp').first() + conversations.append({ + 'user': user, + 'last_message': last_message + }) + + conversations.sort(key=lambda x: x['last_message'].timestamp, reverse=True) + + return render(request, 'core/inbox.html', {'conversations': conversations}) + +@login_required +def chat(request, username): + other_user = User.objects.get(username=username) + if request.method == "POST": + content = request.POST.get('content') + attachment = request.FILES.get('attachment') + sticker_id = request.POST.get('sticker_id') + + msg_type = 'text' + if sticker_id: + msg_type = 'sticker' + elif attachment: + content_type = attachment.content_type + if content_type.startswith('image/'): + msg_type = 'image' + elif content_type.startswith('video/'): + msg_type = 'video' + else: + msg_type = 'file' + + if content or attachment or sticker_id: + Message.objects.create( + sender=request.user, + receiver=other_user, + content=content, + attachment=attachment, + message_type=msg_type, + sticker_id=sticker_id + ) + return redirect('chat', username=username) + + messages = Message.objects.filter( + (Q(sender=request.user) & Q(receiver=other_user)) | + (Q(sender=other_user) & Q(receiver=request.user)) + ).order_by('timestamp') + + # Mark as read + Message.objects.filter(sender=other_user, receiver=request.user, is_read=False).update(is_read=True) + + return render(request, 'core/chat.html', {'other_user': other_user, 'chat_messages': messages}) + +@login_required +def upload_health_report(request): + if request.method == "POST": + title = request.POST.get('title') + hospital_name = request.POST.get('hospital_name') + report_file = request.FILES.get('report_file') + description = request.POST.get('description') + report_date = request.POST.get('report_date') + next_test_date = request.POST.get('next_test_date') + allow_notifications = request.POST.get('allow_notifications') == 'on' + + if title and report_file and report_date: + HealthReport.objects.create( + user=request.user, + title=title, + hospital_name=hospital_name, + report_file=report_file, + description=description, + report_date=report_date, + next_test_date=next_test_date if next_test_date else None, + allow_notifications=allow_notifications + ) + messages.success(request, "Health report uploaded successfully!") + return redirect('vaccination_dashboard') + else: + messages.error(request, "Please fill in all required fields.") + + return render(request, 'core/upload_health_report.html') diff --git a/db/config.php b/db/config.php new file mode 100644 index 0000000..4c9d0e0 --- /dev/null +++ b/db/config.php @@ -0,0 +1,17 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ]); + } + return $pdo; +} diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..8e7ac79 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/media/blood_requests/Garden_City.jpg b/media/blood_requests/Garden_City.jpg new file mode 100644 index 0000000..b709e7b Binary files /dev/null and b/media/blood_requests/Garden_City.jpg differ 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 diff --git a/media/profile_pics/1000048209.jpg b/media/profile_pics/1000048209.jpg new file mode 100644 index 0000000..2b51ef5 Binary files /dev/null and b/media/profile_pics/1000048209.jpg differ diff --git a/media/profile_pics/Best_Organic_Fertilizers_for_Summer_Growth.jpg b/media/profile_pics/Best_Organic_Fertilizers_for_Summer_Growth.jpg new file mode 100644 index 0000000..2499f6e Binary files /dev/null and b/media/profile_pics/Best_Organic_Fertilizers_for_Summer_Growth.jpg differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..12ae042 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,46 @@ +{ + "name": "workspace", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "workspace", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "bootstrap": "^5.3.8" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/bootstrap": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", + "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e12a2c6 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "workspace", + "version": "1.0.0", + "description": "This workspace houses the Django application scaffold used for Python-based templates.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "bootstrap": "^5.3.8" + } +} diff --git a/populate_coords.py b/populate_coords.py new file mode 100644 index 0000000..f1329e7 --- /dev/null +++ b/populate_coords.py @@ -0,0 +1,25 @@ +import os +import django +import random + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +django.setup() + +from core.models import Donor, BloodBank + +# Kathmandu center approx: 27.7172, 85.3240 +def update_coords(): + for donor in Donor.objects.all(): + donor.latitude = 27.7172 + (random.random() - 0.5) * 0.1 + donor.longitude = 85.3240 + (random.random() - 0.5) * 0.1 + donor.save() + + for bank in BloodBank.objects.all(): + bank.latitude = 27.7172 + (random.random() - 0.5) * 0.1 + bank.longitude = 85.3240 + (random.random() - 0.5) * 0.1 + bank.save() + + print("Updated coordinates for donors and banks.") + +if __name__ == "__main__": + update_coords() diff --git a/populate_data.py b/populate_data.py new file mode 100644 index 0000000..2a18687 --- /dev/null +++ b/populate_data.py @@ -0,0 +1,96 @@ +import os +import django +import datetime +import random + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +django.setup() + +from core.models import Donor, BloodRequest, BloodBank +from django.utils import timezone + +def run(): + # Clear existing data + Donor.objects.all().delete() + BloodRequest.objects.all().delete() + BloodBank.objects.all().delete() + + # Create Donors (Nepali Context) + donors_data = [ + ('Arjun Thapa', 'O+', 'Kathmandu', 'New Baneshwor', '9841000000', 'Fully Vaccinated', True), + ('Sita Shrestha', 'A-', 'Lalitpur', 'Patan Durbar Square', '9841111111', 'Fully Vaccinated', True), + ('Rajesh Hamal', 'B+', 'Bhaktapur', 'Suryabinayak', '9841222222', 'Partially Vaccinated', True), + ('Bipana Thapa', 'AB+', 'Kaski', 'Lakeside, Pokhara', '9841333333', 'Fully Vaccinated', False), + ('Nischal Basnet', 'O-', 'Rupandehi', 'Butwal', '9841444444', 'Not Vaccinated', False), + ('Swastima Khadka', 'A+', 'Chitwan', 'Bharatpur', '9841555555', 'Fully Vaccinated', True), + ] + for name, bg, dist, loc, ph, vac, ver in donors_data: + Donor.objects.create( + name=name, + blood_group=bg, + district=dist, + location=loc, + phone=ph, + is_available=True, + is_verified=ver, + citizenship_no=f"123-{random.randint(1000, 9999)}" if ver else None, + vaccination_status=vac, + last_vaccination_date=timezone.now().date() - datetime.timedelta(days=random.randint(30, 180)) + ) + + # Create Blood Requests (Nepali Context) + BloodRequest.objects.create( + patient_name='Ram Bahadur', + blood_group='O+', + location='Teaching Hospital, Maharajgunj', + urgency='CRITICAL', + hospital='Teaching Hospital', + contact_number='9800000001' + ) + BloodRequest.objects.create( + patient_name='Maya Devi', + blood_group='B+', + location='Bir Hospital, Kathmandu', + urgency='URGENT', + hospital='Bir Hospital', + contact_number='9800000002' + ) + BloodRequest.objects.create( + patient_name='Hari Prasad', + blood_group='AB-', + location='Patan Hospital', + urgency='NORMAL', + hospital='Patan Hospital', + contact_number='9800000003' + ) + + # Create Blood Bank (Nepali Context) + BloodBank.objects.create( + name='Nepal Red Cross Society', + location='Soalteemode, Kathmandu', + contact_number='01-4270650', + is_24_7=True, + stock_o_plus=150, + stock_o_minus=45, + stock_a_plus=120, + stock_a_minus=30, + stock_b_plus=90, + stock_b_minus=20, + stock_ab_plus=40, + stock_ab_minus=15 + ) + + BloodBank.objects.create( + name='Bhaktapur Blood Bank', + location='Dudhpati, Bhaktapur', + contact_number='01-6612266', + is_24_7=True, + stock_o_plus=60, + stock_a_plus=45, + stock_b_plus=30 + ) + + print("Sample data populated successfully.") + +if __name__ == "__main__": + run() diff --git a/reduce_capacity.py b/reduce_capacity.py new file mode 100644 index 0000000..1dbdde6 --- /dev/null +++ b/reduce_capacity.py @@ -0,0 +1,17 @@ +import os +import django + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +django.setup() + +from core.models import BloodBank + +def run(): + banks = BloodBank.objects.all() + for bank in banks: + bank.total_capacity = 200 # Reduced from 1000 + bank.save() + print("Capacity reduced for all blood banks.") + +if __name__ == "__main__": + run() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e22994c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Django==5.2.7 +mysqlclient==2.2.7 +python-dotenv==1.1.1 diff --git a/static/css/custom.css b/static/css/custom.css new file mode 100644 index 0000000..925f6ed --- /dev/null +++ b/static/css/custom.css @@ -0,0 +1,4 @@ +/* Custom styles for the application */ +body { + font-family: system-ui, -apple-system, sans-serif; +} diff --git a/staticfiles/@popperjs/core/LICENSE.md b/staticfiles/@popperjs/core/LICENSE.md new file mode 100644 index 0000000..0370c45 --- /dev/null +++ b/staticfiles/@popperjs/core/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2019 Federico Zivolo + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/staticfiles/@popperjs/core/README.md b/staticfiles/@popperjs/core/README.md new file mode 100644 index 0000000..53be7b9 --- /dev/null +++ b/staticfiles/@popperjs/core/README.md @@ -0,0 +1,376 @@ + +

+ Popper +

+ +
+

Tooltip & Popover Positioning Engine

+
+ +

+ + npm version + + + npm downloads per month (popper.js + @popperjs/core) + + + Rolling Versions + +

+ +
+ + +**Positioning tooltips and popovers is difficult. Popper is here to help!** + +Given an element, such as a button, and a tooltip element describing it, Popper +will automatically put the tooltip in the right place near the button. + +It will position _any_ UI element that "pops out" from the flow of your document +and floats near a target element. The most common example is a tooltip, but it +also includes popovers, drop-downs, and more. All of these can be generically +described as a "popper" element. + +## Demo + +[![Popper visualized](https://i.imgur.com/F7qWsmV.jpg)](https://popper.js.org) + +## Docs + +- [v2.x (latest)](https://popper.js.org/docs/v2/) +- [v1.x](https://popper.js.org/docs/v1/) + +We've created a +[Migration Guide](https://popper.js.org/docs/v2/migration-guide/) to help you +migrate from Popper 1 to Popper 2. + +To contribute to the Popper website and documentation, please visit the +[dedicated repository](https://github.com/popperjs/website). + +## Why not use pure CSS? + +- **Clipping and overflow issues**: Pure CSS poppers will not be prevented from + overflowing clipping boundaries, such as the viewport. It will get partially + cut off or overflows if it's near the edge since there is no dynamic + positioning logic. When using Popper, your popper will always be positioned in + the right place without needing manual adjustments. +- **No flipping**: CSS poppers will not flip to a different placement to fit + better in view if necessary. While you can manually adjust for the main axis + overflow, this feature cannot be achieved via CSS alone. Popper automatically + flips the tooltip to make it fit in view as best as possible for the user. +- **No virtual positioning**: CSS poppers cannot follow the mouse cursor or be + used as a context menu. Popper allows you to position your tooltip relative to + any coordinates you desire. +- **Slower development cycle**: When pure CSS is used to position popper + elements, the lack of dynamic positioning means they must be carefully placed + to consider overflow on all screen sizes. In reusable component libraries, + this means a developer can't just add the component anywhere on the page, + because these issues need to be considered and adjusted for every time. With + Popper, you can place your elements anywhere and they will be positioned + correctly, without needing to consider different screen sizes, layouts, etc. + This massively speeds up development time because this work is automatically + offloaded to Popper. +- **Lack of extensibility**: CSS poppers cannot be easily extended to fit any + arbitrary use case you may need to adjust for. Popper is built with + extensibility in mind. + +## Why Popper? + +With the CSS drawbacks out of the way, we now move on to Popper in the +JavaScript space itself. + +Naive JavaScript tooltip implementations usually have the following problems: + +- **Scrolling containers**: They don't ensure the tooltip stays with the + reference element while scrolling when inside any number of scrolling + containers. +- **DOM context**: They often require the tooltip move outside of its original + DOM context because they don't handle `offsetParent` contexts. +- **Compatibility**: Popper handles an incredible number of edge cases regarding + different browsers and environments (mobile viewports, RTL, scrollbars enabled + or disabled, etc.). Popper is a popular and well-maintained library, so you + can be confident positioning will work for your users on any device. +- **Configurability**: They often lack advanced configurability to suit any + possible use case. +- **Size**: They are usually relatively large in size, or require an ancient + jQuery dependency. +- **Performance**: They often have runtime performance issues and update the + tooltip position too slowly. + +**Popper solves all of these key problems in an elegant, performant manner.** It +is a lightweight ~3 kB library that aims to provide a reliable and extensible +positioning engine you can use to ensure all your popper elements are positioned +in the right place. + +When you start writing your own popper implementation, you'll quickly run into +all of the problems mentioned above. These widgets are incredibly common in our +UIs; we've done the hard work figuring this out so you don't need to spend hours +fixing and handling numerous edge cases that we already ran into while building +the library! + +Popper is used in popular libraries like Bootstrap, Foundation, Material UI, and +more. It's likely you've already used popper elements on the web positioned by +Popper at some point in the past few years. + +Since we write UIs using powerful abstraction libraries such as React or Angular +nowadays, you'll also be glad to know Popper can fully integrate with them and +be a good citizen together with your other components. Check out `react-popper` +for the official Popper wrapper for React. + +## Installation + +### 1. Package Manager + +```bash +# With npm +npm i @popperjs/core + +# With Yarn +yarn add @popperjs/core +``` + +### 2. CDN + +```html + + + + + +``` + +### 3. Direct Download? + +Managing dependencies by "directly downloading" them and placing them into your +source code is not recommended for a variety of reasons, including missing out +on feat/fix updates easily. Please use a versioning management system like a CDN +or npm/Yarn. + +## Usage + +The most straightforward way to get started is to import Popper from the `unpkg` +CDN, which includes all of its features. You can call the `Popper.createPopper` +constructor to create new popper instances. + +Here is a complete example: + +```html + +Popper example + + + + + + + + +``` + +Visit the [tutorial](https://popper.js.org/docs/v2/tutorial/) for an example of +how to build your own tooltip from scratch using Popper. + +### Module bundlers + +You can import the `createPopper` constructor from the fully-featured file: + +```js +import { createPopper } from '@popperjs/core'; + +const button = document.querySelector('#button'); +const tooltip = document.querySelector('#tooltip'); + +// Pass the button, the tooltip, and some options, and Popper will do the +// magic positioning for you: +createPopper(button, tooltip, { + placement: 'right', +}); +``` + +All the modifiers listed in the docs menu will be enabled and "just work", so +you don't need to think about setting Popper up. The size of Popper including +all of its features is about 5 kB minzipped, but it may grow a bit in the +future. + +#### Popper Lite (tree-shaking) + +If bundle size is important, you'll want to take advantage of tree-shaking. The +library is built in a modular way to allow to import only the parts you really +need. + +```js +import { createPopperLite as createPopper } from '@popperjs/core'; +``` + +The Lite version includes the most necessary modifiers that will compute the +offsets of the popper, compute and add the positioning styles, and add event +listeners. This is close in bundle size to pure CSS tooltip libraries, and +behaves somewhat similarly. + +However, this does not include the features that makes Popper truly useful. + +The two most useful modifiers not included in Lite are `preventOverflow` and +`flip`: + +```js +import { + createPopperLite as createPopper, + preventOverflow, + flip, +} from '@popperjs/core'; + +const button = document.querySelector('#button'); +const tooltip = document.querySelector('#tooltip'); + +createPopper(button, tooltip, { + modifiers: [preventOverflow, flip], +}); +``` + +As you make more poppers, you may be finding yourself needing other modifiers +provided by the library. + +See [tree-shaking](https://popper.js.org/docs/v2/performance/#tree-shaking) for more +information. + +## Distribution targets + +Popper is distributed in 3 different versions, in 3 different file formats. + +The 3 file formats are: + +- `esm` (works with `import` syntax — **recommended**) +- `umd` (works with `