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 %} {{ user.username }} {% else %}
@@ -388,11 +388,11 @@
{% if user.profile_image %} {{ user.username }} {% else %}
+ style="width: 44px; height: 44px; font-size: 1.2rem;"> {{ user.username|first|upper }}
{% endif %} diff --git a/templates/base.html b/templates/base.html index a2bbb1e..4eb2044 100644 --- a/templates/base.html +++ b/templates/base.html @@ -459,7 +459,7 @@ }); } - form_loader(); + //form_loader(); try{ document.body.addEventListener('htmx:afterRequest', function(evt) { diff --git a/templates/forms/document_form.html b/templates/forms/document_form.html index 4eee056..a8b2f6b 100644 --- a/templates/forms/document_form.html +++ b/templates/forms/document_form.html @@ -1,5 +1,5 @@ {% load i18n %} -
+ {% csrf_token %}
+ +
diff --git a/templates/recruitment/applications_document_review_view.html b/templates/recruitment/applications_document_review_view.html index 3c8d7d1..5993b23 100644 --- a/templates/recruitment/applications_document_review_view.html +++ b/templates/recruitment/applications_document_review_view.html @@ -271,12 +271,13 @@ - + +