Add document requests app

This commit is contained in:
Keannu Bernasol 2024-11-24 02:20:18 +08:00
parent bb9fcc3d7c
commit ba19412d31
23 changed files with 484 additions and 53 deletions

View file

@ -18,3 +18,6 @@ EMAIL_ADDRESS = 'noreply.dev@mehdns.06222001.xyz'
# Admin Credentials
ADMIN_EMAIL = 'admin@test.com'
ADMIN_PASSWORD = ''
# To insert test data or not
TEST_DATA = "True"

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.3 on 2024-11-23 13:04
# Generated by Django 5.1.3 on 2024-11-23 17:01
import django.contrib.auth.models
import django.contrib.auth.validators
@ -69,12 +69,6 @@ class Migration(migrations.Migration):
blank=True, max_length=150, verbose_name="last name"
),
),
(
"email",
models.EmailField(
blank=True, max_length=254, verbose_name="email address"
),
),
(
"is_staff",
models.BooleanField(
@ -91,6 +85,7 @@ class Migration(migrations.Migration):
verbose_name="active",
),
),
("email", models.EmailField(max_length=254, unique=True)),
(
"role",
models.CharField(

View file

@ -1,18 +0,0 @@
# Generated by Django 5.1.3 on 2024-11-23 13:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="customuser",
name="email",
field=models.EmailField(max_length=254, unique=True),
),
]

View file

@ -32,8 +32,4 @@ class CustomUser(AbstractUser):
def save(self, **kwargs):
self.username = self.email
if self.is_staff:
self.role = "staff"
elif self.is_superuser:
self.role = "admin"
super().save(**kwargs)

View file

@ -0,0 +1,22 @@
from rest_framework.permissions import BasePermission
class IsStaff(BasePermission):
"""
Allows access only to users with staff role
"""
def has_permission(self, request, view):
return bool(
request.user and request.user.role in ("head", "admin", "planning", "staff")
)
class IsHead(BasePermission):
"""
Allows access only to users with staff role
"""
def has_permission(self, request, view):
print(request.user.role)
return bool(request.user and request.user.role == "head")

View file

@ -12,6 +12,7 @@ from drf_spectacular.views import (
urlpatterns = [
path("accounts/", include("accounts.urls")),
path("documents/", include("documents.urls")),
path("requests/", include("document_requests.urls")),
path("admin/", admin.site.urls),
path("schema/", SpectacularAPIView.as_view(), name="schema"),
path(

View file

@ -45,7 +45,30 @@ SECRET_KEY = get_secret("SECRET_KEY")
# SECURITY WARNING: don"t run with debug turned on in production!
DEBUG = get_secret("DEBUG")
# URL Prefixes
USE_HTTPS = get_secret("USE_HTTPS")
URL_SCHEME = "https" if USE_HTTPS else "http"
# Building Backend URL
BACKEND_ADDRESS = get_secret("BACKEND_ADDRESS")
BACKEND_PORT = get_secret("BACKEND_PORT")
# Building Frontend URL
FRONTEND_ADDRESS = get_secret("FRONTEND_ADDRESS")
FRONTEND_PORT = get_secret("FRONTEND_PORT")
# Full URLs
BACKEND_URL = f"{URL_SCHEME}://{BACKEND_ADDRESS}"
FRONTEND_URL = f"{URL_SCHEME}://{BACKEND_ADDRESS}"
# Append port to full URLs if deployed locally
if not USE_HTTPS:
BACKEND_URL += f":{BACKEND_PORT}"
FRONTEND_URL += f":{FRONTEND_PORT}"
ALLOWED_HOSTS = ["*"]
CSRF_TRUSTED_ORIGINS = [
FRONTEND_URL,
BACKEND_URL,
# You can also set up https://*.name.xyz for wildcards here
]
# Application definition
@ -67,12 +90,14 @@ INSTALLED_APPS = [
"drf_spectacular_sidecar",
"accounts",
"documents",
"document_requests",
"django_cleanup.apps.CleanupConfig",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
@ -163,7 +188,7 @@ USE_TZ = True
# https://docs.djangoproject.com/en/5.1/howto/static-files/
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
MEDIA_URL = "api/v1/media/"
MEDIA_URL = f"{BACKEND_URL}/api/v1/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
ROOT_URLCONF = "config.urls"
STATIC_URL = "static/"

View file

@ -0,0 +1,26 @@
from django.contrib import admin
from unfold.admin import ModelAdmin
from .models import DocumentRequestUnit, DocumentRequest
from unfold.contrib.filters.admin import RangeDateFilter
# Register your models here.
@admin.register(DocumentRequestUnit)
class DocumentRequestUnitAdmin(ModelAdmin):
search_fields = ["id"]
list_display = ["id", "get_document_title", "copies"]
def get_document_title(self, obj):
return obj.documents.title # Assuming the Document model has a 'title' field
get_document_title.short_description = "Document"
@admin.register(DocumentRequest)
class DocumentRequestAdmin(ModelAdmin):
list_filter = [
("date_requested", RangeDateFilter),
]
list_display = ["id", "date_requested", "status", "college"]

View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class DocumentRequestsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "document_requests"

View file

@ -0,0 +1,94 @@
# Generated by Django 5.1.3 on 2024-11-23 17:01
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 = [
("documents", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="DocumentRequest",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"date_requested",
models.DateTimeField(
default=django.utils.timezone.now, editable=False
),
),
("college", models.CharField(max_length=64)),
("purpose", models.TextField(max_length=512)),
(
"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,
),
),
],
),
migrations.CreateModel(
name="DocumentRequestUnit",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("copies", models.IntegerField(default=1)),
(
"document",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="documents.document",
),
),
(
"document_request",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="document_requests.documentrequest",
),
),
],
),
migrations.AddField(
model_name="documentrequest",
name="documents",
field=models.ManyToManyField(to="document_requests.documentrequestunit"),
),
]

