diff --git a/recruitment/__pycache__/forms.cpython-313.pyc b/recruitment/__pycache__/forms.cpython-313.pyc index 4b8d695..4fbce26 100644 Binary files a/recruitment/__pycache__/forms.cpython-313.pyc and b/recruitment/__pycache__/forms.cpython-313.pyc differ diff --git a/recruitment/__pycache__/linkedin_service.cpython-313.pyc b/recruitment/__pycache__/linkedin_service.cpython-313.pyc index 5e09bbb..4e6d02a 100644 Binary files a/recruitment/__pycache__/linkedin_service.cpython-313.pyc and b/recruitment/__pycache__/linkedin_service.cpython-313.pyc differ diff --git a/recruitment/__pycache__/models.cpython-313.pyc b/recruitment/__pycache__/models.cpython-313.pyc index a9e9a06..2b31f32 100644 Binary files a/recruitment/__pycache__/models.cpython-313.pyc and b/recruitment/__pycache__/models.cpython-313.pyc differ diff --git a/recruitment/__pycache__/signals.cpython-313.pyc b/recruitment/__pycache__/signals.cpython-313.pyc index 61b103c..fc601be 100644 Binary files a/recruitment/__pycache__/signals.cpython-313.pyc and b/recruitment/__pycache__/signals.cpython-313.pyc differ diff --git a/recruitment/__pycache__/urls.cpython-313.pyc b/recruitment/__pycache__/urls.cpython-313.pyc index 3221ce3..cf248c2 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 4c58b5f..56997da 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 eceab8e..251fa3e 100644 Binary files a/recruitment/__pycache__/views.cpython-313.pyc and b/recruitment/__pycache__/views.cpython-313.pyc differ diff --git a/recruitment/migrations/0012_merge_20251014_1403.py b/recruitment/migrations/0012_merge_20251014_1403.py new file mode 100644 index 0000000..2827f2a --- /dev/null +++ b/recruitment/migrations/0012_merge_20251014_1403.py @@ -0,0 +1,14 @@ +# Generated by Django 5.2.6 on 2025-10-14 11:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0010_alter_scheduledinterview_schedule'), + ('recruitment', '0011_alter_jobpostingimage_job_and_more'), + ] + + operations = [ + ] diff --git a/recruitment/migrations/0013_alter_formtemplate_created_by.py b/recruitment/migrations/0013_alter_formtemplate_created_by.py new file mode 100644 index 0000000..cbdb0fb --- /dev/null +++ b/recruitment/migrations/0013_alter_formtemplate_created_by.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.6 on 2025-10-14 11:24 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0012_merge_20251014_1403'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='formtemplate', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='form_templates', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/recruitment/models.py b/recruitment/models.py index 91b82dc..3efe497 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, @@ -375,50 +375,50 @@ class Candidate(Base): return self.resume.size return 0 - def clean(self): - """Validate stage transitions""" - # Only validate if this is an existing record (not being created) - if self.pk and self.stage != self.__class__.objects.get(pk=self.pk).stage: - old_stage = self.__class__.objects.get(pk=self.pk).stage - allowed_next_stages = self.STAGE_SEQUENCE.get(old_stage, []) + # def clean(self): + # """Validate stage transitions""" + # # Only validate if this is an existing record (not being created) + # if self.pk and self.stage != self.__class__.objects.get(pk=self.pk).stage: + # old_stage = self.__class__.objects.get(pk=self.pk).stage + # allowed_next_stages = self.STAGE_SEQUENCE.get(old_stage, []) - if self.stage not in allowed_next_stages: - raise ValidationError( - { - "stage": f'Cannot transition from "{old_stage}" to "{self.stage}". ' - f"Allowed transitions: {', '.join(allowed_next_stages) or 'None (final stage)'}" - } - ) + # if self.stage not in allowed_next_stages: + # raise ValidationError( + # { + # "stage": f'Cannot transition from "{old_stage}" to "{self.stage}". ' + # f"Allowed transitions: {', '.join(allowed_next_stages) or 'None (final stage)'}" + # } + # ) - # Validate that the stage is a valid choice - if self.stage not in [choice[0] for choice in self.Stage.choices]: - raise ValidationError( - { - "stage": f"Invalid stage. Must be one of: {', '.join(choice[0] for choice in self.Stage.choices)}" - } - ) + # # Validate that the stage is a valid choice + # if self.stage not in [choice[0] for choice in self.Stage.choices]: + # raise ValidationError( + # { + # "stage": f"Invalid stage. Must be one of: {', '.join(choice[0] for choice in self.Stage.choices)}" + # } + # ) def save(self, *args, **kwargs): """Override save to ensure validation is called""" self.clean() # Call validation before saving super().save(*args, **kwargs) - def can_transition_to(self, new_stage): - """Check if a stage transition is allowed""" - if not self.pk: # New record - can be in Applied stage - return new_stage == "Applied" + # def can_transition_to(self, new_stage): + # """Check if a stage transition is allowed""" + # if not self.pk: # New record - can be in Applied stage + # return new_stage == "Applied" - old_stage = self.__class__.objects.get(pk=self.pk).stage - allowed_next_stages = self.STAGE_SEQUENCE.get(old_stage, []) - return new_stage in allowed_next_stages + # old_stage = self.__class__.objects.get(pk=self.pk).stage + # 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): @@ -544,7 +544,7 @@ class FormTemplate(Base): blank=True, help_text="Description of the form template" ) created_by = models.ForeignKey( - User, on_delete=models.CASCADE, related_name="form_templates" + User, on_delete=models.CASCADE, related_name="form_templates",null=True,blank=True ) is_active = models.BooleanField( default=False, help_text="Whether this template is active" diff --git a/recruitment/signals.py b/recruitment/signals.py index 2420f64..3795b13 100644 --- a/recruitment/signals.py +++ b/recruitment/signals.py @@ -7,10 +7,10 @@ from .models import FormField,FormStage,FormTemplate,Candidate,JobPosting logger = logging.getLogger(__name__) -@receiver(post_save, sender=JobPosting) -def create_form_for_job(sender, instance, created, **kwargs): - if created: - FormTemplate.objects.create(job=instance, is_active=True, name=instance.title) +# @receiver(post_save, sender=JobPosting) +# def create_form_for_job(sender, instance, created, **kwargs): +# if created: +# FormTemplate.objects.create(job=instance, is_active=True, name=instance.title) @receiver(post_save, sender=Candidate) def score_candidate_resume(sender, instance, created, **kwargs): if not instance.is_resume_parsed: diff --git a/recruitment/urls.py b/recruitment/urls.py index c75a741..2ed3d74 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -17,7 +17,6 @@ urlpatterns = [ path('jobs//candidate/application/success', views.application_success, name='application_success'), path('careers/',views.kaauh_career,name='kaauh_career'), - # LinkedIn Integration URLs path('jobs//post-to-linkedin/', views.post_to_linkedin, name='post_to_linkedin'), path('jobs/linkedin/login/', views.linkedin_login, name='linkedin_login'), @@ -34,7 +33,6 @@ urlpatterns = [ path('candidate//view/', views_frontend.candidate_detail, name='candidate_detail'), path('candidate//update-stage/', views_frontend.candidate_update_stage, name='candidate_update_stage'), - # Training URLs path('training/', views_frontend.TrainingListView.as_view(), name='training_list'), path('training/create/', views_frontend.TrainingCreateView.as_view(), name='training_create'), @@ -75,7 +73,8 @@ urlpatterns = [ path('htmx//candidate_criteria_view/', views.candidate_criteria_view_htmx, name='candidate_criteria_view_htmx'), path('htmx//candidate_set_exam_date/', views.candidate_set_exam_date, name='candidate_set_exam_date'), - path('htmx/bulk_candidate_move_to_exam/', views.bulk_candidate_move_to_exam, name='bulk_candidate_move_to_exam'), + + path('htmx//candidate_update_status/', views.candidate_update_status, name='candidate_update_status'), path('forms/form//submit/', views.submit_form, name='submit_form'), path('forms/form//', views.form_wizard_view, name='form_wizard'), diff --git a/recruitment/views.py b/recruitment/views.py index 9d30f3b..1564c97 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -5,7 +5,7 @@ from rich import print from django.template.loader import render_to_string from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods -from django.http import JsonResponse +from django.http import HttpResponse, JsonResponse from datetime import datetime,time,timedelta from django.views import View from django.db.models import Q @@ -262,12 +262,13 @@ def create_job(request): else: job.created_by = request.POST.get("created_by", "").strip() if not job.created_by: - job.created_by = "University Administrator" + job.created_by = request.user.username job.save() job_apply_url_relative=reverse('job_detail_candidate',kwargs={'slug':job.slug}) job_apply_url_absolute=request.build_absolute_uri(job_apply_url_relative) job.application_url=job_apply_url_absolute + FormTemplate.objects.create(job=job, is_active=True, name=job.title,created_by=request.user) job.save() messages.success(request, f'Job "{job.title}" created successfully!') return redirect("job_list") @@ -328,15 +329,15 @@ def job_detail(request, slug): # Count candidates by stage for summary statistics total_applicant = applicants.count() - + applied_count = applicants.filter(stage="Applied").count() exam_count=applicants.filter(stage="Exam").count - + interview_count = applicants.filter(stage="Interview").count() - + offer_count = applicants.filter(stage="Offer").count() - + status_form = JobPostingStatusForm(instance=job) image_upload_form=JobPostingImageForm(instance=job) @@ -1521,7 +1522,7 @@ def candidate_screening_view(request, slug): 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 @@ -1629,33 +1630,31 @@ def candidate_screening_view(request, slug): min_ai_score_str = request.GET.get('min_ai_score') tier1_count_str = request.GET.get('tier1_count') - + try: # Check if the string value exists and is not an empty string before conversion if min_ai_score_str: min_ai_score = int(min_ai_score_str) else: min_ai_score = 0 - + if tier1_count_str: tier1_count = int(tier1_count_str) else: tier1_count = 0 - + except ValueError: # This catches if the user enters non-numeric text (e.g., "abc") min_ai_score = 0 tier1_count = 0 - print(min_ai_score) - print(tier1_count) + # You can now safely use min_ai_score and tier1_count as integers (0 or greater) if min_ai_score > 0: candidates = candidates.filter(match_score__gte=min_ai_score) - print(candidates) - + if tier1_count > 0: candidates = candidates[:tier1_count] - + context = { "job": job, "candidates": candidates, @@ -1697,7 +1696,6 @@ def update_candidate_exam_status(request, slug): def bulk_update_candidate_exam_status(request,slug): job = get_object_or_404(JobPosting, slug=slug) status = request.headers.get('status') - if status: for candidate in get_candidates_from_request(request): try: @@ -1724,19 +1722,18 @@ 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) -def bulk_candidate_move_to_exam(request): - for candidate in get_candidates_from_request(request): - candidate.stage = "Exam" - candidate.applicant_status = "Candidate" - candidate.exam_date = timezone.now() - candidate.save() +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") - messages.success(request, f"Candidates Moved to Exam stage") - return redirect("candidate_screening_view", slug=candidate.job.slug) - # def response(): - # yield SSE.patch_elements("","") - # yield SSE.execute_script("console.log('hello world');") - # return DatastarResponse(response()) + 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") + response = HttpResponse(redirect("candidate_screening_view", slug=job.slug)) + response.headers["HX-Refresh"] = "true" + return response def candidate_interview_view(request,slug): job = get_object_or_404(JobPosting,slug=slug) diff --git a/templates/base.html b/templates/base.html index dc0738e..bd2fa28 100644 --- a/templates/base.html +++ b/templates/base.html @@ -30,7 +30,7 @@ padding-right: var(--bs-gutter-x, 0.75rem); /* Add Bootstrap padding for responsiveness */ padding-left: var(--bs-gutter-x, 0.75rem); } - + /* === Top Bar === */ .top-bar { background-color: white; @@ -77,7 +77,7 @@ box-shadow: 0 2px 6px rgba(0,0,0,0.12); } /* Change the outer navbar container to fluid, rely on inner max-width */ - .navbar-dark > .container { + .navbar-dark > .container { max-width: 100%; /* Override default container width */ } .nav-link { @@ -276,7 +276,7 @@
Princess Nourah bint Abdulrahman University
King Abdullah bin Abdulaziz University Hospital
- + KAAUH Logo @@ -325,7 +325,7 @@ {% trans "Form Templates" %} - + {% endcomment %} @@ -349,8 +349,8 @@ - - + + - + - + +
  • @@ -518,13 +518,13 @@ {% block content %} {% endblock %} - +