diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/workflows/main_paws-backend.yml b/.github/workflows/main_paws-backend.yml new file mode 100644 index 0000000..0d496b3 --- /dev/null +++ b/.github/workflows/main_paws-backend.yml @@ -0,0 +1,78 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# More GitHub Actions for Azure: https://github.com/Azure/actions +# More info on Python, GitHub Actions, and Azure App Service: https://aka.ms/python-webapps-actions + +name: Build and deploy Python app to Azure Web App - paws-backend + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python version + uses: actions/setup-python@v1 + with: + python-version: '3.11' + + - name: Create and start virtual environment + run: | + python -m venv venv + source venv/bin/activate + + - name: Install dependencies + run: pip install -r requirements.txt + + # Optional: Add step to run tests here (PyTest, Django test suites, etc.) + + - name: Zip artifact for deployment + run: zip release.zip ./* -r + + - name: Upload artifact for deployment jobs + uses: actions/upload-artifact@v3 + with: + name: python-app + path: | + release.zip + !venv/ + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: 'Production' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + permissions: + id-token: write #This is required for requesting the JWT + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v3 + with: + name: python-app + + - name: Unzip artifact for deployment + run: unzip release.zip + + + - name: Login to Azure + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_65385D52358C41A1B340C88B1CF0FE57 }} + tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_1CE927B36F8B4BF398C2DF30CE3502CD }} + subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_C9E0646AF67942A9A660F43365693C20 }} + + - name: 'Deploy to Azure Web App' + uses: azure/webapps-deploy@v2 + id: deploy-to-webapp + with: + app-name: 'paws-backend' + slot-name: 'Production' + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..205314d --- /dev/null +++ b/.gitignore @@ -0,0 +1,154 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ +test.py +*.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..aae16d4 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ + + +## Setup and Installation +clone the repo + +## Install the requirements: + +```bash +pip install -r requirements.txt +``` + +Apply the migrations: + +```bash +python manage.py migrate +``` + +## Running the Project + +To run the project: + +```bash +python manage.py runserver +`````` \ No newline at end of file diff --git a/aniresfr/__init__.py b/aniresfr/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aniresfr/admin.py b/aniresfr/admin.py new file mode 100644 index 0000000..d5eab10 --- /dev/null +++ b/aniresfr/admin.py @@ -0,0 +1,36 @@ +from django.contrib import admin +from .models import Animal, BaseUser, NgoUser, CustomUser, Campaign + +class AnimalAdmin(admin.ModelAdmin): + list_display = ('id', 'user_name', 'user_email', 'user_phone', + 'animal_type', 'numberOfAnimals', 'description', + 'condition', 'image', 'latitude', 'longitude', + 'address', 'landmark', 'status', 'reported_time', + 'response_time', 'assigned_to') + + +class CampaignAdmin(admin.ModelAdmin): + list_display = ('ngo_name', 'title', 'description', 'tags', 'phone_number', + 'email', 'start_date', 'end_date', 'application_deadline', + 'age_group', 'image_link', 'is_over', 'campaign_id', 'applicant_list') + + +class BaseUserAdmin(admin.ModelAdmin): + list_display = ('email', 'name', 'phone_number', 'is_ngo', 'is_active', + 'is_staff', 'is_superuser', 'date_joined', 'notify_token') + search_fields = ['email', 'name', 'phone_number'] + +class NgoUserAdmin(admin.ModelAdmin): + list_display = ('user', 'emergency_contact_number', 'animals_supported', + 'website', 'address', 'latitude', 'longitude', 'no_received_reports', 'created_campaigns_id') + +class CustomUserAdmin(admin.ModelAdmin): + list_display = ('user', 'no_reports', 'level', 'coins', 'applied_campaigns') + + + +admin.site.register(Animal, AnimalAdmin) +admin.site.register(BaseUser, BaseUserAdmin) +admin.site.register(NgoUser, NgoUserAdmin) +admin.site.register(CustomUser, CustomUserAdmin) +admin.site.register(Campaign, CampaignAdmin) \ No newline at end of file diff --git a/aniresfr/apps.py b/aniresfr/apps.py new file mode 100644 index 0000000..f373cd3 --- /dev/null +++ b/aniresfr/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AniresfrConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'aniresfr' diff --git a/aniresfr/managers.py b/aniresfr/managers.py new file mode 100644 index 0000000..f441f74 --- /dev/null +++ b/aniresfr/managers.py @@ -0,0 +1,34 @@ +from django.contrib.auth.base_user import BaseUserManager +from django.utils.translation import gettext_lazy as _ + + +class CustomUserManager(BaseUserManager): + """ + Custom user model manager where email is the unique identifiers + for authentication instead of usernames. + """ + def create_user(self, email, password, **extra_fields): + """ + Create and save a user with the given email and password. + """ + if not email: + raise ValueError(_("The Email must be set")) + email = self.normalize_email(email) + user = self.model(email=email, **extra_fields) + user.set_password(password) + user.save() + return user + + def create_superuser(self, email, password, **extra_fields): + """ + Create and save a SuperUser with the given email and password. + """ + extra_fields.setdefault("is_staff", True) + extra_fields.setdefault("is_superuser", True) + extra_fields.setdefault("is_active", True) + + if extra_fields.get("is_staff") is not True: + raise ValueError(_("Superuser must have is_staff=True.")) + if extra_fields.get("is_superuser") is not True: + raise ValueError(_("Superuser must have is_superuser=True.")) + return self.create_user(email, password, **extra_fields) diff --git a/aniresfr/models.py b/aniresfr/models.py new file mode 100644 index 0000000..d13d6f8 --- /dev/null +++ b/aniresfr/models.py @@ -0,0 +1,100 @@ +from django.db import models +from datetime import timedelta +from django.utils import timezone +from django.contrib.postgres.fields import ArrayField +from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin +from django.utils.translation import gettext_lazy as _ + +from .managers import CustomUserManager + + +def one_day_from_now(): + return timezone.now() + timedelta(days=1) + + +class Animal(models.Model): + user_name = models.CharField(max_length=200) + user_email = models.EmailField() + user_phone = models.CharField(max_length=15) + animal_type = models.CharField(max_length=50) + description = models.CharField(max_length=200) + condition = models.CharField(max_length=50) + image = models.CharField(max_length=200) + latitude = models.FloatField() + longitude = models.FloatField() + numberOfAnimals = models.CharField(max_length=15) + address = models.CharField(max_length=500) + landmark = models.CharField(max_length=200) + status = models.CharField(max_length=50) + reported_time = models.DateTimeField(auto_now_add=True) + response_time = models.DateTimeField(default=one_day_from_now) + assigned_to = models.CharField(max_length=200, default='None') + + def __str__(self): + return self.description + + +class Campaign(models.Model): + ngo_name = models.CharField(max_length=200) + title = models.TextField() + description = models.TextField() + tags = models.JSONField() + phone_number = models.CharField(max_length=15) + email = models.EmailField() + start_date = models.DateTimeField() + end_date = models.DateTimeField() + application_deadline = models.DateTimeField() + age_group = models.IntegerField() + image_link = models.URLField(blank=True, null=True) + is_over = models.BooleanField(default=False) + campaign_id = models.AutoField(primary_key=True) + applicant_list = models.JSONField(blank=True, null=True) + + def __str__(self): + return self.title + + +class BaseUser(AbstractBaseUser, PermissionsMixin): + email = models.EmailField(unique=True) + name = models.CharField(max_length=200) + phone_number = models.CharField(max_length=15) + is_ngo= models.BooleanField(default=False) + is_active = models.BooleanField(default=True) + is_staff = models.BooleanField(default=False) + is_superuser = models.BooleanField(default=False) + date_joined = models.DateTimeField(default=timezone.now) + notify_token = models.CharField(max_length=200, default='') + + USERNAME_FIELD = "email" + REQUIRED_FIELDS = ['name', 'phone_number'] + + objects = CustomUserManager() + + def __str__(self): + return self.email + + +class NgoUser(models.Model): + user = models.OneToOneField(BaseUser, on_delete=models.CASCADE, primary_key=True) + emergency_contact_number = models.CharField(max_length=15, default='') + animals_supported = ArrayField(models.CharField(max_length=200), blank=True, default=list) + website = models.CharField(max_length=200, default='') + address = models.CharField(max_length=500, default='') + latitude = models.FloatField(default=0.0) + longitude = models.FloatField(default=0.0) + no_received_reports = models.IntegerField(default=0) + created_campaigns_id = ArrayField(models.IntegerField(), blank=True, default=list) + + def __str__(self): + return self.user.name + + +class CustomUser(models.Model): + user = models.OneToOneField(BaseUser, on_delete=models.CASCADE, primary_key=True) + no_reports = models.IntegerField(default=0) + applied_campaigns = ArrayField(models.IntegerField(), blank=True, default=list) + level = models.IntegerField(default=1) + coins = models.IntegerField(default=0) + + def __str__(self): + return self.user.name \ No newline at end of file diff --git a/aniresfr/notify.py b/aniresfr/notify.py new file mode 100644 index 0000000..eef4031 --- /dev/null +++ b/aniresfr/notify.py @@ -0,0 +1,24 @@ +from firebase_admin import get_app, messaging + +def send_notification(notify_list=[], title='title', body='body'): + # Define message payload. + message = messaging.MulticastMessage + message = messaging.MulticastMessage( + notification=messaging.Notification( + title=title, + body=body, + image="https://pawss.vercel.app/notification_icon.png" + ), + tokens=notify_list, + ) + try: + response = messaging.send_multicast(message) + print('Successfully sent meassage to firebase.') + # Print the response for each individual message + for idx, resp in enumerate(response.responses): + if resp.success: + print(f"Message {idx} sent successfully") + else: + print(f"Message {idx} could not be sent: {resp.exception}") + except Exception as e: + print(f"Failed to send message: {e}") diff --git a/aniresfr/serializers.py b/aniresfr/serializers.py new file mode 100644 index 0000000..97688b4 --- /dev/null +++ b/aniresfr/serializers.py @@ -0,0 +1,126 @@ +from .models import * +from django.db.models import F +from rest_framework import serializers +from django.contrib.auth import get_user_model +from math import radians, cos, sin, sqrt, atan2 +from .notify import send_notification + + +def increment_user_reports(animal): + try: + user = CustomUser.objects.get(user__email=animal.user_email) + update_fields = { + 'no_reports': F('no_reports') + 1, + 'coins': F('coins') + 2, + } + if (user.no_reports+1) % 5 == 0: + update_fields['level'] = F('level') + 1 + CustomUser.objects.filter(user__email=animal.user_email).update(**update_fields) + except CustomUser.DoesNotExist: + pass + +class AnimalSerializer(serializers.ModelSerializer): + class Meta: + model = Animal + fields = ['id', 'user_name', 'user_email', 'user_phone', + 'animal_type', 'numberOfAnimals', 'description', + 'condition', 'image', 'latitude', 'longitude', + 'address', 'landmark', 'status', 'reported_time', + 'response_time', 'assigned_to'] + + def create(self, validated_data): + animal = Animal.objects.create(**validated_data) + ngos = NgoUser.objects.all() + min_distance = None + nearest_ngo = None + for ngo in ngos: + dlon = radians(ngo.longitude) - radians(animal.longitude) + dlat = radians(ngo.latitude) - radians(animal.latitude) + a = sin(dlat / 2)**2 + cos(radians(animal.latitude)) * cos(radians(ngo.latitude)) * sin(dlon / 2)**2 + c = 2 * atan2(sqrt(a), sqrt(1 - a)) + distance = 6371.01 * c + if min_distance is None or distance < min_distance: + min_distance = distance + nearest_ngo = ngo + print(nearest_ngo.user.email, distance) + if nearest_ngo is not None: + animal.assigned_to = nearest_ngo.user.email + nearest_ngo.no_received_reports = F('no_received_reports') + 1 + nearest_ngo.save(update_fields=['no_received_reports']) + animal.save() + send_notification([nearest_ngo.user.notify_token], 'New Report', 'A new report has been made near you.') + increment_user_reports(animal) + return animal + + +class CampaignSerializer(serializers.ModelSerializer): + class Meta: + model = Campaign + fields = ['ngo_name', 'title', 'description', 'tags', 'phone_number', + 'email', 'start_date', 'end_date', 'application_deadline', + 'age_group', 'image_link', 'is_over', 'campaign_id', 'applicant_list'] + + def create(self, validated_data): + campaign = Campaign.objects.create(**validated_data) + users = CustomUser.objects.all() + #registration_tokens = [user.user.notify_token for user in users if user.user.notify_token] + #send_notification(registration_tokens, 'New Campaign', 'participate in :'+campaign.title+' campaign.') + return campaign + +class BaseUserSerializer(serializers.ModelSerializer): + class Meta: + model = get_user_model() + fields = ['email', 'name', 'phone_number', 'is_ngo', 'date_joined', 'notify_token'] + + +class CustomUserSerializer(serializers.ModelSerializer): + user = BaseUserSerializer(read_only=True) + email = serializers.EmailField(write_only=True) + name = serializers.CharField(write_only=True) + phone_number = serializers.CharField(write_only=True) + password = serializers.CharField(write_only=True) + + class Meta: + model = CustomUser + fields = ['user', 'email', 'name', 'phone_number', + 'password', 'level', 'coins', 'no_reports', + 'applied_campaigns'] + + def create(self, validated_data): + BaseUser = get_user_model() + base_user = BaseUser.objects.create_user( + email=validated_data.pop('email'), + name=validated_data.pop('name'), + phone_number=validated_data.pop('phone_number'), + password=validated_data.pop('password'), + ) + custom_user = CustomUser.objects.create(user=base_user, **validated_data) + return custom_user + + +class NgoUserSerializer(serializers.ModelSerializer): + user = BaseUserSerializer(read_only=True) + email = serializers.EmailField(write_only=True) + name = serializers.CharField(write_only=True) + phone_number = serializers.CharField(write_only=True) + password = serializers.CharField(write_only=True) + + class Meta: + model = NgoUser + fields = ['user', 'email', 'name', 'phone_number', + 'password', 'emergency_contact_number', + 'animals_supported', 'website', 'address', + 'latitude', 'longitude', 'no_received_reports', + 'created_campaigns_id'] + + def create(self, validated_data): + BaseUser = get_user_model() + base_user = BaseUser.objects.create_user( + email=validated_data.pop('email'), + name=validated_data.pop('name'), + phone_number=validated_data.pop('phone_number'), + password=validated_data.pop('password'), + is_ngo=True, + ) + ngo_user = NgoUser.objects.create(user=base_user, **validated_data) + return ngo_user \ No newline at end of file diff --git a/aniresfr/tests.py b/aniresfr/tests.py new file mode 100644 index 0000000..eef1ed5 --- /dev/null +++ b/aniresfr/tests.py @@ -0,0 +1,42 @@ +from django.test import TestCase, Client +from django.urls import reverse +from .models import Animal, BaseUser, NgoUser + +class AnimalViewTest(TestCase): + def setUp(self): + self.client = Client() + self.user = BaseUser.objects.create_user(email='testuser', password='testpass', name = 'testuser', phone_number = '1234567890') + self.ngo_user = NgoUser.objects.create(user=self.user, latitude=22.0, longitude=89.0) + self.user2 = BaseUser.objects.create_user(email='testuser2', password='testpass2', name = 'testuser2', phone_number = '1234567890') + self.ngo_user2 = NgoUser.objects.create(user=self.user2, latitude=22.0, longitude=87.5) + self.animal_data = { + 'user_name': 'testuser', + 'user_email': 'testuser@example.com', + 'user_phone': '1234567890', + 'animal_type': 'Dog', + 'numberOfAnimals': 1, + 'description': 'Test description', + 'condition': 'Healthy', + 'latitude': 22.0, + 'longitude': 88.0, + 'address': 'Test address', + 'landmark': 'Test landmark', + 'status': 'Reported', + 'image': 'Testimage.wq', + 'reported_time': '2022-01-01T00:00:00Z'} + + def test_create_animal(self): + response = self.client.post(reverse('animal-list'), self.animal_data, format='json') + print(response.content.decode('utf-8')) + self.assertEqual(response.status_code, 201) + +class NgoViewTest(TestCase): + def setUp(self): + self.client = Client() + self.user = BaseUser.objects.create_user(email='testuser@example.com', password='testpass', name = 'testuser', phone_number = '1234567890') + self.ngo_user = NgoUser.objects.create(user=self.user, latitude=22.0, longitude=89.0) + + def test_get_ngo_user(self): + response = self.client.get(reverse('ngo'), {'email': 'testuser@example.com'}) + print(response.content.decode('utf-8')) + self.assertEqual(response.status_code, 200) \ No newline at end of file diff --git a/aniresfr/views.py b/aniresfr/views.py new file mode 100644 index 0000000..bc20ee4 --- /dev/null +++ b/aniresfr/views.py @@ -0,0 +1,288 @@ +from django.db import IntegrityError +from django.contrib.auth.hashers import make_password +from rest_framework.exceptions import ValidationError +from django.contrib.auth import authenticate, logout +from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.authtoken.models import Token +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status, viewsets +from .serializers import * +from .models import * +from .notify import send_notification +from django.http import JsonResponse +from django.views.decorators.csrf import csrf_exempt +import requests + +@csrf_exempt +def check_ngo(request): + if request.method == 'POST': + id = request.POST.get('id') + if not id: + return JsonResponse({'error': 'ID not provided'}, status=400) + + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0', + 'Accept-Language': 'en,en-US;q=0.5', + 'Referer': 'https://ngodarpan.gov.in/index.php/search/', + 'X-Requested-With': 'XMLHttpRequest', + 'DNT': '1', + 'Connection': 'keep-alive', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'Sec-GPC': '1', + 'Pragma': 'no-cache', + 'Cache-Control': 'no-cache', + } + + headers['Accept'] = 'application/json, text/javascript, */*; q=0.01' + headers['Cookie'] = '' + response = requests.get('https://ngodarpan.gov.in/index.php/ajaxcontroller/get_csrf', headers=headers) + q = response.json() + csrf = q["csrf_token"] + + headers['Accept'] = '*/*' + headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8' + headers['Origin'] = 'https://ngodarpan.gov.in' + headers['Cookie'] = 'ci_session=; ngd_csrf_cookie_name=' + csrf + + data = { + 'state_search': '', + 'district_search': '', + 'sector_search': 'null', + 'ngo_type_search': 'null', + 'ngo_name_search': '', + 'unique_id_search': id, + 'view_type': 'detail_view', + 'csrf_test_name': csrf, + } + + response = requests.post('https://ngodarpan.gov.in/index.php/ajaxcontroller/search_index_new/0', headers=headers, data=data) + + # Check if an NGO is found + ngo_exists = '