View file

@ -0,0 +1,28 @@
from django.db import models
from django.utils.timezone import now
class DocumentRequestUnit(models.Model):
document_request = models.ForeignKey(
"document_requests.DocumentRequest", on_delete=models.CASCADE
)
document = models.ForeignKey("documents.Document", on_delete=models.CASCADE)
copies = models.IntegerField(default=1, null=False, blank=False)
class DocumentRequest(models.Model):
requester = models.ForeignKey("accounts.CustomUser", on_delete=models.CASCADE)
documents = models.ManyToManyField("document_requests.DocumentRequestUnit")
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"),
)
status = models.CharField(max_length=32, choices=STATUS_CHOICES, default="pending")
# TODO: Add request type (Softcopy/Hardcopy)

View file

@ -0,0 +1,119 @@
from rest_framework import serializers
from documents.models import Document
from documents.serializers import DocumentSerializer, DocumentFileSerializer
from accounts.models import CustomUser
from .models import DocumentRequest, DocumentRequestUnit
class DocumentRequestUnitCreationSerializer(serializers.ModelSerializer):
document = serializers.SlugRelatedField(
many=False, slug_field="id", queryset=Document.objects.all(), required=True
)
class Meta:
model = DocumentRequestUnit
fields = ["document", "copies"]
class DocumentRequestCreationSerializer(serializers.ModelSerializer):
requester = serializers.SlugRelatedField(
many=False, slug_field="id", queryset=CustomUser.objects.all(), required=False
)
documents = DocumentRequestUnitCreationSerializer(many=True, required=True)
college = serializers.CharField(allow_blank=False)
purpose = serializers.CharField(max_length=512, allow_blank=False)
class Meta:
model = DocumentRequest
fields = ["requester", "college", "purpose", "documents"]
def create(self, validated_data):
user = self.context["request"].user
documents_data = validated_data.pop("documents")
# Set requester to user who sent HTTP request to prevent spoofing
validated_data["requester"] = user
DOCUMENT_REQUEST = DocumentRequest.objects.create(**validated_data)
DOCUMENT_REQUEST_UNITS = []
for DOCUMENT_REQUEST_UNIT in documents_data:
DOCUMENT_REQUEST_UNIT = DocumentRequestUnit.objects.create(
document_request=DOCUMENT_REQUEST,
document=DOCUMENT_REQUEST_UNIT["document"],
copies=DOCUMENT_REQUEST_UNIT["copies"],
)
DOCUMENT_REQUEST_UNITS.append(DOCUMENT_REQUEST_UNIT)
DOCUMENT_REQUEST.documents.set(DOCUMENT_REQUEST_UNITS)
DOCUMENT_REQUEST.save()
return DOCUMENT_REQUEST
class DocumentRequestUnitSerializer(serializers.ModelSerializer):
document = DocumentSerializer(many=False)
class Meta:
model = DocumentRequestUnit
fields = ["document", "copies"]
read_only_fields = ["document", "copies"]
class DocumentRequestUnitWithFileSerializer(serializers.ModelSerializer):
document = DocumentFileSerializer(many=False)
class Meta:
model = DocumentRequestUnit
fields = ["document", "copies"]
read_only_fields = ["document", "copies"]
class DocumentRequestSerializer(serializers.ModelSerializer):
documents = serializers.SerializerMethodField()
college = serializers.CharField(allow_blank=False)
purpose = serializers.CharField(max_length=512, allow_blank=False)
class Meta:
model = DocumentRequest
fields = ["id", "requester", "college",
"purpose", "documents", "status"]
read_only_fields = [
"id",
"requester",
"college",
"purpose",
"documents",
"status",
]
def get_documents(self, obj):
if obj.status != "approved":
serializer_class = DocumentRequestUnitSerializer
else:
serializer_class = DocumentRequestUnitWithFileSerializer
return serializer_class(obj.documents, many=True).data
class DocumentRequestUpdateSerializer(serializers.ModelSerializer):
status = serializers.ChoiceField(
choices=DocumentRequest.STATUS_CHOICES, required=True
)
class Meta:
model = DocumentRequest
fields = ["id", "status"]
read_only_fields = ["id", "status"]
def update(self, instance, validated_data):
if instance.status == "denied":
raise serializers.ValidationError(
{
"error": "Denied requests cannot be updated. It is advised you create a new request and approve it from there"
}
)
elif validated_data["status"] == instance.status:
raise serializers.ValidationError(
{"error": "Request form status provided is the same as current status"}
)
return super().update(instance, validated_data)

