/set_meeting_candidate/', views.set_meeting_candidate, name='set_meeting_candidate'),
]
diff --git a/recruitment/views.py b/recruitment/views.py
index bc6899b..9d58fd6 100644
--- a/recruitment/views.py
+++ b/recruitment/views.py
@@ -1,6 +1,9 @@
import json
import requests
from django.contrib.auth.models import User
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django import forms
from rich import print
from django.template.loader import render_to_string
from django.views.decorators.csrf import csrf_exempt
@@ -14,12 +17,13 @@ from django.conf import settings
from django.utils import timezone
from .forms import (
CandidateExamDateForm,
+ InterviewForm,
ZoomMeetingForm,
JobPostingForm,
FormTemplateForm,
InterviewScheduleForm,JobPostingStatusForm,
BreakTimeFormSet,
- JobPostingImageForm
+ JobPostingImageForm,MeetingCommentForm
)
from rest_framework import viewsets
from django.contrib import messages
@@ -51,7 +55,7 @@ from .models import (
ZoomMeeting,
Candidate,
JobPosting,
- ScheduledInterview
+ ScheduledInterview,MeetingComment
)
import logging
from datastar_py.django import (
@@ -61,10 +65,12 @@ from datastar_py.django import (
)
from django.db import transaction
from django_q.tasks import async_task
+from django.db.models import Prefetch
+from django.db.models import Q, Count, Avg
+from django.db.models import FloatField
logger = logging.getLogger(__name__)
-
class JobPostingViewSet(viewsets.ModelViewSet):
queryset = JobPosting.objects.all()
serializer_class = JobPostingSerializer
@@ -75,7 +81,7 @@ class CandidateViewSet(viewsets.ModelViewSet):
serializer_class = CandidateSerializer
-class ZoomMeetingCreateView(CreateView):
+class ZoomMeetingCreateView(LoginRequiredMixin, CreateView):
model = ZoomMeeting
template_name = "meetings/create_meeting.html"
form_class = ZoomMeetingForm
@@ -87,9 +93,8 @@ class ZoomMeetingCreateView(CreateView):
topic = instance.topic
if instance.start_time < timezone.now():
messages.error(self.request, "Start time must be in the future.")
- return redirect("/create-meeting/", status=400)
+ return redirect(reverse("create_meeting",kwargs={"slug": instance.slug}))
start_time = instance.start_time
- # start_time = instance.start_time.isoformat() + "Z"
duration = instance.duration
result = create_zoom_meeting(topic, start_time, duration)
@@ -103,15 +108,16 @@ class ZoomMeetingCreateView(CreateView):
instance.save()
messages.success(self.request, result["message"])
- return redirect("/", status=201)
+ return redirect(reverse("list_meetings"))
else:
messages.error(self.request, result["message"])
- return redirect("/", status=400)
+ return redirect(reverse("create_meeting",kwargs={"slug": instance.slug}))
except Exception as e:
- return redirect("/", status=500)
+ messages.error(self.request, f"Error creating meeting: {e}")
+ return redirect(reverse("create_meeting",kwargs={"slug": instance.slug}))
-class ZoomMeetingListView(ListView):
+class ZoomMeetingListView(LoginRequiredMixin, ListView):
model = ZoomMeeting
template_name = "meetings/list_meetings.html"
context_object_name = "meetings"
@@ -121,7 +127,7 @@ class ZoomMeetingListView(ListView):
queryset = super().get_queryset().order_by("-start_time")
# Prefetch related interview data efficiently
- from django.db.models import Prefetch
+
queryset = queryset.prefetch_related(
Prefetch(
'interview', # related_name from ZoomMeeting to ScheduledInterview
@@ -161,13 +167,13 @@ class ZoomMeetingListView(ListView):
return context
-class ZoomMeetingDetailsView(DetailView):
+class ZoomMeetingDetailsView(LoginRequiredMixin, DetailView):
model = ZoomMeeting
template_name = "meetings/meeting_details.html"
context_object_name = "meeting"
-class ZoomMeetingUpdateView(UpdateView):
+class ZoomMeetingUpdateView(LoginRequiredMixin, UpdateView):
model = ZoomMeeting
form_class = ZoomMeetingForm
context_object_name = "meeting"
@@ -198,7 +204,7 @@ class ZoomMeetingUpdateView(UpdateView):
}
if instance.start_time < timezone.now():
messages.error(self.request, "Start time must be in the future.")
- return redirect(f"/update-meeting/{instance.pk}/", status=400)
+ return redirect(reverse("meeting_details", kwargs={"slug": instance.slug}))
result = update_meeting(instance, updated_data)
@@ -209,21 +215,22 @@ class ZoomMeetingUpdateView(UpdateView):
return redirect(reverse("meeting_details", kwargs={"slug": instance.slug}))
-def ZoomMeetingDeleteView(request, pk):
- meeting = get_object_or_404(ZoomMeeting, pk=pk)
- meeting_id = meeting.meeting_id
- try:
- result = delete_zoom_meeting(meeting_id)
- if result["status"] == "success":
- meeting.delete()
- messages.success(request, result["message"])
- else:
- messages.error(request, result["message"])
- return redirect("/")
- except Exception as e:
- messages.error(request, str(e))
- return redirect("/")
-
+def ZoomMeetingDeleteView(request, slug):
+ meeting = get_object_or_404(ZoomMeeting, slug=slug)
+ if "HX-Request" in request.headers:
+ return render(request, "meetings/delete_meeting_form.html", {"meeting": meeting,"delete_url": reverse("delete_meeting", kwargs={"slug": meeting.slug})})
+ if request.method == "POST":
+ try:
+ result = delete_zoom_meeting(meeting.meeting_id)
+ if result["status"] == "success" or "Meeting does not exist" in result["details"]["message"]:
+ meeting.delete()
+ messages.success(request, "Meeting deleted successfully.")
+ else:
+ messages.error(request, f"{result["message"]} , {result['details']["message"]}")
+ return redirect(reverse("list_meetings"))
+ except Exception as e:
+ messages.error(request, str(e))
+ return redirect(reverse("list_meetings"))
# Job Posting
# def job_list(request):
@@ -247,6 +254,7 @@ def ZoomMeetingDeleteView(request, pk):
# })
+@login_required
def create_job(request):
"""Create a new job posting"""
@@ -285,10 +293,11 @@ def create_job(request):
return render(request, "jobs/create_job.html", {"form": form})
+@login_required
def edit_job(request, slug):
"""Edit an existing job posting"""
+ job = get_object_or_404(JobPosting, slug=slug)
if request.method == "POST":
- job = get_object_or_404(JobPosting, slug=slug)
form = JobPostingForm(
request.POST,
instance=job,
@@ -321,14 +330,13 @@ def edit_job(request, slug):
return render(request, "jobs/edit_job.html", {"form": form, "job": job})
+@login_required
def job_detail(request, slug):
"""View details of a specific job"""
job = get_object_or_404(JobPosting, slug=slug)
- print(job)
# Get all candidates for this job, ordered by most recent
applicants = job.candidates.all().order_by("-created_at")
- print(applicants)
# Count candidates by stage for summary statistics
total_applicant = applicants.count()
@@ -341,12 +349,10 @@ def job_detail(request, slug):
offer_count = applicants.filter(stage="Offer").count()
-
status_form = JobPostingStatusForm(instance=job)
image_upload_form=JobPostingImageForm(instance=job)
-
# 2. Check for POST request (Status Update Submission)
if request.method == 'POST':
@@ -365,6 +371,19 @@ def job_detail(request, slug):
messages.error(request, "Failed to update status due to validation errors.")
+ category_data = applicants.filter(
+ major_category_name__isnull=False,
+ ai_analysis_data__match_score__isnull=False # This was part of the original query, ensure it's intentional
+ ).values('major_category_name').annotate(
+ candidate_count=Count('id'),
+ ai_analysis_data__avg_match_score=Avg('match_score', output_field=FloatField())
+ ).order_by('major_category_name')
+
+ # Prepare data for Chart.js
+ categories = [item['major_category_name'] for item in category_data]
+ candidate_counts = [item['candidate_count'] for item in category_data]
+ avg_scores = [round(item['avg_match_score'], 2) if item['avg_match_score'] is not None else 0 for item in category_data]
+
context = {
"job": job,
@@ -375,10 +394,14 @@ def job_detail(request, slug):
"interview_count": interview_count,
"offer_count": offer_count,
'status_form':status_form,
- 'image_upload_form':image_upload_form
+ 'image_upload_form':image_upload_form,
+ 'categories': categories,
+ 'candidate_counts': candidate_counts,
+ 'avg_scores': avg_scores
}
return render(request, "jobs/job_detail.html", context)
+@login_required
def job_image_upload(request, slug):
#only for handling the post request
job=get_object_or_404(JobPosting,slug=slug)
@@ -417,6 +440,7 @@ def job_detail_candidate(request, slug):
return render(request, "jobs/job_detail_candidate.html", {"job": job})
+@login_required
def post_to_linkedin(request, slug):
"""Post a job to LinkedIn"""
job = get_object_or_404(JobPosting, slug=slug)
@@ -516,243 +540,8 @@ def application_success(request,slug):
job=get_object_or_404(JobPosting,slug=slug)
return render(request,'jobs/application_success.html',{'job':job})
-
-
-
-
-# Form Preview Views
-# from django.http import JsonResponse
-# from django.views.decorators.csrf import csrf_exempt
-# from django.core.paginator import Paginator
-# from django.contrib.auth.decorators import login_required
-# import json
-
-# def form_list(request):
-# """Display list of all available forms"""
-# forms = Form.objects.filter(is_active=True).order_by('-created_at')
-
-# # Pagination
-# paginator = Paginator(forms, 12)
-# page_number = request.GET.get('page')
-# page_obj = paginator.get_page(page_number)
-
-# return render(request, 'forms/form_list.html', {
-# 'page_obj': page_obj
-# })
-
-# def form_preview(request, form_id):
-# """Display form preview for end users"""
-# form = get_object_or_404(Form, id=form_id, is_active=True)
-
-# # Get submission count for analytics
-# submission_count = form.submissions.count()
-
-# return render(request, 'forms/form_preview.html', {
-# 'form': form,
-# 'submission_count': submission_count,
-# 'is_embed': request.GET.get('embed', 'false') == 'true'
-# })
-
-# @csrf_exempt
-# def form_submit(request, form_id):
-# """Handle form submission via AJAX"""
-# if request.method != 'POST':
-# return JsonResponse({'success': False, 'error': 'Only POST method allowed'}, status=405)
-
-# form = get_object_or_404(Form, id=form_id, is_active=True)
-
-# try:
-# # Parse form data
-# submission_data = {}
-# files = {}
-
-# # Process regular form fields
-# for key, value in request.POST.items():
-# if key != 'csrfmiddlewaretoken':
-# submission_data[key] = value
-
-# # Process file uploads
-# for key, file in request.FILES.items():
-# if file:
-# files[key] = file
-
-# # Create form submission
-# submission = FormSubmission.objects.create(
-# form=form,
-# submission_data=submission_data,
-# ip_address=request.META.get('REMOTE_ADDR'),
-# user_agent=request.META.get('HTTP_USER_AGENT', '')
-# )
-
-# # Handle file uploads
-# for field_id, file in files.items():
-# UploadedFile.objects.create(
-# submission=submission,
-# field_id=field_id,
-# file=file,
-# original_filename=file.name
-# )
-
-# # TODO: Send email notification if configured
-
-# return JsonResponse({
-# 'success': True,
-# 'message': 'Form submitted successfully!',
-# 'submission_id': submission.id
-# })
-
-# except Exception as e:
-# logger.error(f"Error submitting form {form_id}: {e}")
-# return JsonResponse({
-# 'success': False,
-# 'error': 'An error occurred while submitting the form. Please try again.'
-# }, status=500)
-
-# def form_embed(request, form_id):
-# """Display embeddable version of form"""
-# form = get_object_or_404(Form, id=form_id, is_active=True)
-
-# return render(request, 'forms/form_embed.html', {
-# 'form': form,
-# 'is_embed': True
-# })
-
-# @login_required
-# def save_form_builder(request):
-# """Save form from builder to database"""
-# if request.method != 'POST':
-# return JsonResponse({'success': False, 'error': 'Only POST method allowed'}, status=405)
-
-# try:
-# data = json.loads(request.body)
-# form_data = data.get('form', {})
-
-# # Check if this is an update or create
-# form_id = data.get('form_id')
-
-# if form_id:
-# # Update existing form
-# form = Form.objects.get(id=form_id, created_by=request.user)
-# form.title = form_data.get('title', 'Untitled Form')
-# form.description = form_data.get('description', '')
-# form.structure = form_data
-# form.save()
-# else:
-# # Create new form
-# form = Form.objects.create(
-# title=form_data.get('title', 'Untitled Form'),
-# description=form_data.get('description', ''),
-# structure=form_data,
-# created_by=request.user
-# )
-
-# return JsonResponse({
-# 'success': True,
-# 'form_id': form.id,
-# 'message': 'Form saved successfully!'
-# })
-
-# except json.JSONDecodeError:
-# return JsonResponse({
-# 'success': False,
-# 'error': 'Invalid JSON data'
-# }, status=400)
-# except Exception as e:
-# logger.error(f"Error saving form: {e}")
-# return JsonResponse({
-# 'success': False,
-# 'error': 'An error occurred while saving the form'
-# }, status=500)
-
-# @login_required
-# def load_form(request, form_id):
-# """Load form data for editing in builder"""
-# try:
-# form = get_object_or_404(Form, id=form_id, created_by=request.user)
-
-# return JsonResponse({
-# 'success': True,
-# 'form': {
-# 'id': form.id,
-# 'title': form.title,
-# 'description': form.description,
-# 'structure': form.structure
-# }
-# })
-
-# except Exception as e:
-# logger.error(f"Error loading form {form_id}: {e}")
-# return JsonResponse({
-# 'success': False,
-# 'error': 'An error occurred while loading the form'
-# }, status=500)
-
-# @csrf_exempt
-# def update_form_builder(request, form_id):
-# """Update existing form from builder"""
-# if request.method != 'POST':
-# return JsonResponse({'success': False, 'error': 'Only POST method allowed'}, status=405)
-
-# try:
-# form = get_object_or_404(Form, id=form_id)
-
-# # Check if user has permission to edit this form
-# if form.created_by != request.user:
-# return JsonResponse({
-# 'success': False,
-# 'error': 'You do not have permission to edit this form'
-# }, status=403)
-
-# data = json.loads(request.body)
-# form_data = data.get('form', {})
-
-# # Update form
-# form.title = form_data.get('title', 'Untitled Form')
-# form.description = form_data.get('description', '')
-# form.structure = form_data
-# form.save()
-
-# return JsonResponse({
-# 'success': True,
-# 'form_id': form.id,
-# 'message': 'Form updated successfully!'
-# })
-
-# except json.JSONDecodeError:
-# return JsonResponse({
-# 'success': False,
-# 'error': 'Invalid JSON data'
-# }, status=400)
-# except Exception as e:
-# logger.error(f"Error updating form {form_id}: {e}")
-# return JsonResponse({
-# 'success': False,
-# 'error': 'An error occurred while updating the form'
-# }, status=500)
-
-# def edit_form(request, form_id):
-# """Display form edit page"""
-# form = get_object_or_404(Form, id=form_id)
-
-# # Check if user has permission to edit this form
-# if form.created_by != request.user:
-# messages.error(request, 'You do not have permission to edit this form.')
-# return redirect('form_list')
-
-# return render(request, 'forms/edit_form.html', {
-# 'form': form
-# })
-
-# def form_submissions(request, form_id):
-# """View submissions for a specific form"""
-# form = get_object_or_404(Form, id=form_id, created_by=request.user)
-# submissions = form.submissions.all().order_by('-submitted_at')
-
-# # Pagination
-#
-
-
@ensure_csrf_cookie
+@login_required
def form_builder(request, template_id=None):
"""Render the form builder interface"""
context = {}
@@ -878,6 +667,7 @@ def load_form_template(request, template_id):
)
+@login_required
def form_templates_list(request):
"""List all form templates for the current user"""
query = request.GET.get("q", "")
@@ -898,6 +688,7 @@ def form_templates_list(request):
return render(request, "forms/form_templates_list.html", context)
+@login_required
def create_form_template(request):
"""Create a new form template"""
if request.method == "POST":
@@ -916,6 +707,7 @@ def create_form_template(request):
return render(request, "forms/create_form_template.html", {"form": form})
+@login_required
@require_http_methods(["GET"])
def list_form_templates(request):
"""List all form templates for the current user"""
@@ -925,6 +717,7 @@ def list_form_templates(request):
return JsonResponse({"success": True, "templates": list(templates)})
+@login_required
@require_http_methods(["DELETE"])
def delete_form_template(request, template_id):
"""Delete a form template"""
@@ -1038,6 +831,7 @@ def submit_form(request, template_id):
)
+@login_required
def form_template_submissions_list(request, slug):
"""List all submissions for a specific form template"""
template = get_object_or_404(FormTemplate, slug=slug)
@@ -1058,6 +852,7 @@ def form_template_submissions_list(request, slug):
)
+@login_required
def form_template_all_submissions(request, template_id):
"""Display all submissions for a form template in table format"""
template = get_object_or_404(FormTemplate, id=template_id)
@@ -1084,6 +879,7 @@ def form_template_all_submissions(request, template_id):
)
+@login_required
def form_submission_details(request, template_id, slug):
"""Display detailed view of a specific form submission"""
# Get the form template and verify ownership
@@ -1123,6 +919,7 @@ def _handle_get_request(request, slug, job):
from the session for persistence.
"""
SESSION_KEY = f"schedule_candidate_ids_{slug}"
+
form = InterviewScheduleForm(slug=slug)
# break_formset = BreakTimeFormSet(prefix='breaktime')
@@ -1131,6 +928,7 @@ def _handle_get_request(request, slug, job):
# 1. Capture IDs from HTMX request and store in session (when first clicked)
if "HX-Request" in request.headers:
candidate_ids = request.GET.getlist("candidate_ids")
+
if candidate_ids:
request.session[SESSION_KEY] = candidate_ids
selected_ids = candidate_ids
@@ -1340,90 +1138,6 @@ def _handle_confirm_schedule(request, slug, job):
if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY]
return redirect("job_detail", slug=slug)
-# def _handle_confirm_schedule(request, slug, job):
-# """
-# Handles the final POST request (Confirm Schedule).
-# Creates all database records (Schedule, Meetings, Interviews) and clears sessions.
-# """
-# SESSION_DATA_KEY = "interview_schedule_data"
-# SESSION_ID_KEY = f"schedule_candidate_ids_{slug}"
-
-# # 1. Get schedule data from session
-# schedule_data = request.session.get(SESSION_DATA_KEY)
-
-# if not schedule_data:
-# messages.error(request, "Session expired. Please try again.")
-# return redirect("schedule_interviews", slug=slug)
-# # 2. Create the Interview Schedule (Your existing logic)
-# schedule = InterviewSchedule.objects.create(
-# job=job,
-# created_by=request.user,
-# start_date=datetime.fromisoformat(schedule_data["start_date"]).date(),
-# end_date=datetime.fromisoformat(schedule_data["end_date"]).date(),
-# working_days=schedule_data["working_days"],
-# start_time=time.fromisoformat(schedule_data["start_time"]),
-# end_time=time.fromisoformat(schedule_data["end_time"]),
-# interview_duration=schedule_data["interview_duration"],
-# buffer_time=schedule_data["buffer_time"],
-# break_start_time=schedule_data["break_start_time"],
-# break_end_time=schedule_data["break_end_time"],
-# )
-
-# # 3. Setup candidates and get slots
-# candidates = Candidate.objects.filter(id__in=schedule_data["candidate_ids"])
-# schedule.candidates.set(candidates)
-# available_slots = get_available_time_slots(schedule)
-
-# # 4. Create scheduled interviews
-# scheduled_count = 0
-# for i, candidate in enumerate(candidates):
-# if i < len(available_slots):
-# slot = available_slots[i]
-# interview_datetime = datetime.combine(slot['date'], slot['time'])
-
-# meeting_topic = f"Interview for {job.title} - {candidate.name}"
-# result = create_zoom_meeting(meeting_topic, interview_datetime, schedule.interview_duration)
-
-# if result["status"] == "success":
-# zoom_meeting = ZoomMeeting.objects.create(
-# topic=meeting_topic,
-# start_time=interview_datetime,
-# duration=schedule.interview_duration,
-# meeting_id=result["meeting_details"]["meeting_id"],
-# join_url=result["meeting_details"]["join_url"],
-# zoom_gateway_response=result["zoom_gateway_response"],
-# )
-# ScheduledInterview.objects.create(
-# candidate=candidate,
-# job=job,
-# zoom_meeting=zoom_meeting,
-# schedule=schedule,
-# interview_date=slot['date'],
-# interview_time=slot['time']
-# )
-# scheduled_count += 1
-# else:
-# messages.error(request, result["message"])
-# schedule.delete()
-# # Clear candidate IDs session key only on error return
-# if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY]
-# return redirect("candidate_interview_view", slug=slug)
-
-# # 5. Success and Cleanup
-# messages.success(
-# request, f"Successfully scheduled {scheduled_count} interviews."
-# )
-
-# # Clear both session data keys upon successful completion
-# if SESSION_DATA_KEY in request.session:
-# del request.session[SESSION_DATA_KEY]
-# if SESSION_ID_KEY in request.session:
-# del request.session[SESSION_ID_KEY]
-
-# return redirect("job_detail", slug=slug)
-
-
-# --- Main View Function ---
def schedule_interviews_view(request, slug):
job = get_object_or_404(JobPosting, slug=slug)
@@ -1436,524 +1150,15 @@ def confirm_schedule_interviews_view(request, slug):
job = get_object_or_404(JobPosting, slug=slug)
if request.method == "POST":
return _handle_confirm_schedule(request, slug, job)
-# def schedule_interviews_view(request, slug):
-# job = get_object_or_404(JobPosting, slug=slug)
-# SESSION_KEY = f"schedule_candidate_ids_{slug}"
-# if request.method == "POST":
-# form = InterviewScheduleForm(slug, request.POST)
-# break_formset = BreakTimeFormSet(request.POST)
-
-# # Check if this is a confirmation request
-# if "confirm_schedule" in request.POST:
-# # Get the schedule data from session
-# schedule_data = request.session.get("interview_schedule_data")
-# if not schedule_data:
-# messages.error(request, "Session expired. Please try again.")
-# return redirect("schedule_interviews", slug=slug)
-
-# # Create the interview schedule
-# schedule = InterviewSchedule.objects.create(
-# job=job,
-# created_by=request.user,
-# start_date=datetime.fromisoformat(schedule_data["start_date"]).date(),
-# end_date=datetime.fromisoformat(schedule_data["end_date"]).date(),
-# working_days=schedule_data["working_days"],
-# start_time=time.fromisoformat(schedule_data["start_time"]),
-# end_time=time.fromisoformat(schedule_data["end_time"]),
-# interview_duration=schedule_data["interview_duration"],
-# buffer_time=schedule_data["buffer_time"],
-# breaks=schedule_data["breaks"],
-# )
-
-# # Add candidates to the schedule
-# candidates = Candidate.objects.filter(id__in=schedule_data["candidate_ids"])
-# schedule.candidates.set(candidates)
-
-# # Create temporary break time objects for slot calculation
-# temp_breaks = []
-# for break_data in schedule_data["breaks"]:
-# temp_breaks.append(
-# BreakTime(
-# start_time=datetime.strptime(
-# break_data["start_time"], "%H:%M:%S"
-# ).time(),
-# end_time=datetime.strptime(
-# break_data["end_time"], "%H:%M:%S"
-# ).time(),
-# )
-# )
-
-# # Get available slots
-# available_slots = get_available_time_slots(schedule)
-
-# # Create scheduled interviews
-# scheduled_count = 0
-# for i, candidate in enumerate(candidates):
-# if i < len(available_slots):
-# slot = available_slots[i]
-# interview_datetime = datetime.combine(slot['date'], slot['time'])
-
-# # Create Zoom meeting
-# meeting_topic = f"Interview for {job.title} - {candidate.name}"
-
-# start_time = interview_datetime
-
-# # zoom_meeting = create_zoom_meeting(
-# # topic=meeting_topic,
-# # start_time=start_time,
-# # duration=schedule.interview_duration
-# # )
-
-# result = create_zoom_meeting(meeting_topic, start_time, schedule.interview_duration)
-# if result["status"] == "success":
-# zoom_meeting = ZoomMeeting.objects.create(
-# topic=meeting_topic,
-# start_time=interview_datetime,
-# duration=schedule.interview_duration,
-# meeting_id=result["meeting_details"]["meeting_id"],
-# join_url=result["meeting_details"]["join_url"],
-# zoom_gateway_response=result["zoom_gateway_response"],
-# )
-# # Create scheduled interview record
-# ScheduledInterview.objects.create(
-# candidate=candidate,
-# job=job,
-# zoom_meeting=zoom_meeting,
-# schedule=schedule,
-# interview_date=slot['date'],
-# interview_time=slot['time']
-# )
-
-# else:
-# messages.error(request, result["message"])
-# schedule.delete()
-# return redirect("candidate_interview_view", slug=slug)
-
-# # Send email to candidate
-# # try:
-# # send_interview_email(scheduled_interview)
-# # except Exception as e:
-# # messages.warning(
-# # request,
-# # f"Interview scheduled for {candidate.name}, but failed to send email: {str(e)}"
-# # )
-
-# scheduled_count += 1
-
-# messages.success(
-# request, f"Successfully scheduled {scheduled_count} interviews."
-# )
-
-# # Clear the session data
-# if "interview_schedule_data" in request.session:
-# del request.session["interview_schedule_data"]
-
-# return redirect("job_detail", slug=slug)
-
-# # This is the initial form submission
-# if form.is_valid() and break_formset.is_valid():
-# # Get the form data
-# candidates = form.cleaned_data["candidates"]
-# start_date = form.cleaned_data["start_date"]
-# end_date = form.cleaned_data["end_date"]
-# working_days = form.cleaned_data["working_days"]
-# start_time = form.cleaned_data["start_time"]
-# end_time = form.cleaned_data["end_time"]
-# interview_duration = form.cleaned_data["interview_duration"]
-# buffer_time = form.cleaned_data["buffer_time"]
-
-# # Process break times
-# breaks = []
-# for break_form in break_formset:
-# if break_form.cleaned_data and not break_form.cleaned_data.get(
-# "DELETE"
-# ):
-# breaks.append(
-# {
-# "start_time": break_form.cleaned_data[
-# "start_time"
-# ].strftime("%H:%M:%S"),
-# "end_time": break_form.cleaned_data["end_time"].strftime("%H:%M:%S"),
-# }
-# )
-
-# # Create a temporary schedule object (not saved to DB)
-# temp_schedule = InterviewSchedule(
-# job=job,
-# start_date=start_date,
-# end_date=end_date,
-# working_days=working_days,
-# start_time=start_time,
-# end_time=end_time,
-# interview_duration=interview_duration,
-# buffer_time=buffer_time,
-# breaks=breaks,
-# )
-
-# # Create temporary break time objects
-# temp_breaks = []
-# for break_data in breaks:
-# temp_breaks.append(
-# BreakTime(
-# start_time=datetime.strptime(
-# break_data["start_time"], "%H:%M:%S"
-# ).time(),
-# end_time=datetime.strptime(
-# break_data["end_time"], "%H:%M:%S"
-# ).time(),
-# )
-# )
-
-# # Get available slots
-# available_slots = get_available_time_slots(temp_schedule)
-
-# if len(available_slots) < len(candidates):
-# messages.error(
-# request,
-# f"Not enough available slots. Required: {len(candidates)}, Available: {len(available_slots)}",
-# )
-# return render(
-# request,
-# "interviews/schedule_interviews.html",
-# {"form": form, "break_formset": break_formset, "job": job},
-# )
-
-# # Create a preview schedule
-# preview_schedule = []
-# for i, candidate in enumerate(candidates):
-# slot = available_slots[i]
-# preview_schedule.append(
-# {"candidate": candidate, "date": slot["date"], "time": slot["time"]}
-# )
-
-# # Save the form data to session for later use
-# schedule_data = {
-# "start_date": start_date.isoformat(),
-# "end_date": end_date.isoformat(),
-# "working_days": working_days,
-# "start_time": start_time.isoformat(),
-# "end_time": end_time.isoformat(),
-# "interview_duration": interview_duration,
-# "buffer_time": buffer_time,
-# "candidate_ids": [c.id for c in candidates],
-# "breaks": breaks,
-# }
-# request.session["interview_schedule_data"] = schedule_data
-
-# # Render the preview page
-# return render(
-# request,
-# "interviews/preview_schedule.html",
-# {
-# "job": job,
-# "schedule": preview_schedule,
-# "start_date": start_date,
-# "end_date": end_date,
-# "working_days": working_days,
-# "start_time": start_time,
-# "end_time": end_time,
-# "breaks": breaks,
-# "interview_duration": interview_duration,
-# "buffer_time": buffer_time,
-# },
-# )
-# else:
-# form = InterviewScheduleForm(slug=slug)
-# break_formset = BreakTimeFormSet()
-
-# selected_ids = []
-
-# # 1. Capture IDs from HTMX request and store in session (when first clicked from timeline)
-# if "HX-Request" in request.headers:
-# candidate_ids = request.GET.getlist("candidate_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:
-# selected_ids = request.session.get(SESSION_KEY, [])
-
-# # 3. Use the list of IDs to initialize the form
-# if selected_ids:
-# # Load Candidate objects corresponding to the IDs
-# candidates_to_load = Candidate.objects.filter(pk__in=selected_ids)
-# # This line sets the selected values for {{ form.candidates }}
-# form.initial["candidates"] = candidates_to_load
-
-# return render(
-# request,
-# "interviews/schedule_interviews.html",
-# {"form": form, "break_formset": break_formset, "job": job},
-# )
-
-# def schedule_interviews_view(request, slug):
-# job = get_object_or_404(JobPosting, slug=slug)
-
-# if request.method == "POST":
-# form = InterviewScheduleForm(slug, request.POST)
-# break_formset = BreakTimeFormSet(request.POST)
-
-# # Check if this is a confirmation request
-# if "confirm_schedule" in request.POST:
-# # Get the schedule data from session
-# schedule_data = request.session.get("interview_schedule_data")
-# if not schedule_data:
-# messages.error(request, "Session expired. Please try again.")
-# return redirect("schedule_interviews", slug=slug)
-
-# # Create the interview schedule
-# schedule = InterviewSchedule.objects.create(
-# job=job,
-# created_by=request.user,
-# start_date=datetime.fromisoformat(schedule_data["start_date"]).date(),
-# end_date=datetime.fromisoformat(schedule_data["end_date"]).date(),
-# working_days=schedule_data["working_days"],
-# start_time=time.fromisoformat(schedule_data["start_time"]),
-# end_time=time.fromisoformat(schedule_data["end_time"]),
-# interview_duration=schedule_data["interview_duration"],
-# buffer_time=schedule_data["buffer_time"],
-# breaks=schedule_data["breaks"], # Direct assignment for JSON field
-# )
-
-# # Add candidates to the schedule
-# candidates = Candidate.objects.filter(id__in=schedule_data["candidate_ids"])
-# schedule.candidates.set(candidates)
-
-# # Schedule the interviews
-# try:
-# scheduled_count = schedule_interviews(schedule)
-# messages.success(
-# request, f"Successfully scheduled {scheduled_count} interviews."
-# )
-# # Clear the session data
-# if "interview_schedule_data" in request.session:
-# del request.session["interview_schedule_data"]
-# return redirect("job_detail", slug=slug)
-# except Exception as e:
-# messages.error(request, f"Error scheduling interviews: {str(e)}")
-# return redirect("schedule_interviews", slug=slug)
-
-# # This is the initial form submission
-# if form.is_valid() and break_formset.is_valid():
-# # Get the form data
-# candidates = form.cleaned_data["candidates"]
-# start_date = form.cleaned_data["start_date"]
-# end_date = form.cleaned_data["end_date"]
-# working_days = form.cleaned_data["working_days"]
-# start_time = form.cleaned_data["start_time"]
-# end_time = form.cleaned_data["end_time"]
-# interview_duration = form.cleaned_data["interview_duration"]
-# buffer_time = form.cleaned_data["buffer_time"]
-
-# # Process break times
-# breaks = []
-# for break_form in break_formset:
-# if break_form.cleaned_data and not break_form.cleaned_data.get(
-# "DELETE"
-# ):
-# breaks.append(
-# {
-# "start_time": break_form.cleaned_data[
-# "start_time"
-# ].strftime("%H:%M:%S"),
-# "end_time": break_form.cleaned_data["end_time"].strftime("%H:%M:%S"),
-# }
-# )
-
-# # Create a temporary schedule object (not saved to DB)
-# temp_schedule = InterviewSchedule(
-# job=job,
-# start_date=start_date,
-# end_date=end_date,
-# working_days=working_days,
-# start_time=start_time,
-# end_time=end_time,
-# interview_duration=interview_duration,
-# buffer_time=buffer_time,
-# breaks=breaks, # Direct assignment for JSON field
-# )
-
-# # Get available slots
-# available_slots = get_available_time_slots(temp_schedule)
-
-# if len(available_slots) < len(candidates):
-# messages.error(
-# request,
-# f"Not enough available slots. Required: {len(candidates)}, Available: {len(available_slots)}",
-# )
-# return render(
-# request,
-# "interviews/schedule_interviews.html",
-# {"form": form, "break_formset": break_formset, "job": job},
-# )
-
-# # Create a preview schedule
-# preview_schedule = []
-# for i, candidate in enumerate(candidates):
-# slot = available_slots[i]
-# preview_schedule.append(
-# {"candidate": candidate, "date": slot["date"], "time": slot["time"]}
-# )
-
-# # Save the form data to session for later use
-# schedule_data = {
-# "start_date": start_date.isoformat(),
-# "end_date": end_date.isoformat(),
-# "working_days": working_days,
-# "start_time": start_time.isoformat(),
-# "end_time": end_time.isoformat(),
-# "interview_duration": interview_duration,
-# "buffer_time": buffer_time,
-# "candidate_ids": [c.id for c in candidates],
-# "breaks": breaks,
-# }
-# request.session["interview_schedule_data"] = schedule_data
-
-# # Render the preview page
-# return render(
-# request,
-# "interviews/preview_schedule.html",
-# {
-# "job": job,
-# "schedule": preview_schedule,
-# "start_date": start_date,
-# "end_date": end_date,
-# "working_days": working_days,
-# "start_time": start_time,
-# "end_time": end_time,
-# "breaks": breaks,
-# "interview_duration": interview_duration,
-# "buffer_time": buffer_time,
-# },
-# )
-# else:
-# form = InterviewScheduleForm(slug=slug)
-# break_formset = BreakTimeFormSet()
-
-# return render(
-# request,
-# "interviews/schedule_interviews.html",
-# {"form": form, "break_formset": break_formset, "job": job},
-# )
+@login_required
def candidate_screening_view(request, slug):
"""
Manage candidate tiers and stage transitions
"""
job = get_object_or_404(JobPosting, slug=slug)
- applied_count=job.candidates.filter(stage='Applied').count()
- exam_count=job.candidates.filter(stage='Exam').count()
- interview_count=job.candidates.filter(stage='Interview').count()
- offer_count=job.candidates.filter(stage='Offer').count()
- # Get all candidates for this job, ordered by match score (descending)
- candidates = job.candidates.filter(stage="Applied").order_by("-match_score")
-
-
-
- # Get tier categorization parameters
- # tier1_count = int(request.GET.get("tier1_count", 100))
-
- # # Categorize candidates into tiers
- # tier1_candidates = candidates[:tier1_count] if tier1_count > 0 else []
- # remaining_candidates = candidates[tier1_count:] if tier1_count > 0 else []
-
- # if len(remaining_candidates) > 0:
- # # Tier 2: Next 50% of remaining candidates
- # tier2_count = max(1, len(remaining_candidates) // 2)
- # tier2_candidates = remaining_candidates[:tier2_count]
- # tier3_candidates = remaining_candidates[tier2_count:]
- # else:
- # tier2_candidates = []
- # tier3_candidates = []
-
- # # Handle form submissions
- # if request.method == "POST":
- # # Update tier categorization
- # if "update_tiers" in request.POST:
- # tier1_count = int(request.POST.get("tier1_count", 100))
- # messages.success(request, f"Tier categorization updated. Tier 1: {tier1_count} candidates")
- # return redirect("candidate_screening_view", slug=slug)
-
- # # Update individual candidate stages
- # elif "update_stage" in request.POST:
- # candidate_id = request.POST.get("candidate_id")
- # new_stage = request.POST.get("new_stage")
- # candidate = get_object_or_404(Candidate, id=candidate_id, job=job)
-
- # if candidate.can_transition_to(new_stage):
- # old_stage = candidate.stage
- # candidate.stage = new_stage
- # candidate.save()
- # messages.success(request, f"Updated {candidate.name} from {old_stage} to {new_stage}")
- # else:
- # messages.error(request, f"Cannot transition {candidate.name} from {candidate.stage} to {new_stage}")
-
- # # Update exam status
- # elif "update_exam_status" in request.POST:
- # candidate_id = request.POST.get("candidate_id")
- # exam_status = request.POST.get("exam_status")
- # exam_date = request.POST.get("exam_date")
- # candidate = get_object_or_404(Candidate, id=candidate_id, job=job)
-
- # if candidate.stage == "Exam":
- # candidate.exam_status = exam_status
- # if exam_date:
- # candidate.exam_date = exam_date
- # candidate.save()
- # messages.success(request, f"Updated exam status for {candidate.name}")
- # else:
- # messages.error(request, f"Can only update exam status for candidates in Exam stage")
-
- # # Bulk stage update
- # elif "bulk_update_stage" in request.POST:
- # selected_candidates = request.POST.getlist("selected_candidates")
- # new_stage = request.POST.get("bulk_new_stage")
- # updated_count = 0
-
- # for candidate_id in selected_candidates:
- # candidate = get_object_or_404(Candidate, id=candidate_id, job=job)
- # if candidate.can_transition_to(new_stage):
- # candidate.stage = new_stage
- # candidate.save()
- # updated_count += 1
-
- # messages.success(request, f"Updated {updated_count} candidates to {new_stage} stage")
-
- # # Mark individual candidate as Candidate
- # elif "mark_as_candidate" in request.POST:
- # candidate_id = request.POST.get("candidate_id")
- # candidate = get_object_or_404(Candidate, id=candidate_id, job=job)
-
- # if candidate.applicant_status == "Applicant":
- # candidate.applicant_status = "Candidate"
- # candidate.save()
- # messages.success(request, f"Marked {candidate.name} as Candidate")
- # else:
- # messages.info(request, f"{candidate.name} is already marked as Candidate")
-
- # # Mark all Tier 1 candidates as Candidates
- # elif "mark_as_candidates" in request.POST:
- # updated_count = 0
- # for candidate in tier1_candidates:
- # if candidate.applicant_status == "Applicant":
- # candidate.applicant_status = "Candidate"
- # candidate.save()
- # updated_count += 1
-
- # if updated_count > 0:
- # messages.success(request, f"Marked {updated_count} Tier 1 candidates as Candidates")
- # else:
- # messages.info(request, "All Tier 1 candidates are already marked as Candidates")
-
- # Group candidates by current stage for display
- # stage_groups = {
- # "Applied": candidates.filter(stage="Applied"),
- # "Exam": candidates.filter(stage="Exam"),
- # "Interview": candidates.filter(stage="Interview"),
- # "Offer": candidates.filter(stage="Offer"),
- # }
+ candidates = job.screening_candidates
min_ai_score_str = request.GET.get('min_ai_score')
tier1_count_str = request.GET.get('tier1_count')
@@ -1985,42 +1190,28 @@ def candidate_screening_view(request, slug):
context = {
"job": job,
"candidates": candidates,
- # "stage_groups": stage_groups,
- # "tier1_count": tier1_count,
- # "total_candidates": candidates.count(),
'min_ai_score':min_ai_score,
'tier1_count':tier1_count,
- 'applied_count':applied_count,
- 'exam_count':exam_count,
- 'interview_count':interview_count,
- 'offer_count':offer_count,
"current_stage" : "Applied"
}
return render(request, "recruitment/candidate_screening_view.html", context)
+@login_required
def candidate_exam_view(request, slug):
"""
Manage candidate tiers and stage transitions
"""
job = get_object_or_404(JobPosting, slug=slug)
- applied_count=job.candidates.filter(stage='Applied').count()
- exam_count=job.candidates.filter(stage='Exam').count()
- interview_count=job.candidates.filter(stage='Interview').count()
- offer_count=job.candidates.filter(stage='Offer').count()
- candidates = job.candidates.filter(stage="Exam").order_by("-match_score")
context = {
"job": job,
- "candidates": candidates,
- 'applied_count':applied_count,
- 'exam_count':exam_count,
- 'interview_count':interview_count,
- 'offer_count':offer_count,
+ "candidates": job.exam_candidates,
'current_stage' : "Exam"
}
return render(request, "recruitment/candidate_exam_view.html", context)
+@login_required
def update_candidate_exam_status(request, slug):
candidate = get_object_or_404(Candidate, slug=slug)
if request.method == "POST":
@@ -2031,6 +1222,7 @@ def update_candidate_exam_status(request, slug):
else:
form = CandidateExamDateForm(request.POST, instance=candidate)
return render(request, "includes/candidate_exam_status_form.html", {"candidate": candidate,"form": form})
+@login_required
def bulk_update_candidate_exam_status(request,slug):
job = get_object_or_404(JobPosting, slug=slug)
status = request.headers.get('status')
@@ -2053,6 +1245,7 @@ def candidate_criteria_view_htmx(request, pk):
return render(request, "includes/candidate_modal_body.html", {"candidate": candidate})
+@login_required
def candidate_set_exam_date(request, slug):
candidate = get_object_or_404(Candidate, slug=slug)
candidate.exam_date = timezone.now()
@@ -2060,27 +1253,27 @@ def candidate_set_exam_date(request, slug):
messages.success(request, f"Set exam date for {candidate.name} to {candidate.exam_date}")
return redirect("candidate_screening_view", slug=candidate.job.slug)
+@login_required
def candidate_update_status(request, slug):
job = get_object_or_404(JobPosting, slug=slug)
mark_as = request.POST.get('mark_as')
- candidate_ids = request.POST.getlist("candidate_ids")
- if c := Candidate.objects.filter(pk__in = candidate_ids):
- c.update(stage=mark_as,exam_date=timezone.now(),applicant_status="Candidate" if mark_as in ["Exam","Interview","Offer"] else "Applicant")
+ if mark_as != '----------':
+ candidate_ids = request.POST.getlist("candidate_ids")
+ if c := Candidate.objects.filter(pk__in = candidate_ids):
+ c.update(stage=mark_as,exam_date=timezone.now(),applicant_status="Candidate" if mark_as in ["Exam","Interview","Offer"] else "Applicant")
- messages.success(request, f"Candidates Updated")
+ messages.success(request, f"Candidates Updated")
response = HttpResponse(redirect("candidate_screening_view", slug=job.slug))
response.headers["HX-Refresh"] = "true"
return response
+@login_required
def candidate_interview_view(request,slug):
job = get_object_or_404(JobPosting,slug=slug)
- applied_count=job.candidates.filter(stage='Applied').count()
- exam_count=job.candidates.filter(stage='Exam').count()
- interview_count=job.candidates.filter(stage='Interview').count()
- offer_count=job.candidates.filter(stage='Offer').count()
- context = {"job":job,"candidates":job.candidates.filter(stage="Interview").order_by("-match_score"),'applied_count':applied_count,'exam_count':exam_count,'interview_count':interview_count,'offer_count':offer_count,"current_stage":"Interview"}
+ context = {"job":job,"candidates":job.interview_candidates,'current_stage':'Interview'}
return render(request,"recruitment/candidate_interview_view.html",context)
+@login_required
def reschedule_meeting_for_candidate(request,slug,candidate_id,meeting_id):
job = get_object_or_404(JobPosting,slug=slug)
candidate = get_object_or_404(Candidate,pk=candidate_id)
@@ -2112,22 +1305,24 @@ def reschedule_meeting_for_candidate(request,slug,candidate_id,meeting_id):
return render(request,"meetings/reschedule_meeting.html",context)
+@login_required
def delete_meeting_for_candidate(request,slug,candidate_pk,meeting_id):
job = get_object_or_404(JobPosting,slug=slug)
candidate = get_object_or_404(Candidate,pk=candidate_pk)
meeting = get_object_or_404(ZoomMeeting,pk=meeting_id)
if request.method == "POST":
result = delete_zoom_meeting(meeting.meeting_id)
- if result["status"] == "success":
+ if result["status"] == "success" or "Meeting does not exist" in result["details"]["message"]:
meeting.delete()
- messages.success(request, result["message"])
+ messages.success(request, "Meeting deleted successfully")
else:
messages.error(request, result["message"])
return redirect(reverse("candidate_interview_view", kwargs={"slug": job.slug}))
- context = {"job":job,"candidate":candidate,"meeting":meeting}
+ context = {"job":job,"candidate":candidate,"meeting":meeting,'delete_url':reverse("delete_meeting_for_candidate",kwargs={"slug":job.slug,"candidate_pk":candidate_pk,"meeting_id":meeting_id})}
return render(request,"meetings/delete_meeting_form.html",context)
+@login_required
def interview_calendar_view(request, slug):
job = get_object_or_404(JobPosting, slug=slug)
@@ -2181,6 +1376,7 @@ def interview_calendar_view(request, slug):
return render(request, 'recruitment/interview_calendar.html', context)
+@login_required
def interview_detail_view(request, slug, interview_id):
job = get_object_or_404(JobPosting, slug=slug)
interview = get_object_or_404(
@@ -2711,6 +1907,7 @@ def schedule_meeting_for_candidate(request, slug, candidate_pk):
+@login_required
def user_detail(requests,pk):
user=get_object_or_404(User,pk=pk)
return render(requests,'user/profile.html')
@@ -2727,8 +1924,155 @@ def zoom_webhook_view(request):
payload = json.loads(request.body)
async_task("recruitment.tasks.handle_zoom_webhook_event", payload)
return HttpResponse(status=200)
-
except Exception:
- # Bad data or internal server error
return HttpResponse(status=400)
- return HttpResponse(status=405) # Method Not Allowed
\ No newline at end of file
+ return HttpResponse(status=405)
+
+
+# Meeting Comments Views
+@login_required
+def add_meeting_comment(request, slug):
+ """Add a comment to a meeting"""
+ meeting = get_object_or_404(ZoomMeeting, slug=slug)
+
+ if request.method == 'POST':
+ form = MeetingCommentForm(request.POST)
+ if form.is_valid():
+ comment = form.save(commit=False)
+ comment.meeting = meeting
+ comment.author = request.user
+ comment.save()
+ messages.success(request, 'Comment added successfully!')
+
+ # HTMX response - return just the comment section
+ if 'HX-Request' in request.headers:
+ return render(request, 'includes/comment_list.html', {
+ 'comments': meeting.comments.all().order_by('-created_at'),
+ 'meeting': meeting
+ })
+
+ return redirect('meeting_details', slug=slug)
+ else:
+ form = MeetingCommentForm()
+
+ context = {
+ 'form': form,
+ 'meeting': meeting,
+ }
+
+ # HTMX response - return the comment form
+ if 'HX-Request' in request.headers:
+ return render(request, 'includes/comment_form.html', context)
+
+ return redirect('meeting_details', slug=slug)
+
+
+@login_required
+def edit_meeting_comment(request, slug, comment_id):
+ """Edit a meeting comment"""
+ meeting = get_object_or_404(ZoomMeeting, slug=slug)
+ comment = get_object_or_404(MeetingComment, id=comment_id, meeting=meeting)
+
+ # Check if user is the author
+ if comment.author != request.user:
+ messages.error(request, 'You can only edit your own comments.')
+ return redirect('meeting_details', slug=slug)
+
+ if request.method == 'POST':
+ form = MeetingCommentForm(request.POST, instance=comment)
+ if form.is_valid():
+ form.save()
+ messages.success(request, 'Comment updated successfully!')
+
+ # HTMX response - return just the comment section
+ if 'HX-Request' in request.headers:
+ return render(request, 'includes/comment_list.html', {
+ 'comments': meeting.comments.all().order_by('-created_at'),
+ 'meeting': meeting
+ })
+
+ return redirect('meeting_details', slug=slug)
+ else:
+ form = MeetingCommentForm(instance=comment)
+
+ context = {
+ 'form': form,
+ 'meeting': meeting,
+ 'comment': comment,
+ }
+
+ # HTMX response - return the comment form
+ if 'HX-Request' in request.headers:
+ return render(request, 'includes/edit_comment_form.html', context)
+
+ return redirect('meeting_details', slug=slug)
+
+
+@login_required
+def delete_meeting_comment(request, slug, comment_id):
+ """Delete a meeting comment"""
+ meeting = get_object_or_404(ZoomMeeting, slug=slug)
+ comment = get_object_or_404(MeetingComment, id=comment_id, meeting=meeting)
+
+ # Check if user is the author
+ if comment.author != request.user and not request.user.is_staff:
+ messages.error(request, 'You can only delete your own comments.')
+ return redirect('meeting_details', slug=slug)
+
+ if request.method == 'POST':
+ comment.delete()
+ messages.success(request, 'Comment deleted successfully!')
+
+ # HTMX response - return just the comment section
+ if 'HX-Request' in request.headers:
+ return render(request, 'includes/comment_list.html', {
+ 'comments': meeting.comments.all().order_by('-created_at'),
+ 'meeting': meeting
+ })
+
+ return redirect('meeting_details', slug=slug)
+
+ # HTMX response - return the delete confirmation modal
+ if 'HX-Request' in request.headers:
+ return render(request, 'includes/delete_comment_form.html', {
+ 'meeting': meeting,
+ 'comment': comment,
+ 'delete_url': reverse('delete_meeting_comment', kwargs={'slug': slug, 'comment_id': comment_id})
+ })
+
+ return redirect('meeting_details', slug=slug)
+
+
+
+@login_required
+def set_meeting_candidate(request,slug):
+ meeting = get_object_or_404(ZoomMeeting, slug=slug)
+ if request.method == 'POST' and 'HX-Request' not in request.headers:
+ form = InterviewForm(request.POST)
+ if form.is_valid():
+ candidate = form.save(commit=False)
+ candidate.zoom_meeting = meeting
+ candidate.interview_date = meeting.start_time.date()
+ candidate.interview_time = meeting.start_time.time()
+ candidate.save()
+ messages.success(request, 'Candidate added successfully!')
+ return redirect('list_meetings')
+ job = request.GET.get("job")
+ form = InterviewForm()
+
+ if job:
+ form.fields['candidate'].queryset = Candidate.objects.filter(job=job)
+
+ else:
+ form.fields['candidate'].queryset = Candidate.objects.none()
+ form.fields['job'].widget.attrs.update({
+ 'hx-get': reverse('set_meeting_candidate', kwargs={'slug': slug}),
+ 'hx-target': '#div_id_candidate',
+ 'hx-select': '#div_id_candidate',
+ 'hx-swap': 'outerHTML'
+ })
+ context = {
+ "form": form,
+ "meeting": meeting
+ }
+ return render(request, 'meetings/set_candidate_form.html', context)
diff --git a/recruitment/views_frontend.py b/recruitment/views_frontend.py
index 51bf44f..5f71202 100644
--- a/recruitment/views_frontend.py
+++ b/recruitment/views_frontend.py
@@ -16,7 +16,8 @@ from django.contrib.messages.views import SuccessMessageMixin
from django.views.generic import ListView, CreateView, UpdateView, DeleteView, DetailView
# JobForm removed - using JobPostingForm instead
from django.urls import reverse_lazy
-from django.db.models import Q
+from django.db.models import Q, Count, Avg
+from django.db.models import FloatField
from datastar_py.django import (
DatastarResponse,
@@ -224,6 +225,7 @@ def training_list(request):
return render(request, 'recruitment/training_list.html', {'materials': materials})
+@login_required
def candidate_detail(request, slug):
from rich.json import JSON
candidate = get_object_or_404(models.Candidate, slug=slug)
@@ -235,7 +237,7 @@ def candidate_detail(request, slug):
# Create stage update form for staff users
stage_form = None
if request.user.is_staff:
- stage_form = forms.CandidateStageForm(candidate=candidate)
+ stage_form = forms.CandidateStageForm()
# parsed = JSON(json.dumps(parsed), indent=2, highlight=True, skip_keys=False, ensure_ascii=False, check_circular=True, allow_nan=True, default=None, sort_keys=False)
# parsed = json_to_markdown_table([parsed])
@@ -245,10 +247,11 @@ def candidate_detail(request, slug):
'stage_form': stage_form,
})
+@login_required
def candidate_update_stage(request, slug):
"""Handle HTMX stage update requests"""
candidate = get_object_or_404(models.Candidate, slug=slug)
- form = forms.CandidateStageForm(request.POST, candidate=candidate)
+ form = forms.CandidateStageForm(request.POST, instance=candidate)
if form.is_valid():
stage_value = form.cleaned_data['stage']
candidate.stage = stage_value
@@ -318,6 +321,7 @@ class TrainingDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
success_message = 'Training material deleted successfully.'
+@login_required
def dashboard_view(request):
total_jobs = models.JobPosting.objects.count()
total_candidates = models.Candidate.objects.count()
@@ -335,3 +339,79 @@ def dashboard_view(request):
'average_applications': average_applications,
}
return render(request, 'recruitment/dashboard.html', context)
+
+
+@login_required
+def candidate_offer_view(request, slug):
+ """View for candidates in the Offer stage"""
+ job = get_object_or_404(models.JobPosting, slug=slug)
+
+ # Filter candidates for this specific job and stage
+ candidates = job.offer_candidates
+
+ # Handle search
+ search_query = request.GET.get('search', '')
+ if search_query:
+ candidates = candidates.filter(
+ Q(first_name__icontains=search_query) |
+ Q(last_name__icontains=search_query) |
+ Q(email__icontains=search_query) |
+ Q(phone__icontains=search_query)
+ )
+
+ candidates = candidates.order_by('-created_at')
+
+ context = {
+ 'job': job,
+ 'candidates': candidates,
+ 'search_query': search_query,
+ 'current_stage': 'Offer',
+ }
+ return render(request, 'recruitment/candidate_offer_view.html', context)
+
+
+@login_required
+def update_candidate_status(request, job_slug, candidate_slug, stage_type, status):
+ """Handle exam/interview/offer status updates"""
+ from django.utils import timezone
+
+ job = get_object_or_404(models.JobPosting, slug=job_slug)
+ candidate = get_object_or_404(models.Candidate, slug=candidate_slug, job=job)
+ print(stage_type,status)
+
+ if request.method == "POST":
+ if stage_type == 'exam':
+ candidate.exam_status = status
+ candidate.exam_date = timezone.now()
+ candidate.save(update_fields=['exam_status', 'exam_date'])
+ elif stage_type == 'interview':
+ candidate.interview_status = status
+ candidate.interview_date = timezone.now()
+ candidate.save(update_fields=['interview_status', 'interview_date'])
+ elif stage_type == 'offer':
+ candidate.offer_status = status
+ candidate.offer_date = timezone.now()
+ candidate.save(update_fields=['offer_status', 'offer_date'])
+ messages.success(request, f"Candidate {status} successfully!")
+ else:
+ messages.error(request, "No changes made.")
+
+ if stage_type == 'exam':
+ return redirect('candidate_exam_view', job.slug)
+ elif stage_type == 'interview':
+ return redirect('candidate_interview_view', job.slug)
+ elif stage_type == 'offer':
+ return redirect('candidate_offer_view', job.slug)
+
+ return redirect('candidate_detail', candidate.slug)
+ else:
+ if stage_type == 'exam':
+ return render(request,"includes/candidate_update_exam_form.html",{'candidate':candidate,'job':job})
+ elif stage_type == 'interview':
+ return render(request,"includes/candidate_update_interview_form.html",{'candidate':candidate,'job':job})
+ elif stage_type == 'offer':
+ return render(request,"includes/candidate_update_offer_form.html",{'candidate':candidate,'job':job})
+
+
+# Removed incorrect JobDetailView class.
+# The job_detail view is handled by function-based view in recruitment.views
diff --git a/run.py b/run.py
index 7d1388c..cd38ff7 100644
--- a/run.py
+++ b/run.py
@@ -37,4 +37,8 @@ if __name__ == "__main__":
duration = 60
host_email = "your_zoom_email"
response = create_zoom_meeting(topic, start_time, duration, host_email)
- print(response.json())
\ No newline at end of file
+ print(response.json())
+
+
+
+
diff --git a/template_partials/__init__.py b/template_partials/__init__.py
new file mode 100644
index 0000000..579c83f
--- /dev/null
+++ b/template_partials/__init__.py
@@ -0,0 +1 @@
+# Template partials app
diff --git a/template_partials/apps.py b/template_partials/apps.py
new file mode 100644
index 0000000..e5dddab
--- /dev/null
+++ b/template_partials/apps.py
@@ -0,0 +1,7 @@
+from django.apps import AppConfig
+
+
+class TemplatePartialsConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'template_partials'
+ verbose_name = 'Template Partials'
diff --git a/templates/base.html b/templates/base.html
index ffe4948..819171c 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -1,5 +1,5 @@
{% load i18n static %}
-{% load partials %}
+
diff --git a/templates/forms/form_template_all_submissions.html b/templates/forms/form_template_all_submissions.html
index 9b0e498..b62f404 100644
--- a/templates/forms/form_template_all_submissions.html
+++ b/templates/forms/form_template_all_submissions.html
@@ -1,6 +1,6 @@
{% extends 'base.html' %}
{% load static i18n form_filters %}
-{% load partials %}
+
{% block title %}All Submissions for {{ template.name }} - ATS{% endblock %}
diff --git a/templates/forms/form_template_submissions_list.html b/templates/forms/form_template_submissions_list.html
index f552085..5ef90ba 100644
--- a/templates/forms/form_template_submissions_list.html
+++ b/templates/forms/form_template_submissions_list.html
@@ -1,6 +1,6 @@
{% extends 'base.html' %}
{% load static i18n crispy_forms_tags %}
-{% load partials %}
+
{% block title %}Submissions for {{ template.name }} - ATS{% endblock %}
diff --git a/templates/forms/form_templates_list.html b/templates/forms/form_templates_list.html
index 6c14e28..e20a366 100644
--- a/templates/forms/form_templates_list.html
+++ b/templates/forms/form_templates_list.html
@@ -1,6 +1,6 @@
{% extends 'base.html' %}
{% load static i18n crispy_forms_tags %}
-{% load partials %}
+
{% block title %}Form Templates - {{ block.super }}{% endblock %}
@@ -76,13 +76,13 @@
transform: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
-
+
/* Card Header (For Search/Filter Card) */
.card-header {
font-weight: 600;
padding: 1.25rem;
border-bottom: 1px solid var(--kaauh-border);
- background-color: var(--kaauh-gray-light);
+ background-color: var(--kaauh-gray-light);
}
/* Stats Theming */
@@ -205,7 +205,7 @@
{{ template.job|default:"N/A" }}
-
+
{# Stats #}
@@ -217,7 +217,7 @@
{% trans "Fields" %}
-
+
{# Description #}
{% if template.description %}
diff --git a/templates/icons/delete.html b/templates/icons/delete.html
index 56aef3d..a35dfcb 100644
--- a/templates/icons/delete.html
+++ b/templates/icons/delete.html
@@ -1,3 +1,3 @@
-