diff --git a/recruitment/forms.py b/recruitment/forms.py index 60e043a..5702b1f 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -1496,6 +1496,7 @@ class MessageForm(forms.ModelForm): self.helper.form_class = "g-3" self._filter_recipient_field() + self._filter_job_field() self.helper.layout = Layout( Row( @@ -1516,6 +1517,7 @@ class MessageForm(forms.ModelForm): """Filter job options based on user type""" if self.user.user_type == "agency": + print("jhjkshfjksd") job_assignments =AgencyJobAssignment.objects.filter( agency__user=self.user, @@ -1528,11 +1530,18 @@ class MessageForm(forms.ModelForm): print("Agency user job queryset:", self.fields["job"].queryset) elif self.user.user_type == "candidate": + print("sjhdakjhsdkjashkdjhskd") # Candidates can only see jobs they applied for + person=self.user.person_profile + print(person) + applications=person.applications.all() + print(applications) + self.fields["job"].queryset = JobPosting.objects.filter( - applications__person=self.user.person_profile, + applications__in=applications, ).distinct().order_by("-created_at") else: + print("shhadjkhkd") # Staff can see all jobs self.fields["job"].queryset = JobPosting.objects.filter( status="ACTIVE" @@ -2145,3 +2154,22 @@ KAAUH Hiring Team self.fields['message'].initial = initial_message + + +class InterviewResultForm(forms.ModelForm): + class Meta: + model = Interview + + fields = ['interview_result', 'result_comments'] + widgets = { + 'interview_result': forms.Select(attrs={ + 'class': 'form-select', # Standard Bootstrap class + 'required': 'required' + }), + 'result_comments': forms.Textarea(attrs={ + 'class': 'form-control', + 'rows': 3, + 'placeholder': 'Enter setting value', + 'required': True + }), + } \ No newline at end of file diff --git a/recruitment/migrations/0003_interview_interview_result_interview_result_comments.py b/recruitment/migrations/0003_interview_interview_result_interview_result_comments.py new file mode 100644 index 0000000..738e042 --- /dev/null +++ b/recruitment/migrations/0003_interview_interview_result_interview_result_comments.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2025-12-15 12:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0002_alter_source_name_alter_source_source_type'), + ] + + operations = [ + migrations.AddField( + model_name='interview', + name='interview_result', + field=models.CharField(blank=True, choices=[('passed', 'Passed'), ('failed', 'Failed'), ('on_hold', 'ON Hold')], default='on_hold', max_length=10, null=True, verbose_name='Interview Result'), + ), + migrations.AddField( + model_name='interview', + name='result_comments', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/recruitment/models.py b/recruitment/models.py index dced2ab..8564211 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -1122,6 +1122,11 @@ class Interview(Base): STARTED = "started", _("Started") ENDED = "ended", _("Ended") CANCELLED = "cancelled", _("Cancelled") + + class InterviewResult(models.TextChoices): + PASSED="passed",_("Passed") + FAILED="failed",_("Failed") + ON_HOLD="on_hold",_("ON Hold") location_type = models.CharField( max_length=10, @@ -1129,6 +1134,18 @@ class Interview(Base): verbose_name=_("Location Type"), db_index=True, ) + interview_result=models.CharField( + max_length=10, + choices=InterviewResult.choices, + verbose_name=_("Interview Result"), + null=True, + blank=True, + default='on_hold' + ) + result_comments=models.TextField( + null=True, + blank=True + ) # Common fields topic = models.CharField( diff --git a/recruitment/services/email_service.py b/recruitment/services/email_service.py index 35b3214..cdbcf06 100644 --- a/recruitment/services/email_service.py +++ b/recruitment/services/email_service.py @@ -56,34 +56,34 @@ class EmailService: return 0 - def send_single_email( - self, - user: User, - subject: str, - template_name: str, - context: dict, - from_email: str = settings.DEFAULT_FROM_EMAIL - ) -> int: - """ - Sends a single, template-based email to one user. - """ - recipient_list = [user.email] + # def send_single_email( + # self, + # user: User, + # subject: str, + # template_name: str, + # context: dict, + # from_email: str = settings.DEFAULT_FROM_EMAIL + # ) -> int: + # """ + # Sends a single, template-based email to one user. + # """ + # recipient_list = [user.email] - # 1. Render content from template - html_content = render_to_string(template_name, context) - # You can optionally render a plain text version as well: - # text_content = strip_tags(html_content) + # # 1. Render content from template + # html_content = render_to_string(template_name, context) + # # You can optionally render a plain text version as well: + # # text_content = strip_tags(html_content) - # 2. Call internal sender - return self._send_email_internal( - subject=subject, - body="", # Can be empty if html_content is provided - recipient_list=recipient_list, - from_email=from_email, - html_content=html_content - ) + # # 2. Call internal sender + # return self._send_email_internal( + # subject=subject, + # body="", # Can be empty if html_content is provided + # recipient_list=recipient_list, + # from_email=from_email, + # html_content=html_content + # ) - def send_bulk_email( + def send_email_service( self, recipient_emails: List[str], subject: str, diff --git a/recruitment/tasks.py b/recruitment/tasks.py index ceeaad3..68105e4 100644 --- a/recruitment/tasks.py +++ b/recruitment/tasks.py @@ -1534,7 +1534,7 @@ def send_job_closed_notification(job_id): ) -def send_bulk_email_task( +def send_email_task( recipient_emails, subject: str, template_name: str, @@ -1551,7 +1551,7 @@ def send_bulk_email_task( service = EmailService() # Execute the bulk sending method - processed_count = service.send_bulk_email( + processed_count = service.send_email_service( recipient_emails=recipient_emails, subject=subject, template_name=template_name, @@ -1565,33 +1565,33 @@ def send_bulk_email_task( "message": f"Attempted to send email to {len(recipient_emails)} recipients. Service reported processing {processed_count}." }) -def send_single_email_task( - recipient_emails, - subject: str, - template_name: str, - context: dict, -) -> str: - """ - Django-Q task to send a bulk email asynchronously. - """ - from .services.email_service import EmailService +# def send_single_email_task( +# recipient_emails, +# subject: str, +# template_name: str, +# context: dict, +# ) -> str: +# """ +# Django-Q task to send a bulk email asynchronously. +# """ +# from .services.email_service import EmailService - if not recipient_emails: - return json.dumps({"status": "error", "message": "No recipients provided."}) +# if not recipient_emails: +# return json.dumps({"status": "error", "message": "No recipients provided."}) - service = EmailService() +# service = EmailService() - # Execute the bulk sending method - processed_count = service.send_bulk_email( - recipient_emails=recipient_emails, - subject=subject, - template_name=template_name, - context=context, - ) +# # Execute the bulk sending method +# processed_count = service.send_bulk_email( +# recipient_emails=recipient_emails, +# subject=subject, +# template_name=template_name, +# context=context, +# ) - # The return value is stored in the result object for monitoring - return json.dumps({ - "status": "success", - "count": processed_count, - "message": f"Attempted to send email to {len(recipient_emails)} recipients. Service reported processing {processed_count}." - }) \ No newline at end of file +# # The return value is stored in the result object for monitoring +# return json.dumps({ +# "status": "success", +# "count": processed_count, +# "message": f"Attempted to send email to {len(recipient_emails)} recipients. Service reported processing {processed_count}." +# }) \ No newline at end of file diff --git a/recruitment/urls.py b/recruitment/urls.py index 1798112..ad41baa 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -83,6 +83,8 @@ urlpatterns = [ path("interviews/", views.interview_list, name="interview_list"), path("interviews//", views.interview_detail, name="interview_detail"), path("interviews//update_interview_status", views.update_interview_status, name="update_interview_status"), + path("interviews//update_interview_result", views.update_interview_result, name="update_interview_result"), + path("interviews//cancel_interview_for_application", views.cancel_interview_for_application, name="cancel_interview_for_application"), path("interview//interview-email/",views.send_interview_email,name="send_interview_email"), diff --git a/recruitment/views.py b/recruitment/views.py index 89f25c6..bfa2d13 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -94,6 +94,7 @@ from .forms import ( InterviewCancelForm, InterviewEmailForm, ApplicationStageForm, + InterviewResultForm ) from .utils import generate_random_password from django.views.decorators.csrf import csrf_exempt @@ -1764,6 +1765,7 @@ def _handle_confirm_schedule(request, slug, job): for i, application in enumerate(applications): if i < len(available_slots): slot = available_slots[i] + # schedule=ScheduledInterview.objects.create(application=application,job=job) async_task( "recruitment.tasks.create_interview_and_meeting", application.pk, @@ -2141,6 +2143,7 @@ def reschedule_meeting_for_application(request, slug): if request.method == "POST": if interview.location_type == "Remote": + form = ScheduledInterviewForm(request.POST) else: form = OnsiteScheduleInterviewUpdateForm(request.POST) @@ -3055,6 +3058,8 @@ def applicant_portal_dashboard(request): # Get candidate's documents using the Person documents property documents = applicant.documents.order_by("-created_at") + + print(documents) # Add password change form for modal password_form = PasswordResetForm() @@ -3683,7 +3688,7 @@ def message_create(request): subject=message.subject email_result=async_task( - "recruitment.tasks.send_bulk_email_task", + "recruitment.tasks.send_email_task", email_addresses, subject, # message, @@ -3750,7 +3755,7 @@ def message_create(request): and "HX-Request" in request.headers and request.user.user_type in ["candidate", "agency"] ): - print() + job_id = request.GET.get("job") if job_id: job = get_object_or_404(JobPosting, id=job_id) @@ -4252,7 +4257,7 @@ def cancel_interview_for_application(request, slug): Handles POST request to cancel an interview, setting the status and saving the form data (likely a reason for cancellation). """ - scheduled_interview = get_object_or_404(ScheduledInterview) + scheduled_interview = get_object_or_404(ScheduledInterview,slug=slug) form = InterviewCancelForm(request.POST, instance=scheduled_interview) if form.is_valid(): @@ -4276,6 +4281,33 @@ def cancel_interview_for_application(request, slug): return redirect("interview_detail", slug=scheduled_interview.slug) +@require_POST +@login_required # Assuming this should be protected +@staff_user_required # Assuming only staff can cancel +def update_interview_result(request,slug): + interview = get_object_or_404(Interview,slug=slug) + schedule=interview.scheduled_interview + form = InterviewResultForm(request.POST, instance=interview) + + if form.is_valid(): + + interview.save(update_fields=['interview_result', 'result_comments']) + + form.save() # Saves form data + + messages.success(request, _("Interview cancelled successfully.")) + return redirect("interview_detail", slug=schedule.slug) + else: + error_list = [ + f"{field}: {', '.join(errors)}" for field, errors in form.errors.items() + ] + error_message = _("Please correct the following errors: ") + " ".join( + error_list + ) + messages.error(request, error_message) + + return redirect("interview_detail", slug=schedule.slug) + @login_required @staff_user_required def agency_access_link_deactivate(request, slug): @@ -4724,6 +4756,7 @@ def application_signup(request, slug): @login_required @staff_user_required def interview_list(request): + """List all interviews with filtering and pagination""" interviews = ScheduledInterview.objects.select_related( "application", @@ -4750,7 +4783,7 @@ def interview_list(request): interviews = interviews.filter( Q(application__person__first_name=search_query) | Q(application__person__last_name__icontains=search_query) - | Q(application__person__email=search_query) + | Q(application__person__email__icontains=search_query) | Q(job__title__icontains=search_query) ) @@ -4779,8 +4812,11 @@ def interview_detail(request, slug): OnsiteScheduleInterviewUpdateForm, ) + + schedule = get_object_or_404(ScheduledInterview, slug=slug) interview = schedule.interview + interview_result_form=InterviewResultForm(instance=interview) application = schedule.application job = schedule.job print(interview.location_type) @@ -4803,6 +4839,7 @@ def interview_detail(request, slug): "interview_status_form": ScheduledInterviewUpdateStatusForm(), "cancel_form": InterviewCancelForm(instance=meeting), "interview_email_form": interview_email_form, + "interview_result_form":interview_result_form, } return render(request, "interviews/interview_detail.html", context) @@ -6486,7 +6523,7 @@ def send_interview_email(request, slug): # 2. Match the context expected by your task/service # We pass IDs for the sender/job to avoid serialization issues async_task( - "recruitment.tasks.send_bulk_email_task", + "recruitment.tasks.send_email_task", recipient_list, subject, "emails/email_template.html", @@ -6558,7 +6595,7 @@ def compose_application_email(request, slug): async_task( - "recruitment.tasks.send_bulk_email_task", + "recruitment.tasks.send_email_task", email_addresses, subject, # message, diff --git a/templates/applicant/partials/candidate_facing_base.html b/templates/applicant/partials/candidate_facing_base.html index 0dd7d6f..9d85b52 100644 --- a/templates/applicant/partials/candidate_facing_base.html +++ b/templates/applicant/partials/candidate_facing_base.html @@ -360,6 +360,7 @@ {% endif %} + {% if request.user.is_authenticated%} +
  • - + {% trans "My Profile" %}
  • +
  • @@ -432,6 +435,8 @@
  • + + {% endif %} diff --git a/templates/interviews/interview_detail.html b/templates/interviews/interview_detail.html index 9580c80..00f14dc 100644 --- a/templates/interviews/interview_detail.html +++ b/templates/interviews/interview_detail.html @@ -670,22 +670,9 @@