View file

@ -0,0 +1,12 @@
from django.urls import path, include
from .views import (
DocumentRequestCreateView,
DocumentRequestListView,
DocumentRequestUpdateView,
)
urlpatterns = [
path("create/", DocumentRequestCreateView.as_view()),
path("list/", DocumentRequestListView.as_view()),
path("update/<int:pk>/", DocumentRequestUpdateView.as_view()),
]

View file

@ -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
from rest_framework.pagination import PageNumberPagination
from .serializers import (
DocumentRequestCreationSerializer,
DocumentRequestSerializer,
DocumentRequestUpdateSerializer,
)
from .models import DocumentRequest
class DocumentRequestCreateView(generics.CreateAPIView):
"""
Used by clients to create document requests. Requires passing in request information in addition to the documents themselves
"""
http_method_names = ["post"]
serializer_class = DocumentRequestCreationSerializer
permission_classes = [IsAuthenticated]
class DocumentRequestListView(generics.ListAPIView):
"""
Returns document requests. If document requests are approved, also returns the link to download the document.
Staff are able to view all document requests here. Clients are only able to view their own requests.
"""
http_method_names = ["get"]
serializer_class = DocumentRequestSerializer
pagination_class = PageNumberPagination
permission_classes = [IsAuthenticated]
def get_queryset(self):
user = self.request.user
if user.role == "client":
queryset = DocumentRequest.objects.filter(requester=user)
else:
queryset = DocumentRequest.objects.all()
return queryset
class DocumentRequestUpdateView(generics.UpdateAPIView):
"""
Used by head approve or deny document requests.
"""
http_method_names = ["patch"]
serializer_class = DocumentRequestUpdateSerializer
permission_classes = [IsAuthenticated, IsHead]
queryset = DocumentRequest.objects.all()

View file

