From 3a1307a7a43fc9d080f07f09817343af941e3755 Mon Sep 17 00:00:00 2001 From: Faheed Date: Mon, 1 Dec 2025 14:04:04 +0300 Subject: [PATCH] afeter pull --- recruitment/views.py | 614 ++++++++++++----------------------- templates/jobs/job_list.html | 2 +- 2 files changed, 217 insertions(+), 399 deletions(-) diff --git a/recruitment/views.py b/recruitment/views.py index 4efc311..325d45b 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -630,7 +630,7 @@ def job_detail(request, slug): # New statistics "avg_match_score": avg_match_score, "high_potential_count": high_potential_count, - # "high_potential_ratio": high_potential_ratio, + "high_potential_ratio": high_potential_ratio, "avg_t2i_days": avg_t2i_days, "avg_t_in_exam_days": avg_t_in_exam_days, "linkedin_content_form": linkedin_content_form, @@ -887,7 +887,7 @@ def post_to_linkedin(request, slug): job.linkedin_post_url = "" job.linkedin_post_status = "QUEUED" job.linkedin_posted_at = None - job.save() + job.save() # ENQUEUE THE TASK # Pass the function path, the job slug, and the token as arguments @@ -1209,26 +1209,12 @@ def application_submit_form(request, template_slug): return redirect("job_application_detail", slug=job.slug) - job = get_object_or_404(JobPosting, form_template__slug=template_slug) - if request.user.user_type == "candidate": - person=request.user.person_profile - if job.has_already_applied_to_this_job(person): - messages.error( - request, - _( - "You have already applied to this job: Multiple applications are not allowed." - ), - ) - return redirect("job_application_detail", slug=job.slug) - - template = get_object_or_404(FormTemplate, slug=template_slug, is_active=True) stage = template.stages.filter(name="Contact Information") - # job_id = template.job.internal_job_id - # job = template. - job_id=job.internal_job_id + job_id = template.job.internal_job_id + job = template.job is_limit_exceeded = job.is_application_limit_reached if is_limit_exceeded: messages.error( @@ -1262,7 +1248,7 @@ def application_submit_form(request, template_slug): @require_POST def application_submit(request, template_slug): """Handle form submission""" - if not request.user.is_authenticated :# or request.user.user_type != "application": + if not request.user.is_authenticated :# or request.user.user_type != "candidate": return JsonResponse({"success": False, "message": "Unauthorized access."}) template = get_object_or_404(FormTemplate, slug=template_slug) job = template.job @@ -1464,10 +1450,10 @@ def form_submission_details(request, template_id, slug): # def _handle_get_request(request, slug, job): # """ -# Handles GET requests, setting up forms and restoring application selections +# Handles GET requests, setting up forms and restoring candidate selections # from the session for persistence. # """ -# SESSION_KEY = f"schedule_application_ids_{slug}" +# SESSION_KEY = f"schedule_candidate_ids_{slug}" # form = BulkInterviewTemplateForm(slug=slug) # # break_formset = BreakTimeFormSet(prefix='breaktime') @@ -1476,11 +1462,11 @@ def form_submission_details(request, template_id, slug): # # 1. Capture IDs from HTMX request and store in session (when first clicked) # if "HX-Request" in request.headers: -# application_ids = request.GET.getlist("application_ids") +# candidate_ids = request.GET.getlist("candidate_ids") -# if application_ids: -# request.session[SESSION_KEY] = application_ids -# selected_ids = application_ids +# if candidate_ids: +# request.session[SESSION_KEY] = candidate_ids +# selected_ids = candidate_ids # # 2. Restore IDs from session (on refresh or navigation) # if not selected_ids: @@ -1488,9 +1474,9 @@ def form_submission_details(request, template_id, slug): # # 3. Use the list of IDs to initialize the form # if selected_ids: -# applications_to_load = Application.objects.filter(pk__in=selected_ids) -# print(applications_to_load) -# form.initial["applications"] = applications_to_load +# candidates_to_load = Application.objects.filter(pk__in=selected_ids) +# print(candidates_to_load) +# form.initial["applications"] = candidates_to_load # return render( # request, @@ -1581,7 +1567,7 @@ def form_submission_details(request, template_id, slug): # "buffer_time": buffer_time, # "break_start_time": break_start_time.isoformat() if break_start_time else None, # "break_end_time": break_end_time.isoformat() if break_end_time else None, -# "application_ids": [c.id for c in applications], +# "candidate_ids": [c.id for c in applications], # "schedule_interview_type":schedule_interview_type # } @@ -1623,7 +1609,7 @@ def form_submission_details(request, template_id, slug): # """ # SESSION_DATA_KEY = "interview_schedule_data" -# SESSION_ID_KEY = f"schedule_application_ids_{slug}" +# SESSION_ID_KEY = f"schedule_candidate_ids_{slug}" # # 1. Get schedule data from session # schedule_data = request.session.get(SESSION_DATA_KEY) @@ -1660,22 +1646,22 @@ def form_submission_details(request, template_id, slug): # if SESSION_DATA_KEY in request.session: del request.session[SESSION_DATA_KEY] # return redirect("schedule_interviews", slug=slug) -# # 3. Setup applications and get slots -# applications = Application.objects.filter(id__in=schedule_data["application_ids"]) -# schedule.applications.set(applications) +# # 3. Setup candidates and get slots +# candidates = Application.objects.filter(id__in=schedule_data["candidate_ids"]) +# schedule.applications.set(candidates) # available_slots = get_available_time_slots(schedule) # # 4. Handle Remote/Onsite logic # if schedule_data.get("schedule_interview_type") == 'Remote': # # ... (Remote logic remains unchanged) # queued_count = 0 -# for i, application in enumerate(applications): +# for i, candidate in enumerate(candidates): # if i < len(available_slots): # slot = available_slots[i] # async_task( # "recruitment.tasks.create_interview_and_meeting", -# application.pk, job.pk, schedule.pk, slot["date"], slot["time"], schedule.interview_duration, +# candidate.pk, job.pk, schedule.pk, slot["date"], slot["time"], schedule.interview_duration, # ) # queued_count += 1 @@ -1708,8 +1694,8 @@ def form_submission_details(request, template_id, slug): # try: -# # 1. Iterate over applications and create a NEW Location object for EACH -# for i, application in enumerate(applications): +# # 1. Iterate over candidates and create a NEW Location object for EACH +# for i, candidate in enumerate(candidates): # if i < len(available_slots): # slot = available_slots[i] @@ -1729,7 +1715,7 @@ def form_submission_details(request, template_id, slug): # # 2. Create the ScheduledInterview, linking the unique location # ScheduledInterview.objects.create( -# application=application, +# application=candidate, # job=job, # schedule=schedule, # interview_date=slot['date'], @@ -1739,7 +1725,7 @@ def form_submission_details(request, template_id, slug): # messages.success( # request, -# f"Onsite schedule interviews created successfully for {len(applications)} applications." +# f"Onsite schedule interviews created successfully for {len(candidates)} candidates." # ) # # Clear session data keys upon successful completion @@ -1784,7 +1770,7 @@ def form_submission_details(request, template_id, slug): @staff_user_required def applications_screening_view(request, slug): """ - Manage application tiers and stage transitions + Manage candidate tiers and stage transitions """ job = get_object_or_404(JobPosting, slug=slug) applications = job.screening_applications @@ -1866,7 +1852,7 @@ def applications_screening_view(request, slug): @staff_user_required def applications_exam_view(request, slug): """ - Manage application tiers and stage transitions + Manage candidate tiers and stage transitions """ job = get_object_or_404(JobPosting, slug=slug) context = {"job": job, "applications": job.exam_applications, "current_stage": "Exam"} @@ -1931,10 +1917,8 @@ def application_set_exam_date(request, slug): def application_update_status(request, slug): job = get_object_or_404(JobPosting, slug=slug) mark_as = request.POST.get("mark_as") - print(mark_as) if mark_as != "----------": application_ids = request.POST.getlist("candidate_ids") - print(application_ids) if c := Application.objects.filter(pk__in=application_ids): if mark_as == "Exam": @@ -1945,7 +1929,7 @@ def application_update_status(request, slug): offer_date=None, hired_date=None, stage=mark_as, - applicant_status="application" + applicant_status="Candidate" if mark_as in ["Exam", "Interview","Document Review", "Offer"] else "Applicant", ) @@ -1956,7 +1940,7 @@ def application_update_status(request, slug): stage=mark_as, offer_date=None, hired_date=None, - applicant_status="application" + applicant_status="Candidate" if mark_as in ["Exam", "Interview", "Document Review","Offer"] else "Applicant", ) @@ -1966,7 +1950,7 @@ def application_update_status(request, slug): stage=mark_as, offer_date=None, hired_date=None, - applicant_status="application" + applicant_status="Candidate" if mark_as in ["Exam", "Interview", "Document Review","Offer"] else "Applicant", ) @@ -1976,7 +1960,7 @@ def application_update_status(request, slug): stage=mark_as, offer_date=timezone.now(), hired_date=None, - applicant_status="application" + applicant_status="Candidate" if mark_as in ["Exam", "Interview", "Document Review","Offer"] else "Applicant", ) @@ -1985,7 +1969,7 @@ def application_update_status(request, slug): c.update( stage=mark_as, hired_date=timezone.now(), - applicant_status="application" + applicant_status="Candidate" if mark_as in ["Exam", "Interview", "Offer"] else "Applicant", ) @@ -1997,7 +1981,7 @@ def application_update_status(request, slug): interview_date=None, offer_date=None, hired_date=None, - applicant_status="application" + applicant_status="Candidate" if mark_as in ["Exam", "Interview", "Offer"] else "Applicant", ) @@ -2023,11 +2007,11 @@ def applications_interview_view(request, slug): @staff_user_required def applications_document_review_view(request, slug): """ - Document review view for applications after interview stage and before offer stage + Document review view for candidates after interview stage and before offer stage """ job = get_object_or_404(JobPosting, slug=slug) - # Get applications from Interview stage who need document review + # Get candidates from Interview stage who need document review applications = job.document_review_applications.select_related('person') # Get search query for filtering search_query = request.GET.get('q', '') @@ -2160,62 +2144,62 @@ def reschedule_meeting_for_application(request, slug): # @staff_user_required # def interview_calendar_view(request, slug): - # job = get_object_or_404(JobPosting, slug=slug) + job = get_object_or_404(JobPosting, slug=slug) - # # Get all scheduled interviews for this job - # scheduled_interviews = ScheduledInterview.objects.filter(job=job).select_related( - # "applicaton", "zoom_meeting" - # ) + # Get all scheduled interviews for this job + scheduled_interviews = ScheduledInterview.objects.filter(job=job).select_related( + "applicaton", "zoom_meeting" + ) - # # Convert interviews to calendar events - # events = [] - # for interview in scheduled_interviews: - # # Create start datetime - # start_datetime = datetime.combine( - # interview.interview_date, interview.interview_time - # ) + # Convert interviews to calendar events + events = [] + for interview in scheduled_interviews: + # Create start datetime + start_datetime = datetime.combine( + interview.interview_date, interview.interview_time + ) - # # Calculate end datetime based on interview duration - # duration = interview.zoom_meeting.duration if interview.zoom_meeting else 60 - # end_datetime = start_datetime + timedelta(minutes=duration) + # Calculate end datetime based on interview duration + duration = interview.zoom_meeting.duration if interview.zoom_meeting else 60 + end_datetime = start_datetime + timedelta(minutes=duration) - # # Determine event color based on status - # color = "#00636e" # Default color - # if interview.status == "confirmed": - # color = "#00a86b" # Green for confirmed - # elif interview.status == "cancelled": - # color = "#e74c3c" # Red for cancelled - # elif interview.status == "completed": - # color = "#95a5a6" # Gray for completed + # Determine event color based on status + color = "#00636e" # Default color + if interview.status == "confirmed": + color = "#00a86b" # Green for confirmed + elif interview.status == "cancelled": + color = "#e74c3c" # Red for cancelled + elif interview.status == "completed": + color = "#95a5a6" # Gray for completed - # events.append( - # { - # "title": f"Interview: {interview.candidate.name}", - # "start": start_datetime.isoformat(), - # "end": end_datetime.isoformat(), - # "url": f"{request.path}interview/{interview.id}/", - # "color": color, - # "extendedProps": { - # "candidate": interview.candidate.name, - # "email": interview.candidate.email, - # "status": interview.status, - # "meeting_id": interview.zoom_meeting.meeting_id - # if interview.zoom_meeting - # else None, - # "join_url": interview.zoom_meeting.join_url - # if interview.zoom_meeting - # else None, - # }, - # } - # ) + events.append( + { + "title": f"Interview: {interview.candidate.name}", + "start": start_datetime.isoformat(), + "end": end_datetime.isoformat(), + "url": f"{request.path}interview/{interview.id}/", + "color": color, + "extendedProps": { + "candidate": interview.candidate.name, + "email": interview.candidate.email, + "status": interview.status, + "meeting_id": interview.zoom_meeting.meeting_id + if interview.zoom_meeting + else None, + "join_url": interview.zoom_meeting.join_url + if interview.zoom_meeting + else None, + }, + } + ) - # context = { - # "job": job, - # "events": events, - # "calendar_color": "#00636e", - # } + context = { + "job": job, + "events": events, + "calendar_color": "#00636e", + } - # return render(request, "recruitment/interview_calendar.html", context) + return render(request, "recruitment/interview_calendar.html", context) # @staff_user_required @@ -3319,7 +3303,6 @@ def agency_create(request): if request.method == "POST": form = HiringAgencyForm(request.POST) if form.is_valid(): - agency = form.save() messages.success(request, f'Agency "{agency.name}" created successfully!') return redirect("agency_detail", slug=agency.slug) @@ -3341,27 +3324,27 @@ def agency_detail(request, slug): """View details of a specific hiring agency""" agency = get_object_or_404(HiringAgency, slug=slug) - # Get applications associated with this agency - applications = Application.objects.filter(hiring_agency=agency).order_by( + # Get candidates associated with this agency + candidates = Application.objects.filter(hiring_agency=agency).order_by( "-created_at" ) # Statistics - total_applications = applications.count() - active_applications = applications.filter( + total_candidates = candidates.count() + active_candidates = candidates.filter( stage__in=["Applied", "Screening", "Exam", "Interview", "Offer"] ).count() - hired_applications = applications.filter(stage="Hired").count() - rejected_applications = applications.filter(stage="Rejected").count() + hired_candidates = candidates.filter(stage="Hired").count() + rejected_candidates = candidates.filter(stage="Rejected").count() job_assignments=AgencyJobAssignment.objects.filter(agency=agency) print(job_assignments) context = { "agency": agency, - "applications": applications[:10], # Show recent 10 applications - "total_applications": total_applications, - "active_applications": active_applications, - "hired_applications": hired_applications, - "rejected_applications": rejected_applications, + "candidates": candidates[:10], # Show recent 10 candidates + "total_candidates": total_candidates, + "active_candidates": active_candidates, + "hired_candidates": hired_candidates, + "rejected_candidates": rejected_candidates, "generated_password": agency.generated_password if agency.generated_password else None, @@ -4099,19 +4082,19 @@ def applicant_portal_dashboard(request): if not request.user.is_authenticated: return redirect("account_login") - # Get application profile (Person record) + # Get candidate profile (Person record) try: applicant = request.user.person_profile except: - messages.error(request, "No application profile found.") + messages.error(request, "No candidate profile found.") return redirect("account_login") - # Get application's applications with related job data + # Get candidate's applications with related job data applications = Application.objects.filter( person=applicant ).select_related('job').order_by('-created_at') - # Get application's documents using the Person documents property + # Get candidate's documents using the Person documents property documents = applicant.documents.order_by('-created_at') # Add password change form for modal @@ -4137,19 +4120,19 @@ def applicant_application_detail(request, slug): if not request.user.is_authenticated: return redirect("account_login") - # Get application profile (Person record) + # Get candidate profile (Person record) agency = getattr(request.user,"agency_profile",None) if agency: - application = get_object_or_404(Application,slug=slug) + candidate = get_object_or_404(Application,slug=slug) # if Application.objects.filter(person=candidate,hirin).exists() else: try: - applicant = request.user.person_profile + candidate = request.user.person_profile except: - messages.error(request, "No applicant profile found.") + messages.error(request, "No candidate profile found.") return redirect("account_login") - # Get the specific application and verify it belongs to this applicant + # Get the specific application and verify it belongs to this candidate application = get_object_or_404( Application.objects.select_related( 'job', 'person' @@ -4157,7 +4140,7 @@ def applicant_application_detail(request, slug): 'scheduled_interviews' # Only prefetch interviews, not documents (Generic FK) ), slug=slug, - person=application.person if agency else applicant + person=candidate.person if agency else candidate ) # Get AI analysis data if available @@ -4176,7 +4159,7 @@ def applicant_application_detail(request, slug): context = { "application": application, - "applicant": applicant, + "candidate": candidate, "ai_analysis": ai_analysis, "interviews": interviews, "documents": documents, @@ -4340,11 +4323,11 @@ def agency_portal_submit_application_page(request, slug): if request.method == "POST": form = ApplicationForm(request.POST, request.FILES,current_agency=current_agency,current_job=current_job) if form.is_valid(): - application = form.save(commit=False) + candidate = form.save(commit=False) - application.hiring_source = "AGENCY" - application.hiring_agency = assignment.agency - application.save() + candidate.hiring_source = "AGENCY" + candidate.hiring_agency = assignment.agency + candidate.save() assignment.increment_submission_count() return redirect("agency_portal_dashboard") @@ -4360,12 +4343,12 @@ def agency_portal_submit_application_page(request, slug): "total_submitted": total_submitted, "job": assignment.job, } - return render(request, "recruitment/agency_portal_submit_application.html", context) + return render(request, "recruitment/agency_portal_submit_candidate.html", context) @agency_user_required def agency_portal_submit_application(request): - """Handle application submission via AJAX (for embedded form)""" + """Handle candidate submission via AJAX (for embedded form)""" assignment_id = request.session.get("agency_assignment_id") if not assignment_id: return redirect("agency_portal_login") @@ -4374,24 +4357,24 @@ def agency_portal_submit_application(request): AgencyJobAssignment.objects.select_related("agency", "job"), id=assignment_id ) if assignment.is_full: - messages.error(request, "Maximum application limit reached for this assignment.") + messages.error(request, "Maximum candidate limit reached for this assignment.") return redirect("agency_portal_assignment_detail", slug=assignment.slug) # Check if assignment allows submission if not assignment.can_submit: messages.error( request, - "Cannot submit applications: Assignment is not active, expired, or full.", + "Cannot submit candidates: Assignment is not active, expired, or full.", ) return redirect("agency_portal_dashboard") if request.method == "POST": form = AgencyApplicationSubmissionForm(assignment, request.POST, request.FILES) if form.is_valid(): - application = form.save(commit=False) - application.hiring_source = "AGENCY" - application.hiring_agency = assignment.agency - application.save() + candidate = form.save(commit=False) + candidate.hiring_source = "AGENCY" + candidate.hiring_agency = assignment.agency + candidate.save() # Increment the assignment's submitted count assignment.increment_submission_count() @@ -4400,12 +4383,12 @@ def agency_portal_submit_application(request): return JsonResponse( { "success": True, - "message": f"application {application.name} submitted successfully!", + "message": f"Candidate {candidate.name} submitted successfully!", } ) else: messages.success( - request, f"application {application.name} submitted successfully!" + request, f"Candidate {candidate.name} submitted successfully!" ) return redirect("agency_portal_dashboard") else: @@ -4421,10 +4404,10 @@ def agency_portal_submit_application(request): context = { "form": form, "assignment": assignment, - "title": f"Submit application for {assignment.job.title}", - "button_text": "Submit application", + "title": f"Submit Candidate for {assignment.job.title}", + "button_text": "Submit Candidate", } - return render(request, "recruitment/agency_portal_submit_application.html", context) + return render(request, "recruitment/agency_portal_submit_candidate.html", context) def agency_portal_assignment_detail(request, slug): @@ -4466,8 +4449,8 @@ def agency_assignment_detail_agency(request, slug, assignment_id): ) return redirect("agency_portal_dashboard") - # Get applications submitted by this agency for this job - applications = Application.objects.filter( + # Get candidates submitted by this agency for this job + candidates = Application.objects.filter( hiring_agency=assignment.agency, job=assignment.job ).order_by("-created_at") @@ -4477,8 +4460,8 @@ def agency_assignment_detail_agency(request, slug, assignment_id): # Mark messages as read # No messages to mark as read - # Pagination for applications - paginator = Paginator(applications, 20) # Show 20 applications per page + # Pagination for candidates + paginator = Paginator(candidates, 20) # Show 20 candidates per page page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) @@ -4488,12 +4471,12 @@ def agency_assignment_detail_agency(request, slug, assignment_id): message_page_obj = message_paginator.get_page(message_page_number) # Calculate progress ring offset for circular progress indicator - total_applications = applications.count() - max_applications = assignment.max_candidates + total_candidates = candidates.count() + max_candidates = assignment.max_candidates circumference = 326.73 # 2 * π * r where r=52 - if max_applications > 0: - progress_percentage = total_applications / max_applications + if max_candidates > 0: + progress_percentage = total_candidates / max_candidates stroke_dashoffset = circumference - (circumference * progress_percentage) else: stroke_dashoffset = circumference @@ -4502,7 +4485,7 @@ def agency_assignment_detail_agency(request, slug, assignment_id): "assignment": assignment, "page_obj": page_obj, "message_page_obj": message_page_obj, - "total_applications": total_applications, + "total_candidates": total_candidates, "stroke_dashoffset": stroke_dashoffset, } return render(request, "recruitment/agency_portal_assignment_detail.html", context) @@ -4515,8 +4498,8 @@ def agency_assignment_detail_admin(request, slug): AgencyJobAssignment.objects.select_related("agency", "job"), slug=slug ) - # Get applications submitted by this agency for this job - applications = Application.objects.filter( + # Get candidates submitted by this agency for this job + candidates = Application.objects.filter( hiring_agency=assignment.agency, job=assignment.job ).order_by("-created_at") @@ -4528,9 +4511,9 @@ def agency_assignment_detail_admin(request, slug): context = { "assignment": assignment, - "applications": applications, + "candidates": candidates, "access_link": access_link, - "total_applications": applications.count(), + "total_candidates": candidates.count(), } return render(request, "recruitment/agency_assignment_detail.html", context) @@ -4538,8 +4521,8 @@ def agency_assignment_detail_admin(request, slug): #will check the changes application to appliaction in this function @agency_user_required -def agency_portal_edit_application(request, application_id): - """Edit a application for agency portal""" +def agency_portal_edit_application(request, candidate_id): + """Edit a candidate for agency portal""" assignment_id = request.session.get("agency_assignment_id") if not assignment_id: return redirect("agency_portal_login") @@ -4551,45 +4534,45 @@ def agency_portal_edit_application(request, application_id): agency = current_assignment.agency - # Get application and verify it belongs to this agency - application = get_object_or_404(Application, id=application_id, hiring_agency=agency) + # Get candidate and verify it belongs to this agency + candidate = get_object_or_404(Application, id=candidate_id, hiring_agency=agency) if request.method == "POST": # Handle form submission - application.first_name = request.POST.get("first_name", application.first_name) - application.last_name = request.POST.get("last_name", application.last_name) - application.email = request.POST.get("email", application.email) - application.phone = request.POST.get("phone", application.phone) - application.address = request.POST.get("address", application.address) + candidate.first_name = request.POST.get("first_name", candidate.first_name) + candidate.last_name = request.POST.get("last_name", candidate.last_name) + candidate.email = request.POST.get("email", candidate.email) + candidate.phone = request.POST.get("phone", candidate.phone) + candidate.address = request.POST.get("address", candidate.address) # Handle resume upload if provided if "resume" in request.FILES: - application.resume = request.FILES["resume"] + candidate.resume = request.FILES["resume"] try: - application.save() + candidate.save() messages.success( - request, f"Application {application.name} updated successfully!" + request, f"Candidate {candidate.name} updated successfully!" ) return redirect( "agency_assignment_detail", - slug=application.job.agencyjobassignment_set.first().slug, + slug=candidate.job.agencyjobassignment_set.first().slug, ) except Exception as e: - messages.error(request, f"Error updating application: {e}") + messages.error(request, f"Error updating candidate: {e}") # For GET requests or POST errors, return JSON response for AJAX if request.headers.get("X-Requested-With") == "XMLHttpRequest": return JsonResponse( { "success": True, - "application": { - "id": application.id, - "first_name": application.first_name, - "last_name": application.last_name, - "email": application.email, - "phone": application.phone, - "address": application.address, + "candidate": { + "id": candidate.id, + "first_name": candidate.first_name, + "last_name": candidate.last_name, + "email": candidate.email, + "phone": candidate.phone, + "address": candidate.address, }, } ) @@ -4599,8 +4582,8 @@ def agency_portal_edit_application(request, application_id): @agency_user_required -def agency_portal_delete_application(request, application_id): - """Delete a application for agency portal""" +def agency_portal_delete_application(request, candidate_id): + """Delete a candidate for agency portal""" assignment_id = request.session.get("agency_assignment_id") if not assignment_id: return redirect("agency_portal_login") @@ -4612,20 +4595,20 @@ def agency_portal_delete_application(request, application_id): agency = current_assignment.agency - # Get application and verify it belongs to this agency - application = get_object_or_404(Application, id=application_id, hiring_agency=agency) + # Get candidate and verify it belongs to this agency + candidate = get_object_or_404(Application, id=candidate_id, hiring_agency=agency) if request.method == "POST": try: - application_name = application.name - application.delete() + candidate_name = candidate.name + candidate.delete() - current_assignment.candidates_submitted -= 1 #in the modal + current_assignment.candidates_submitted -= 1 current_assignment.status = current_assignment.AssignmentStatus.ACTIVE current_assignment.save(update_fields=["candidates_submitted", "status"]) messages.success( - request, f"Application {application_name} removed successfully!" + request, f"Candidate {candidate_name} removed successfully!" ) return JsonResponse({"success": True}) except Exception as e: @@ -4684,7 +4667,7 @@ def message_list(request): "search_query": search_query, } if request.user.user_type != "staff": - return render(request, "messages/application_message_list.html", context) + return render(request, "messages/candidate_message_list.html", context) return render(request, "messages/message_list.html", context) @@ -4710,7 +4693,7 @@ def message_detail(request, message_id): "message": message, } if request.user.user_type != "staff": - return render(request, "messages/application_message_detail.html", context) + return render(request, "messages/candidate_message_detail.html", context) return render(request, "messages/message_detail.html", context) @@ -4766,7 +4749,7 @@ def message_create(request): "form": form, } if request.user.user_type != "staff": - return render(request, "messages/application_message_form.html", context) + return render(request, "messages/candidate_message_form.html", context) return render(request, "messages/message_form.html", context) @@ -4834,7 +4817,7 @@ def message_reply(request, message_id): "parent_message": parent_message, } if request.user.user_type != "staff": - return render(request, "messages/application_message_form.html", context) + return render(request, "messages/candidate_message_form.html", context) return render(request, "messages/message_form.html", context) @@ -4899,22 +4882,13 @@ def message_delete(request, message_id): or HTMX's hx-redirect header). """ - # 1. Retrieve the message - # Use select_related to fetch linked objects efficiently for checks/logging - """ - Deletes a message using a POST request, primarily designed for HTMX. - Redirects to the message list on success (either via standard redirect - or HTMX's hx-redirect header). - """ - # 1. Retrieve the message # Use select_related to fetch linked objects efficiently for checks/logging message = get_object_or_404( Message.objects.select_related("sender", "recipient"), id=message_id ) - # 2. Permission Check - # Only the sender or recipient can delete the message + # Check if user has permission to delete this message if message.sender != request.user and message.recipient != request.user: messages.error(request, "You don't have permission to delete this message.") @@ -4927,21 +4901,25 @@ def message_delete(request, message_id): # Standard navigation redirect return redirect("message_list") - # 3. Handle POST Request (Deletion) if request.method == "POST": message.delete() messages.success(request, "Message deleted successfully.") # Handle HTMX requests if "HX-Request" in request.headers: - # 1. Set the HTMX response header for redirection - response = HttpResponse(status=200) - response["HX-Redirect"] = reverse("message_list") # <--- EXPLICIT HEADER - return response + return HttpResponse(status=200) # HTMX success response + + return redirect("message_list") + + # For GET requests, show confirmation page + context = { + "message": message, + "title": "Delete Message", + "message": f"Are you sure you want to delete this message from {message.sender.get_full_name() or message.sender.username}?", + "cancel_url": reverse("message_detail", kwargs={"message_id": message_id}), + } + return render(request, "messages/message_confirm_delete.html", context) - # Standard navigation fallback - return redirect("message_list") - @login_required def api_unread_count(request): @@ -4971,10 +4949,10 @@ def document_upload(request, slug): # Handle Person document upload try: person = get_object_or_404(Person, id=actual_application_id) - # Check if user owns this person (for applicant portal) + # Check if user owns this person (for candidate portal) if request.user.user_type == "candidate": - applicant = request.user.person_profile - if person != applicant: + candidate = request.user.person_profile + if person != candidate: messages.error(request, "You can only upload documents to your own profile.") return JsonResponse({"success": False, "error": "Permission denied"}) except (ValueError, Person.DoesNotExist): @@ -4986,15 +4964,15 @@ def document_upload(request, slug): except (ValueError, Application.DoesNotExist): return JsonResponse({"success": False, "error": "Invalid application ID"}) - # Check if user owns this application (for applicant portal) + # Check if user owns this application (for candidate portal) if request.user.user_type == "candidate": try: - applicant = request.user.person_profile - if application.person != applicant: + candidate = request.user.person_profile + if application.person != candidate: messages.error(request, "You can only upload documents to your own applications.") return JsonResponse({"success": False, "error": "Permission denied"}) except: - messages.error(request, "No applicant profile found.") + messages.error(request, "No candidate profile found.") return JsonResponse({"success": False, "error": "Permission denied"}) if request.method == "POST": @@ -5068,58 +5046,9 @@ def document_upload(request, slug): return redirect("application_detail", slug=application.job.slug) -# @login_required -# def document_delete(request, document_id): -# """Delete a document""" -# document = get_object_or_404(Document, id=document_id) - -# # Check permission - document is now linked to Application or Person via Generic Foreign Key -# if hasattr(document.content_object, "job"): -# # Application document -# if ( -# document.content_object.job.assigned_to != request.user -# and not request.user.is_superuser -# ): -# messages.error( -# request, "You don't have permission to delete this document." -# ) -# return JsonResponse({"success": False, "error": "Permission denied"}) -# job_slug = document.content_object.job.slug -# redirect_url = "applicant_portal_dashboard" if request.user.user_type == "candidate" else "job_detail" -# elif hasattr(document.content_object, "person"): -# # Person document -# if request.user.user_type == "candidate": -# applicant = request.user.person_profile -# if document.content_object != applicant: -# messages.error( -# request, "You can only delete your own documents." -# ) -# return JsonResponse({"success": False, "error": "Permission denied"}) -# redirect_url = "applicant_portal_dashboard" -# else: -# # Handle other content object types -# messages.error(request, "You don't have permission to delete this document.") -# return JsonResponse({"success": False, "error": "Permission denied"}) - -# if request.method == "POST": -# file_name = document.file.name if document.file else "Unknown" -# document.delete() -# messages.success(request, f'Document "{file_name}" deleted successfully!') - -# # Handle AJAX requests -# if request.headers.get("X-Requested-With") == "XMLHttpRequest": -# return JsonResponse( -# {"success": True, "message": "Document deleted successfully!"} -# ) -# else: -# return redirect("application_detail", slug=job_slug) - -# return JsonResponse({"success": False, "error": "Method not allowed"}) - - @login_required def document_delete(request, document_id): - """Delete a document using a POST request (ideal for HTMX).""" + """Delete a document""" document = get_object_or_404(Document, id=document_id) # Initialize variables for redirection outside of the complex logic @@ -5141,26 +5070,6 @@ def document_delete(request, document_id): has_permission = True # Determine redirect URL for non-HTMX requests (fallback) - - # Initialize variables for redirection outside of the complex logic - is_htmx = "HX-Request" in request.headers - - # 1. Permission and Context Initialization - has_permission = False - - content_object = document.content_object - - # Case A: Document linked to an Application (via content_object) - if hasattr(content_object, "job"): - # Staff/Superuser checking against Application's Job assignment - if (content_object.job.assigned_to == request.user) or request.user.is_superuser: - has_permission = True - - # Candidate checking if the Application belongs to them - elif request.user.user_type == "candidate" and content_object.person.user == request.user: - has_permission = True - - # Determine redirect URL for non-HTMX requests (fallback) if request.user.user_type == "candidate": # Assuming you redirect to the candidate's main dashboard after deleting their app document redirect_view_name = "applicant_portal_dashboard" @@ -5193,38 +5102,6 @@ def document_delete(request, document_id): return HttpResponse(status=403) - # 3. Handle POST Request (Deletion) - # Assuming you redirect to the candidate's main dashboard after deleting their app document - redirect_view_name = "applicant_portal_dashboard" - else: - # Assuming you redirect to the job detail page for staff - redirect_view_name = "job_detail" - redirect_args = [content_object.job.slug] # Pass the job slug - - # Case B: Document linked directly to a Person (e.g., profile document) - elif hasattr(content_object, "user"): - # Check if the document belongs to the requesting candidate - if request.user.user_type == "candidate" and content_object.user == request.user: - has_permission = True - redirect_view_name = "applicant_portal_dashboard" - # Check if the requesting user is staff/superuser (Staff can delete profile docs) - elif request.user.is_staff or request.user.is_superuser: - has_permission = True - # Staff should probably go to the person's profile detail, but defaulting to a safe spot. - redirect_view_name = "dashboard" - - # Case C: No clear content object linkage or unhandled type - else: - has_permission = request.user.is_superuser # Only superuser can delete unlinked docs - - - # 2. Enforce Permissions - if not has_permission: - messages.error(request, "Permission denied: You cannot delete this document.") - # Return a 403 response for HTMX/AJAX - return HttpResponse(status=403) - - # 3. Handle POST Request (Deletion) if request.method == "POST": file_name = document.file.name if document.file else "Unknown" @@ -5237,13 +5114,6 @@ def document_delete(request, document_id): # to remove the element, or hx-redirect to navigate. return HttpResponse(status=200) - # --- Standard Navigation Fallback --- - # --- HTMX / AJAX Response --- - if is_htmx or request.headers.get("X-Requested-With") == "XMLHttpRequest": - # For HTMX, return a 200 OK. The front-end is expected to use hx-swap='outerHTML' - # to remove the element, or hx-redirect to navigate. - return HttpResponse(status=200) - # --- Standard Navigation Fallback --- else: try: @@ -5256,19 +5126,6 @@ def document_delete(request, document_id): # If no specific redirect_view_name was set (e.g., Case C failure) return redirect("dashboard") - # 4. Handle non-POST (e.g., GET) - # The delete view should not be accessed via GET. - return HttpResponse(status=405) # Method Not Allowed - try: - # Use the calculated redirect view name and arguments - if 'redirect_args' in locals(): - return redirect(redirect_view_name, *redirect_args) - else: - return redirect(redirect_view_name) - except NameError: - # If no specific redirect_view_name was set (e.g., Case C failure) - return redirect("dashboard") - # 4. Handle non-POST (e.g., GET) # The delete view should not be accessed via GET. return HttpResponse(status=405) # Method Not Allowed @@ -5293,8 +5150,8 @@ def document_download(request, document_id): elif hasattr(document.content_object, "person"): # Person document if request.user.user_type == "candidate": - applicant = request.user.person_profile - if document.content_object != applicant: + candidate = request.user.person_profile + if document.content_object != candidate: messages.error( request, "You can only download your own documents." ) @@ -5314,7 +5171,6 @@ def document_download(request, document_id): return JsonResponse({"success": False, "error": "File not found"}) - @login_required @@ -5331,20 +5187,10 @@ def portal_logout(request): def interview_create_type_selection(request, application_slug): """Show interview type selection page for a application""" application = get_object_or_404(Application, slug=application_slug) -def interview_create_type_selection(request, application_slug): - """Show interview type selection page for a application""" - application = get_object_or_404(Application, slug=application_slug) - - # Validate application is in Interview stage - if application.stage != 'Interview': - messages.error(request, f"application {application.name} is not in Interview stage.") - return redirect('applications_interview_view', slug=application.job.slug) context = { 'application': application, 'job': application.job, - 'application': application, - 'job': application.job, } return render(request, 'interviews/interview_create_type_selection.html', context) @@ -5353,14 +5199,6 @@ def interview_create_type_selection(request, application_slug): def interview_create_remote(request, application_slug): """Create remote interview for a candidate""" application = get_object_or_404(Application, slug=application_slug) -def interview_create_remote(request, application_slug): - """Create remote interview for a application""" - application = get_object_or_404(Application, slug=application_slug) - - # Validate application is in Interview stage - # if application.stage != 'Interview': - # messages.error(request, f"application {application.name} is not in Interview stage.") - # return redirect('application_interview_view', slug=application.job.slug) if request.method == 'POST': form = RemoteInterviewForm(request.POST) @@ -5382,7 +5220,6 @@ def interview_create_remote(request, application_slug): form = RemoteInterviewForm() form.initial['topic'] = f"Interview for {application.job.title} - {application.name}" context = { - 'application': application, 'application': application, 'job': application.job, 'form': form, @@ -5417,13 +5254,10 @@ def interview_create_onsite(request, application_slug): else: # Pre-populate topic form.initial['topic'] = f"Interview for {application.job.title} - {application.name}" - form.initial['topic'] = f"Interview for {application.job.title} - {application.name}" form = OnsiteInterviewForm() form.initial['topic'] = f"Interview for {application.job.title} - {application.name}" context = { - 'application': application, - 'job': application.job, 'application': application, 'job': application.job, 'form': form, @@ -5541,10 +5375,10 @@ def agency_access_link_reactivate(request, slug): @agency_user_required -def api_application_detail(request, application_id): - """API endpoint to get application details for agency portal""" +def api_application_detail(request, candidate_id): + """API endpoint to get candidate details for agency portal""" try: - # Get application from session-based agency access + # Get candidate from session-based agency access assignment_id = request.session.get("agency_assignment_id") if not assignment_id: return JsonResponse({"success": False, "error": "Access denied"}) @@ -5556,20 +5390,20 @@ def api_application_detail(request, application_id): agency = current_assignment.agency - # Get application and verify it belongs to this agency - application = get_object_or_404( - Application, id=application_id, hiring_agency=agency + # Get candidate and verify it belongs to this agency + candidate = get_object_or_404( + Application, id=candidate_id, hiring_agency=agency ) - # Return application data + # Return candidate data response_data = { "success": True, - "id": application.id, - "first_name": application.first_name, - "last_name": application.last_name, - "email": application.email, - "phone": application.phone, - "address": application.address, + "id": candidate.id, + "first_name": candidate.first_name, + "last_name": candidate.last_name, + "email": candidate.email, + "phone": candidate.phone, + "address": candidate.address, } return JsonResponse(response_data) @@ -5580,7 +5414,7 @@ def api_application_detail(request, application_id): @staff_user_required def compose_application_email(request, job_slug): - """Compose email to participants about a application""" + """Compose email to participants about a candidate""" from .email_service import send_bulk_email job = get_object_or_404(JobPosting, slug=job_slug) @@ -5589,11 +5423,7 @@ def compose_application_email(request, job_slug): # if request.method == "POST": # form = CandidateEmailForm(job, candidate, request.POST) candidate_ids=request.GET.getlist('candidate_ids') - - print("candidate_ids:",candidate_ids) - - - applications=Application.objects.filter(id__in=candidate_ids) + candidates=Application.objects.filter(id__in=candidate_ids) if request.method == 'POST': @@ -5601,10 +5431,6 @@ def compose_application_email(request, job_slug): candidate_ids = request.POST.getlist('candidate_ids') print("candidate_ids from post:", candidate_ids) - applications=Application.objects.filter(id__in=candidate_ids) - form = CandidateEmailForm(job, applications, request.POST) - print("candidate_ids from post:", candidate_ids) - applications=Application.objects.filter(id__in=candidate_ids) form = CandidateEmailForm(job, applications, request.POST) if form.is_valid(): @@ -5642,8 +5468,6 @@ def compose_application_email(request, job_slug): ) if email_result["success"]: - for application in applications: - if hasattr(application, 'person') and application.person: for application in applications: if hasattr(application, 'person') and application.person: try: @@ -5653,26 +5477,20 @@ def compose_application_email(request, job_slug): print(message) print(job) - print(request.user) - print(application.person.user) - print(subject) - print(message) - print(job) - Message.objects.create( sender=request.user, recipient=application.person.user, - recipient=application.person.user, subject=subject, content=message, job=job, - message_type='job_related', - is_read=False, + message_type='email', + is_email_sent=True, + email_address=application.person.email if application.person.email else application.email ) except Exception as e: # Log error but don't fail the entire process - print(f"CRITICAL ERROR creating message for {application.person.user.email}: {e}") + print(f"Error creating message") messages.success( request, @@ -5702,7 +5520,7 @@ def compose_application_email(request, job_slug): return render( request, "includes/email_compose_form.html", - {"form": form, "job": job, "applications": applications}, + {"form": form, "job": job, "candidate": candidates}, ) @@ -5723,18 +5541,18 @@ def compose_application_email(request, job_slug): return render( request, "includes/email_compose_form.html", - {"form": form, "job": job, "applications": applications}, + {"form": form, "job": job, "candidates": candidates}, ) else: # GET request - show the form - form = CandidateEmailForm(job, applications) + form = CandidateEmailForm(job, candidates) return render( request, "includes/email_compose_form.html", - # {"form": form, "job": job, "applications": applications}, - {"form": form,"job":job,"applications":applications}, + # {"form": form, "job": job, "candidates": candidates}, + {"form": form,"job":job}, ) diff --git a/templates/jobs/job_list.html b/templates/jobs/job_list.html index 2756d85..3e1a6d0 100644 --- a/templates/jobs/job_list.html +++ b/templates/jobs/job_list.html @@ -287,7 +287,7 @@ {% trans "All" %} - {% trans "Screened" %} + {% trans "Screening" %} {% trans "Exam" %} {% trans "Interview" %} {% trans "DOC Review" %}