From 9529560fed319fcdc34cd2f542ac9852564c8d3c Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Thu, 9 Jan 2025 02:10:42 +0800 Subject: [PATCH] Add authorization requests --- docmanager_backend/api/urls.py | 1 + .../authorization_requests/__init__.py | 0 .../authorization_requests/admin.py | 11 ++ .../authorization_requests/apps.py | 6 + .../migrations/0001_initial.py | 61 ++++++++++ .../migrations/__init__.py | 0 .../authorization_requests/models.py | 22 ++++ .../authorization_requests/serializers.py | 109 ++++++++++++++++++ .../authorization_requests/urls.py | 12 ++ .../authorization_requests/views.py | 53 +++++++++ docmanager_backend/config/settings.py | 1 + .../document_requests/serializers.py | 4 + 12 files changed, 280 insertions(+) create mode 100644 docmanager_backend/authorization_requests/__init__.py create mode 100644 docmanager_backend/authorization_requests/admin.py create mode 100644 docmanager_backend/authorization_requests/apps.py create mode 100644 docmanager_backend/authorization_requests/migrations/0001_initial.py create mode 100644 docmanager_backend/authorization_requests/migrations/__init__.py create mode 100644 docmanager_backend/authorization_requests/models.py create mode 100644 docmanager_backend/authorization_requests/serializers.py create mode 100644 docmanager_backend/authorization_requests/urls.py create mode 100644 docmanager_backend/authorization_requests/views.py diff --git a/docmanager_backend/api/urls.py b/docmanager_backend/api/urls.py index ace4bed..9f68925 100644 --- a/docmanager_backend/api/urls.py +++ b/docmanager_backend/api/urls.py @@ -13,6 +13,7 @@ urlpatterns = [ path("accounts/", include("accounts.urls")), path("documents/", include("documents.urls")), path("requests/", include("document_requests.urls")), + path("authorization_requests/", include("authorization_requests.urls")), path("questionnaires/", include("questionnaires.urls")), path("admin/", admin.site.urls), path("schema/", SpectacularAPIView.as_view(), name="schema"), diff --git a/docmanager_backend/authorization_requests/__init__.py b/docmanager_backend/authorization_requests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docmanager_backend/authorization_requests/admin.py b/docmanager_backend/authorization_requests/admin.py new file mode 100644 index 0000000..bee255a --- /dev/null +++ b/docmanager_backend/authorization_requests/admin.py @@ -0,0 +1,11 @@ +from django.contrib import admin +from unfold.admin import ModelAdmin +from .models import AuthorizationRequest + +# Register your models here. + + +@admin.register(AuthorizationRequest) +class AuthorizationRequestAdmin(ModelAdmin): + search_fields = ["id"] + list_display = ["id", "date_requested", "status", "college"] diff --git a/docmanager_backend/authorization_requests/apps.py b/docmanager_backend/authorization_requests/apps.py new file mode 100644 index 0000000..25197db --- /dev/null +++ b/docmanager_backend/authorization_requests/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AuthorizationRequestsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "authorization_requests" diff --git a/docmanager_backend/authorization_requests/migrations/0001_initial.py b/docmanager_backend/authorization_requests/migrations/0001_initial.py new file mode 100644 index 0000000..90b6b32 --- /dev/null +++ b/docmanager_backend/authorization_requests/migrations/0001_initial.py @@ -0,0 +1,61 @@ +# Generated by Django 5.1.3 on 2025-01-08 16:56 + +import django.db.models.deletion +import django.utils.timezone +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="AuthorizationRequest", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("documents", models.TextField(max_length=2048)), + ( + "date_requested", + models.DateTimeField( + default=django.utils.timezone.now, editable=False + ), + ), + ("college", models.CharField(max_length=64)), + ("purpose", models.TextField(max_length=512)), + ("remarks", models.TextField(blank=True, max_length=512, null=True)), + ( + "status", + models.CharField( + choices=[ + ("pending", "Pending"), + ("approved", "Approved"), + ("denied", "Denied"), + ], + default="pending", + max_length=32, + ), + ), + ( + "requester", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/docmanager_backend/authorization_requests/migrations/__init__.py b/docmanager_backend/authorization_requests/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docmanager_backend/authorization_requests/models.py b/docmanager_backend/authorization_requests/models.py new file mode 100644 index 0000000..9389641 --- /dev/null +++ b/docmanager_backend/authorization_requests/models.py @@ -0,0 +1,22 @@ +from django.db import models +from django.utils.timezone import now + + +class AuthorizationRequest(models.Model): + requester = models.ForeignKey( + "accounts.CustomUser", on_delete=models.CASCADE) + documents = models.TextField(max_length=2048, blank=False, null=False) + date_requested = models.DateTimeField(default=now, editable=False) + college = models.CharField(max_length=64, blank=False, null=False) + purpose = models.TextField(max_length=512, blank=False, null=False) + + STATUS_CHOICES = ( + ("pending", "Pending"), + ("approved", "Approved"), + ("denied", "Denied"), + ) + + remarks = models.TextField(max_length=512, blank=True, null=True) + + status = models.CharField( + max_length=32, choices=STATUS_CHOICES, default="pending") diff --git a/docmanager_backend/authorization_requests/serializers.py b/docmanager_backend/authorization_requests/serializers.py new file mode 100644 index 0000000..521f5b3 --- /dev/null +++ b/docmanager_backend/authorization_requests/serializers.py @@ -0,0 +1,109 @@ +from rest_framework import serializers +from accounts.models import CustomUser +from emails.templates import RequestUpdateEmail +from .models import AuthorizationRequest + + +class AuthorizationRequestCreationSerializer(serializers.ModelSerializer): + requester = serializers.SlugRelatedField( + many=False, slug_field="id", queryset=CustomUser.objects.all(), required=False + ) + documents = serializers.CharField(max_length=2048, required=True) + college = serializers.CharField(max_length=64) + purpose = serializers.CharField(max_length=512) + + class Meta: + model = AuthorizationRequest + fields = ["requester", "college", "purpose", "documents"] + + def create(self, validated_data): + user = self.context["request"].user + + # Set requester to user who sent HTTP request to prevent spoofing + validated_data["requester"] = user + + return AuthorizationRequest.objects.create(**validated_data) + + +class AuthorizationRequestSerializer(serializers.ModelSerializer): + requester = serializers.SlugRelatedField( + many=False, + slug_field="full_name", + queryset=CustomUser.objects.all(), + ) + date_requested = serializers.DateTimeField( + format="%m-%d-%Y %I:%M %p", read_only=True + ) + + class Meta: + model = AuthorizationRequest + fields = [ + "id", + "requester", + "college", + "purpose", + "date_requested", + "documents", + "remarks", + "status", + ] + read_only_fields = [ + "id", + "requester", + "college", + "purpose", + "date_requested", + "documents", + "remarks," + "status", + ] + + +class AuthorizationRequestUpdateSerializer(serializers.ModelSerializer): + status = serializers.ChoiceField( + choices=AuthorizationRequest.STATUS_CHOICES, required=True + ) + + class Meta: + model = AuthorizationRequest + fields = ["id", "status", "remarks"] + read_only_fields = ["id"] + + def update(self, instance, validated_data): + print(validated_data) + if instance.status == "denied" or instance.status == "approved": + raise serializers.ValidationError( + { + "error": "Already approved/denied requests cannot be updated. You should instead create a new request and approve it from there" + } + ) + elif "status" not in validated_data: + raise serializers.ValidationError( + { + "error": "No status value update provided" + } + ) + elif validated_data["status"] == instance.status: + raise serializers.ValidationError( + {"error": "Request form status provided is the same as current status"} + ) + elif validated_data["status"] == "denied" and "remarks" not in validated_data: + raise serializers.ValidationError( + {"error": "Request denial requires remarks"} + ) + representation = super().update(instance, validated_data) + + # Send an email on request status update + try: + email = RequestUpdateEmail() + email.context = {"request_status": validated_data["status"]} + if validated_data["status"] == "denied": + email.context = {"remarks": validated_data["remarks"]} + else: + email.context = {"remarks": "N/A"} + email.send(to=[instance.requester.email]) + except: + # Silence out errors if email sending fails + pass + + return representation diff --git a/docmanager_backend/authorization_requests/urls.py b/docmanager_backend/authorization_requests/urls.py new file mode 100644 index 0000000..7691179 --- /dev/null +++ b/docmanager_backend/authorization_requests/urls.py @@ -0,0 +1,12 @@ +from django.urls import path, include +from .views import ( + AuthorizationRequestCreateView, + AuthorizationRequestUpdateView, + AuthorizationRequestListView, +) + +urlpatterns = [ + path("create/", AuthorizationRequestCreateView.as_view()), + path("list/", AuthorizationRequestListView.as_view()), + path("update//", AuthorizationRequestUpdateView.as_view()), +] diff --git a/docmanager_backend/authorization_requests/views.py b/docmanager_backend/authorization_requests/views.py new file mode 100644 index 0000000..8dc0573 --- /dev/null +++ b/docmanager_backend/authorization_requests/views.py @@ -0,0 +1,53 @@ +from rest_framework import generics +from rest_framework.permissions import IsAuthenticated +from rest_framework.pagination import PageNumberPagination +from accounts.permissions import IsHead, IsStaff +from rest_framework.pagination import PageNumberPagination +from .serializers import ( + AuthorizationRequestCreationSerializer, + AuthorizationRequestSerializer, + AuthorizationRequestUpdateSerializer +) + +from .models import AuthorizationRequest + + +class AuthorizationRequestCreateView(generics.CreateAPIView): + """ + Used by clients to create authorization requests. Requires passing in request information in addition to the documents themselves + """ + + http_method_names = ["post"] + serializer_class = AuthorizationRequestCreationSerializer + permission_classes = [IsAuthenticated] + + +class AuthorizationRequestListView(generics.ListAPIView): + """ + Returns authorization requests. If authorization requests are approved, also returns the link to download the document. + Staff/Head are able to view all authorization requests here. Clients are only able to view their own requests. + """ + + http_method_names = ["get"] + serializer_class = AuthorizationRequestSerializer + pagination_class = PageNumberPagination + permission_classes = [IsAuthenticated] + + def get_queryset(self): + user = self.request.user + if user.role == "client": + queryset = AuthorizationRequest.objects.filter(requester=user) + else: + queryset = AuthorizationRequest.objects.all() + return queryset + + +class AuthorizationRequestUpdateView(generics.UpdateAPIView): + """ + Used by head approve or deny authorization requests. + """ + + http_method_names = ["patch"] + serializer_class = AuthorizationRequestUpdateSerializer + permission_classes = [IsAuthenticated, IsHead] + queryset = AuthorizationRequest.objects.all() diff --git a/docmanager_backend/config/settings.py b/docmanager_backend/config/settings.py index 2191966..20ef910 100644 --- a/docmanager_backend/config/settings.py +++ b/docmanager_backend/config/settings.py @@ -96,6 +96,7 @@ INSTALLED_APPS = [ "accounts", "documents", "document_requests", + "authorization_requests", "questionnaires", "django_cleanup.apps.CleanupConfig", ] diff --git a/docmanager_backend/document_requests/serializers.py b/docmanager_backend/document_requests/serializers.py index 332a1c4..7374558 100644 --- a/docmanager_backend/document_requests/serializers.py +++ b/docmanager_backend/document_requests/serializers.py @@ -35,6 +35,10 @@ class DocumentRequestCreationSerializer(serializers.ModelSerializer): def create(self, validated_data): user = self.context["request"].user documents_data = validated_data.pop("documents") + if not documents_data: + raise serializers.ValidationError( + {"error": "No documents provided"} + ) # Set requester to user who sent HTTP request to prevent spoofing validated_data["requester"] = user