diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..75b0780 Binary files /dev/null and b/.DS_Store differ diff --git a/Pawtential/.DS_Store b/Pawtential/.DS_Store new file mode 100644 index 0000000..de0a5cd Binary files /dev/null and b/Pawtential/.DS_Store differ diff --git a/Pawtential/Pawtential/__init__.py b/Pawtential/Pawtential/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Pawtential/Pawtential/asgi.py b/Pawtential/Pawtential/asgi.py new file mode 100644 index 0000000..487bd58 --- /dev/null +++ b/Pawtential/Pawtential/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for Pawtential 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.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Pawtential.settings') + +application = get_asgi_application() diff --git a/Pawtential/Pawtential/settings.py b/Pawtential/Pawtential/settings.py new file mode 100644 index 0000000..fc197fc --- /dev/null +++ b/Pawtential/Pawtential/settings.py @@ -0,0 +1,135 @@ +""" +Django settings for Pawtential project. + +Generated by 'django-admin startproject' using Django 5.1.3. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.1/ref/settings/ +""" + +from pathlib import Path +import os + + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-3!tw7xfpn(!-ju07_(yphklcdwe%u37)(0_=wc)t42dldt9s@$' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'main', + 'accounts', + 'adoptions', + 'donations', + 'pets', + 'widget_tweaks', + +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'Pawtential.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'Pawtential.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.1/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.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.1/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR,'media') \ No newline at end of file diff --git a/Pawtential/Pawtential/urls.py b/Pawtential/Pawtential/urls.py new file mode 100644 index 0000000..1f303f0 --- /dev/null +++ b/Pawtential/Pawtential/urls.py @@ -0,0 +1,30 @@ +""" +URL configuration for Pawtential project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.1/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 path ,include +from django.conf import settings +from django.conf.urls.static import static + +urlpatterns = [ + path('admin/', admin.site.urls), + path('',include("main.urls")), + path('accounts/',include("accounts.urls")), + path('adoptions/',include("adoptions.urls")), + path('donations/',include("donations.urls")), + path('pets/',include("pets.urls")), +] +static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + diff --git a/Pawtential/Pawtential/wsgi.py b/Pawtential/Pawtential/wsgi.py new file mode 100644 index 0000000..c048849 --- /dev/null +++ b/Pawtential/Pawtential/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for Pawtential 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.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Pawtential.settings') + +application = get_wsgi_application() diff --git a/Pawtential/accounts/__init__.py b/Pawtential/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Pawtential/accounts/admin.py b/Pawtential/accounts/admin.py new file mode 100644 index 0000000..0436f0f --- /dev/null +++ b/Pawtential/accounts/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin +from .models import IndividualUser, Shelter +# Register your models here. + +class IndividualUserAdmin(admin.ModelAdmin): + list_display = ('user', 'first_name', 'last_name', 'email', 'phone_number', 'birth_date', 'profile_picture') + search_fields = ['user__username', 'email', 'first_name', 'last_name'] + list_filter = ['birth_date'] + + +class ShelterAdmin(admin.ModelAdmin): + list_display = ('user', 'name', 'phone_number', 'address', 'license_number', 'profile_picture') + search_fields = ['user__username', 'name', 'phone_number', 'address'] + list_filter = ['address'] + +admin.site.register(IndividualUser, IndividualUserAdmin) +admin.site.register(Shelter, ShelterAdmin) diff --git a/Pawtential/accounts/apps.py b/Pawtential/accounts/apps.py new file mode 100644 index 0000000..3e3c765 --- /dev/null +++ b/Pawtential/accounts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'accounts' diff --git a/Pawtential/accounts/forms.py b/Pawtential/accounts/forms.py new file mode 100644 index 0000000..1c599f3 --- /dev/null +++ b/Pawtential/accounts/forms.py @@ -0,0 +1,16 @@ +from django import forms +from .models import IndividualUser, Shelter + + +class IndividualUserForm(forms.ModelForm): + class Meta: + model = IndividualUser + fields = ['first_name', 'last_name', 'username', 'email', 'birth_date', 'phone_number', 'bio', 'profile_picture'] + widgets = { + 'birth_date': forms.DateInput(attrs={'type': 'date'}), + } +class ShelterProfileForm(forms.ModelForm): + class Meta: + model = Shelter + fields = ['name', 'phone_number', 'address', 'license_number', 'bio', 'profile_picture'] + diff --git a/Pawtential/accounts/migrations/0001_initial.py b/Pawtential/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..c70c70c --- /dev/null +++ b/Pawtential/accounts/migrations/0001_initial.py @@ -0,0 +1,49 @@ +# Generated by Django 5.1.3 on 2024-11-29 14:21 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='IndividualUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('first_name', models.CharField(max_length=100)), + ('last_name', models.CharField(max_length=100)), + ('username', models.CharField(max_length=100, unique=True)), + ('email', models.EmailField(max_length=254, unique=True)), + ('password', models.CharField(max_length=255)), + ('birth_date', models.DateField(blank=True, null=True)), + ('profile_picture', models.ImageField(blank=True, default='images/default_profile_pic.jpg', null=True, upload_to='profile_pics/')), + ('phone_number', models.CharField(blank=True, max_length=15, null=True)), + ('bio', models.TextField(blank=True, null=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='individual_user', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Shelter', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('username', models.CharField(max_length=100, unique=True)), + ('email', models.EmailField(max_length=254, unique=True)), + ('password', models.CharField(max_length=255)), + ('phone_number', models.CharField(max_length=15)), + ('address', models.CharField(max_length=255)), + ('profile_picture', models.ImageField(blank=True, null=True, upload_to='profile_pics/')), + ('bio', models.TextField(blank=True, null=True)), + ('license_number', models.CharField(max_length=50, unique=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='shelter', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/Pawtential/accounts/migrations/__init__.py b/Pawtential/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Pawtential/accounts/models.py b/Pawtential/accounts/models.py new file mode 100644 index 0000000..870355a --- /dev/null +++ b/Pawtential/accounts/models.py @@ -0,0 +1,38 @@ +from django.db import models +from django.contrib.auth.models import User + +# Create your models here. + +class IndividualUser(models.Model): + + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='individual_user') + first_name = models.CharField(max_length=100) + last_name = models.CharField(max_length=100) + username = models.CharField(max_length=100, unique=True) + email = models.EmailField(unique=True) + password = models.CharField(max_length=255) + birth_date = models.DateField(null=True,blank=True) + profile_picture = models.ImageField(upload_to='profile_pics/',blank=True , null=True , default='images/default_profile_pic.jpg' ) + phone_number = models.CharField(max_length=15,blank=True,null=True) + bio = models.TextField(blank=True,null=True) + + USERNAME_FIELD = 'username' + def __str__(self): + return f"{self.first_name} {self.last_name} ({self.username})" + +class Shelter(models.Model): + + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='shelter') + name = models.CharField(max_length=200) + username = models.CharField(max_length=100, unique=True) + email = models.EmailField(unique=True) + password = models.CharField(max_length=255) + phone_number = models.CharField(max_length=15) + address = models.CharField(max_length=255) + profile_picture = models.ImageField(upload_to='profile_pics/', blank=True, null=True) + bio = models.TextField(blank=True,null=True) + license_number = models.CharField(max_length=50, unique=True) + + USERNAME_FIELD = 'username' + def __str__(self): + return super().__str__() \ No newline at end of file diff --git a/Pawtential/accounts/templates/accounts/individual_registration.html b/Pawtential/accounts/templates/accounts/individual_registration.html new file mode 100644 index 0000000..fae85a5 --- /dev/null +++ b/Pawtential/accounts/templates/accounts/individual_registration.html @@ -0,0 +1,74 @@ +{% extends 'main/base.html' %} +{% block title %}Register Individual{% endblock %} + +{% block content %} +
+
+
+
+
+

Create Individual Account

+
+
+ {% if messages %} + + {% endif %} +
+ {% csrf_token %} +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + Please provide your phone number if you would like to be contacted about the adoption request. + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+

Already have an account? Login

+
+
+
+
+
+
+
+{% endblock %} diff --git a/Pawtential/accounts/templates/accounts/login.html b/Pawtential/accounts/templates/accounts/login.html new file mode 100644 index 0000000..db86920 --- /dev/null +++ b/Pawtential/accounts/templates/accounts/login.html @@ -0,0 +1,50 @@ +{% extends 'main/base.html' %} +{% load static %} + +{% block title %}Login{% endblock %} + +{% block content %} + +
+
+
+
+
+

Login to Your Account

+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} +
+
+
+ {% csrf_token %} + +
+ + +
+ +
+ + +
+ + + +
+

Don't have an account?

+ Register as Individual + Register as Shelter +
+
+
+
+
+
+
+ +{% endblock %} diff --git a/Pawtential/accounts/templates/accounts/registration.html b/Pawtential/accounts/templates/accounts/registration.html new file mode 100644 index 0000000..270c0af --- /dev/null +++ b/Pawtential/accounts/templates/accounts/registration.html @@ -0,0 +1,145 @@ +{% extends 'main/base.html' %} +{% load static %} +{% block title %}Register{% endblock %} + +{% block content %} +
+
+
+
+
+

Create New Account

+
+
+ {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} +
+ {% csrf_token %} + +
+ + +
+ + + + + + + + + + + +
+

Already have an account? Login

+
+
+
+
+
+
+
+ + + +{% endblock %} diff --git a/Pawtential/accounts/templates/accounts/shelter_registration.html b/Pawtential/accounts/templates/accounts/shelter_registration.html new file mode 100644 index 0000000..2386bb5 --- /dev/null +++ b/Pawtential/accounts/templates/accounts/shelter_registration.html @@ -0,0 +1,72 @@ +{% extends 'main/base.html' %} +{% block title %}Register Shelter{% endblock %} + +{% block content %} +
+
+
+
+
+

Create Shelter Account

+
+
+ {% if messages %} + + {% endif %} +
+ {% csrf_token %} +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+

Already have an account? Login

+
+
+
+
+
+
+
+{% endblock %} diff --git a/Pawtential/accounts/templates/profile/individual_edit_profile.html b/Pawtential/accounts/templates/profile/individual_edit_profile.html new file mode 100644 index 0000000..9f92b70 --- /dev/null +++ b/Pawtential/accounts/templates/profile/individual_edit_profile.html @@ -0,0 +1,98 @@ +{% extends 'main/base.html' %} +{% load widget_tweaks %} + +{% block title %}Edit Individual Profile{% endblock %} + +{% block content %} +
+
+
+
+
+

Edit Profile for {{ form.instance.user.username }}

+
+
+ {% if messages %} + + {% endif %} + +
+ {% csrf_token %} + + +
+ + {{ form.first_name|add_class:"form-control" }} +
+ + +
+ + {{ form.last_name|add_class:"form-control" }} +
+ + +
+ + {{ form.username|add_class:"form-control" }} +
+ + +
+ + {{ form.email|add_class:"form-control" }} +
+ + +
+ + {{ form.birth_date|add_class:"form-control" }} +
+ + +
+ + {{ form.phone_number|add_class:"form-control" }} +
+ + +
+ + {{ form.bio|add_class:"form-control" }} +
+ + +
+ + {{ form.profile_picture|add_class:"form-control" }} + {% if form.instance.profile_picture %} +
+ Profile Picture +
+ {% else %} +
+ Profile Picture +
+ {% endif %} +
+ + +
+ +
+ {% if profile.id %} + Back to Profile + {% else %} +

Profile ID is not available.

+ {% endif %} +
+
+
+
+
+
+{% endblock %} diff --git a/Pawtential/accounts/templates/profile/individual_profile.html b/Pawtential/accounts/templates/profile/individual_profile.html new file mode 100644 index 0000000..81a529b --- /dev/null +++ b/Pawtential/accounts/templates/profile/individual_profile.html @@ -0,0 +1,163 @@ +{% extends 'main/base.html' %} +{% load static %} +{% block title %}{{ profile.username }}{% endblock %} + +{% block content %} +
+ + {% if messages %} + + {% endif %} + +
+
+
+
+
+ User Picture +
+

+ + {{ profile.phone_number }} +

+

+ + {{ profile.email }} +

+ {% if profile.user == request.user %} + Edit Profile + {% endif %} +
+
+
+ +
+
+
+

{{ profile.first_name }} {{ profile.last_name }}

+

Username: {{ profile.username }}

+

Birth Date: {{ profile.birth_date }}

+

Bio: {{ profile.bio }}

+
+
+
+
+ +
+ +
+

Pets Added

+ {% if pets %} +
+ {% for pet in pets %} +
+
+
+ {{ pet.name }} +
{{ pet.name }}
+

Species: {{ pet.species }}

+

Age: {{ pet.age_in_years_and_months.0 }} years and {{ pet.age_in_years_and_months.1 }} months

+

Status: {{ pet.get_adoption_status_display }}

+
+ Details + {% if pet.user == request.user %} + Edit +
+ {% csrf_token %} + +
+ {% endif %} +
+
+
+
+ {% endfor %} +
+ {% else %} +

No pets added yet.

+ {% endif %} +
+ +
+ {% if profile.user == request.user %} +
+

Sent Adoption Requests

+
+ {% for request in sent_requests %} +
+
+ Pet: {{ request.pet.name }} | + Status: {{ request.get_status_display }} + {% if request.comments %} +
Comment: {{ request.comments }} + {% endif %} + {% if request.status == 'accepted' or request.status == 'rejected' %} +
+ Approver's Comment: {{ request.comment_by_approver }} +
+ {% else %} +
+ No comment added yet by approver. +
+ {% endif %} +
+
+ {% if request.status == 'pending' %} + Cancel Request + {% endif %} +
+
+ {% endfor %} +
+ {% else %} +

+ {% endif %} +
+ +
+ {% if profile.user == request.user %} +
+

Adoption Requests

+
+ {% for request in adoption_requests %} +
+
+ Pet: {{ request.pet.name }} | + User: + {% if request.user %} + {% if request.user.individual_user %} + + {{ request.user.username }} + + {% elif request.user.shelter %} + + {{ request.user.username }} + + {% endif %} + {% else %} + No user linked with this request + {% endif %} + | + Status: {{ request.get_status_display }} +
+
+ {% if request.status == 'pending' %} + Accept + Reject + {% endif %} +
+
+ {% endfor %} +
+ {% else %} +

+ {% endif %} +
+
+{% endblock %} diff --git a/Pawtential/accounts/templates/profile/shelter_edit_profile.html b/Pawtential/accounts/templates/profile/shelter_edit_profile.html new file mode 100644 index 0000000..d3c0eb6 --- /dev/null +++ b/Pawtential/accounts/templates/profile/shelter_edit_profile.html @@ -0,0 +1,76 @@ +{% extends 'main/base.html' %} +{% load static %} +{% load widget_tweaks %} +{% block title %}Edit Shelter Profile{% endblock %} + +{% block content %} +
+
+
+
+
+

Edit {{ form.instance.user.username }}'s Shelter Profile

+
+
+ {% if messages %} + + {% endif %} + +
+ {% csrf_token %} + + +
+ + {{ form.name|add_class:"form-control" }} +
+ + +
+ + {{ form.phone_number|add_class:"form-control" }} +
+ +
+ + {{ form.address|add_class:"form-control" }} +
+ +
+ + {{ form.license_number|add_class:"form-control" }} +
+ + +
+ + {{ form.bio|add_class:"form-control" }} +
+ + +
+ + {{ form.profile_picture|add_class:"form-control" }} + {% if form.instance.profile_picture %} + Profile Picture + {% else %} + Profile Picture + {% endif %} +
+ + +
+ + +
+
+
+
+
+{% endblock %} diff --git a/Pawtential/accounts/templates/profile/shelter_profile.html b/Pawtential/accounts/templates/profile/shelter_profile.html new file mode 100644 index 0000000..1ad73df --- /dev/null +++ b/Pawtential/accounts/templates/profile/shelter_profile.html @@ -0,0 +1,163 @@ +{% extends 'main/base.html' %} +{% load static %} +{% block title %}{{ profile.username }}{% endblock %} +{% block content %} +
+ {% if messages %} + + {% endif %} + +
+
+
+
+ User Picture +

+ + {{ profile.address }} +

+

+ + {{ profile.phone_number }} +

+

+ + {{ profile.email }} +

+ {% if profile.user == request.user %} + Edit Profile + {% endif %} +
+
+
+ +
+
+
+

{{ profile.name }}

+

Username: {{ profile.user.username }}

+

License Number: {{ profile.license_number }}

+

Bio: {{ profile.bio }}

+
+
+
+
+ +
+ +
+

Pets Added by {{ profile.first_name }} {{ profile.last_name }}

+ {% if pets %} +
+ {% for pet in pets %} +
+
+
+ {{ pet.name }} + +
{{ pet.name }}
+

Species: {{ pet.species }}

+

Age: {{ pet.age_in_years_and_months.0 }} years and {{ pet.age_in_years_and_months.1 }} months

+

Status: {{ pet.get_adoption_status_display }}

+ +
+ Details + {% if pet.user == request.user %} + Edit +
+ {% csrf_token %} + +
+ {% endif %} +
+
+
+
+ {% endfor %} +
+ {% else %} +

No pets added yet.

+ {% endif %} +
+ {% if profile.user == request.user %} +
+

Adoption Requests

+ + {% endif %} + +
+ + {% if profile.user == request.user %} +

Donation Requests

+ {% if donation_requests %} + + {% else %} +

No donation requests added yet.

+ {% endif %} + {% endif %} +
+{% endblock %} diff --git a/Pawtential/accounts/tests.py b/Pawtential/accounts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Pawtential/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Pawtential/accounts/urls.py b/Pawtential/accounts/urls.py new file mode 100644 index 0000000..74bbf7f --- /dev/null +++ b/Pawtential/accounts/urls.py @@ -0,0 +1,17 @@ +from django.urls import path +from . import views + +app_name = 'accounts' +urlpatterns = [ + path('login/', views.login_view, name='login'), + path('logout/', views.logout_view, name='logout'), + path('register/individual/', views.register_individual_view, name='register_individual'), + path('register/shelter/', views.register_shelter_view, name='register_shelter'), + path('profile/individual//', views.individual_profile, name='individual_profile'), + path('profile/edit/', views.edit_individual_profile, name='individual_edit_profile'), + path('profile/shelter//', views.shelter_profile, name='shelter_profile'), + path('profile/shelter/edit//', views.edit_shelter_profile, name='shelter_edit_profile'), +] + + + diff --git a/Pawtential/accounts/views.py b/Pawtential/accounts/views.py new file mode 100644 index 0000000..43be6c0 --- /dev/null +++ b/Pawtential/accounts/views.py @@ -0,0 +1,208 @@ +from django.shortcuts import render, redirect , get_object_or_404 +from django.contrib import messages +from django.contrib.auth import authenticate, login, logout +from django.contrib.auth.models import User +from .models import IndividualUser, Shelter +from django.contrib.auth.decorators import login_required +from .forms import ShelterProfileForm ,IndividualUserForm +from pets.models import Pet +from adoptions.models import AdoptionRequest +from donations.models import DonationRequest + +def login_view(request): + if request.method == 'POST': + username = request.POST['username'] + password = request.POST['password'] + user = authenticate(request, username=username, password=password) + + if user is not None: + login(request, user) + messages.success(request, 'Login successful! Welcome') + return redirect('main:home_view') + else: + messages.error(request, 'Invalid username or password.') + return render(request, 'accounts/login.html') + + return render(request, 'accounts/login.html') + + +def register_individual_view(request): + if request.method == 'POST': + username = request.POST.get('username') + email = request.POST.get('email') + password = request.POST.get('password') + confirm_password = request.POST.get('confirm_password') + + if not username: + messages.error(request, "Username is required.") + return render(request, 'accounts/individual_registration.html') + + if User.objects.filter(username=username).exists(): + messages.error(request, "Username already exists. Please choose a different username.") + return render(request, 'accounts/individual_registration.html') + + if password != confirm_password: + messages.error(request, "Passwords do not match.") + return render(request, 'accounts/individual_registration.html') + + user = User.objects.create_user(username=username, email=email, password=password) + + first_name = request.POST.get('first_name') + last_name = request.POST.get('last_name') + phone_number = request.POST.get('phone_number', '') + bio = request.POST.get('bio', '') + birth_date = request.POST.get('birth_date', None) + profile_picture = request.FILES.get('profile_picture', None) + + individual_user = IndividualUser( + user=user, + first_name=first_name, + last_name=last_name, + phone_number=phone_number, + bio=bio, + birth_date=birth_date, + profile_picture=profile_picture + ) + individual_user.save() + + messages.success(request, "Account created successfully. Please log in.") + return redirect('accounts:login') + + return render(request, 'accounts/individual_registration.html') + + +def register_shelter_view(request): + if request.method == 'POST': + username = request.POST.get('username') + email = request.POST.get('email') + password = request.POST.get('password') + confirm_password = request.POST.get('confirm_password') + + if User.objects.filter(username=username).exists(): + messages.error(request, "Username is already taken. Please choose a different one.") + return render(request, 'accounts/shelter_registration.html') + + if User.objects.filter(email=email).exists(): + messages.error(request, "This email is already registered. Please choose a different one.") + return render(request, 'accounts/shelter_registration.html') + + if password != confirm_password: + messages.error(request, "Passwords do not match.") + return render(request, 'accounts/shelter_registration.html') + + user = User.objects.create_user(username=username, email=email, password=password) + + shelter_name = request.POST.get('shelter_name') + phone_number_shelter = request.POST.get('phone_number_shelter') + address = request.POST.get('address') + license_number = request.POST.get('license_number') + bio = request.POST.get('bio', '') + profile_picture = request.FILES.get('profile_picture_shelter', None) + + shelter = Shelter( + user=user, + username=username, + name=shelter_name, + phone_number=phone_number_shelter, + address=address, + license_number=license_number, + bio=bio, + profile_picture=profile_picture, + email=email + ) + shelter.save() + + messages.success(request, "Shelter account created successfully. Please log in.") + return redirect('accounts:login') + + return render(request, 'accounts/shelter_registration.html') + + +def logout_view(request): + logout(request) + messages.success(request, "You have logged out successfully.") + return redirect('main:home_view') + + +def individual_profile(request, individual_id): + try: + individual_profile = IndividualUser.objects.get(id=individual_id) + except IndividualUser.DoesNotExist: + individual_profile = None + + if individual_profile: + pets = Pet.objects.filter(user=individual_profile.user) + sent_requests = AdoptionRequest.objects.filter(user=individual_profile.user) + adoption_requests = AdoptionRequest.objects.filter(pet__user=individual_profile.user) + else: + pets = [] + sent_requests = [] + adoption_requests = [] + + return render(request, 'profile/individual_profile.html', { + 'profile': individual_profile, + 'pets': pets, + 'sent_requests': sent_requests, + 'adoption_requests': adoption_requests + }) + + +def edit_individual_profile(request, individual_id): + + profile = get_object_or_404(IndividualUser, id=individual_id) + + if request.method == 'POST': + form = IndividualUserForm(request.POST, request.FILES, instance=profile) + + if form.is_valid(): + form.save() + messages.success(request, 'Your profile has been updated successfully.') + return redirect('accounts:individual_profile', individual_id=profile.id) + else: + messages.error(request, 'Please correct the errors below.') + else: + form = IndividualUserForm(instance=profile) + + return render(request, 'profile/individual_edit_profile.html', {'form': form}) + +def shelter_profile(request, shelter_id): + try: + shelter_profile = Shelter.objects.get(id=shelter_id) + except Shelter.DoesNotExist: + shelter_profile = None + + if shelter_profile: + + pets = Pet.objects.filter(user=shelter_profile.user) + adoption_requests = AdoptionRequest.objects.filter(pet__user=shelter_profile.user) + donation_requests = DonationRequest.objects.filter(shelter=shelter_profile).all() + + else: + pets = [] + adoption_requests = [] + donation_requests = [] + return render(request, 'profile/shelter_profile.html', + {'profile': shelter_profile , + 'pets': pets , + 'adoption_requests': adoption_requests, + 'donation_requests':donation_requests}) + + +def edit_shelter_profile(request, shelter_id): + profile = Shelter.objects.get(id=shelter_id) + + if request.method == 'POST': + form = ShelterProfileForm(request.POST, request.FILES, instance=profile) + + if form.is_valid(): + form.save() + messages.success(request, 'Profile updated successfully!') + return redirect('accounts:shelter_profile', shelter_id=profile.id) + else: + form = ShelterProfileForm(instance=profile) + + return render(request, 'profile/shelter_edit_profile.html', {'form': form}) + + + + diff --git a/Pawtential/adoptions/__init__.py b/Pawtential/adoptions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Pawtential/adoptions/admin.py b/Pawtential/adoptions/admin.py new file mode 100644 index 0000000..8742bb3 --- /dev/null +++ b/Pawtential/adoptions/admin.py @@ -0,0 +1,12 @@ +from django.contrib import admin +from .models import AdoptionRequest + + +# Register your models here. + +class AdoptionRequestAdmin(admin.ModelAdmin): + list_display = ('user', 'pet', 'status', 'created_at', 'updated_at') + list_filter = ('status', 'pet') + search_fields = ('user__username', 'pet__name') + +admin.site.register(AdoptionRequest, AdoptionRequestAdmin) \ No newline at end of file diff --git a/Pawtential/adoptions/apps.py b/Pawtential/adoptions/apps.py new file mode 100644 index 0000000..0b40e1d --- /dev/null +++ b/Pawtential/adoptions/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AdoptionsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'adoptions' diff --git a/Pawtential/adoptions/migrations/0001_initial.py b/Pawtential/adoptions/migrations/0001_initial.py new file mode 100644 index 0000000..ebd90b5 --- /dev/null +++ b/Pawtential/adoptions/migrations/0001_initial.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.3 on 2024-11-30 16:27 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('pets', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='AdoptionRequest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('pending', 'Pending'), ('accepted', 'Accepted'), ('rejected', 'Rejected')], default='pending', max_length=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('comments', models.TextField(blank=True, null=True)), + ('pet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='adoption_requests', to='pets.pet')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='adoption_requests', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('pet', 'user')}, + }, + ), + ] diff --git a/Pawtential/adoptions/migrations/0002_adoptionrequest_comment_by_approver.py b/Pawtential/adoptions/migrations/0002_adoptionrequest_comment_by_approver.py new file mode 100644 index 0000000..f2a8091 --- /dev/null +++ b/Pawtential/adoptions/migrations/0002_adoptionrequest_comment_by_approver.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.3 on 2024-12-01 21:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adoptions', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='adoptionrequest', + name='comment_by_approver', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/Pawtential/adoptions/migrations/__init__.py b/Pawtential/adoptions/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Pawtential/adoptions/models.py b/Pawtential/adoptions/models.py new file mode 100644 index 0000000..e5c14d8 --- /dev/null +++ b/Pawtential/adoptions/models.py @@ -0,0 +1,27 @@ +from django.db import models +from django.contrib.auth.models import User +from pets.models import Pet + +# Create your models here. + +class AdoptionRequest(models.Model): + + STATUS_CHOICES = [ + ('pending', 'Pending'), + ('accepted', 'Accepted'), + ('rejected', 'Rejected'), + ] + + pet = models.ForeignKey(Pet , related_name='adoption_requests',on_delete=models.CASCADE) + user = models.ForeignKey(User , related_name='adoption_requests', on_delete=models.CASCADE) + status = models.CharField(max_length=10, choices=STATUS_CHOICES , default='pending') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + comments = models.TextField(blank=True, null=True) + comment_by_approver = models.TextField(null=True, blank=True) + + class Meta: + unique_together = ['pet', 'user'] + + def __str__(self): + return f"Adoption Request by {self.user.username} for {self.pet.name} ({self.status})" \ No newline at end of file diff --git a/Pawtential/adoptions/templates/adoptions/already_requested.html b/Pawtential/adoptions/templates/adoptions/already_requested.html new file mode 100644 index 0000000..cd7f68a --- /dev/null +++ b/Pawtential/adoptions/templates/adoptions/already_requested.html @@ -0,0 +1,16 @@ +{% extends 'main/base.html' %} +{% load static %} + +{% block content %} +
+

Adoption Request Already Sent

+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + Go Back to Pet Details +
+{% endblock %} diff --git a/Pawtential/adoptions/templates/adoptions/request_adoption.html b/Pawtential/adoptions/templates/adoptions/request_adoption.html new file mode 100644 index 0000000..1f6d342 --- /dev/null +++ b/Pawtential/adoptions/templates/adoptions/request_adoption.html @@ -0,0 +1,48 @@ +{% extends 'main/base.html' %} +{% load static %} + +{% block content %} +
+

Request Adoption for {{ pet.name }}

+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} +
+
+ {{ pet.name }} +
+
+

{{ pet.name }}

+

Species: {{ pet.species }}

+

Age: {{ pet.age }} years

+

Description: {{ pet.description }}

+

Status: {{ pet.get_adoption_status_display }}

+ + {% if pet.adoption_status == 'available' %} + {% if existing_request %} +
+ You have already requested to adopt this pet! +
+ {% else %} +
+ {% csrf_token %} +
+ + +
+ +
+ {% endif %} + {% else %} +
+ This pet is not available for adoption at the moment. +
+ {% endif %} +
+
+
+{% endblock %} diff --git a/Pawtential/adoptions/tests.py b/Pawtential/adoptions/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Pawtential/adoptions/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Pawtential/adoptions/urls.py b/Pawtential/adoptions/urls.py new file mode 100644 index 0000000..bba155c --- /dev/null +++ b/Pawtential/adoptions/urls.py @@ -0,0 +1,11 @@ +from django.urls import path +from . import views + +app_name = 'adoptions' + +urlpatterns = [ + path('request//', views.request_adoption, name='request_adoption'), + path('adoption_request///', views.handle_adoption_request, name='handle_adoption_request'), +] + + diff --git a/Pawtential/adoptions/views.py b/Pawtential/adoptions/views.py new file mode 100644 index 0000000..1e0a3d6 --- /dev/null +++ b/Pawtential/adoptions/views.py @@ -0,0 +1,75 @@ +from django.shortcuts import render ,redirect, get_object_or_404 +from .models import AdoptionRequest +from pets.models import Pet +from django.contrib import messages +from django.contrib.auth.decorators import login_required + + + +# Create your views here. + +@login_required +def request_adoption(request, pet_id): + pet = get_object_or_404(Pet, id=pet_id) + + existing_request = AdoptionRequest.objects.filter(pet=pet, user=request.user).first() + if existing_request: + messages.warning(request, 'You have already sent an adoption request for this pet.') + return render(request, 'adoptions/already_requested.html', {'pet': pet}) + + if request.method == 'POST': + comment = request.POST.get('comments', '') + + adoption_request = AdoptionRequest.objects.create( + pet=pet, + user=request.user, + status='pending', + comments=comment + ) + messages.success(request, 'Your adoption request has been sent successfully!') + return redirect('pets:pet_detail', pet_id=pet.id) + + return render(request, 'adoptions/request_adoption.html', {'pet': pet}) + + + +def handle_adoption_request(request, request_id, action): + adoption_request = get_object_or_404(AdoptionRequest, id=request_id) + + pet = adoption_request.pet + + if not (pet.user == request.user or pet.shelter == request.user): + messages.error(request, "You are not authorized to take this action.") + return redirect('pets:pet_detail', pet_id=pet.id) + + if adoption_request.status == 'pending': + comment = request.POST.get('comments', '') + if action == 'accept': + adoption_request.status = 'accepted' + if not comment: + comment = "Congratulations! Your adoption request has been accepted, and you will be contacted shortly." + adoption_request.comment_by_approver = comment + adoption_request.save() + + messages.success(request, f"The adoption request for {adoption_request.pet.name} has been accepted successfully! {comment}") + + elif action == 'reject': + adoption_request.status = 'rejected' + if not comment: + comment = "We regret to inform you that your adoption request has not been accepted. We wish you all the best in your future endeavors." + adoption_request.comment_by_approver = comment + adoption_request.save() + messages.info(request, f"The adoption request for {adoption_request.pet.name} has been rejected.") + + elif action == 'cancel' and adoption_request.user == request.user: + adoption_request.delete() + messages.info(request, "The adoption request has been successfully canceled.") + + if hasattr(request.user, 'individual_user'): + return redirect('accounts:individual_profile', individual_id=request.user.individual_user.id) + elif hasattr(request.user, 'shelter'): + return redirect('accounts:shelter_profile', shelter_id=request.user.shelter.id) + else: + messages.error(request, "An error occurred while processing the request.") + return redirect('accounts:login') + diff --git a/Pawtential/db.sqlite3 b/Pawtential/db.sqlite3 new file mode 100644 index 0000000..749c449 Binary files /dev/null and b/Pawtential/db.sqlite3 differ diff --git a/Pawtential/donations/__init__.py b/Pawtential/donations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Pawtential/donations/admin.py b/Pawtential/donations/admin.py new file mode 100644 index 0000000..b4e83e2 --- /dev/null +++ b/Pawtential/donations/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from .models import DonationRequest + +# Register your models here. + +admin.site.register(DonationRequest) diff --git a/Pawtential/donations/apps.py b/Pawtential/donations/apps.py new file mode 100644 index 0000000..916def2 --- /dev/null +++ b/Pawtential/donations/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DonationsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'donations' diff --git a/Pawtential/donations/forms.py b/Pawtential/donations/forms.py new file mode 100644 index 0000000..cd72268 --- /dev/null +++ b/Pawtential/donations/forms.py @@ -0,0 +1,14 @@ +from django import forms +from .models import Donation + +class DonationForm(forms.Form): + donor_name = forms.CharField(required=False, max_length=100) + donation_type = forms.ChoiceField(choices=[('monetary', 'Monetary Donation'), ('supplies', 'Supplies Donation')], required=True) + + amount = forms.DecimalField(required=False, max_digits=10, decimal_places=2, min_value=0.01) + payment_method = forms.ChoiceField(choices=[('bank_transfer', 'Bank Transfer'), ('apple_pay', 'Apple Pay')], required=False) + + shipping_company = forms.CharField(required=False, max_length=100) + tracking_number = forms.CharField(required=False, max_length=100) + + payment_proof = forms.FileField(required=False) \ No newline at end of file diff --git a/Pawtential/donations/migrations/0001_initial.py b/Pawtential/donations/migrations/0001_initial.py new file mode 100644 index 0000000..1621431 --- /dev/null +++ b/Pawtential/donations/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.3 on 2024-12-01 09:00 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='DonationRequest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('donation_type', models.CharField(choices=[('food', 'Food'), ('supplies', 'Supplies'), ('medical', 'Medical'), ('other', 'Other')], max_length=20)), + ('amount_requested', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('date_requested', models.DateTimeField(auto_now_add=True)), + ('fulfilled', models.BooleanField(default=False)), + ('shelter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='donation_requests', to='accounts.shelter')), + ], + ), + ] diff --git a/Pawtential/donations/migrations/0002_donationrequest_amount_remaining_donation.py b/Pawtential/donations/migrations/0002_donationrequest_amount_remaining_donation.py new file mode 100644 index 0000000..3a8a000 --- /dev/null +++ b/Pawtential/donations/migrations/0002_donationrequest_amount_remaining_donation.py @@ -0,0 +1,35 @@ +# Generated by Django 5.1.3 on 2024-12-01 16:38 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('donations', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='donationrequest', + name='amount_remaining', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True), + ), + migrations.CreateModel( + name='Donation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('donor_name', models.CharField(blank=True, max_length=255, null=True)), + ('donation_type', models.CharField(choices=[('cash', 'Cash Donation'), ('supplies', 'Supplies Donation')], max_length=20)), + ('amount', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('shipping_company', models.CharField(blank=True, max_length=255, null=True)), + ('tracking_number', models.CharField(blank=True, max_length=255, null=True)), + ('payment_method', models.CharField(blank=True, choices=[('bank_transfer', 'Bank Transfer'), ('apple_pay', 'Apple Pay')], max_length=50, null=True)), + ('date_donated', models.DateTimeField(auto_now_add=True)), + ('donation_status', models.CharField(default='pending', max_length=50)), + ('payment_proof', models.FileField(blank=True, null=True, upload_to='payment_proofs/')), + ('donation_request', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='donations', to='donations.donationrequest')), + ], + ), + ] diff --git a/Pawtential/donations/migrations/__init__.py b/Pawtential/donations/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Pawtential/donations/models.py b/Pawtential/donations/models.py new file mode 100644 index 0000000..2837d72 --- /dev/null +++ b/Pawtential/donations/models.py @@ -0,0 +1,82 @@ +from django.db import models +from django.contrib.auth.models import User +from accounts.models import Shelter + +# Create your models here. + +class DonationRequest(models.Model): + DONATION_TYPE_CHOICES = [ + ('food', 'Food'), + ('supplies', 'Supplies'), + ('medical', 'Medical'), + ('other', 'Other'), + ] + + shelter = models.ForeignKey(Shelter, on_delete=models.CASCADE, related_name='donation_requests') + donation_type = models.CharField(max_length=20, choices=DONATION_TYPE_CHOICES) + amount_requested = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True) + amount_remaining = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True) + description = models.TextField(blank=True, null=True) + date_requested = models.DateTimeField(auto_now_add=True) + fulfilled = models.BooleanField(default=False) + + def __str__(self): + return f"Donation Request for {self.shelter.name} - {self.donation_type}" + + def city(self): + return self.shelter.address + + def check_fulfilled(self): + total_donated = self.donations.aggregate(models.Sum('amount'))['amount__sum'] or 0 + if total_donated >= self.amount_requested: + self.fulfilled = True + else: + self.fulfilled = False + self.save() + + def update_remaining_amount(self): + if self.amount_requested is None: + return + + total_donated = self.donations.aggregate(models.Sum('amount'))['amount__sum'] or 0 + self.amount_remaining = self.amount_requested - total_donated + self.check_fulfilled() + self.save() + +class Donation(models.Model): + DONATION_METHOD_CHOICES = [ + ('cash', 'Cash Donation'), + ('supplies', 'Supplies Donation'), + ] + + PAYMENT_METHOD_CHOICES = [ + ('bank_transfer', 'Bank Transfer'), + ('apple_pay', 'Apple Pay'), + ] + + donation_request = models.ForeignKey(DonationRequest, on_delete=models.CASCADE, related_name='donations') + donor_name = models.CharField(max_length=255, blank=True, null=True) + donation_type = models.CharField(max_length=20, choices=DONATION_METHOD_CHOICES) + amount = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True) + shipping_company = models.CharField(max_length=255, blank=True, null=True) + tracking_number = models.CharField(max_length=255, blank=True, null=True) + payment_method = models.CharField(max_length=50, choices=PAYMENT_METHOD_CHOICES, blank=True, null=True) + date_donated = models.DateTimeField(auto_now_add=True) + donation_status = models.CharField(max_length=50, default='pending') + payment_proof = models.FileField(upload_to='payment_proofs/', blank=True, null=True) + + + def __str__(self): + return f"{self.donor_name if self.donor_name else 'Anonymous'} - {self.donation_type} ({self.amount} SAR)" + + def is_bank_transfer(self): + return self.payment_method == 'bank_transfer' + + def check_payment_proof(self): + if self.is_bank_transfer() and not self.payment_proof: + return False + return True + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + self.donation_request.update_remaining_amount() diff --git a/Pawtential/donations/templates/donations/add_donation_request.html b/Pawtential/donations/templates/donations/add_donation_request.html new file mode 100644 index 0000000..8e5aecc --- /dev/null +++ b/Pawtential/donations/templates/donations/add_donation_request.html @@ -0,0 +1,51 @@ +{% extends 'main/base.html' %} +{% load static %} +{% block title %}Request Donation{% endblock %} +{% block content %} +
+
+
+
+
+

Request a Donation

+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} +
+
+
+ {% csrf_token %} + +
+ + +
+ +
+ + + Leave empty if you do not want to request a specific amount (e.g. for food or supplies). +
+ +
+ + +
+ + +
+
+
+
+
+
+{% endblock %} diff --git a/Pawtential/donations/templates/donations/donate.html b/Pawtential/donations/templates/donations/donate.html new file mode 100644 index 0000000..15e337e --- /dev/null +++ b/Pawtential/donations/templates/donations/donate.html @@ -0,0 +1,97 @@ +{% extends 'main/base.html' %} +{% load static %} + +{% block title %}Donate{% endblock %} + + +{% block content %} +
+

Make a Donation

+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + {% if remaining_amount %} +
+ Remaining amount to be donated: {{ remaining_amount }}. +
+ {% endif %} + +
+ {% csrf_token %} + +
+ + +
+ +
+ + +
+ + + + + + + + + + +
+
+ + + +{% endblock %} diff --git a/Pawtential/donations/templates/donations/donation_request_list.html b/Pawtential/donations/templates/donations/donation_request_list.html new file mode 100644 index 0000000..0841a6d --- /dev/null +++ b/Pawtential/donations/templates/donations/donation_request_list.html @@ -0,0 +1,132 @@ +{% extends 'main/base.html' %} +{% load static %} + +{% block title %}Donation Requests{% endblock %} +{% block content %} +
+

List of Donation Requests

+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+ {% for request in donation_requests %} +
+
+ +
+
{{ request.donation_type|capfirst }}
+

Amount Requested: {{ request.amount_requested|floatformat:2 }} SAR

+

Amount Donated: + {% if request.total_donated %} + {{ request.total_donated|floatformat:2 }} SAR + {% else %} + 0 SAR + {% endif %} +

+

Amount Remaining: + {% if request.amount_requested %} + {% if request.remaining > 0 %} + {{ request.remaining|floatformat:2 }} SAR + {% else %} + Fully Fulfilled + {% endif %} + {% else %} + N/A + {% endif %} +

+

Description: {{ request.description }}

+

Status: + {% if request.fulfilled %} + Fulfilled + {% else %} + Pending + {% endif %} +

+

Shelter: + {{ request.shelter.name }} +

+ + {% if request.fulfilled %} + + {% else %} + {% if request.donation_type == 'medical' %} + Donate Now + {% elif request.donation_type == 'supplies' %} + Donate Supplies + {% else %} + Donate Supplies + {% endif %} + {% endif %} +
+
+
+ {% empty %} +

No donation requests found.

+ {% endfor %} +
+ +
+ +
+
+{% endblock %} diff --git a/Pawtential/donations/templates/donations/edit_donation_request.html b/Pawtential/donations/templates/donations/edit_donation_request.html new file mode 100644 index 0000000..dd0c375 --- /dev/null +++ b/Pawtential/donations/templates/donations/edit_donation_request.html @@ -0,0 +1,84 @@ +{% extends 'main/base.html' %} +{% load static %} + +{% block title %}Edit Donation Request{% endblock %} +{% block content %} +
+
+
+
+
+

Edit Donation Request

+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} +
+
+ {% if messages %} + + {% endif %} + +
+ {% csrf_token %} + +
+ + +
+ +
+ + + Leave empty if you do not want to request a specific amount (e.g. for food or supplies). +
+ +
+ + + This is the total amount donated so far. +
+ +
+ + + This is the remaining amount to be donated. +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+
+
+ +
+ Back to Donation Requests +
+{% endblock %} diff --git a/Pawtential/donations/templates/donations/medical_donate.html b/Pawtential/donations/templates/donations/medical_donate.html new file mode 100644 index 0000000..28e1046 --- /dev/null +++ b/Pawtential/donations/templates/donations/medical_donate.html @@ -0,0 +1,44 @@ +{% extends 'main/base.html' %} +{% block title %}Medical Donation{% endblock %} + +{% block content %} +
+

Make a Medical Donation

+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + + {% if remaining_amount %} +
+ Remaining amount to be donated: {{ remaining_amount }}. +
+ {% endif %} + +
+ {% csrf_token %} +
+ + +
+ +
+ + +
+ +
+ + + We currently only support Apple Pay +
+ + +
+
+{% endblock %} diff --git a/Pawtential/donations/templates/donations/supply_donate.html b/Pawtential/donations/templates/donations/supply_donate.html new file mode 100644 index 0000000..a8746b9 --- /dev/null +++ b/Pawtential/donations/templates/donations/supply_donate.html @@ -0,0 +1,34 @@ +{% extends 'main/base.html' %} +{% block title %}Supplies Donation{% endblock %} + +{% block content %} +
+

Make a Supplies Donation

+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + +
+ {% csrf_token %} +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+{% endblock %} diff --git a/Pawtential/donations/tests.py b/Pawtential/donations/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Pawtential/donations/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Pawtential/donations/urls.py b/Pawtential/donations/urls.py new file mode 100644 index 0000000..40cb171 --- /dev/null +++ b/Pawtential/donations/urls.py @@ -0,0 +1,14 @@ +from django.urls import path +from . import views + +app_name = 'donations' + +urlpatterns = [ + path('add/', views.add_donation_request, name='add_donation_request'), + path('list/', views.donation_request_list, name='donation_request_list'), + path('delete//', views.delete_donation_request, name='delete_donation_request'), + path('edit//', views.edit_donation_request, name='edit_donation_request'), + #path('donate//', views.make_donation, name='make_donation'), + path('donate/medical//', views.make_medical_donation, name='make_medical_donation'), + path('donate/supplies//', views.make_supply_donation, name='make_supply_donation'), +] diff --git a/Pawtential/donations/views.py b/Pawtential/donations/views.py new file mode 100644 index 0000000..36e4bd5 --- /dev/null +++ b/Pawtential/donations/views.py @@ -0,0 +1,290 @@ +from django.shortcuts import render , redirect , get_object_or_404 +from django.contrib import messages +from .models import DonationRequest , Donation +from django.db.models import Sum +from django.http import HttpResponseBadRequest +from django.core.paginator import Paginator + + + +# Create your views here. + +def add_donation_request(request): + if request.method == 'POST': + if not hasattr(request.user, 'shelter'): + return HttpResponseBadRequest("You do not have shelter permissions.") + + shelter = request.user.shelter + donation_type = request.POST.get('donation_type') + amount_requested = request.POST.get('amount_requested') + description = request.POST.get('description') + + if donation_type in ['food', 'supplies'] and not amount_requested: + amount_requested = None + else: + try: + amount_requested = float(amount_requested) + except ValueError: + messages.error(request, 'Invalid amount requested.') + return redirect('donations:add_donation_request') + + + donation_request = DonationRequest( + shelter=shelter, + donation_type=donation_type, + amount_requested=amount_requested, + description=description, + ) + donation_request.save() + + messages.success(request, 'Your donation request has been successfully submitted!') + return redirect('donations:donation_request_list') + + return render(request, 'donations/add_donation_request.html') + +def donation_request_list(request): + filters = {} + + donation_type = request.GET.get('donation_type', '') + if donation_type: + filters['donation_type__icontains'] = donation_type + + fulfilled_status = request.GET.get('fulfilled_status', '') + if fulfilled_status: + filters['fulfilled'] = fulfilled_status.lower() == 'true' + + donation_requests = DonationRequest.objects.filter(**filters).annotate( + total_donated=Sum('donations__amount') + ) + + for req in donation_requests: + if req.amount_requested and req.total_donated: + req.remaining = req.amount_requested - req.total_donated + else: + req.remaining = req.amount_requested if req.amount_requested else 0 + + + paginator = Paginator(donation_requests, 8) + page_number = request.GET.get('page') + page_obj = paginator.get_page(page_number) + + return render(request, 'donations/donation_request_list.html', {'donation_requests': page_obj}) + + +def edit_donation_request(request, request_id): + donation_request = get_object_or_404(DonationRequest, id=request_id) + + if not hasattr(request.user, 'shelter') or request.user != donation_request.shelter.user: + messages.error(request, "You are not authorized to edit this request.") + return redirect('donations:donation_request_list') + total_donated = Donation.objects.filter(donation_request=donation_request).aggregate(Sum('amount'))['amount__sum'] or 0 + remaining_amount = donation_request.amount_requested - total_donated if donation_request.amount_requested else None + + + total_donated = Donation.objects.filter(donation_request=donation_request).aggregate(total_amount=Sum('amount'))['total_amount'] or 0 + remaining_amount = donation_request.amount_requested - total_donated if donation_request.amount_requested else None + + if request.method == 'POST': + donation_type = request.POST.get('donation_type') + amount_requested = request.POST.get('amount_requested') + description = request.POST.get('description') + fulfilled = 'fulfilled' in request.POST + + if donation_type in ['food', 'supplies'] and not amount_requested: + amount_requested = None + else: + try: + amount_requested = float(amount_requested) + except ValueError: + amount_requested = None + + donation_request.donation_type = donation_type + donation_request.amount_requested = amount_requested + donation_request.description = description + donation_request.fulfilled = fulfilled + + donation_request.save() + + messages.success(request, "Donation request updated successfully.") + return redirect('donations:donation_request_list') + + return render(request, 'donations/edit_donation_request.html', { + 'donation_request': donation_request, + 'total_donated': total_donated, + 'remaining_amount': remaining_amount + }) + + +def delete_donation_request(request, donation_id): + donation_request = get_object_or_404(DonationRequest, id=donation_id) + + if request.user != donation_request.shelter.user: + messages.error(request, "You are not authorized to delete this request.") + return redirect('donations:donation_request_list') + + shelter_id = donation_request.shelter.id + donation_request.delete() + messages.success(request, "Donation request has been successfully deleted.") + return redirect('accounts:shelter_profile', shelter_id=shelter_id) + +def make_medical_donation(request, donation_request_id): + donation_request = get_object_or_404(DonationRequest, id=donation_request_id) + + total_donated = Donation.objects.filter(donation_request=donation_request).aggregate(Sum('amount'))['amount__sum'] or 0 + remaining_amount = donation_request.amount_requested - total_donated if donation_request.donation_type == 'medical' else None + is_fulfilled = (remaining_amount is not None) and (remaining_amount <= 0) + + if request.method == 'POST': + donor_name = request.POST.get('donor_name', '') + donation_type = request.POST.get('donation_type', '') + amount = request.POST.get('amount', '') + payment_method = request.POST.get('payment_method', '') + payment_proof = request.FILES.get('payment_proof', None) + + if not amount or not payment_method: + messages.error(request, "Amount and Payment Method are required for monetary donations.") + return redirect('donations:make_medical_donation', donation_request_id=donation_request.id) + + try: + amount = float(amount) + if remaining_amount is not None and amount > remaining_amount: + messages.error(request, f"The donation amount cannot exceed the remaining amount of {remaining_amount}.") + return redirect('donations:make_medical_donation', donation_request_id=donation_request.id) + except ValueError: + messages.error(request, "Please enter a valid donation amount.") + return redirect('donations:make_medical_donation', donation_request_id=donation_request.id) + + donation = Donation( + donor_name=donor_name, + donation_type=donation_type, + amount=amount, + payment_method=payment_method, + donation_request=donation_request + ) + + if payment_proof: + donation.payment_proof = payment_proof + + donation.save() + + messages.success(request, "Thank you for your monetary donation!") + return redirect('donations:donation_request_list') + + return render(request, 'donations/medical_donate.html', { + 'donation_request': donation_request, + 'is_fulfilled': is_fulfilled, + 'remaining_amount': remaining_amount + }) + +def make_supply_donation(request, donation_request_id): + donation_request = get_object_or_404(DonationRequest, id=donation_request_id) + + if request.method == 'POST': + donor_name = request.POST.get('donor_name', '') + donation_type = request.POST.get('donation_type', '') + shipping_company = request.POST.get('shipping_company', '') + tracking_number = request.POST.get('tracking_number', '') + payment_proof = request.FILES.get('payment_proof', None) + + if not shipping_company or not tracking_number: + messages.error(request, "Please provide both the shipping company and tracking number for supplies donation.") + return redirect('donations:make_supply_donation', donation_request_id=donation_request.id) + + if request.POST.get('amount'): + messages.error(request, "Monetary donations are not required for supplies donations.") + return redirect('donations:make_supply_donation', donation_request_id=donation_request.id) + + donation = Donation( + donor_name=donor_name, + donation_type=donation_type, + shipping_company=shipping_company, + tracking_number=tracking_number, + donation_request=donation_request + ) + if payment_proof: + donation.payment_proof = payment_proof + donation.save() + messages.success(request, "Thank you for your supplies donation!") + return redirect('donations:donation_request_list') + + return render(request, 'donations/supply_donate.html', { + 'donation_request': donation_request, + }) + +''' +def make_donation(request, donation_request_id): + donation_request = get_object_or_404(DonationRequest, id=donation_request_id) + + total_donated = Donation.objects.filter(donation_request=donation_request).aggregate(Sum('amount'))['amount__sum'] or 0 + + if donation_request.donation_type == 'medical' and donation_request.amount_requested: + remaining_amount = donation_request.amount_requested - total_donated + else: + remaining_amount = None + + is_fulfilled = (remaining_amount is not None) and (remaining_amount <= 0) + + if request.method == 'POST': + donor_name = request.POST.get('donor_name', '') + donation_type = request.POST.get('donation_type', '') + amount = request.POST.get('amount', '') + payment_method = request.POST.get('payment_method', '') + shipping_company = request.POST.get('shipping_company', '') + tracking_number = request.POST.get('tracking_number', '') + payment_proof = request.FILES.get('payment_proof', None) + + logger.debug(f"Donor Name: {donor_name}, Donation Type: {donation_type}, Shipping Company: {shipping_company}, Tracking Number: {tracking_number}") + + if donation_type == 'monetary': + try: + amount = float(amount) + if remaining_amount is not None and amount > remaining_amount: + messages.error(request, f"The donation amount cannot exceed the remaining amount of {remaining_amount}.") + return redirect('donations:make_donation', donation_request_id=donation_request.id) + except ValueError: + messages.error(request, "Please enter a valid donation amount.") + return redirect('donations:make_donation', donation_request_id=donation_request.id) + + if amount and payment_method: + donation = Donation( + donor_name=donor_name, + donation_type=donation_type, + amount=amount, + payment_method=payment_method, + donation_request=donation_request + ) + donation.save() + messages.success(request, "Thank you for your monetary donation!") + return redirect('donations:donation_request_list') + + elif donation_type == 'supplies': + if not shipping_company or not tracking_number: + messages.error(request, "Please provide both the shipping company and tracking number for supplies donation.") + return redirect('donations:make_donation', donation_request_id=donation_request.id) + + if amount: + messages.error(request, "Monetary donations are not required for supplies donations.") + return redirect('donations:make_donation', donation_request_id=donation_request.id) + + donation = Donation( + donor_name=donor_name, + donation_type=donation_type, + shipping_company=shipping_company, + tracking_number=tracking_number, + donation_request=donation_request + ) + if payment_proof: + donation.payment_proof = payment_proof + donation.save() + messages.success(request, "Thank you for your supplies donation!") + return redirect('donations:donation_request_list') + + else: + messages.error(request, "Please fill all required fields.") + return redirect('donations:make_donation', donation_request_id=donation_request.id) + + return render(request, 'donations/donate.html', { + 'donation_request': donation_request, + 'is_fulfilled': is_fulfilled, + 'remaining_amount': remaining_amount + })''' \ No newline at end of file diff --git a/Pawtential/main/.DS_Store b/Pawtential/main/.DS_Store new file mode 100644 index 0000000..fd547c6 Binary files /dev/null and b/Pawtential/main/.DS_Store differ diff --git a/Pawtential/main/__init__.py b/Pawtential/main/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Pawtential/main/admin.py b/Pawtential/main/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/Pawtential/main/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/Pawtential/main/apps.py b/Pawtential/main/apps.py new file mode 100644 index 0000000..167f044 --- /dev/null +++ b/Pawtential/main/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class MainConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'main' diff --git a/Pawtential/main/migrations/__init__.py b/Pawtential/main/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Pawtential/main/models.py b/Pawtential/main/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/Pawtential/main/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/Pawtential/main/static/.DS_Store b/Pawtential/main/static/.DS_Store new file mode 100644 index 0000000..10f526f Binary files /dev/null and b/Pawtential/main/static/.DS_Store differ diff --git a/Pawtential/main/static/css/styles.css b/Pawtential/main/static/css/styles.css new file mode 100644 index 0000000..b315bbf --- /dev/null +++ b/Pawtential/main/static/css/styles.css @@ -0,0 +1,218 @@ +.root{ + --primary-color:#FFAAA7; + --secondary-color: #FF8551; + --bakground-color: #FAF0E4; + --text-color : #98ddca; + --text-color-h: #24A19C; +} +html, body{ + font-family: "Geist Mono", serif; + height: 100%; + margin: 0; + background-color: #FAF0E4; + scroll-behavior: smooth; +} + +main{ + flex: 1; + margin: 10px; +} +header{ + height: 200px; + border-bottom: 2px solid #FF8551; +} +.navbar-logo img{ + width: 200px; + height:auto; +} +.navbar-nav .nav-link , .bi-box-arrow-in-right{ + color: #98ddca; + font-size: 20px; + font-weight: bold; + } + +.navbar-nav .nav-link:hover , .bi-box-arrow-in-right{ + + color: #24A19C; + +} +.navbar-toggler { + background-color: #FF8551; + border: none; + border-radius: 5px; +} + +.navbar-toggler-icon { + background-color: #FAF0E4; + border-radius: 2px; + width: 30px; + height: 3px; +} + +.navbar-toggler-icon::before, +.navbar-toggler-icon::after { + background-color: #FAF0E4; + width: 100%; + height: 3px; +} +.navbar-collapse { + background-color: #FAF0E4; + z-index: 1000; +} + +h1{ + color: #24A19C; +} +.search-form { + position: absolute; + top: 50%; + left: 80%; + transform: translate(-50%, -50%); + width: 120%; + justify-content: center; + align-items: center; + display: flex; +} +.search-form .form-control { + flex-grow: 1; + margin-right: 10px; + background-color: rgba(255, 255, 255, 0.636); + border: 1px solid #FFAAA7; +} +.search-form i { + color: #98DDCA; + font-size: 1.5rem; + cursor: pointer; +} +#home-img { + max-width: 80; + height: auto; + margin-bottom: 20px; +} + +#slogan { + color: #333; + margin-bottom: 10px; +} + +form.search-form { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} +#title { + font-size: 3rem; + font-weight: bold; + color: #ff8c00; + display: inline-block; + margin-bottom: 0; +} + + +footer{ + padding: 10px 0; + align-items: center; + text-align: center; +} + +/*accounts*/ + +.custom-form-container { + max-width: 800px; + margin: 0 auto; + padding: 20px; + background-color: #f8f9fa91; + border-radius: 30px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.custom-form-container .card-header { + color: #24A19C; + font-size: 1.5rem; + padding: 10px; +} + +.form-label { + font-weight: bold; +} + +.form-control { + border-radius: 0.375rem; + border-color: #ccc; +} + +.btn-primary { + background-color: #24A19C; + border-color: #24A19C; + padding: 10px 20px; + border-radius: 5px; +} + +.btn-primary:hover { + background-color: #98ddca; + border-color: #98ddca; +} + +.account-fields { + display: none; +} + + +/* pet list */ +.pet-img { + width: 100%; + height: 200px; + object-fit: cover; +} + +.pet-card { + height: 450px; + display: flex; + flex-direction: column; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + border-radius: 10px; + overflow: hidden; +} + +.pet-card .card-body { + flex-grow: 1; + padding: 20px; +} + +.pet-card .card-title { + font-size: 1.2rem; + font-weight: bold; + margin-bottom: 10px; +} + +.pet-card .card-text { + font-size: 0.9rem; +} + +.pet-card .btn-link { + font-size: 0.9rem; + color: #FFAAA7; +} + +.pet-card .btn-link:hover { + text-decoration: underline; +} + +.pet-card .btn-secondary { + background-color: #24A19C; + border: none; + font-size: 1rem; + padding: 5px 15px; + position: absolute; + bottom: 20px; + right: 10px; + border-radius: 4px; +} + +.pet-card .btn-secondary:hover { + background-color: #98DDCA; +} + + diff --git a/Pawtential/main/static/images/.DS_Store b/Pawtential/main/static/images/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/Pawtential/main/static/images/.DS_Store differ diff --git a/Pawtential/main/static/images/Pawtential.png b/Pawtential/main/static/images/Pawtential.png new file mode 100644 index 0000000..2002547 Binary files /dev/null and b/Pawtential/main/static/images/Pawtential.png differ diff --git a/Pawtential/main/static/images/cat.jpeg b/Pawtential/main/static/images/cat.jpeg new file mode 100644 index 0000000..d2e4fc5 Binary files /dev/null and b/Pawtential/main/static/images/cat.jpeg differ diff --git a/Pawtential/main/static/images/cat2.jpeg b/Pawtential/main/static/images/cat2.jpeg new file mode 100644 index 0000000..3b0833b Binary files /dev/null and b/Pawtential/main/static/images/cat2.jpeg differ diff --git a/Pawtential/main/static/images/icon.png b/Pawtential/main/static/images/icon.png new file mode 100644 index 0000000..b7a6dda Binary files /dev/null and b/Pawtential/main/static/images/icon.png differ diff --git a/Pawtential/main/static/images/paw.png b/Pawtential/main/static/images/paw.png new file mode 100644 index 0000000..1c6bdc9 Binary files /dev/null and b/Pawtential/main/static/images/paw.png differ diff --git a/Pawtential/main/templates/main/base.html b/Pawtential/main/templates/main/base.html new file mode 100644 index 0000000..79736ea --- /dev/null +++ b/Pawtential/main/templates/main/base.html @@ -0,0 +1,100 @@ +{% load static %} + + + + + + + + + + + + + + + {% block title %}{% endblock %} + + + + +
+ +
+ +
+ {% block content %} + {% endblock %} +
+ + + + + + + + diff --git a/Pawtential/main/templates/main/home.html b/Pawtential/main/templates/main/home.html new file mode 100644 index 0000000..178b415 --- /dev/null +++ b/Pawtential/main/templates/main/home.html @@ -0,0 +1,56 @@ +{% extends 'main/base.html' %} +{% load static %} +{% block content %} +{% block title %}Home Page{% endblock %} +
+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} +
+
+ home.png +
+

we believe every pet has the potential to thrive in a loving home

+
+
+
+
+
+ Before rescue +
+ +
+ After rescue +
+
+
+ +
+
+ +
+
+ {% for pet in pets %} +
+
+
+ {{ pet.name }} +
{{ pet.name }}
+

Species: {{ pet.species }}

+

Age: {{ pet.age_in_years_and_months.0 }} years and {{ pet.age_in_years_and_months.1 }} months

+

Status: {{ pet.get_adoption_status_display }}

+
+ Details +
+
+
+
+ {% endfor %} +
+
+
+{% endblock %} diff --git a/Pawtential/main/tests.py b/Pawtential/main/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Pawtential/main/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Pawtential/main/urls.py b/Pawtential/main/urls.py new file mode 100644 index 0000000..6dbbdad --- /dev/null +++ b/Pawtential/main/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from . import views + +app_name = "main" +urlpatterns = [ + path('', views.home_view, name='home_view'), +] \ No newline at end of file diff --git a/Pawtential/main/views.py b/Pawtential/main/views.py new file mode 100644 index 0000000..9edffee --- /dev/null +++ b/Pawtential/main/views.py @@ -0,0 +1,10 @@ +from django.shortcuts import render +from django.http import HttpRequest +from pets.models import Pet + +# Create your views here. + +def home_view(request): + pets = Pet.objects.all().order_by('-created_at')[:3] + + return render(request, 'main/home.html', {'pets': pets}) \ No newline at end of file diff --git a/Pawtential/manage.py b/Pawtential/manage.py new file mode 100755 index 0000000..a96ba89 --- /dev/null +++ b/Pawtential/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', 'Pawtential.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/Pawtential/media/.DS_Store b/Pawtential/media/.DS_Store new file mode 100644 index 0000000..4bd96f0 Binary files /dev/null and b/Pawtential/media/.DS_Store differ diff --git a/Pawtential/media/pet_images/.DS_Store b/Pawtential/media/pet_images/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/Pawtential/media/pet_images/.DS_Store differ diff --git a/Pawtential/media/pet_images/1e405ee5628cb57a4a2434ff34440605.jpg b/Pawtential/media/pet_images/1e405ee5628cb57a4a2434ff34440605.jpg new file mode 100644 index 0000000..9baaa19 Binary files /dev/null and b/Pawtential/media/pet_images/1e405ee5628cb57a4a2434ff34440605.jpg differ diff --git a/Pawtential/media/pet_images/2003eebe96d91baf94cd39623185229d.jpg b/Pawtential/media/pet_images/2003eebe96d91baf94cd39623185229d.jpg new file mode 100644 index 0000000..8203780 Binary files /dev/null and b/Pawtential/media/pet_images/2003eebe96d91baf94cd39623185229d.jpg differ diff --git a/Pawtential/media/pet_images/4c862b00b6b6307bf35ad13c49568376.jpg b/Pawtential/media/pet_images/4c862b00b6b6307bf35ad13c49568376.jpg new file mode 100644 index 0000000..3e3349d Binary files /dev/null and b/Pawtential/media/pet_images/4c862b00b6b6307bf35ad13c49568376.jpg differ diff --git a/Pawtential/media/pet_images/88495fb89f2a251b4f73f30448ce7147.jpg b/Pawtential/media/pet_images/88495fb89f2a251b4f73f30448ce7147.jpg new file mode 100644 index 0000000..2b75d24 Binary files /dev/null and b/Pawtential/media/pet_images/88495fb89f2a251b4f73f30448ce7147.jpg differ diff --git a/Pawtential/media/pet_images/89466355de3a9651bdc7fa2daf49c17b_2FAmOJc.jpg b/Pawtential/media/pet_images/89466355de3a9651bdc7fa2daf49c17b_2FAmOJc.jpg new file mode 100644 index 0000000..1c9d9cb Binary files /dev/null and b/Pawtential/media/pet_images/89466355de3a9651bdc7fa2daf49c17b_2FAmOJc.jpg differ diff --git a/Pawtential/media/pet_images/IMG_2485.heic b/Pawtential/media/pet_images/IMG_2485.heic new file mode 100644 index 0000000..430aeed Binary files /dev/null and b/Pawtential/media/pet_images/IMG_2485.heic differ diff --git a/Pawtential/media/pet_images/ae466a84dd653d3453f7b6640dafd1b5.jpg b/Pawtential/media/pet_images/ae466a84dd653d3453f7b6640dafd1b5.jpg new file mode 100644 index 0000000..9104163 Binary files /dev/null and b/Pawtential/media/pet_images/ae466a84dd653d3453f7b6640dafd1b5.jpg differ diff --git a/Pawtential/media/pet_images/eff76bfc3d6e2ede1e9991a8ae5cb898.jpg b/Pawtential/media/pet_images/eff76bfc3d6e2ede1e9991a8ae5cb898.jpg new file mode 100644 index 0000000..acf1140 Binary files /dev/null and b/Pawtential/media/pet_images/eff76bfc3d6e2ede1e9991a8ae5cb898.jpg differ diff --git a/Pawtential/media/pet_images/f33ee213a9fcc778d88f52eb1fe9d3d4.jpg b/Pawtential/media/pet_images/f33ee213a9fcc778d88f52eb1fe9d3d4.jpg new file mode 100644 index 0000000..bbf3bc3 Binary files /dev/null and b/Pawtential/media/pet_images/f33ee213a9fcc778d88f52eb1fe9d3d4.jpg differ diff --git a/Pawtential/media/pet_images/pexels-photo-14840603.webp b/Pawtential/media/pet_images/pexels-photo-14840603.webp new file mode 100644 index 0000000..2a79742 Binary files /dev/null and b/Pawtential/media/pet_images/pexels-photo-14840603.webp differ diff --git a/Pawtential/media/pet_images/star.png b/Pawtential/media/pet_images/star.png new file mode 100644 index 0000000..92360b4 Binary files /dev/null and b/Pawtential/media/pet_images/star.png differ diff --git a/Pawtential/media/profile_pics/.DS_Store b/Pawtential/media/profile_pics/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/Pawtential/media/profile_pics/.DS_Store differ diff --git a/Pawtential/media/profile_pics/c8dad48a5761d33538edc01b620b8ee3.jpg b/Pawtential/media/profile_pics/c8dad48a5761d33538edc01b620b8ee3.jpg new file mode 100644 index 0000000..2c29660 Binary files /dev/null and b/Pawtential/media/profile_pics/c8dad48a5761d33538edc01b620b8ee3.jpg differ diff --git a/Pawtential/media/profile_pics/default_profile_pic.jpg b/Pawtential/media/profile_pics/default_profile_pic.jpg new file mode 100644 index 0000000..e2fbedd Binary files /dev/null and b/Pawtential/media/profile_pics/default_profile_pic.jpg differ diff --git a/Pawtential/media/profile_pics/fdf52f8f929c01f63fc389d50d653b85.jpg b/Pawtential/media/profile_pics/fdf52f8f929c01f63fc389d50d653b85.jpg new file mode 100644 index 0000000..f8a1f05 Binary files /dev/null and b/Pawtential/media/profile_pics/fdf52f8f929c01f63fc389d50d653b85.jpg differ diff --git a/Pawtential/pets/__init__.py b/Pawtential/pets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Pawtential/pets/admin.py b/Pawtential/pets/admin.py new file mode 100644 index 0000000..b5f5ea8 --- /dev/null +++ b/Pawtential/pets/admin.py @@ -0,0 +1,13 @@ +from django.contrib import admin +from .models import Pet + +# Register your models here. + + +class PetAdmin(admin.ModelAdmin): + list_display = ['name','health_status','adoption_status','user','created_at'] + search_fields=['name','species','location'] + list_filter=['adoption_status'] + + +admin.site.register(Pet, PetAdmin) \ No newline at end of file diff --git a/Pawtential/pets/apps.py b/Pawtential/pets/apps.py new file mode 100644 index 0000000..8f49ff2 --- /dev/null +++ b/Pawtential/pets/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PetsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'pets' diff --git a/Pawtential/pets/forms.py b/Pawtential/pets/forms.py new file mode 100644 index 0000000..abed0b4 --- /dev/null +++ b/Pawtential/pets/forms.py @@ -0,0 +1,7 @@ +from django import forms +from .models import Pet + +class PetForm(forms.ModelForm): + class Meta: + model = Pet + fields = ['name', 'image', 'species', 'breed', 'age', 'health_status', 'adoption_status'] diff --git a/Pawtential/pets/migrations/0001_initial.py b/Pawtential/pets/migrations/0001_initial.py new file mode 100644 index 0000000..3da386e --- /dev/null +++ b/Pawtential/pets/migrations/0001_initial.py @@ -0,0 +1,35 @@ +# Generated by Django 5.1.3 on 2024-11-29 17:55 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Pet', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('image', models.ImageField(blank=True, null=True, upload_to='pet_images/')), + ('species', models.CharField(max_length=150)), + ('breed', models.CharField(blank=True, max_length=50, null=True)), + ('age', models.FloatField()), + ('health_status', models.CharField(choices=[('healthy', 'Healthy'), ('sick', 'Sick'), ('treatment', 'Under Treatment')], default='healthy', max_length=20)), + ('adoption_status', models.CharField(choices=[('available', 'Available for Adoption'), ('adopted', 'Adopted'), ('not_available', 'Not Available for Adoption')], default='available', max_length=20)), + ('medical_history', models.TextField(blank=True, null=True)), + ('location', models.CharField(blank=True, max_length=255, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pets', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/Pawtential/pets/migrations/__init__.py b/Pawtential/pets/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Pawtential/pets/models.py b/Pawtential/pets/models.py new file mode 100644 index 0000000..17a197e --- /dev/null +++ b/Pawtential/pets/models.py @@ -0,0 +1,40 @@ +from django.db import models +from django.contrib.auth.models import User + +# Create your models here. + +class Pet(models.Model): + + HEALTH_CHOICES = [ + ('healthy', 'Healthy'), + ('sick', 'Sick'), + ('treatment', 'Under Treatment'), + ] + + ADOPTION_CHOICES = [ + ('available', 'Available for Adoption'), + ('adopted', 'Adopted'), + ('not_available', 'Not Available for Adoption'), + ] + + name = models.CharField(max_length=100) + image = models.ImageField(upload_to='pet_images/', blank=True, null=True) + species = models.CharField(max_length=150) + breed = models.CharField(max_length=50, blank=True, null=True) + age = models.FloatField() + health_status = models.CharField(max_length=20, choices=HEALTH_CHOICES, default='healthy') + adoption_status = models.CharField(max_length=20, choices=ADOPTION_CHOICES, default='available') + medical_history = models.TextField(blank=True, null=True) + location = models.CharField(max_length=255, blank=True, null=True) + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='pets') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.name + @property + def age_in_years_and_months(self): + + years = int(self.age) + months = round((self.age - years) * 12) + return years, months \ No newline at end of file diff --git a/Pawtential/pets/templates/pets/add_pet.html b/Pawtential/pets/templates/pets/add_pet.html new file mode 100644 index 0000000..90c65b7 --- /dev/null +++ b/Pawtential/pets/templates/pets/add_pet.html @@ -0,0 +1,96 @@ +{% extends 'main/base.html' %} +{% load static %} +{% block title %}Add New Pet{% endblock %} + +{% block content %} +
+
+
+
+
+

Add New Pet

+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} +
+
+
+ {% csrf_token %} + + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+

Need to see your pets? View Pets

+
+
+
+
+
+
+{% endblock %} diff --git a/Pawtential/pets/templates/pets/edit_pet.html b/Pawtential/pets/templates/pets/edit_pet.html new file mode 100644 index 0000000..687ed17 --- /dev/null +++ b/Pawtential/pets/templates/pets/edit_pet.html @@ -0,0 +1,94 @@ +{% extends 'main/base.html' %} +{% load static %} + +{% block title %}Edit {{ pet.name }}{% endblock %} + +{% block content %} +
+
+
+
+
+

Edit Pet

+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} +
+
+
+ {% csrf_token %} + + +
+ + +
+ +
+ + + {% if pet.image %} + + {% endif %} +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+

Need to see this pet again? + View Pet Details +

+
+
+
+
+
+
+{% endblock %} diff --git a/Pawtential/pets/templates/pets/pet_detail.html b/Pawtential/pets/templates/pets/pet_detail.html new file mode 100644 index 0000000..88a5bd6 --- /dev/null +++ b/Pawtential/pets/templates/pets/pet_detail.html @@ -0,0 +1,71 @@ +{% extends 'main/base.html' %} +{% load static %} + +{% block title %}{{ pet.name }} Details{% endblock %} + +{% block content %} +
+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} +
+
+
+
+
+ +
+ {{ pet.name }} +
+
+
{{ pet.name }}
+ +

Species: {{ pet.species }}

+

Breed: {{ pet.breed }}

+

+ Age: {{ pet.age_in_years_and_months.0 }} years and {{ pet.age_in_years_and_months.1 }} months +

+

Health Status: {{ pet.get_health_status_display }}

+

Adoption Status: {{ pet.get_adoption_status_display }}

+

Medical History: {{ pet.medical_history|default:"No medical history" }}

+

Location: {{ pet.location|default:"Not specified" }}

+ +

+ Added by: + {% if pet.user.individual_user %} + {{ pet.user.username }} + {% elif pet.user.shelter %} + {{ pet.user.username }} + {% endif %} +

+ +

Created At: {{ pet.created_at|date:"F j, Y, g:i a" }}

+

Last Updated: {{ pet.updated_at|date:"F j, Y, g:i a" }}

+ {% if request.user.is_authenticated %} + {% if pet.adoption_status == 'available' %} + Request Adoption + {% endif %} + {% else %} +

Please log in to request adoption.

+ {% endif %} + + {% if pet.user == request.user %} + Edit Pet +
+ {% csrf_token %} + +
+ {% endif %} + Back to Pet List +
+
+
+
+
+
+
+{% endblock %} diff --git a/Pawtential/pets/templates/pets/pet_list.html b/Pawtential/pets/templates/pets/pet_list.html new file mode 100644 index 0000000..6fa59e9 --- /dev/null +++ b/Pawtential/pets/templates/pets/pet_list.html @@ -0,0 +1,103 @@ +{% extends 'main/base.html' %} +{% load static %} + +{% block title %}Pet List{% endblock %} + +{% block content %} +
+

List of Pets

+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} +
+
+
+ +
+
+ +
+
+ +
+
+
+
+ {% for pet in pets %} +
+
+ +
+ {% if pet.image %} + {{ pet.name }} + {% else %} + Default image + {% endif %} +
+ +
+
{{ pet.name }}
+

Health Status: {{ pet.get_health_status_display }}

+

Adoption Status: {{ pet.get_adoption_status_display }}

+ Read More +

+ Added by: + {% if pet.user.individual_user %} + {{ pet.user.username }} + {% elif pet.user.shelter %} + {{ pet.user.username }} + {% endif %} +

+
+ +
+
+ {% endfor %} +
+ +
+ +
+
+{% endblock %} diff --git a/Pawtential/pets/tests.py b/Pawtential/pets/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Pawtential/pets/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Pawtential/pets/urls.py b/Pawtential/pets/urls.py new file mode 100644 index 0000000..7818674 --- /dev/null +++ b/Pawtential/pets/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from . import views + +app_name = 'pets' + +urlpatterns = [ + path('add/', views.add_pet, name='add_pet'), + path('list/', views.pet_list_view, name='pet_list_view'), + path('pet//', views.pet_detail_view, name='pet_detail'), + path('pet//edit/', views.edit_pet, name='edit_pet'), + path('pet//delete/', views.delete_pet, name='delete_pet'), +] \ No newline at end of file diff --git a/Pawtential/pets/views.py b/Pawtential/pets/views.py new file mode 100644 index 0000000..fc5eee5 --- /dev/null +++ b/Pawtential/pets/views.py @@ -0,0 +1,116 @@ +from django.shortcuts import render ,redirect , get_object_or_404 +from django.contrib.auth.decorators import login_required +from .models import Pet +from django.core.paginator import Paginator +from .forms import PetForm +from adoptions.models import AdoptionRequest +from django.contrib import messages + +# Create your views here. + +@login_required +def add_pet(request): + if request.method == 'POST': + + name = request.POST.get('name') + image = request.FILES.get('image') + species = request.POST.get('species') + breed = request.POST.get('breed') + age = request.POST.get('age') + health_status = request.POST.get('health_status') + adoption_status = request.POST.get('adoption_status') + medical_history = request.POST.get('medical_history') + location = request.POST.get('location') + + + if not name or not species or not age or not health_status or not adoption_status: + messages.error(request, "All required fields must be filled!") + return redirect('pets:add_pet') + + + pet = Pet( + name=name, + image=image, + species=species, + breed=breed, + age=age, + health_status=health_status, + adoption_status=adoption_status, + medical_history=medical_history, + location=location, + user=request.user + ) + pet.save() + + messages.success(request, f"{name} has been successfully added!") + return redirect('pets:pet_list_view') + + return render(request, 'pets/add_pet.html') + + +def pet_list_view(request): + search_query = request.GET.get('search', '') + adoption_status = request.GET.get('adoption_status', '') + + pets = Pet.objects.all().order_by('-created_at') + + if search_query: + pets = pets.filter(name__icontains=search_query) + + if adoption_status: + pets = pets.filter(adoption_status=adoption_status) + + + paginator = Paginator(pets, 8) + page_number = request.GET.get('page') + pets_page = paginator.get_page(page_number) + + return render(request, 'pets/pet_list.html', {'pets': pets_page}) + + +def pet_detail_view(request, pet_id): + pet = get_object_or_404(Pet, id=pet_id) + + if request.user.is_authenticated: + already_requested = AdoptionRequest.objects.filter(pet=pet, user=request.user).exists() + else: + already_requested = False + + return render(request, 'pets/pet_detail.html', {'pet': pet, 'already_requested': already_requested}) + + +def edit_pet(request, pet_id): + try: + pet = Pet.objects.get(id=pet_id) + except Pet.DoesNotExist: + messages.error(request, "Pet not found.") + return redirect('pets:pet_list_view') + + if request.method == 'POST': + form = PetForm(request.POST, request.FILES, instance=pet) + if form.is_valid(): + form.save() + messages.success(request, "Pet details have been successfully updated.") + return redirect('pets:pet_detail', pet_id=pet.id) + else: + messages.error(request, "Please correct the errors below.") + else: + form = PetForm(instance=pet) + + return render(request, 'pets/edit_pet.html', {'form': form, 'pet': pet}) + + +def delete_pet(request, pet_id): + pet = Pet.objects.filter(id=pet_id).first() + + if pet: + if request.user == pet.user: + pet.delete() + messages.success(request, "Pet has been successfully deleted.") + return redirect('pets:pet_list_view') + else: + messages.error(request, "You cannot delete this pet. It does not belong to you.") + return redirect('pets:pet_detail', pet.id) + else: + messages.error(request, "Pet not found.") + return redirect('pets:pet_list_view') \ No newline at end of file diff --git a/README.md b/README.md index cb428d7..6694997 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,14 @@ # UNIT-PROJECT-3 +## Pawtential +- **Overview:** A platform for managing pet adoption and rescue, allowing individuals and organizations to list animals, request adoptions, and provide or receive support for animal care +- **Features:** +- User registration and profile management. +- Pet listing and adoption request submission. +- Financial and material support (donations for food, medical care, etc.) requested by shelters. +- Adoption status updates + +## UML link +- [UML](https://lucid.app/lucidchart/9d07aaa2-e133-4f98-a805-d37d963d7819/edit?viewport_loc=-719%2C-275%2C2384%2C1395%2CHWEp-vi-RSFO&invitationId=inv_6f54713b-2af9-4aea-abcc-d1001242bd77) ## Create a Project of your own choosing