From b0a9b6b6f0586c3df18d20f0363c1bdebaa4130c Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Sun, 24 Nov 2024 10:34:41 +0800 Subject: [PATCH] Finish up questionnaire app --- docmanager_backend/accounts/permissions.py | 1 - docmanager_backend/accounts/serializers.py | 8 +- docmanager_backend/api/urls.py | 1 + docmanager_backend/config/settings.py | 1 + .../migrations/0002_documentrequest_type.py | 22 ++ .../document_requests/models.py | 7 +- .../document_requests/serializers.py | 42 ++- docmanager_backend/documents/serializers.py | 1 - .../questionnaires/migrations/0001_initial.py | 259 ++++++++++++++++++ docmanager_backend/questionnaires/models.py | 112 +++++++- .../questionnaires/serializers.py | 90 ++++++ docmanager_backend/questionnaires/urls.py | 10 + docmanager_backend/questionnaires/views.py | 29 +- 13 files changed, 558 insertions(+), 25 deletions(-) create mode 100644 docmanager_backend/document_requests/migrations/0002_documentrequest_type.py create mode 100644 docmanager_backend/questionnaires/migrations/0001_initial.py create mode 100644 docmanager_backend/questionnaires/serializers.py create mode 100644 docmanager_backend/questionnaires/urls.py diff --git a/docmanager_backend/accounts/permissions.py b/docmanager_backend/accounts/permissions.py index 75297e1..d426829 100644 --- a/docmanager_backend/accounts/permissions.py +++ b/docmanager_backend/accounts/permissions.py @@ -18,5 +18,4 @@ class IsHead(BasePermission): """ def has_permission(self, request, view): - print(request.user.role) return bool(request.user and request.user.role == "head") diff --git a/docmanager_backend/accounts/serializers.py b/docmanager_backend/accounts/serializers.py index a11592e..06b3883 100644 --- a/docmanager_backend/accounts/serializers.py +++ b/docmanager_backend/accounts/serializers.py @@ -25,12 +25,8 @@ class CustomUserRegistrationSerializer(serializers.ModelSerializer): password = serializers.CharField( write_only=True, style={"input_type": "password", "placeholder": "Password"} ) - first_name = serializers.CharField( - required=True, allow_blank=False, allow_null=False - ) - last_name = serializers.CharField( - required=True, allow_blank=False, allow_null=False - ) + first_name = serializers.CharField(required=True) + last_name = serializers.CharField(required=True) class Meta: model = CustomUser diff --git a/docmanager_backend/api/urls.py b/docmanager_backend/api/urls.py index 888b05d..ace4bed 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("questionnaires/", include("questionnaires.urls")), path("admin/", admin.site.urls), path("schema/", SpectacularAPIView.as_view(), name="schema"), path( diff --git a/docmanager_backend/config/settings.py b/docmanager_backend/config/settings.py index b75ca76..c3ce523 100644 --- a/docmanager_backend/config/settings.py +++ b/docmanager_backend/config/settings.py @@ -92,6 +92,7 @@ INSTALLED_APPS = [ "accounts", "documents", "document_requests", + "questionnaires", "django_cleanup.apps.CleanupConfig", ] diff --git a/docmanager_backend/document_requests/migrations/0002_documentrequest_type.py b/docmanager_backend/document_requests/migrations/0002_documentrequest_type.py new file mode 100644 index 0000000..6ca05b4 --- /dev/null +++ b/docmanager_backend/document_requests/migrations/0002_documentrequest_type.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.3 on 2024-11-24 02:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("document_requests", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="documentrequest", + name="type", + field=models.CharField( + choices=[("softcopy", "Softcopy"), ("hardcopy", "Hardcopy")], + default="softcopy", + max_length=16, + ), + ), + ] diff --git a/docmanager_backend/document_requests/models.py b/docmanager_backend/document_requests/models.py index e848eed..c6e421c 100644 --- a/docmanager_backend/document_requests/models.py +++ b/docmanager_backend/document_requests/models.py @@ -25,4 +25,9 @@ class DocumentRequest(models.Model): status = models.CharField(max_length=32, choices=STATUS_CHOICES, default="pending") - # TODO: Add request type (Softcopy/Hardcopy) + TYPE_CHOICES = ( + ("softcopy", "Softcopy"), + ("hardcopy", "Hardcopy"), + ) + + type = models.CharField(max_length=16, choices=TYPE_CHOICES, default="softcopy") diff --git a/docmanager_backend/document_requests/serializers.py b/docmanager_backend/document_requests/serializers.py index efb5613..396629a 100644 --- a/docmanager_backend/document_requests/serializers.py +++ b/docmanager_backend/document_requests/serializers.py @@ -10,6 +10,7 @@ class DocumentRequestUnitCreationSerializer(serializers.ModelSerializer): document = serializers.SlugRelatedField( many=False, slug_field="id", queryset=Document.objects.all(), required=True ) + copies = serializers.IntegerField(min_value=1) class Meta: model = DocumentRequestUnit @@ -21,12 +22,13 @@ class DocumentRequestCreationSerializer(serializers.ModelSerializer): 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) + college = serializers.CharField(max_length=64) + purpose = serializers.CharField(max_length=512) + type = serializers.ChoiceField(choices=DocumentRequest.TYPE_CHOICES, required=True) class Meta: model = DocumentRequest - fields = ["requester", "college", "purpose", "documents"] + fields = ["requester", "college", "type", "purpose", "documents"] def create(self, validated_data): user = self.context["request"].user @@ -71,18 +73,30 @@ class DocumentRequestUnitWithFileSerializer(serializers.ModelSerializer): class DocumentRequestSerializer(serializers.ModelSerializer): documents = serializers.SerializerMethodField() - college = serializers.CharField(allow_blank=False) - purpose = serializers.CharField(max_length=512, allow_blank=False) + purpose = serializers.CharField(max_length=512) + date_requested = serializers.DateTimeField( + format="%m-%d-%Y %I:%M %p", read_only=True + ) class Meta: model = DocumentRequest - fields = ["id", "requester", "college", - "purpose", "documents", "status"] + fields = [ + "id", + "requester", + "college", + "type", + "purpose", + "date_requested", + "documents", + "status", + ] read_only_fields = [ "id", "requester", "college", + "type", "purpose", + "date_requested", "documents", "status", ] @@ -109,7 +123,7 @@ class DocumentRequestUpdateSerializer(serializers.ModelSerializer): 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" + "error": "Denied requests cannot be updated. You should instead create a new request and approve it from there" } ) elif validated_data["status"] == instance.status: @@ -120,10 +134,12 @@ class DocumentRequestUpdateSerializer(serializers.ModelSerializer): representation = super().update(instance, validated_data) # Send an email on request status update - email = RequestUpdateEmail() - email.context = { - "request_status": instance.status - } - email.send(to=[instance.requester.email]) + try: + email = RequestUpdateEmail() + email.context = {"request_status": instance.status} + email.send(to=[instance.requester.email]) + except: + # Silence out errors if email sending fails + pass return representation diff --git a/docmanager_backend/documents/serializers.py b/docmanager_backend/documents/serializers.py index 492f038..80a3594 100644 --- a/docmanager_backend/documents/serializers.py +++ b/docmanager_backend/documents/serializers.py @@ -4,7 +4,6 @@ from .models import Document class DocumentUploadSerializer(serializers.ModelSerializer): # For staff document uploads - file = serializers.FileField() date_uploaded = serializers.DateTimeField( format="%m-%d-%Y %I:%M %p", read_only=True ) diff --git a/docmanager_backend/questionnaires/migrations/0001_initial.py b/docmanager_backend/questionnaires/migrations/0001_initial.py new file mode 100644 index 0000000..949b1b1 --- /dev/null +++ b/docmanager_backend/questionnaires/migrations/0001_initial.py @@ -0,0 +1,259 @@ +# Generated by Django 5.1.3 on 2024-11-24 02:27 + +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="Questionnaire", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "client_type", + models.CharField( + choices=[ + ("citizen", "Citizen"), + ("business", "Business"), + ("government", "Government (Employee or Another Agency)"), + ], + max_length=32, + ), + ), + ( + "date_submitted", + models.DateTimeField( + default=django.utils.timezone.now, editable=False + ), + ), + ( + "sex", + models.CharField( + choices=[("male", "Male"), ("female", "Female")], max_length=16 + ), + ), + ("age", models.IntegerField()), + ("region_of_residence", models.CharField(max_length=64)), + ("service_availed", models.CharField(max_length=64)), + ( + "i_am_a", + models.CharField( + choices=[ + ("faculty", "Faculty"), + ("non-teaching staff", "Non-Teaching Staff"), + ("student", "Student"), + ("guardian", "Guardian/Parent of Student"), + ("alumna", "Alumna"), + ("other", "Other"), + ], + max_length=32, + ), + ), + ( + "i_am_a_other", + models.CharField(blank=True, max_length=64, null=True), + ), + ( + "q1_answer", + models.CharField( + choices=[ + ("1", "I know what a CC is and I saw this office's CC"), + ( + "2", + "I know what a CC is but I did NOT see this office's CC", + ), + ( + "3", + "I learned of the CC only when I saw this office's CC", + ), + ( + "4", + "I do not know what a CC is and I did not see one in this office", + ), + ], + max_length=64, + ), + ), + ( + "q2_answer", + models.CharField( + choices=[ + ("1", "Easy to see"), + ("2", "Somewhat easy to see"), + ("3", "Difficult to see"), + ("4", "Not visible at all"), + ("5", "N/A"), + ], + max_length=64, + ), + ), + ( + "q3_answer", + models.CharField( + choices=[ + ("1", "Helped very much"), + ("2", "Somewhat helped"), + ("3", "Did not help"), + ("4", "N/A"), + ], + max_length=64, + ), + ), + ( + "sqd0_answer", + models.CharField( + choices=[ + ("1", "Strongly disagree"), + ("2", "Disagree"), + ("3", "Neither Agree nor Disagree"), + ("4", "Agree"), + ("5", "Strongly Agree"), + ("6", "N/A"), + ], + max_length=16, + ), + ), + ( + "sqd1_answer", + models.CharField( + choices=[ + ("1", "Strongly disagree"), + ("2", "Disagree"), + ("3", "Neither Agree nor Disagree"), + ("4", "Agree"), + ("5", "Strongly Agree"), + ("6", "N/A"), + ], + max_length=16, + ), + ), + ( + "sqd2_answer", + models.CharField( + choices=[ + ("1", "Strongly disagree"), + ("2", "Disagree"), + ("3", "Neither Agree nor Disagree"), + ("4", "Agree"), + ("5", "Strongly Agree"), + ("6", "N/A"), + ], + max_length=16, + ), + ), + ( + "sqd3_answer", + models.CharField( + choices=[ + ("1", "Strongly disagree"), + ("2", "Disagree"), + ("3", "Neither Agree nor Disagree"), + ("4", "Agree"), + ("5", "Strongly Agree"), + ("6", "N/A"), + ], + max_length=16, + ), + ), + ( + "sqd4_answer", + models.CharField( + choices=[ + ("1", "Strongly disagree"), + ("2", "Disagree"), + ("3", "Neither Agree nor Disagree"), + ("4", "Agree"), + ("5", "Strongly Agree"), + ("6", "N/A"), + ], + max_length=16, + ), + ), + ( + "sqd5_answer", + models.CharField( + choices=[ + ("1", "Strongly disagree"), + ("2", "Disagree"), + ("3", "Neither Agree nor Disagree"), + ("4", "Agree"), + ("5", "Strongly Agree"), + ("6", "N/A"), + ], + max_length=16, + ), + ), + ( + "sqd6_answer", + models.CharField( + choices=[ + ("1", "Strongly disagree"), + ("2", "Disagree"), + ("3", "Neither Agree nor Disagree"), + ("4", "Agree"), + ("5", "Strongly Agree"), + ("6", "N/A"), + ], + max_length=16, + ), + ), + ( + "sqd7_answer", + models.CharField( + choices=[ + ("1", "Strongly disagree"), + ("2", "Disagree"), + ("3", "Neither Agree nor Disagree"), + ("4", "Agree"), + ("5", "Strongly Agree"), + ("6", "N/A"), + ], + max_length=16, + ), + ), + ( + "sqd8_answer", + models.CharField( + choices=[ + ("1", "Strongly disagree"), + ("2", "Disagree"), + ("3", "Neither Agree nor Disagree"), + ("4", "Agree"), + ("5", "Strongly Agree"), + ("6", "N/A"), + ], + max_length=16, + ), + ), + ( + "extra_suggestions", + models.TextField(blank=True, max_length=512, null=True), + ), + ( + "client", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/docmanager_backend/questionnaires/models.py b/docmanager_backend/questionnaires/models.py index 71a8362..00b94cc 100644 --- a/docmanager_backend/questionnaires/models.py +++ b/docmanager_backend/questionnaires/models.py @@ -1,3 +1,113 @@ from django.db import models +from django.utils.timezone import now -# Create your models here. + +class Questionnaire(models.Model): + # Personal Information + # Email address is derived from the user and is no longer optional + client = models.ForeignKey("accounts.CustomUser", on_delete=models.CASCADE) + CLIENT_TYPE_CHOICES = ( + ("citizen", "Citizen"), + ("business", "Business"), + ("government", "Government (Employee or Another Agency)"), + ) + client_type = models.CharField( + max_length=32, choices=CLIENT_TYPE_CHOICES, null=False, blank=False + ) + + date_submitted = models.DateTimeField(default=now, editable=False) + SEX_CHOICES = ( + ("male", "Male"), + ("female", "Female"), + ) + sex = models.CharField( + max_length=16, choices=SEX_CHOICES, null=False, blank=False) + age = models.IntegerField(null=False, blank=False) + region_of_residence = models.CharField( + max_length=64, null=False, blank=False) + service_availed = models.CharField(max_length=64, null=False, blank=False) + I_AM_I_CHOICES = ( + ("faculty", "Faculty"), + ("non-teaching staff", "Non-Teaching Staff"), + ("student", "Student"), + ("guardian", "Guardian/Parent of Student"), + ("alumna", "Alumna"), + ("other", "Other"), + ) + i_am_a = models.CharField( + max_length=32, choices=I_AM_I_CHOICES, null=False, blank=False + ) + # This is filled up if i_am_a=other + i_am_a_other = models.CharField(max_length=64, null=True, blank=True) + + # CC Questions + Q1_CHOICES = ( + ("1", "I know what a CC is and I saw this office's CC"), + ("2", "I know what a CC is but I did NOT see this office's CC"), + ("3", "I learned of the CC only when I saw this office's CC"), + ("4", "I do not know what a CC is and I did not see one in this office"), + ) + q1_answer = models.CharField( + max_length=64, choices=Q1_CHOICES, null=False, blank=False + ) + + Q2_CHOICES = ( + ("1", "Easy to see"), + ("2", "Somewhat easy to see"), + ("3", "Difficult to see"), + ("4", "Not visible at all"), + ("5", "N/A"), + ) + q2_answer = models.CharField( + max_length=64, choices=Q2_CHOICES, null=False, blank=False + ) + + Q3_CHOICES = ( + ("1", "Helped very much"), + ("2", "Somewhat helped"), + ("3", "Did not help"), + ("4", "N/A"), + ) + q3_answer = models.CharField( + max_length=64, choices=Q3_CHOICES, null=False, blank=False + ) + + # SQD Questions + SQD_CHOICES = ( + ("1", "Strongly disagree"), + ("2", "Disagree"), + ("3", "Neither Agree nor Disagree"), + ("4", "Agree"), + ("5", "Strongly Agree"), + ("6", "N/A"), + ) + + sqd0_answer = models.CharField( + max_length=16, choices=SQD_CHOICES, null=False, blank=False + ) + sqd1_answer = models.CharField( + max_length=16, choices=SQD_CHOICES, null=False, blank=False + ) + sqd2_answer = models.CharField( + max_length=16, choices=SQD_CHOICES, null=False, blank=False + ) + sqd3_answer = models.CharField( + max_length=16, choices=SQD_CHOICES, null=False, blank=False + ) + sqd4_answer = models.CharField( + max_length=16, choices=SQD_CHOICES, null=False, blank=False + ) + sqd5_answer = models.CharField( + max_length=16, choices=SQD_CHOICES, null=False, blank=False + ) + sqd6_answer = models.CharField( + max_length=16, choices=SQD_CHOICES, null=False, blank=False + ) + sqd7_answer = models.CharField( + max_length=16, choices=SQD_CHOICES, null=False, blank=False + ) + sqd8_answer = models.CharField( + max_length=16, choices=SQD_CHOICES, null=False, blank=False + ) + + extra_suggestions = models.TextField(max_length=512, null=True, blank=True) diff --git a/docmanager_backend/questionnaires/serializers.py b/docmanager_backend/questionnaires/serializers.py new file mode 100644 index 0000000..625d254 --- /dev/null +++ b/docmanager_backend/questionnaires/serializers.py @@ -0,0 +1,90 @@ +from rest_framework import serializers +from accounts.models import CustomUser +from .models import Questionnaire + + +class QuestionnaireSerializer(serializers.ModelSerializer): + client = serializers.SlugRelatedField( + many=False, slug_field="id", queryset=CustomUser.objects.all(), required=False + ) + client_type = serializers.ChoiceField( + choices=Questionnaire.CLIENT_TYPE_CHOICES) + + date_submitted = serializers.DateTimeField( + format="%m-%d-%Y %I:%M %p", read_only=True + ) + sex = serializers.ChoiceField(choices=Questionnaire.SEX_CHOICES) + age = serializers.IntegerField(min_value=1) + region_of_residence = serializers.CharField(max_length=64) + service_availed = serializers.CharField(max_length=64) + i_am_a = serializers.ChoiceField(choices=Questionnaire.I_AM_I_CHOICES) + i_am_a_type_other = serializers.CharField(required=False) + q1_answer = serializers.ChoiceField(choices=Questionnaire.Q1_CHOICES) + q2_answer = serializers.ChoiceField(choices=Questionnaire.Q2_CHOICES) + q3_answer = serializers.ChoiceField(choices=Questionnaire.Q3_CHOICES) + sqd0_answer = serializers.ChoiceField(choices=Questionnaire.SQD_CHOICES) + sqd1_answer = serializers.ChoiceField(choices=Questionnaire.SQD_CHOICES) + sqd3_answer = serializers.ChoiceField(choices=Questionnaire.SQD_CHOICES) + sqd4_answer = serializers.ChoiceField(choices=Questionnaire.SQD_CHOICES) + sqd5_answer = serializers.ChoiceField(choices=Questionnaire.SQD_CHOICES) + sqd6_answer = serializers.ChoiceField(choices=Questionnaire.SQD_CHOICES) + sqd7_answer = serializers.ChoiceField(choices=Questionnaire.SQD_CHOICES) + sqd8_answer = serializers.ChoiceField(choices=Questionnaire.SQD_CHOICES) + extra_suggestions = serializers.CharField(max_length=512, required=False) + + def to_representation(self, instance): + representation = super().to_representation(instance) + representation["client"] = instance.client.email + return super().to_representation(instance) + + def create(self, validated_data): + user = self.context["request"].user + # Set questionnaire user to the one who sent the HTTP request to prevent spoofing + validated_data["client"] = user + + if ( + validated_data["client_type"] == "other" + and not validated_data["client_type_other"] + ): + raise serializers.ValidationError( + {"error": "Missing description for client type: Other"} + ) + + # Create the instance without calling super().create() + instance = self.Meta.model(**validated_data) + + # Explicitly set the client_type attribute + instance.client_type = validated_data.get("client_type") + + # Save the instance + instance.save() + return instance + + class Meta: + model = Questionnaire + fields = [ + "id", + "client", + "client_type", + "date_submitted", + "sex", + "age", + "region_of_residence", + "service_availed", + "i_am_a", + "i_am_a_type_other", + "q1_answer", + "q2_answer", + "q3_answer", + "sqd0_answer", + "sqd1_answer", + "sqd2_answer", + "sqd3_answer", + "sqd4_answer", + "sqd5_answer", + "sqd6_answer", + "sqd7_answer", + "sqd8_answer", + "extra_suggestions", + ] + read_only_fields = ["id", "date_submitted"] diff --git a/docmanager_backend/questionnaires/urls.py b/docmanager_backend/questionnaires/urls.py new file mode 100644 index 0000000..b9ff524 --- /dev/null +++ b/docmanager_backend/questionnaires/urls.py @@ -0,0 +1,10 @@ +from django.urls import include, path +from .views import ( + QuestionnaireListAPIView, + QuestionnaireSubmitView, +) + +urlpatterns = [ + path("submit/", QuestionnaireSubmitView.as_view()), + path("list/", QuestionnaireListAPIView.as_view()), +] diff --git a/docmanager_backend/questionnaires/views.py b/docmanager_backend/questionnaires/views.py index 91ea44a..23d8986 100644 --- a/docmanager_backend/questionnaires/views.py +++ b/docmanager_backend/questionnaires/views.py @@ -1,3 +1,28 @@ -from django.shortcuts import render +from rest_framework import generics +from .serializers import QuestionnaireSerializer +from rest_framework.permissions import IsAuthenticated +from .models import Questionnaire +from rest_framework.pagination import PageNumberPagination +from accounts.permissions import IsStaff -# Create your views here. + +class QuestionnaireListAPIView(generics.ListAPIView): + """ + Used by staff to view questionnaires + """ + + http_method_names = ["get"] + serializer_class = QuestionnaireSerializer + queryset = Questionnaire.objects.all() + pagination_class = PageNumberPagination + permission_classes = [IsAuthenticated, IsStaff] + + +class QuestionnaireSubmitView(generics.CreateAPIView): + """ + Used by clients to submit questionnaires + """ + + http_method_names = ["post"] + serializer_class = QuestionnaireSerializer + permission_classes = [IsAuthenticated]