From 9300bdea83e7ba469b6433bad00e67e3e503ead4 Mon Sep 17 00:00:00 2001 From: Parth K <87690475+Parth858@users.noreply.github.com> Date: Fri, 12 Jan 2024 01:00:00 +0530 Subject: [PATCH 1/2] Functionality such as reupload , download and remove documents (media files) added --- apps/jobs/views.py | 187 ++++++++++++++++++++++++++-------- null_jobs_backend/settings.py | 4 +- null_jobs_backend/urls.py | 5 + 3 files changed, 153 insertions(+), 43 deletions(-) diff --git a/apps/jobs/views.py b/apps/jobs/views.py index 4f75cc1..600a09a 100644 --- a/apps/jobs/views.py +++ b/apps/jobs/views.py @@ -1,7 +1,9 @@ import jwt import uuid +import os from re import search import django.core.exceptions +from django.http import FileResponse from django.db.utils import IntegrityError from django_filters.rest_framework import DjangoFilterBackend from rest_framework import status, viewsets @@ -78,7 +80,9 @@ def list(self, request): # get number of applicants if serialized_job_data: - serialized_job_data = JobViewSets.get_number_of_applicants(serialized_job_data) + serialized_job_data = JobViewSets.get_number_of_applicants( + serialized_job_data + ) return response.create_response( serialized_job_data.data, status.HTTP_200_OK @@ -129,7 +133,6 @@ def details(self, request, *args, **kwargs): ) return response.create_response(serialized_job_data.data, status.HTTP_200_OK) - @staticmethod def get_number_of_applicants(serialized_data): """ @@ -153,7 +156,7 @@ def get_number_of_applicants(serialized_data): jobdata.update({"Number of Applicants": number_of_applicants}) return serialized_data - + @staticmethod def get_active_jobs_count(serialized_company_data): """ @@ -162,10 +165,12 @@ def get_active_jobs_count(serialized_company_data): """ for company in serialized_company_data.data: - jobs_belong_to_company = Job.objects.filter(company_id=company.get("company_id")) - active_jobs = sum(1 for job in jobs_belong_to_company if job.is_active) - company.update({"Active Jobs": active_jobs}) - + jobs_belong_to_company = Job.objects.filter( + company_id=company.get("company_id") + ) + active_jobs = sum(1 for job in jobs_belong_to_company if job.is_active) + company.update({"Active Jobs": active_jobs}) + return serialized_company_data @action(detail=True, methods=["get"]) @@ -319,38 +324,38 @@ def featured_jobs(self, request): """ # past 3 weeks datetime specified for featured jobs (can be modified as per use) - past_3_weeks_datetime = datetime_safe.datetime.now(tz=timezone.utc) - timedelta(days=18) + past_3_weeks_datetime = datetime_safe.datetime.now(tz=timezone.utc) - timedelta( + days=18 + ) # get the jobs_data and sort it in DESC order # `-` with column name indicates to return the result in DESC order try: jobs_data = Job.objects.filter( - created_at__gt=past_3_weeks_datetime, - is_active=True + created_at__gt=past_3_weeks_datetime, is_active=True ).order_by("-created_at") jobs_posted_within_3_weeks = JobSerializer(jobs_data, many=True) # find out how many jobs were posted within past_3_weeks - jobs_posted_within_3_weeks = JobViewSets.get_number_of_applicants(jobs_posted_within_3_weeks) + jobs_posted_within_3_weeks = JobViewSets.get_number_of_applicants( + jobs_posted_within_3_weeks + ) # find 5 jobs with maximum number of applicants featured_jobs = sorted( jobs_posted_within_3_weeks.data, - key=lambda k: (k.get('Number of Applicants')), - reverse=True + key=lambda k: (k.get("Number of Applicants")), + reverse=True, ) - return response.create_response( - featured_jobs[0:5], - status.HTTP_200_OK - ) + return response.create_response(featured_jobs[0:5], status.HTTP_200_OK) except Exception: return response.create_response( - response.SOMETHING_WENT_WRONG, - status.HTTP_500_INTERNAL_SERVER_ERROR + response.SOMETHING_WENT_WRONG, status.HTTP_500_INTERNAL_SERVER_ERROR ) + class UserViewSets(viewsets.ModelViewSet): """ User object viewsets @@ -498,16 +503,16 @@ def update(self, request, *args, **kwargs): UserSerializer(user_data).data, status.HTTP_200_OK ) - @action(detail=False, methods=['get']) + @action(detail=False, methods=["get"]) def get_profile_details(self, request): """ API: /api/v1/user/myProfile - Returns user profile data in the response. + Returns user profile data in the response. This method gets the user_id from "access-token". """ - + # Get the value from Access-Token header - if access_token:=request.headers.get(response.ACCESS_TOKEN, None): + if access_token := request.headers.get(response.ACCESS_TOKEN, None): try: # decode JWT Token; for any issues with it, raise an exception decoded_user_access_token = jwt.decode( @@ -516,29 +521,25 @@ def get_profile_details(self, request): user_id = decoded_user_access_token.get(values.USER_ID, None) if not user_id: raise Exception - + # get user data user_data = self.queryset.filter(user_id=user_id) if user_data: serialized_user_data = self.serializer_class(user_data, many=True) return response.create_response( - serialized_user_data.data, - status.HTTP_200_OK + serialized_user_data.data, status.HTTP_200_OK ) else: return response.create_response( - response.USER_DATA_NOT_PRESENT, - status.HTTP_404_NOT_FOUND + response.USER_DATA_NOT_PRESENT, status.HTTP_404_NOT_FOUND ) except Exception: return response.create_response( - response.ACCESS_TOKEN_NOT_VALID, - status.HTTP_400_BAD_REQUEST + response.ACCESS_TOKEN_NOT_VALID, status.HTTP_400_BAD_REQUEST ) else: return response.create_response( - response.ACCESS_TOKEN_NOT_PRESENT, - status.HTTP_401_UNAUTHORIZED + response.ACCESS_TOKEN_NOT_PRESENT, status.HTTP_401_UNAUTHORIZED ) @action(detail=True, methods=["get"]) @@ -597,6 +598,104 @@ def get_application_status(self, serialized_data): return serialized_data + @action(detail=True, methods=["put"]) + def reupload_documents(self, request, pk=None): + """ + API: /api/v1/user/{pk}/reupload-documents + Allows users to re-upload their documents (resume, profile picture, cover letter). + """ + user = User.objects.get(user_id=pk) + if not user: + return response.create_response( + "User does not exist", status.HTTP_404_NOT_FOUND + ) + + # Resume re-upload + resume_data = request.FILES.get("resume") + if resume_data: + user.resume = resume_data + user.save() + + # Profile Picture re-upload + profile_picture_data = request.FILES.get("profile_picture") + if profile_picture_data: + user.profile_picture = profile_picture_data + user.save() + + # Cover Letter re-upload + cover_letter_data = request.FILES.get("cover_letter") + if cover_letter_data: + user.cover_letter = cover_letter_data + user.save() + + return response.create_response( + "Documents re-uploaded successfully", status.HTTP_200_OK + ) + + @action(detail=True, methods=["get"]) + def download_documents(self, request, pk=None): + """ + API: /api/v1/user/{pk}/download-documents + Allows users to download their documents (resume, profile picture, cover letter). + """ + user = User.objects.get(user_id=pk) + if not user: + return response.create_response( + "User does not exist", status.HTTP_404_NOT_FOUND + ) + + document_type = request.query_params.get("document_type", "") + file_path = None + + # Determine the file path based on the requested document type + if document_type == "resume": + file_path = user.resume.path + elif document_type == "profile_picture": + file_path = user.profile_picture.path + elif document_type == "cover_letter": + file_path = user.cover_letter.path + + if not file_path or not os.path.exists(file_path): + return response.create_response( + f"{document_type.capitalize()} not found", status.HTTP_404_NOT_FOUND + ) + + # Serve the file using Django FileResponse + return FileResponse(open(file_path, "rb"), as_attachment=True) + + @action(detail=True, methods=["delete"]) + def remove_documents(self, request, pk=None): + """ + API: /api/v1/user/{pk}/remove-documents + Allows users to remove their documents (resume, profile picture, cover letter). + """ + user = User.objects.get(user_id=pk) + if not user: + return response.create_response( + "User does not exist", status.HTTP_404_NOT_FOUND + ) + + document_type = request.query_params.get("document_type", "") + if not document_type: + return response.create_response( + "Please provide a valid document_type query parameter", + status.HTTP_400_BAD_REQUEST, + ) + + # Remove the document based on the requested document type + if document_type == "resume": + user.resume = None + elif document_type == "profile_picture": + user.profile_picture = None + elif document_type == "cover_letter": + user.cover_letter = None + + user.save() + + return response.create_response( + f"{document_type.capitalize()} removed successfully", status.HTTP_200_OK + ) + class CompanyViewSets(viewsets.ModelViewSet): """ @@ -619,7 +718,7 @@ class CompanyViewSets(viewsets.ModelViewSet): def list(self, request): """ - Method to return a list of companies available, + Method to return a list of companies available, Along with the count of active jobs present in the company """ @@ -630,17 +729,18 @@ def list(self, request): # get number of applicants if serialized_company_data: - serialized_company_data = JobViewSets.get_active_jobs_count(serialized_company_data) + serialized_company_data = JobViewSets.get_active_jobs_count( + serialized_company_data + ) return response.create_response( serialized_company_data.data, status.HTTP_200_OK ) except Exception: return response.create_response( - response.SOMETHING_WENT_WRONG, - status.HTTP_500_INTERNAL_SERVER_ERROR + response.SOMETHING_WENT_WRONG, status.HTTP_500_INTERNAL_SERVER_ERROR ) - + def retrieve(self, request, pk=None): """ retrieve the data of given company id @@ -657,12 +757,15 @@ def retrieve(self, request, pk=None): company_data = Company.objects.filter(company_id=pk) serialized_company_data = self.serializer_class(company_data, many=True) if serialized_company_data: - serialized_company_data = JobViewSets.get_active_jobs_count(serialized_company_data) - return response.create_response(serialized_company_data.data, status.HTTP_200_OK) + serialized_company_data = JobViewSets.get_active_jobs_count( + serialized_company_data + ) + return response.create_response( + serialized_company_data.data, status.HTTP_200_OK + ) except Exception: return response.create_response( - response.SOMETHING_WENT_WRONG, - status.HTTP_500_INTERNAL_SERVER_ERROR + response.SOMETHING_WENT_WRONG, status.HTTP_500_INTERNAL_SERVER_ERROR ) @action(detail=False, methods=["get"]) @@ -751,4 +854,4 @@ def list(self, request, *args, **kwargs): else: return super().list(request, *args, **kwargs) else: - return super().list(request, *args, **kwargs) \ No newline at end of file + return super().list(request, *args, **kwargs) diff --git a/null_jobs_backend/settings.py b/null_jobs_backend/settings.py index fbda99f..88d8343 100644 --- a/null_jobs_backend/settings.py +++ b/null_jobs_backend/settings.py @@ -48,7 +48,7 @@ "rest_framework", "rest_framework.authtoken", "rest_framework_simplejwt", - 'rest_framework_simplejwt.token_blacklist', # used to blacklist the refresh token + "rest_framework_simplejwt.token_blacklist", # used to blacklist the refresh token "drf_yasg", "apps.accounts", "apps.jobs", @@ -242,3 +242,5 @@ # GOOGLE AUTH SITE_ID = 2 +MEDIA_URL = "/media/" +MEDIA_ROOT = os.path.join(BASE_DIR, "media") diff --git a/null_jobs_backend/urls.py b/null_jobs_backend/urls.py index 9672608..05b4e76 100644 --- a/null_jobs_backend/urls.py +++ b/null_jobs_backend/urls.py @@ -19,6 +19,8 @@ from rest_framework import permissions from drf_yasg.views import get_schema_view from drf_yasg import openapi +from django.conf import settings +from django.conf.urls.static import static schema_view = get_schema_view( openapi.Info( @@ -43,3 +45,6 @@ "api/redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc" ), ] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) From bcfbabaec19e8afdf5faf5d0086ed6afc5c811fb Mon Sep 17 00:00:00 2001 From: Parth K <87690475+Parth858@users.noreply.github.com> Date: Tue, 16 Jan 2024 00:26:32 +0530 Subject: [PATCH 2/2] updated media file storage path according to user_id --- apps/jobs/models.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/jobs/models.py b/apps/jobs/models.py index 3047386..4abcddc 100644 --- a/apps/jobs/models.py +++ b/apps/jobs/models.py @@ -63,7 +63,6 @@ class Meta: category = models.CharField(max_length=20, default=None, null=True) is_active = models.BooleanField(default=None, null=False) - # These fields will be displayed as a part of "description" field job_responsibilities = models.TextField( default="No Job Responsibilities provided", max_length=1000 @@ -87,6 +86,9 @@ class User(models.Model): This class has two foreign keys that point to Job and Company table """ + def media_upload_path(instance, filename): + return f"user_{instance.user_id}/data/{filename}" + class Meta: db_table = values.DB_TABLE_USER_PROFILE @@ -97,11 +99,11 @@ class Meta: address = models.TextField(max_length=100, null=True, default=None) about = models.TextField(max_length=100, default=None, null=True) job = models.ForeignKey(Job, on_delete=models.CASCADE, null=True, default=None) - resume = models.FileField(upload_to="resume/", null=True, default=None) + resume = models.FileField(upload_to=media_upload_path, null=True, default=None) profile_picture = models.FileField( - upload_to="profile_picture/", null=True, default=None + upload_to=media_upload_path, null=True, default=None ) - cover_letter = models.FileField(upload_to="cover_letter/", null=True) + cover_letter = models.FileField(upload_to=media_upload_path, null=True) company = models.ForeignKey( Company, on_delete=models.CASCADE, null=True, default=None ) @@ -112,8 +114,9 @@ class Meta: age = models.PositiveIntegerField(default=None, null=True) education = models.TextField(max_length=500, default=None, null=True) professional_skills = models.TextField(max_length=500, default=None, null=True) - hiring_status = models.CharField(max_length=15, choices=HIRING_STATUS, default="Not Applied Yet", null=True) - + hiring_status = models.CharField( + max_length=15, choices=HIRING_STATUS, default="Not Applied Yet", null=True + ) # These fields will be displayed as a part of "Contact" field email = models.CharField(max_length=30, null=False)