diff --git a/NorahUniversity/settings.py b/NorahUniversity/settings.py
index 775dc13..bd12b79 100644
--- a/NorahUniversity/settings.py
+++ b/NorahUniversity/settings.py
@@ -67,6 +67,7 @@ INSTALLED_APPS = [
"widget_tweaks",
"easyaudit",
"secured_fields",
+
]
diff --git a/recruitment/forms.py b/recruitment/forms.py
index b58f18c..565476d 100644
--- a/recruitment/forms.py
+++ b/recruitment/forms.py
@@ -1889,12 +1889,16 @@ class RemoteInterviewForm(forms.Form):
duration = forms.IntegerField(
min_value=1,
- required=False,
+ required=True,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'placeholder': 'Duration in minutes'
}),
- label=_('Duration (minutes)')
+ label=_('Duration (minutes)'),
+ error_messages={
+ 'required': _('Please enter how long the interview will last.'),
+ 'min_value': _('Duration must be at least 1 minute.')
+ }
)
@@ -1947,12 +1951,16 @@ class OnsiteInterviewForm(forms.Form):
)
duration = forms.IntegerField(
min_value=1,
- required=False,
+ required=True,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'placeholder': 'Duration in minutes'
}),
- label=_('Duration (minutes)')
+ label=_('Duration (minutes)'),
+ error_messages={
+ 'required': _('Please enter how long the interview will last.'),
+ 'min_value': _('Duration must be at least 1 minute.')
+ }
)
class ScheduledInterviewForm(forms.Form):
@@ -1975,12 +1983,16 @@ class ScheduledInterviewForm(forms.Form):
)
duration = forms.IntegerField(
min_value=1,
- required=False,
+ required=True,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'placeholder': 'Duration in minutes'
}),
- label=_('Duration (minutes)')
+ label=_('Duration (minutes)'),
+ error_messages={
+ 'required': _('Please enter how long the interview will last.'),
+ 'min_value': _('Duration must be at least 1 minute.')
+ }
)
def clean_start_time(self):
@@ -2010,12 +2022,16 @@ class OnsiteScheduleInterviewUpdateForm(forms.Form):
)
duration = forms.IntegerField(
min_value=1,
- required=False,
+ required=True,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'placeholder': 'Duration in minutes'
}),
- label=_('Duration (minutes)')
+ label=_('Duration (minutes)'),
+ error_messages={
+ 'required': _('Please enter how long the interview will last.'),
+ 'min_value': _('Duration must be at least 1 minute.')
+ }
)
physical_address = forms.CharField(
max_length=255,
@@ -2212,20 +2228,48 @@ Job: {job.title}
-class InterviewResultForm(forms.ModelForm):
- class Meta:
- model = Interview
+# 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
+# fields = ['interview_result', 'result_comments']
+# widgets = {
+# 'interview_result': forms.Select(attrs={
+# 'class': 'form-select', # Standard Bootstrap class
+# 'required': True
+# }),
+# 'result_comments': forms.Textarea(attrs={
+# 'class': 'form-control',
+# 'rows': 3,
+# 'placeholder': 'Enter setting value',
+# 'required': True
+# }),
+# }
+
+
+
+
+
+RESULT_CHOICES = (
+ ('passed', 'Passed'),
+ ('failed', 'Failed'),
+ ('on_hold', 'On Hold'),
+)
+
+class InterviewResultForm(forms.Form):
+
+ interview_result = forms.ChoiceField(
+ choices=RESULT_CHOICES,
+ widget=forms.Select(attrs={
+ 'class': 'form-select',
+ })
+ )
+
+
+ result_comments = forms.CharField(
+ widget=forms.Textarea(attrs={
+ 'class': 'form-control',
+ 'rows': 3,
+ 'placeholder': 'Enter result comment',
+ })
+ )
\ No newline at end of file
diff --git a/recruitment/tasks.py b/recruitment/tasks.py
index 0d47fea..23f37a3 100644
--- a/recruitment/tasks.py
+++ b/recruitment/tasks.py
@@ -1654,9 +1654,13 @@ def generate_interview_questions(schedule_id: int) -> dict:
if not questions:
return {"status": "error", "message": "No questions generated"}
-
- schedule.interview_questions.update(questions)
+
+ if schedule.interview_questions is None:
+ schedule.interview_questions={}
+
+ schedule.interview_questions=questions
schedule.save(update_fields=["interview_questions"])
+ # schedule.save(update_fields=["interview_questions"])
logger.info(f"Successfully generated questions for schedule {schedule_id}")
diff --git a/recruitment/views.py b/recruitment/views.py
index 5fce1f0..cfe4c05 100644
--- a/recruitment/views.py
+++ b/recruitment/views.py
@@ -246,7 +246,34 @@ class PersonCreateView(CreateView, LoginRequiredMixin, StaffOrAgencyRequiredMixi
if view == "job":
return redirect("application_create")
return super().form_valid(form)
+ def form_invalid(self, form):
+ """
+ Re-renders the form with error messages while maintaining the UI state.
+ """
+ messages.error(self.request, "There was an error saving the applicant. Please check the details below.")
+
+ # Optional: Add specific field errors as messages
+ for field, errors in form.errors.items():
+ for error in errors:
+ messages.error(self.request, f"{field.title()}: {error}")
+ view = self.request.POST.get("view")
+ agency_slug = self.request.POST.get("agency")
+
+
+ context = self.get_context_data(form=form)
+
+
+ context['view_type'] = view
+ context['agency_slug'] = agency_slug
+
+ if view == "portal":
+
+ return redirect('agency_portal_dashboard')
+
+
+ return self.render_to_response(context)
+
class PersonDetailView(DetailView, LoginRequiredMixin, StaffRequiredMixin):
model = Person
@@ -4149,6 +4176,8 @@ def interview_create_onsite(request, application_slug):
form.initial["topic"] = (
f"Interview for {application.job.title} - {application.name}"
)
+ messages.error(request, "Please fix the highlighted errors below.")
+
form = OnsiteInterviewForm()
form.initial["topic"] = (
@@ -4253,14 +4282,15 @@ def cancel_interview_for_application(request, slug):
def update_interview_result(request,slug):
interview = get_object_or_404(Interview,slug=slug)
schedule=interview.scheduled_interview
- form = InterviewResultForm(request.POST, instance=interview)
+ form = InterviewResultForm(request.POST)
if form.is_valid():
-
+ interview_result=form.cleaned_data.get("interview_result")
+ result_comments=form.cleaned_data.get("result_comments")
+ interview.interview_result=interview_result
+ interview.result_comments=result_comments
interview.save(update_fields=['interview_result', 'result_comments'])
- form.save() # Saves form data
-
messages.success(request, _(f"Interview result updated successfully to {interview.interview_result}."))
return redirect("interview_detail", slug=schedule.slug)
else:
@@ -4769,6 +4799,7 @@ def interview_list(request):
return render(request, "interviews/interview_list.html", context)
+from django_ratelimit.decorators import ratelimit
@login_required
@staff_user_required
def generate_ai_questions(request, slug):
@@ -4776,14 +4807,17 @@ def generate_ai_questions(request, slug):
from django_q.tasks import async_task
schedule = get_object_or_404(ScheduledInterview, slug=slug)
+ messages.info(request,_("Generating interview questions."))
if request.method == "POST":
# Queue the AI question generation task
+
task_id = async_task(
"recruitment.tasks.generate_interview_questions",
schedule.id,
- sync=True
+ sync=False
)
+
# if request.headers.get("X-Requested-With") == "XMLHttpRequest":
# return JsonResponse({
@@ -4830,7 +4864,7 @@ def interview_detail(request, slug):
)
schedule = get_object_or_404(ScheduledInterview, slug=slug)
interview = schedule.interview
- interview_result_form=InterviewResultForm(instance=interview)
+ interview_result_form=InterviewResultForm()
application = schedule.application
job = schedule.job
if interview.location_type == "Remote":
diff --git a/requirements.txt b/requirements.txt
index 30147be..a5534dc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -47,12 +47,16 @@ django-ckeditor-5==0.2.18
django-cors-headers==4.9.0
django-countries==7.6.1
django-crispy-forms==2.4
-django-easy-audit==1.3.7s
+django-easy-audit==1.3.7
django-extensions==4.1
+django-fernet-encrypted-fields==0.3.1
django-filter==25.1
django-js-asset==3.1.2
+django-mathfilters==1.0.0
django-picklefield==3.3
django-q2==1.8.0
+django-ratelimit==4.1.0
+django-secured-fields==0.4.4
django-template-partials==25.2
django-unfold==0.67.0
django-widget-tweaks==1.5.0
@@ -136,6 +140,7 @@ Pygments==2.19.2
PyJWT==2.10.1
PyMuPDF==1.26.4
pyparsing==3.2.5
+pypdf==6.4.2
PyPDF2==3.0.1
pypdfium2==4.30.0
PyPrind==2.11.3
@@ -206,9 +211,3 @@ wrapt==1.17.3
wurst==0.4
xlrd==2.0.2
xlsxwriter==3.2.9
-locust==2.32.0
-psutil==6.1.0
-matplotlib==3.9.2
-pandas==2.3.2
-faker==37.8.0
-requests==2.32.3
diff --git a/templates/applicant/partials/candidate_facing_base.html b/templates/applicant/partials/candidate_facing_base.html
index 9d85b52..ac55cf2 100644
--- a/templates/applicant/partials/candidate_facing_base.html
+++ b/templates/applicant/partials/candidate_facing_base.html
@@ -107,7 +107,7 @@
}
.dropdown-menu .dropdown-item:hover {
- background-color: var(--kaauh-teal);
+ border-color: var(--kaauh-teal);
color: white;
transform: translateX(4px);
}
@@ -369,7 +369,7 @@
{% if user.profile_image %}
{% else %}