From 05e32711520fb94d7f9f8022926dc2b61d07f347 Mon Sep 17 00:00:00 2001 From: Faheed Date: Wed, 22 Oct 2025 14:38:42 +0300 Subject: [PATCH] new chnage --- NorahUniversity/settings.py | 6 +- recruitment/forms.py | 88 ++-- recruitment/models.py | 52 +- recruitment/views.py | 29 +- templates/jobs/create_job.html | 233 ++++----- templates/jobs/edit_job.html | 233 ++++----- templates/jobs/job_detail.html | 262 ++++------ templates/jobs/job_list.html | 4 +- templates/meetings/meeting_details.html | 551 ++++++++------------ templates/recruitment/candidate_detail.html | 8 +- 10 files changed, 645 insertions(+), 821 deletions(-) diff --git a/NorahUniversity/settings.py b/NorahUniversity/settings.py index 5c00084..d741e46 100644 --- a/NorahUniversity/settings.py +++ b/NorahUniversity/settings.py @@ -135,9 +135,9 @@ WSGI_APPLICATION = 'NorahUniversity.wsgi.application' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'norahuniversity', - 'USER': 'norahuniversity', - 'PASSWORD': 'norahuniversity', + 'NAME': 'haikal_db', + 'USER': 'faheed', + 'PASSWORD': 'Faheed@215', 'HOST': '127.0.0.1', 'PORT': '5432', } diff --git a/recruitment/forms.py b/recruitment/forms.py index 93d73a1..faaeefb 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -290,6 +290,7 @@ class TrainingMaterialForm(forms.ModelForm): ) ) + class JobPostingForm(forms.ModelForm): """Form for creating and editing job postings""" @@ -298,12 +299,13 @@ class JobPostingForm(forms.ModelForm): fields = [ 'title', 'department', 'job_type', 'workplace_type', 'location_city', 'location_state', 'location_country', - 'description', 'qualifications', 'salary_range', 'benefits','application_start_date' - ,'application_deadline', 'application_instructions', - 'position_number', 'reporting_to', 'joining_date', - 'created_by','open_positions','hash_tags','max_applications' + 'description', 'qualifications', 'salary_range', 'benefits', + 'application_deadline', 'application_instructions', + 'position_number', 'reporting_to', + 'open_positions', 'hash_tags', 'max_applications' ] widgets = { + # Basic Information 'title': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Assistant Professor of Computer Science', @@ -321,6 +323,8 @@ class JobPostingForm(forms.ModelForm): 'class': 'form-select', 'required': True }), + + # Location 'location_city': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Boston' @@ -333,18 +337,25 @@ class JobPostingForm(forms.ModelForm): 'class': 'form-control', 'value': 'United States' }), + 'salary_range': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': '$60,000 - $80,000' }), - 'application_start_date': forms.DateInput(attrs={ - 'class': 'form-control', - 'type': 'date' - }), + + # Application Information + # 'application_url': forms.URLInput(attrs={ + # 'class': 'form-control', + # 'placeholder': 'https://university.edu/careers/job123', + # 'required': True + # }), + 'application_deadline': forms.DateInput(attrs={ 'class': 'form-control', - 'type': 'date' + 'type': 'date', + 'required': True }), + 'open_positions': forms.NumberInput(attrs={ 'class': 'form-control', 'min': 1, @@ -353,7 +364,10 @@ class JobPostingForm(forms.ModelForm): 'hash_tags': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': '#hiring,#jobopening', + # 'validators':validate_hash_tags, # Assuming this is available }), + + # Internal Information 'position_number': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'UNIV-2025-001' @@ -362,14 +376,7 @@ class JobPostingForm(forms.ModelForm): 'class': 'form-control', 'placeholder': 'Department Chair, Director, etc.' }), - 'joining_date': forms.DateInput(attrs={ - 'class': 'form-control', - 'type': 'date' - }), - 'created_by': forms.TextInput(attrs={ - 'class': 'form-control', - 'placeholder': 'University Administrator' - }), + 'max_applications': forms.NumberInput(attrs={ 'class': 'form-control', 'min': 1, @@ -377,40 +384,51 @@ class JobPostingForm(forms.ModelForm): }), } - def __init__(self,*args,**kwargs): - self.is_anonymous_user = kwargs.pop('is_anonymous_user', False) + def __init__(self, *args, **kwargs): + + # Now call the parent __init__ with remaining args super().__init__(*args, **kwargs) - if not self.instance.pk: - if not self.is_anonymous_user: - self.fields['created_by'].initial = 'University Administrator' - self.fields['location_city'].initial='Riyadh' - self.fields['location_state'].initial='Riyadh Province' - self.fields['location_country'].initial='Saudi Arabia' + if not self.instance.pk: # Creating new job posting + # self.fields['status'].initial = 'Draft' + self.fields['location_city'].initial = 'Riyadh' + self.fields['location_state'].initial = 'Riyadh Province' + self.fields['location_country'].initial = 'Saudi Arabia' def clean_hash_tags(self): - hash_tags=self.cleaned_data.get('hash_tags') + hash_tags = self.cleaned_data.get('hash_tags') if hash_tags: - tags=[tag.strip() for tag in hash_tags.split(',') if tag.strip()] + tags = [tag.strip() for tag in hash_tags.split(',') if tag.strip()] for tag in tags: if not tag.startswith('#'): - raise forms.ValidationError("Each hashtag must start with '#' symbol and must be comma(,) sepearted.") + raise forms.ValidationError( + "Each hashtag must start with '#' symbol and must be comma(,) sepearted.") return ','.join(tags) - return hash_tags + return hash_tags # Allow blank def clean_title(self): - title=self.cleaned_data.get('title') - if not title or len(title.strip())<3: + title = self.cleaned_data.get('title') + if not title or len(title.strip()) < 3: raise forms.ValidationError("Job title must be at least 3 characters long.") - if len(title)>200: + if len(title) > 200: raise forms.ValidationError("Job title cannot exceed 200 characters.") return title.strip() def clean_description(self): - description=self.cleaned_data.get('description') - if not description or len(description.strip())<20: + description = self.cleaned_data.get('description') + if not description or len(description.strip()) < 20: raise forms.ValidationError("Job description must be at least 20 characters long.") - return description.strip() + return description.strip() # to remove leading/trailing whitespace + + def clean_application_url(self): + url = self.cleaned_data.get('application_url') + if url: + validator = URLValidator() + try: + validator(url) + except forms.ValidationError: + raise forms.ValidationError('Please enter a valid URL (e.g., https://example.com)') + return url class JobPostingImageForm(forms.ModelForm): class Meta: diff --git a/recruitment/models.py b/recruitment/models.py index 91a1fb6..abae095 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -33,6 +33,7 @@ class Profile(models.Model): def __str__(self): return f"image for user {self.user}" + class JobPosting(Base): # Basic Job Information JOB_TYPES = [ @@ -66,16 +67,16 @@ class JobPosting(Base): # Job Details description = CKEditor5Field( 'Description', - config_name='extends' # Matches the config name you defined in settings.py + config_name='extends' # Matches the config name you defined in settings.py ) - qualifications = CKEditor5Field(blank=True,null=True, - config_name='extends' - ) + qualifications = CKEditor5Field(blank=True, null=True, + config_name='extends' + ) salary_range = models.CharField( max_length=200, blank=True, help_text="e.g., $60,000 - $80,000" ) - benefits = CKEditor5Field(blank=True,null=True,config_name='extends') + benefits = CKEditor5Field(blank=True, null=True, config_name='extends') # Application Information ---job detail apply link for the candidates application_url = models.URLField( @@ -84,10 +85,10 @@ class JobPosting(Base): null=True, blank=True, ) - application_start_date=models.DateField(null=True, blank=True) - application_deadline = models.DateField(db_index=True, null=True, blank=True) # Added index - application_instructions =CKEditor5Field( - blank=True, null=True,config_name='extends' + + application_deadline = models.DateField(db_index=True) # Added index + application_instructions = CKEditor5Field( + blank=True, null=True, config_name='extends' ) # Internal Tracking @@ -105,7 +106,7 @@ class JobPosting(Base): ("ARCHIVED", "Archived"), ] status = models.CharField( - db_index=True, max_length=20, choices=STATUS_CHOICES, default="DRAFT" # Added index + db_index=True, max_length=20, choices=STATUS_CHOICES, default="DRAFT" # Added index ) # hashtags for social media @@ -129,7 +130,7 @@ class JobPosting(Base): ) linkedin_posted_at = models.DateTimeField(null=True, blank=True) - published_at = models.DateTimeField(db_index=True, null=True, blank=True) # Added index + published_at = models.DateTimeField(db_index=True, null=True, blank=True) # Added index # University Specific Fields position_number = models.CharField( max_length=50, blank=True, help_text="University position number" @@ -137,7 +138,7 @@ class JobPosting(Base): reporting_to = models.CharField( max_length=100, blank=True, help_text="Who this position reports to" ) - joining_date = models.DateField(null=True, blank=True, help_text="Desired start date") + open_positions = models.PositiveIntegerField( default=1, help_text="Number of open positions for this job" ) @@ -149,11 +150,11 @@ class JobPosting(Base): null=True, blank=True, help_text="The system or channel from which this job posting originated or was first published.", - db_index=True # Explicitly index ForeignKey + db_index=True # Explicitly index ForeignKey ) max_applications = models.PositiveIntegerField( - default=1000, help_text="Maximum number of applications allowed",null=True,blank=True - ) + default=1000, help_text="Maximum number of applications allowed", null=True, blank=True + ) hiring_agency = models.ManyToManyField( "HiringAgency", blank=True, @@ -181,7 +182,7 @@ class JobPosting(Base): verbose_name = "Job Posting" verbose_name_plural = "Job Postings" indexes = [ - models.Index(fields=['status', 'created_at','title']), + models.Index(fields=['status', 'created_at', 'title']), models.Index(fields=['slug']), ] @@ -258,7 +259,6 @@ class JobPosting(Base): # Return True if any content remains after stripping tags and spaces. return bool(final_content) - @property def has_description_content(self): """Returns True if the description field has meaningful content.""" @@ -277,6 +277,7 @@ class JobPosting(Base): @property def has_application_instructions_content(self): return self._check_content(self.application_instructions) + @property def current_applications_count(self): """Returns the current number of candidates associated with this job.""" @@ -289,9 +290,13 @@ class JobPosting(Base): return True return self.current_applications_count >= self.max_applications + @property def all_candidates(self): - return self.candidates.annotate(sortable_score=Coalesce(Cast('ai_analysis_data__analysis_data__match_score',output_field=IntegerField()),0)).order_by('-sortable_score') + return self.candidates.annotate( + sortable_score=Coalesce(Cast('ai_analysis_data__analysis_data__match_score', output_field=IntegerField()), + 0)).order_by('-sortable_score') + @property def screening_candidates(self): return self.all_candidates.filter(stage="Applied") @@ -299,6 +304,7 @@ class JobPosting(Base): @property def exam_candidates(self): return self.all_candidates.filter(stage="Exam") + @property def interview_candidates(self): return self.all_candidates.filter(stage="Interview") @@ -307,11 +313,13 @@ class JobPosting(Base): def offer_candidates(self): return self.all_candidates.filter(stage="Offer") - - #counts + # counts @property def all_candidates_count(self): - return self.candidates.annotate(sortable_score=Cast('ai_analysis_data__match_score',output_field=CharField())).order_by('-sortable_score').count() + return self.candidates.annotate( + sortable_score=Cast('ai_analysis_data__match_score', output_field=CharField())).order_by( + '-sortable_score').count() + @property def screening_candidates_count(self): return self.all_candidates.filter(stage="Applied").count() @@ -319,6 +327,7 @@ class JobPosting(Base): @property def exam_candidates_count(self): return self.all_candidates.filter(stage="Exam").count() + @property def interview_candidates_count(self): return self.all_candidates.filter(stage="Interview").count() @@ -327,6 +336,7 @@ class JobPosting(Base): def offer_candidates_count(self): return self.all_candidates.filter(stage="Offer").count() + class JobPostingImage(models.Model): job=models.OneToOneField('JobPosting',on_delete=models.CASCADE,related_name='post_images') post_image = models.ImageField(upload_to='post/',validators=[validate_image_size]) diff --git a/recruitment/views.py b/recruitment/views.py index 73fc246..3565fd1 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -273,21 +273,12 @@ def create_job(request): if request.method == "POST": form = JobPostingForm( - request.POST, is_anonymous_user=not request.user.is_authenticated + request.POST ) # to check user is authenticated or not if form.is_valid(): try: job = form.save(commit=False) - if request.user.is_authenticated: - job.created_by = ( - request.user.get_full_name() or request.user.username - ) - else: - job.created_by = request.POST.get("created_by", "").strip() - if not job.created_by: - 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) @@ -302,7 +293,7 @@ def create_job(request): else: messages.error(request, f"Please correct the errors below.{form.errors}") else: - form = JobPostingForm(is_anonymous_user=not request.user.is_authenticated) + form = JobPostingForm() return render(request, "jobs/create_job.html", {"form": form}) @@ -313,21 +304,11 @@ def edit_job(request, slug): if request.method == "POST": form = JobPostingForm( request.POST, - instance=job, - is_anonymous_user=not request.user.is_authenticated, + instance=job ) if form.is_valid(): try: - job = form.save(commit=False) - if request.user.is_authenticated: - job.created_by = ( - request.user.get_full_name() or request.user.username - ) - else: - job.created_by = request.POST.get("created_by", "").strip() - if not job.created_by: - job.created_by = "University Administrator" - job.save() + form.save() messages.success(request, f'Job "{job.title}" updated successfully!') return redirect("job_list") except Exception as e: @@ -338,7 +319,7 @@ def edit_job(request, slug): else: job = get_object_or_404(JobPosting, slug=slug) form = JobPostingForm( - instance=job, is_anonymous_user=not request.user.is_authenticated + instance=job ) return render(request, "jobs/edit_job.html", {"form": form, "job": job}) diff --git a/templates/jobs/create_job.html b/templates/jobs/create_job.html index 5e3fa74..51cf403 100644 --- a/templates/jobs/create_job.html +++ b/templates/jobs/create_job.html @@ -71,7 +71,7 @@ border: 1px solid #ced4da; width: 100%; padding: 0.375rem 0.75rem; - box-sizing: border-box; + box-sizing: border-box; } /* ================================================= */ @@ -100,7 +100,7 @@ {% endblock %} {% block content %} -
+

{% if form.instance.pk %} {% trans "Edit Job Posting" %} {% else %} {% trans "Create New Job Posting" %} {% endif %}

@@ -150,8 +150,113 @@
+ {# ================================================= #} - {# SECTION 2: JOB CONTENT (CKEDITOR 5 Fields) #} + {# SECTION 2: INTERNAL AND PROMOTION #} + {# ================================================= #} + +
+
+
{% trans "Internal & Promotion" %}
+
+
+
+
+
+ + {{ form.position_number }} + {% if form.position_number.errors %}
{{ form.position_number.errors }}
{% endif %} +
+
+
+
+ + {{ form.reporting_to }} + {% if form.reporting_to.errors %}
{{ form.reporting_to.errors }}
{% endif %} +
+
+
+
+ + {{ form.open_positions }} + {% if form.open_positions.errors %}
{{ form.open_positions.errors }}
{% endif %} +
+
+
+
+ + {{ form.max_applications }} + {% if form.max_applications.errors %}
{{ form.max_applications.errors }}
{% endif %} +
+
+ + +
+
+ + {{ form.hash_tags }} + {% if form.hash_tags.errors %}
{{ form.hash_tags.errors }}
{% endif %} +
{% trans "Comma-separated list of hashtags, e.g., #hiring, #professor" %}
+
+
+
+
+
+ + + {# ================================================= #} + {# SECTION 3: LOCATION AND DATES #} + {# ================================================= #} + +
+
+
{% trans "Location, Dates, & Salary" %}
+
+
+
+
+
+ + {{ form.location_city }} + {% if form.location_city.errors %}
{{ form.location_city.errors }}
{% endif %} +
+
+
+
+ + {{ form.location_state }} + {% if form.location_state.errors %}
{{ form.location_state.errors }}
{% endif %} +
+
+
+
+ + {{ form.location_country }} + {% if form.location_country.errors %}
{{ form.location_country.errors }}
{% endif %} +
+
+ +
+
+ + {{ form.application_deadline }} + {% if form.application_deadline.errors %}
{{ form.application_deadline.errors }}
{% endif %} +
+
+ +
+
+ + {{ form.salary_range }} + {% if form.salary_range.errors %}
{{ form.salary_range.errors }}
{% endif %} +
+
+ +
+
+
+ {# ================================================= #} + {# SECTION 4: JOB CONTENT (CKEDITOR 5 Fields) #} {# ================================================= #}
@@ -179,21 +284,15 @@
{# ================================================= #} - {# SECTION 3: COMPENSATION AND APPLICATION #} + {# SECTION 5: APPLICATION Instructions #} {# ================================================= #}
-
{% trans "Compensation & Application" %}
+
{% trans "Benefits & Application Instructions" %}
-
-
- - {{ form.salary_range }} - {% if form.salary_range.errors %}
{{ form.salary_range.errors }}
{% endif %} -
-
+ {% comment %} (application_url comment removed for brevity) {% endcomment %}
@@ -215,117 +314,7 @@
- {# ================================================= #} - {# SECTION 4: LOCATION AND DATES #} - {# ================================================= #} -
-
-
{% trans "Location, Dates, & Status" %}
-
-
-
-
-
- - {{ form.location_city }} - {% if form.location_city.errors %}
{{ form.location_city.errors }}
{% endif %} -
-
-
-
- - {{ form.location_state }} - {% if form.location_state.errors %}
{{ form.location_state.errors }}
{% endif %} -
-
-
-
- - {{ form.location_country }} - {% if form.location_country.errors %}
{{ form.location_country.errors }}
{% endif %} -
-
-
-
- - {{ form.application_deadline }} - {% if form.application_deadline.errors %}
{{ form.application_deadline.errors }}
{% endif %} -
-
-
-
- - {{ form.application_start_date }} - {% if form.application_start_date.errors %}
{{ form.application_start_date.errors }}
{% endif %} -
-
-
-
- - {{ form.joining_date }} - {% if form.joining_date.errors %}
{{ form.joining_date.errors }}
{% endif %} -
-
-
-
-
- - {# ================================================= #} - {# SECTION 5: INTERNAL AND PROMOTION #} - {# ================================================= #} -
-
-
{% trans "Internal & Promotion" %}
-
-
-
-
-
- - {{ form.position_number }} - {% if form.position_number.errors %}
{{ form.position_number.errors }}
{% endif %} -
-
-
-
- - {{ form.reporting_to }} - {% if form.reporting_to.errors %}
{{ form.reporting_to.errors }}
{% endif %} -
-
-
-
- - {{ form.open_positions }} - {% if form.open_positions.errors %}
{{ form.open_positions.errors }}
{% endif %} -
-
-
-
- - {{ form.max_applications }} - {% if form.max_applications.errors %}
{{ form.max_applications.errors }}
{% endif %} -
-
-
-
- - {{ form.created_by }} - {% if form.created_by.errors %}
{{ form.created_by.errors }}
{% endif %} -
-
-
-
- - {{ form.hash_tags }} - {% if form.hash_tags.errors %}
{{ form.hash_tags.errors }}
{% endif %} -
{% trans "Comma-separated list of hashtags, e.g., #hiring, #professor" %}
-
-
-
-
-
{# ================================================= #} {# ACTION BUTTONS #} diff --git a/templates/jobs/edit_job.html b/templates/jobs/edit_job.html index 5e3fa74..51cf403 100644 --- a/templates/jobs/edit_job.html +++ b/templates/jobs/edit_job.html @@ -71,7 +71,7 @@ border: 1px solid #ced4da; width: 100%; padding: 0.375rem 0.75rem; - box-sizing: border-box; + box-sizing: border-box; } /* ================================================= */ @@ -100,7 +100,7 @@ {% endblock %} {% block content %} -
+

{% if form.instance.pk %} {% trans "Edit Job Posting" %} {% else %} {% trans "Create New Job Posting" %} {% endif %}

@@ -150,8 +150,113 @@
+ {# ================================================= #} - {# SECTION 2: JOB CONTENT (CKEDITOR 5 Fields) #} + {# SECTION 2: INTERNAL AND PROMOTION #} + {# ================================================= #} + +
+
+
{% trans "Internal & Promotion" %}
+
+
+
+
+
+ + {{ form.position_number }} + {% if form.position_number.errors %}
{{ form.position_number.errors }}
{% endif %} +
+
+
+
+ + {{ form.reporting_to }} + {% if form.reporting_to.errors %}
{{ form.reporting_to.errors }}
{% endif %} +
+
+
+
+ + {{ form.open_positions }} + {% if form.open_positions.errors %}
{{ form.open_positions.errors }}
{% endif %} +
+
+
+
+ + {{ form.max_applications }} + {% if form.max_applications.errors %}
{{ form.max_applications.errors }}
{% endif %} +
+
+ + +
+
+ + {{ form.hash_tags }} + {% if form.hash_tags.errors %}
{{ form.hash_tags.errors }}
{% endif %} +
{% trans "Comma-separated list of hashtags, e.g., #hiring, #professor" %}
+
+
+
+
+
+ + + {# ================================================= #} + {# SECTION 3: LOCATION AND DATES #} + {# ================================================= #} + +
+
+
{% trans "Location, Dates, & Salary" %}
+
+
+
+
+
+ + {{ form.location_city }} + {% if form.location_city.errors %}
{{ form.location_city.errors }}
{% endif %} +
+
+
+
+ + {{ form.location_state }} + {% if form.location_state.errors %}
{{ form.location_state.errors }}
{% endif %} +
+
+
+
+ + {{ form.location_country }} + {% if form.location_country.errors %}
{{ form.location_country.errors }}
{% endif %} +
+
+ +
+
+ + {{ form.application_deadline }} + {% if form.application_deadline.errors %}
{{ form.application_deadline.errors }}
{% endif %} +
+
+ +
+
+ + {{ form.salary_range }} + {% if form.salary_range.errors %}
{{ form.salary_range.errors }}
{% endif %} +
+
+ +
+
+
+ {# ================================================= #} + {# SECTION 4: JOB CONTENT (CKEDITOR 5 Fields) #} {# ================================================= #}
@@ -179,21 +284,15 @@
{# ================================================= #} - {# SECTION 3: COMPENSATION AND APPLICATION #} + {# SECTION 5: APPLICATION Instructions #} {# ================================================= #}
-
{% trans "Compensation & Application" %}
+
{% trans "Benefits & Application Instructions" %}
-
-
- - {{ form.salary_range }} - {% if form.salary_range.errors %}
{{ form.salary_range.errors }}
{% endif %} -
-
+ {% comment %} (application_url comment removed for brevity) {% endcomment %}
@@ -215,117 +314,7 @@
- {# ================================================= #} - {# SECTION 4: LOCATION AND DATES #} - {# ================================================= #} -
-
-
{% trans "Location, Dates, & Status" %}
-
-
-
-
-
- - {{ form.location_city }} - {% if form.location_city.errors %}
{{ form.location_city.errors }}
{% endif %} -
-
-
-
- - {{ form.location_state }} - {% if form.location_state.errors %}
{{ form.location_state.errors }}
{% endif %} -
-
-
-
- - {{ form.location_country }} - {% if form.location_country.errors %}
{{ form.location_country.errors }}
{% endif %} -
-
-
-
- - {{ form.application_deadline }} - {% if form.application_deadline.errors %}
{{ form.application_deadline.errors }}
{% endif %} -
-
-
-
- - {{ form.application_start_date }} - {% if form.application_start_date.errors %}
{{ form.application_start_date.errors }}
{% endif %} -
-
-
-
- - {{ form.joining_date }} - {% if form.joining_date.errors %}
{{ form.joining_date.errors }}
{% endif %} -
-
-
-
-
- - {# ================================================= #} - {# SECTION 5: INTERNAL AND PROMOTION #} - {# ================================================= #} -
-
-
{% trans "Internal & Promotion" %}
-
-
-
-
-
- - {{ form.position_number }} - {% if form.position_number.errors %}
{{ form.position_number.errors }}
{% endif %} -
-
-
-
- - {{ form.reporting_to }} - {% if form.reporting_to.errors %}
{{ form.reporting_to.errors }}
{% endif %} -
-
-
-
- - {{ form.open_positions }} - {% if form.open_positions.errors %}
{{ form.open_positions.errors }}
{% endif %} -
-
-
-
- - {{ form.max_applications }} - {% if form.max_applications.errors %}
{{ form.max_applications.errors }}
{% endif %} -
-
-
-
- - {{ form.created_by }} - {% if form.created_by.errors %}
{{ form.created_by.errors }}
{% endif %} -
-
-
-
- - {{ form.hash_tags }} - {% if form.hash_tags.errors %}
{{ form.hash_tags.errors }}
{% endif %} -
{% trans "Comma-separated list of hashtags, e.g., #hiring, #professor" %}
-
-
-
-
-
{# ================================================= #} {# ACTION BUTTONS #} diff --git a/templates/jobs/job_detail.html b/templates/jobs/job_detail.html index 06ecf71..4b4ff5d 100644 --- a/templates/jobs/job_detail.html +++ b/templates/jobs/job_detail.html @@ -1,4 +1,3 @@ - {% extends "base.html" %} {% load i18n static %} @@ -217,8 +216,17 @@ padding-top: 0.5rem; } - - + /* Custom CSS for simplified stat card (from previous answer) */ + .stats-grid .kpi-card { + border-left: 4px solid var(--kaauh-teal); + background-color: #f0faff; + } + .stats-grid .card-body { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } {% endblock %} @@ -236,7 +244,7 @@
{# LEFT COLUMN: JOB DETAILS WITH TABS #} -
+
{# HEADER SECTION #} @@ -248,6 +256,7 @@
+ {# Corrected status badge logic to close the span correctly #} {% if job.status == "ACTIVE" %} {% elif job.status == "DRAFT" %} @@ -262,6 +271,7 @@ {% endif %} {{ job.get_status_display }} + @@ -281,13 +291,12 @@ {% trans "Description & Requirements" %} - {% if job.application_instructions %} - {% endif %} +
@@ -323,21 +332,20 @@
- + - -
+ +
{% trans "Financial & Timeline" %}
@@ -392,19 +400,74 @@
{{ job.benefits|safe}}
{% endif %} -
- - {# TAB 3 CONTENT: APPLICATION INSTRUCTIONS #} - {% if job.application_instructions %} -
-
+ {% if job.application_instructions %} +
{% trans "Application Instructions" %}
{{ job.application_instructions|safe }}
+ {% endif %} + +
+ + {# TAB 3 CONTENT: APPLICATION KPIS #} +
+
+ + {# 1. Job Avg. Score #} +
+
+
+ +
{{ avg_match_score|floatformat:1 }}
+ {% trans "Avg. AI Score" %} +
+
+
+ + {# 2. High Potential Count #} +
+
+
+ +
{{ high_potential_count }}
+ {% trans "High Potential" %} +
+
+
+ + {# 3. Avg. Time to Interview #} +
+
+
+ +
{{ avg_t2i_days|floatformat:1 }}d
+ {% trans "Time to Interview" %} +
+
+
+ + {# 4. Avg. Exam Review Time #} +
+
+
+ +
{{ avg_t_in_exam_days|floatformat:1 }}d
+ {% trans "Avg. Exam Review" %} +
+
+
+
+ +

+ {% trans "KPIs based on completed applicant data." %} +

+
- {% endif %}
+ + +
{# FOOTER ACTIONS #} @@ -423,7 +486,7 @@
{# RIGHT COLUMN: TABBED CARDS #} -
+
{# New Card for Candidate Category Chart #}
@@ -440,44 +503,9 @@
-
-
-
{% trans "Applicant Tracking" %}
- {% include 'jobs/partials/applicant_tracking.html' %} -
-
+ {# REMOVED: Standalone Applicant Tracking Card (It is now in a tab) #} -
-
-
-

{% trans "Job Avg. Score" %}

-
-
{{ avg_match_score|floatformat:1 }}
-
{% trans "Average AI Match Score (0-100)" %}
-
-
-
-

{% trans "High Potential Count" %}

-
-
{{ high_potential_count }}
-
{% trans "Candidates with Score ≥ 75%" %}
-
-
-
-

{% trans "Avg. Time to Interview" %}

-
-
{{ avg_t2i_days|floatformat:1 }}d
-
{% trans "Applied to Interview (Total Funnel Speed)" %}
-
-
-
-

{% trans "Avg. Exam Review Time" %}

-
-
{{ avg_t_in_exam_days|floatformat:1 }}d
-
{% trans "Days spent between Exam and Interview" %}
-
-
-
+
{# RIGHT TABS NAVIGATION #} @@ -503,34 +536,6 @@ {# TAB 1: APPLICANTS CONTENT #}
{% trans "Total Applicants" %} ({{ total_applicants }})
- {% comment %} {% if total_applicants > 0 %} -
-
-
-
{{ applied_count }}
- {% trans "Applied" %} -
-
-
-
-
{{ interview_count }}
- {% trans "Interview" %} -
-
-
-
-
{{ offer_count }}
- {% trans "Offer" %} -
-
-
- - - {% endif %} {% endcomment %}
- {# TAB 2: MANAGEMENT (LinkedIn & Forms) CONTENT #} + {# NEW TAB 2: APPLICANT TRACKING CONTENT #} +
+
{% trans "Pipeline Stages" %}
+ {% include 'jobs/partials/applicant_tracking.html' %} +

{% trans "View the number of candidates currently in each stage of the hiring pipeline." %}

+
+ + {# TAB 3: MANAGEMENT (Form Template) CONTENT #}
- {# LinkedIn Integration (Content from old card) #} - - - {# Applicant Form Management (Content from old card) #}
{% trans "Form Management" %}

@@ -569,8 +577,8 @@

- {# TAB 3: INTERNAL INFO CONTENT #} -
- - {# TAB 3: INTERNAL INFO CONTENT #} -
-
{% trans "Internal Information" %}
-
-

{% trans "Internal Job ID:" %} {{ job.internal_job_id }}

-

{% trans "Created:" %} {{ job.created_at|date:"M d, Y" }}

-

{% trans "Last Updated:" %} {{ job.updated_at|date:"M d, Y" }}

- {% if job.reporting_to %} -

{% trans "Reports To:" %} {{ job.reporting_to }}

- {% endif %} -
- -
@@ -660,10 +626,8 @@
- {% include "jobs/partials/image_upload.html" %} -