@ -1,6 +1,7 @@
# Generated by Django 5.1.3 on 2024-11-23 14:13
# Generated by Django 5.1.3 on 2024-11-23 17:01
import django.utils.timezone
import documents.models
from django.db import migrations, models
@ -38,7 +39,11 @@ class Migration(migrations.Migration):
max_length=32,
),
),
("file", models.FileField(upload_to="documents/")),
("number_pages", models.IntegerField()),
(
"file",
models.FileField(upload_to=documents.models.Document.upload_to),
),
(
"date_uploaded",
models.DateTimeField(

View file

@ -17,14 +17,15 @@ class Document(models.Model):
document_type = models.CharField(
max_length=32, choices=DOCUMENT_TYPE_CHOICES, null=False, blank=False
)
number_pages = models.IntegerField(null=False, blank=False)
def upload_to(instance, filename):
_, extension = filename.split(".")
return "documents/%s_%s.%s" % (now, str(uuid.uuid4()), extension)
return "documents/%s_%s.%s" % (now(), str(uuid.uuid4()), extension)
file = models.FileField(upload_to=upload_to)
date_uploaded = models.DateTimeField(default=now, editable=False)
def __str__(self):
return self.name
return f"{self.name} ({self.document_type})"

View file

@ -1,10 +0,0 @@
from rest_framework.permissions import BasePermission
class IsStaff(BasePermission):
"""
Allows access only to users with staff role
"""
def has_permission(self, request, view):
return bool(request.user and request.user.role == "staff")

View file

@ -1,4 +1,5 @@
from rest_framework import serializers
from config import settings
from .models import Document
@ -11,7 +12,14 @@ class DocumentUploadSerializer(serializers.ModelSerializer):
class Meta:
model = Document
fields = ["id", "name", "file", "document_type", "date_uploaded"]
fields = [
"id",
"name",
"file",
"document_type",
"number_pages",
"date_uploaded",
]
read_only_fields = ["id", "date-uploaded"]
@ -29,5 +37,38 @@ class DocumentSerializer(serializers.ModelSerializer):
class Meta:
model = Document
fields = ["id", "name", "document_type", "date_uploaded"]
read_only_fields = ["id", "name", "document_type", "date_uploaded"]
fields = ["id", "name", "document_type", "number_pages", "date_uploaded"]
read_only_fields = [
"id",
"name",
"document_type",
"number_pages",
"date_uploaded",
]
class DocumentFileSerializer(serializers.ModelSerializer):
# Read-only serializer which includes the actual link to the file
date_uploaded = serializers.DateTimeField(
format="%m-%d-%Y %I:%M %p", read_only=True
)
file = serializers.FileField()
class Meta:
model = Document
fields = [
"id",
"name",
"document_type",
"file",
"number_pages",
"date_uploaded",
]
read_only_fields = [
"id",
"name",
"document_type",
"number_pages",
"date_uploaded",
"file",
]

View file

@ -1,8 +1,14 @@
from django.urls import include, path
from .views import DocumentUploadView, DocumentDeleteView, DocumentListView
from .views import (
DocumentUploadView,
DocumentDeleteView,
DocumentListView,
DocumentStaffListView,
)
urlpatterns = [
path("upload/", DocumentUploadView.as_view()),
path("delete/<int:pk>/", DocumentDeleteView.as_view()),
path("list/", DocumentListView.as_view()),
path("list/staff/", DocumentStaffListView.as_view()),
]

View file

@ -1,31 +1,56 @@
from rest_framework import generics
from .serializers import (
DocumentSerializer,
DocumentFileSerializer,
DocumentUploadSerializer,
DocumentDeleteSerializer,
)
from .permissions import IsStaff
from .models import Document
from rest_framework.permissions import IsAuthenticated
from rest_framework.pagination import PageNumberPagination
from accounts.permissions import IsStaff
from .models import Document
class DocumentUploadView(generics.CreateAPIView):
"""
Used by staff to upload documents.
"""
http_method_names = ["post"]
serializer_class = DocumentUploadSerializer
# permission_classes = [IsAuthenticated, IsStaff]
permission_classes = [IsAuthenticated, IsStaff]
class DocumentDeleteView(generics.DestroyAPIView):
"""
Used by staff to delete documents. Accepts the document id as a URL parameter
"""
http_method_names = ["delete"]
serializer_class = DocumentDeleteSerializer
queryset = Document.objects.all()
# permission_classes = [IsAuthenticated, IsStaff]
permission_classes = [IsAuthenticated, IsStaff]
class DocumentListView(generics.ListAPIView):
"""
Used by clients to view documents. Does not include actual download links to documents
"""
http_method_names = ["get"]
serializer_class = DocumentSerializer
queryset = Document.objects.all()
pagination_class = PageNumberPagination
# permission_classes = [IsAuthenticated]
permission_classes = [IsAuthenticated]
class DocumentStaffListView(generics.ListAPIView):
"""
Used by staff to view documents. Includes actual download links to documents
"""
http_method_names = ["get"]
serializer_class = DocumentFileSerializer
queryset = Document.objects.all()
pagination_class = PageNumberPagination
permission_classes = [IsAuthenticated, IsStaff]

View file

@ -9,6 +9,7 @@ colorama==0.4.6
cryptography==43.0.3
defusedxml==0.8.0rc2
Django==5.1.3
django-cleanup==9.0.0
django-cors-headers==4.6.0
django-rest-framework==0.1.0
django-unfold==0.41.0