diff --git a/NorahUniversity/__pycache__/settings.cpython-313.pyc b/NorahUniversity/__pycache__/settings.cpython-313.pyc index 23e0028..b4559a9 100644 Binary files a/NorahUniversity/__pycache__/settings.cpython-313.pyc and b/NorahUniversity/__pycache__/settings.cpython-313.pyc differ diff --git a/NorahUniversity/settings.py b/NorahUniversity/settings.py index 8b79fea..6bb6388 100644 --- a/NorahUniversity/settings.py +++ b/NorahUniversity/settings.py @@ -57,6 +57,7 @@ INSTALLED_APPS = [ 'django_countries', 'django_celery_results', 'django_q', + 'widget_tweaks', 'easyaudit' ] @@ -69,13 +70,13 @@ SITE_ID = 1 LOGIN_REDIRECT_URL = '/dashboard/' -ACCOUNT_LOGOUT_REDIRECT_URL = '/' +ACCOUNT_LOGOUT_REDIRECT_URL = '/' -ACCOUNT_SIGNUP_REDIRECT_URL = '/dashboard/' +ACCOUNT_SIGNUP_REDIRECT_URL = '/dashboard/' -LOGIN_URL = '/accounts/login/' +LOGIN_URL = '/accounts/login/' diff --git a/recruitment/__pycache__/forms.cpython-313.pyc b/recruitment/__pycache__/forms.cpython-313.pyc index 4fbce26..c5aac7d 100644 Binary files a/recruitment/__pycache__/forms.cpython-313.pyc and b/recruitment/__pycache__/forms.cpython-313.pyc differ diff --git a/recruitment/__pycache__/models.cpython-313.pyc b/recruitment/__pycache__/models.cpython-313.pyc index 2b31f32..2cf2357 100644 Binary files a/recruitment/__pycache__/models.cpython-313.pyc and b/recruitment/__pycache__/models.cpython-313.pyc differ diff --git a/recruitment/__pycache__/urls.cpython-313.pyc b/recruitment/__pycache__/urls.cpython-313.pyc index cf248c2..4f3d897 100644 Binary files a/recruitment/__pycache__/urls.cpython-313.pyc and b/recruitment/__pycache__/urls.cpython-313.pyc differ diff --git a/recruitment/__pycache__/utils.cpython-313.pyc b/recruitment/__pycache__/utils.cpython-313.pyc index 56997da..fbf2b0d 100644 Binary files a/recruitment/__pycache__/utils.cpython-313.pyc and b/recruitment/__pycache__/utils.cpython-313.pyc differ diff --git a/recruitment/__pycache__/views.cpython-313.pyc b/recruitment/__pycache__/views.cpython-313.pyc index 251fa3e..433256d 100644 Binary files a/recruitment/__pycache__/views.cpython-313.pyc and b/recruitment/__pycache__/views.cpython-313.pyc differ diff --git a/recruitment/models.py b/recruitment/models.py index 3efe497..be2c445 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -273,12 +273,12 @@ class Candidate(Base): CANDIDATE = "Candidate", _("Candidate") # Stage transition validation constants - # STAGE_SEQUENCE = { - # "Applied": ["Exam", "Interview", "Offer"], - # "Exam": ["Interview", "Offer"], - # "Interview": ["Offer"], - # "Offer": [], # Final stage - no further transitions - # } + STAGE_SEQUENCE = { + "Applied": ["Exam", "Interview", "Offer"], + "Exam": ["Interview", "Offer"], + "Interview": ["Offer"], + "Offer": [], # Final stage - no further transitions + } job = models.ForeignKey( JobPosting, @@ -412,13 +412,13 @@ class Candidate(Base): # allowed_next_stages = self.STAGE_SEQUENCE.get(old_stage, []) # return new_stage in allowed_next_stages - # def get_available_stages(self): - # """Get list of stages this candidate can transition to""" - # if not self.pk: # New record - # return ["Applied"] + def get_available_stages(self): + """Get list of stages this candidate can transition to""" + if not self.pk: # New record + return ["Applied"] - # old_stage = self.__class__.objects.get(pk=self.pk).stage - # return self.STAGE_SEQUENCE.get(old_stage, []) + old_stage = self.__class__.objects.get(pk=self.pk).stage + return self.STAGE_SEQUENCE.get(old_stage, []) @property def submission(self): diff --git a/recruitment/urls.py b/recruitment/urls.py index 30dcb9c..4aefef5 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -68,6 +68,8 @@ urlpatterns = [ path('jobs//candidate_exam_view/', views.candidate_exam_view, name='candidate_exam_view'), path('jobs//candidate_interview_view/', views.candidate_interview_view, name='candidate_interview_view'), + path('jobs///reschedule_meeting_for_candidate//', views.reschedule_meeting_for_candidate, name='reschedule_meeting_for_candidate'), + path('jobs//update_candidate_exam_status/', views.update_candidate_exam_status, name='update_candidate_exam_status'), path('jobs//bulk_update_candidate_exam_status/', views.bulk_update_candidate_exam_status, name='bulk_update_candidate_exam_status'), @@ -100,7 +102,8 @@ urlpatterns = [ path('jobs//candidates//reschedule-meeting//', views.reschedule_candidate_meeting, name='reschedule_candidate_meeting'), path('api/jobs//candidates//reschedule-meeting//', views.api_reschedule_candidate_meeting, name='api_reschedule_candidate_meeting'), # New URL for simple page-based meeting scheduling - path('jobs//candidates//schedule-meeting-page/', views.schedule_meeting_for_candidate, name='schedule_meeting_for_candidate'), + path('jobs//candidates//schedule-meeting-page/', views.schedule_meeting_for_candidate, name='schedule_meeting_for_candidate'), + path('jobs//candidates//delete_meeting_for_candidate//', views.delete_meeting_for_candidate, name='delete_meeting_for_candidate'), # users urls diff --git a/recruitment/utils.py b/recruitment/utils.py index bb51ebf..b7b19a6 100644 --- a/recruitment/utils.py +++ b/recruitment/utils.py @@ -579,3 +579,38 @@ def get_candidates_from_request(request): except Exception as e: logger.error(e) yield None + + + +def update_meeting(instance, updated_data): + result = update_zoom_meeting(instance.meeting_id, updated_data) + if result["status"] == "success": + # Fetch the latest details from Zoom after successful update + details_result = get_zoom_meeting_details(instance.meeting_id) + + if details_result["status"] == "success": + zoom_details = details_result["meeting_details"] + # Update instance with fetched details + + instance.topic = zoom_details.get("topic", instance.topic) + + instance.duration = zoom_details.get("duration", instance.duration) + instance.join_url = zoom_details.get("join_url", instance.join_url) + instance.password = zoom_details.get("password", instance.password) + # Corrected status assignment: instance.status, not instance.password + instance.status = zoom_details.get("status") + + instance.zoom_gateway_response = details_result.get("meeting_details") # Store full response + instance.save() + logger.info(f"Successfully updated Zoom meeting {instance.meeting_id}.") + return {"status": "success", "message": "Zoom meeting updated successfully."} + elif details_result["status"] == "error": + # If fetching details fails, save with form data and log a warning + logger.warning( + f"Successfully updated Zoom meeting {instance.meeting_id}, but failed to fetch updated details. " + f"Error: {details_result.get('message', 'Unknown error')}" + ) + return {"status": "success", "message": "Zoom meeting updated successfully."} + + logger.warning(f"Failed to update Zoom meeting {instance.meeting_id}. Error: {result.get('message', 'Unknown error')}") + return {"status": "error", "message": result.get("message", "Zoom meeting update failed.")} \ No newline at end of file diff --git a/recruitment/views.py b/recruitment/views.py index 5c89c40..58b4be2 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -32,6 +32,7 @@ from .utils import ( create_zoom_meeting, delete_zoom_meeting, get_candidates_from_request, + update_meeting, update_zoom_meeting, get_zoom_meeting_details, schedule_interviews, @@ -171,39 +172,14 @@ 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) - result = update_zoom_meeting(instance.meeting_id, updated_data) + + result = update_meeting(instance, updated_data) if result["status"] == "success": - # Fetch the latest details from Zoom after successful update - details_result = get_zoom_meeting_details(instance.meeting_id) - - if details_result["status"] == "success": - zoom_details = details_result["meeting_details"] - # Update instance with fetched details - - instance.topic = zoom_details.get("topic", instance.topic) - - instance.duration = zoom_details.get("duration", instance.duration) - instance.join_url = zoom_details.get("join_url", instance.join_url) - instance.password = zoom_details.get("password", instance.password) - # Corrected status assignment: instance.status, not instance.password - instance.status = zoom_details.get("status") - - instance.zoom_gateway_response = details_result.get("meeting_details") # Store full response - instance.save() - messages.success(self.request, result["message"] + " Local data updated from Zoom.") - else: - # If fetching details fails, save with form data and log a warning - logger.warning( - f"Successfully updated Zoom meeting {instance.meeting_id}, but failed to fetch updated details. " - f"Error: {details_result.get('message', 'Unknown error')}" - ) - instance.save() # Save with data from the form - messages.success(self.request, result["message"] + " (Note: Could not refresh local data from Zoom.)") - return redirect(reverse("meeting_details", kwargs={"slug": instance.slug})) + messages.success(self.request, result["message"]) else: messages.error(self.request, result["message"]) - return redirect(reverse("meeting_details", kwargs={"slug": instance.slug})) + return redirect(reverse("meeting_details", kwargs={"slug": instance.slug})) def ZoomMeetingDeleteView(request, pk): @@ -1020,7 +996,7 @@ def submit_form(request, template_id): address=address.display_value, resume=resume.get_file if resume.is_file else None, job=submission.template.job, - + ) return redirect('application_success') @@ -1125,17 +1101,6 @@ def form_submission_details(request, template_id, slug): def schedule_interviews_view(request, slug): job = get_object_or_404(JobPosting, slug=slug) - - # if request.method == "POST" and "Datastar-Request" in request.headers: - # form = InterviewScheduleForm(slug=slug) - # break_formset = BreakTimeFormSet() - # form.initial["candidates"] = get_candidates_from_request(request) - # def response(): - # html = render_to_string("includes/schedule_interview_div.html",{"form": form, "break_formset": break_formset, "job": job}) - # yield SSE.patch_elements(html,"#candidateviewModalBody") - # return DatastarResponse(response()) - - if request.method == "POST": form = InterviewScheduleForm(slug, request.POST) break_formset = BreakTimeFormSet(request.POST) @@ -1193,16 +1158,15 @@ def schedule_interviews_view(request, slug): # Create Zoom meeting meeting_topic = f"Interview for {job.title} - {candidate.name}" - start_time = interview_datetime.isoformat() + "Z" + start_time = interview_datetime - zoom_meeting = create_zoom_meeting( - topic=meeting_topic, - start_time=start_time, - duration=schedule.interview_duration - ) + # 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, @@ -1212,16 +1176,19 @@ def schedule_interviews_view(request, slug): join_url=result["meeting_details"]["join_url"], zoom_gateway_response=result["zoom_gateway_response"], ) - - # Create scheduled interview record - scheduled_interview = ScheduledInterview.objects.create( - candidate=candidate, - job=job, - zoom_meeting=zoom_meeting, - schedule=schedule, - interview_date=slot['date'], - interview_time=slot['time'] - ) + # 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: @@ -1354,9 +1321,9 @@ def schedule_interviews_view(request, slug): else: form = InterviewScheduleForm(slug=slug) break_formset = BreakTimeFormSet() - print(request.headers) - if "Hx-Request" in request.headers: - form.initial["candidates"] = [Candidate.objects.get(pk=c[0]) for c in request.GET.items()] + if "HX-Request" in request.headers: + candidate_ids = request.GET.getlist("candidate_ids") + form.initial["candidates"] = Candidate.objects.filter(pk__in = candidate_ids) return render( request, @@ -1675,7 +1642,6 @@ def candidate_screening_view(request, slug): return render(request, "recruitment/candidate_screening_view.html", context) - def candidate_exam_view(request, slug): """ Manage candidate tiers and stage transitions @@ -1728,7 +1694,6 @@ 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") @@ -1739,14 +1704,55 @@ def candidate_update_status(request, slug): def candidate_interview_view(request,slug): job = get_object_or_404(JobPosting,slug=slug) - if "Datastar-Request" in request.headers: - for candidate in get_candidates_from_request(request): - print(candidate) - context = {"job":job,"candidates":job.candidates.all()} + context = {"job":job,"candidates":job.candidates.filter(stage="Interview").order_by("-match_score")} return render(request,"recruitment/candidate_interview_view.html",context) +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) + meeting = get_object_or_404(ZoomMeeting,pk=meeting_id) + form = ZoomMeetingForm(instance=meeting) + + if request.method == "POST": + form = ZoomMeetingForm(request.POST,instance=meeting) + if form.is_valid(): + instance = form.save(commit=False) + updated_data = { + "topic": instance.topic, + "start_time": instance.start_time.isoformat() + "Z", + "duration": instance.duration, + } + if instance.start_time < timezone.now(): + messages.error(request, "Start time must be in the future.") + return redirect("reschedule_meeting_for_candidate",slug=job.slug,candidate_id=candidate_id,meeting_id=meeting_id) + + result = update_meeting(instance, updated_data) + + if result["status"] == "success": + messages.success(request, result["message"]) + else: + messages.error(request, result["message"]) + return redirect(reverse("candidate_interview_view", kwargs={"slug": job.slug})) + + context = {"job":job,"candidate":candidate,"meeting":meeting,"form":form} + return render(request,"meetings/reschedule_meeting.html",context) +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": + meeting.delete() + messages.success(request, result["message"]) + else: + messages.error(request, result["message"]) + return redirect(reverse("candidate_interview_view", kwargs={"slug": job.slug})) + + context = {"job":job,"candidate":candidate,"meeting":meeting} + return render(request,"meetings/delete_meeting_form.html",context) def interview_calendar_view(request, slug): job = get_object_or_404(JobPosting, slug=slug) @@ -2230,12 +2236,12 @@ def reschedule_candidate_meeting(request, job_slug, candidate_pk, interview_pk): }) -def schedule_meeting_for_candidate(request, job_slug, candidate_pk): +def schedule_meeting_for_candidate(request, slug, candidate_pk): """ Handles GET to display a simple form for scheduling a meeting for a candidate. Handles POST to process the form, create the meeting, and redirect back. """ - job = get_object_or_404(JobPosting, slug=job_slug) + job = get_object_or_404(JobPosting, slug=slug) candidate = get_object_or_404(Candidate, pk=candidate_pk, job=job) if request.method == "POST": @@ -2253,14 +2259,15 @@ def schedule_meeting_for_candidate(request, job_slug, candidate_pk): if start_time_val <= timezone.now(): messages.error(request, "Start time must be in the future.") # Re-render form with error and initial data - return render(request, "recruitment/schedule_meeting_form.html", { - 'form': form, - 'job': job, - 'candidate': candidate, - 'initial_topic': topic_val, - 'initial_start_time': start_time_val.strftime('%Y-%m-%dT%H:%M') if start_time_val else '', - 'initial_duration': duration_val - }) + return redirect('candidate_interview_view', slug=job.slug) + # return render(request, "recruitment/schedule_meeting_form.html", { + # 'form': form, + # 'job': job, + # 'candidate': candidate, + # 'initial_topic': topic_val, + # 'initial_start_time': start_time_val.strftime('%Y-%m-%dT%H:%M') if start_time_val else '', + # 'initial_duration': duration_val + # }) # Create Zoom meeting using utility function # The create_zoom_meeting expects start_time as a datetime object @@ -2307,7 +2314,7 @@ def schedule_meeting_for_candidate(request, job_slug, candidate_pk): }) else: # Form validation errors - return render(request, "recruitment/schedule_meeting_form.html", { + return render(request, "meetings/schedule_meeting_form.html", { 'form': form, 'job': job, 'candidate': candidate, @@ -2322,7 +2329,7 @@ def schedule_meeting_for_candidate(request, job_slug, candidate_pk): 'duration': 60, # Default duration } form = ZoomMeetingForm(initial=initial_data) - return render(request, "recruitment/schedule_meeting_form.html", { + return render(request, "meetings/schedule_meeting_form.html", { 'form': form, 'job': job, 'candidate': candidate diff --git a/templates/base.html b/templates/base.html index de49a92..ffe4948 100644 --- a/templates/base.html +++ b/templates/base.html @@ -12,272 +12,18 @@ {% comment %} {% endcomment %} - + - {% block customCSS %}{% endblock %}
- {# Changed container to container-fluid and added max-width-1600 to inner div #}
- {% comment %}
- - info@kaauh.edu.sa -
-
- - +966 11 820 0000 -
{% endcomment %}
{% trans 'Saudi Vision 2030' %} @@ -297,7 +43,6 @@