From b7d0dc8257535418ecfdac6b16a5799a542d3e9a Mon Sep 17 00:00:00 2001 From: Faheed Date: Sun, 26 Oct 2025 18:27:09 +0300 Subject: [PATCH 01/13] dashboard and spinner --- .../__pycache__/models.cpython-312.pyc | Bin 60206 -> 60689 bytes .../views_frontend.cpython-312.pyc | Bin 24310 -> 24309 bytes recruitment/models.py | 15 ++++ recruitment/views_frontend.py | 8 +- templates/jobs/job_detail.html | 10 +++ templates/recruitment/candidate_detail.html | 69 +++++++++++++++++- templates/recruitment/candidate_list.html | 65 ++++++++++++++++- .../recruitment/candidate_screening_view.html | 54 ++++++++++++++ templates/recruitment/dashboard.html | 23 +++--- 9 files changed, 226 insertions(+), 18 deletions(-) diff --git a/recruitment/__pycache__/models.cpython-312.pyc b/recruitment/__pycache__/models.cpython-312.pyc index 1dc5ea38285f681b63463fff72d3e67b5b663b16..a8ac33d8df1f062e9d365f7f9ac28935112b7ba3 100644 GIT binary patch delta 4737 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 delta 4269 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 diff --git a/recruitment/__pycache__/views_frontend.cpython-312.pyc b/recruitment/__pycache__/views_frontend.cpython-312.pyc index 85f9dabd04e1fbc4cb84b45bf60062e2774f768d..9de137fed81c6176bd96e62fc586eba11fb5848e 100644 GIT binary patch delta 279 zcmeyim+|XfM&8rByj%=GaN6i!#`2B4x7?WuvM0ZE*Jrr}WNeo8_{_+7f3l#LnbcLF zqM~acA_+v?01-Dq#P!LEUdFn&fXrJwCHW+tp*v!3}Uf>2sRMGKAAfr1gJbA!je&N k^Q?$#jErwL7e=KpaeoJ?{sSTeCqIahWmMk$FXjg$0Hg(0$4V}1&VH#@%YThcyF?RmzmTR zAiwA;h)4nv*FnS$5OHmCf|s%GO(63YPf31BVorQ2O3@vV>@5&+8$_sph;ty~ z{^SE*)@=7c%m>Rk;ofC 0: + vacancy_fill_rate = no_of_positions_filled / total_positions + else: + vacancy_fill_rate = 0.0 + + return vacancy_fill_rate + class JobPostingImage(models.Model): @@ -632,6 +646,7 @@ class Candidate(Base): ).exists() return future_meetings or today_future_meetings + class TrainingMaterial(Base): diff --git a/recruitment/views_frontend.py b/recruitment/views_frontend.py index d204d97..2c06fc7 100644 --- a/recruitment/views_frontend.py +++ b/recruitment/views_frontend.py @@ -395,12 +395,12 @@ def dashboard_view(request): high_potential_ratio = round((high_potential_count / total_candidates) * 100, 1) if total_candidates > 0 else 0 jobs=models.JobPosting.objects.all().order_by('internal_job_id') - selected_job_id=request.GET.get('selected_job_id','') + selected_job_pk=request.GET.get('selected_job_pk','') candidate_stage=['APPLIED','EXAM','INTERVIEW','OFFER'] apply_count,exam_count,interview_count,offer_count=[0]*4 - if selected_job_id: - job=jobs.get(internal_job_id=selected_job_id) + if selected_job_pk: + job=jobs.get(pk=selected_job_pk) apply_count=job.screening_candidates_count exam_count=job.exam_candidates_count interview_count=job.interview_candidates_count @@ -430,7 +430,7 @@ def dashboard_view(request): 'high_potential_count': high_potential_count, 'high_potential_ratio': high_potential_ratio, 'scored_ratio': scored_ratio, - 'current_job_id':selected_job_id, + 'current_job_id':selected_job_pk, 'jobs':jobs, 'all_candidates_count':all_candidates_count, 'candidate_stage':json.dumps(candidate_stage), diff --git a/templates/jobs/job_detail.html b/templates/jobs/job_detail.html index 3d5139f..a55900d 100644 --- a/templates/jobs/job_detail.html +++ b/templates/jobs/job_detail.html @@ -474,6 +474,16 @@ + +
+
+
+ +
{{ job.vacancy_fill_rate|floatformat:2 }}
+ {% trans "Vacancy Fill Rate" %} +
+
+
diff --git a/templates/recruitment/candidate_detail.html b/templates/recruitment/candidate_detail.html index f5a405f..5c33e40 100644 --- a/templates/recruitment/candidate_detail.html +++ b/templates/recruitment/candidate_detail.html @@ -176,6 +176,59 @@ .timeline-bg-interview { background-color: #ffc107 !important; } .timeline-bg-offer { background-color: #28a745 !important; } .timeline-bg-rejected { background-color: #dc3545 !important; } + + + + +/* ------------------------------------------- */ +/* 1. Base Spinner Styling */ +/* ------------------------------------------- */ +.kaats-spinner { + animation: kaats-spinner-rotate 1.5s linear infinite; /* Faster rotation */ + width: 40px; /* Standard size */ + height: 40px; + display: inline-block; /* Useful for table cells */ + vertical-align: middle; +} + +.kaats-spinner .path { + stroke: var(--kaauh-teal, #00636e); /* Use Teal color, fallback to dark teal */ + stroke-linecap: round; + /* Optional: Add a lighter background circle for contrast */ + /* stroke-dashoffset will be reset by the dash animation */ +} + +/* Optional: Background circle for better contrast (similar to Bootstrap) */ +.kaats-spinner circle { + stroke: var(--kaauh-border, #e9ecef); /* Light gray background */ + fill: none; + stroke-width: 5; /* Keep stroke-width on both circles */ +} + +/* ------------------------------------------- */ +/* 2. Keyframe Animations */ +/* ------------------------------------------- */ +@keyframes kaats-spinner-rotate { + 100% { + transform: rotate(360deg); + } +} + +@keyframes kaats-spinner-dash { + 0% { + stroke-dasharray: 1, 150; + stroke-dashoffset: 0; + } + 50% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -35; + } + 100% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -124; + } +} + {% endblock %} @@ -572,8 +625,20 @@ - -{% include 'recruitment/candidate_resume_template.html' %} +{% if candidate.is_resume_parsed %} + {% include 'recruitment/candidate_resume_template.html' %} +{% else %} + +
+ + + + + AI Score Loading..... +
+
+{% endif %} {% if user.is_staff %} diff --git a/templates/recruitment/candidate_list.html b/templates/recruitment/candidate_list.html index 72867db..41c2be9 100644 --- a/templates/recruitment/candidate_list.html +++ b/templates/recruitment/candidate_list.html @@ -132,6 +132,57 @@ display: flex; gap: 0.5rem; } + + +/* ------------------------------------------- */ +/* 1. Base Spinner Styling */ +/* ------------------------------------------- */ +.kaats-spinner { + animation: kaats-spinner-rotate 1.5s linear infinite; /* Faster rotation */ + width: 40px; /* Standard size */ + height: 40px; + display: inline-block; /* Useful for table cells */ + vertical-align: middle; +} + +.kaats-spinner .path { + stroke: var(--kaauh-teal, #00636e); /* Use Teal color, fallback to dark teal */ + stroke-linecap: round; + /* Optional: Add a lighter background circle for contrast */ + /* stroke-dashoffset will be reset by the dash animation */ +} + +/* Optional: Background circle for better contrast (similar to Bootstrap) */ +.kaats-spinner circle { + stroke: var(--kaauh-border, #e9ecef); /* Light gray background */ + fill: none; + stroke-width: 5; /* Keep stroke-width on both circles */ +} + +/* ------------------------------------------- */ +/* 2. Keyframe Animations */ +/* ------------------------------------------- */ +@keyframes kaats-spinner-rotate { + 100% { + transform: rotate(360deg); + } +} + +@keyframes kaats-spinner-dash { + 0% { + stroke-dasharray: 1, 150; + stroke-dashoffset: 0; + } + 50% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -35; + } + 100% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -124; + } +} + {% endblock %} @@ -232,11 +283,23 @@ {{ candidate.phone }} {{ candidate.job.title }} + {% if candidate.is_resume_parsed %} {% if candidate.professional_category != 'Uncategorized' %} {{ candidate.professional_category }} - {% endif %} + {% endif %} + {% else %} + +
+ + + + +
+
+ {% endif %} diff --git a/templates/recruitment/candidate_screening_view.html b/templates/recruitment/candidate_screening_view.html index b7e33b8..b828b8c 100644 --- a/templates/recruitment/candidate_screening_view.html +++ b/templates/recruitment/candidate_screening_view.html @@ -163,7 +163,51 @@ } +.kaats-spinner { + animation: kaats-spinner-rotate 1.5s linear infinite; /* Faster rotation */ + width: 40px; /* Standard size */ + height: 40px; + display: inline-block; /* Useful for table cells */ + vertical-align: middle; +} +.kaats-spinner .path { + stroke: var(--kaauh-teal, #00636e); /* Use Teal color, fallback to dark teal */ + stroke-linecap: round; + /* Optional: Add a lighter background circle for contrast */ + /* stroke-dashoffset will be reset by the dash animation */ +} + +/* Optional: Background circle for better contrast (similar to Bootstrap) */ +.kaats-spinner circle { + stroke: var(--kaauh-border, #e9ecef); /* Light gray background */ + fill: none; + stroke-width: 5; /* Keep stroke-width on both circles */ +} + +/* ------------------------------------------- */ +/* 2. Keyframe Animations */ +/* ------------------------------------------- */ +@keyframes kaats-spinner-rotate { + 100% { + transform: rotate(360deg); + } +} + +@keyframes kaats-spinner-dash { + 0% { + stroke-dasharray: 1, 150; + stroke-dashoffset: 0; + } + 50% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -35; + } + 100% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -124; + } +} {% endblock %} {% block content %} @@ -351,11 +395,21 @@ + {% if candidate.is_resume_parsed %} {% if candidate.match_score %} {{ candidate.match_score|default:"0" }}% {% endif %} + {% else %} +
+ + + + +
+ {% endif %} {% if candidate.screening_stage_rating %} diff --git a/templates/recruitment/dashboard.html b/templates/recruitment/dashboard.html index d55c31b..c297668 100644 --- a/templates/recruitment/dashboard.html +++ b/templates/recruitment/dashboard.html @@ -123,7 +123,7 @@ {% block content %}
-

{% trans "Recruitment Intelligence" %} 🧠

+

{% trans "Recruitment Analytics" %}

{# -------------------------------------------------------------------------- #} {# STATS CARDS SECTION #} @@ -156,7 +156,7 @@
-

{% trans "Avg. Match Score" %}

+

{% trans "Avg. Match Score" %}

{{ avg_match_score|floatformat:1 }}
{% trans "Average AI Score (0-100)" %}
@@ -164,7 +164,7 @@
-

{% trans "High Potential" %}

+

{% trans "High Potential" %}

{{ high_potential_count }}
{% trans "Candidates with Score ≥ 75 ({{ high_potential_ratio }}%)" %}
@@ -172,7 +172,7 @@
-

{% trans "Scored Profiles" %}

+

{% trans "Scored Profiles" %}

{{ scored_ratio|floatformat:1 }}%
{% trans "Percent of profiles processed by AI" %}
@@ -210,13 +210,13 @@ {{my_job}} {# Job Filter Dropdown - Consistent with Card Header Layout #} -
+ - {% for job in jobs%} - {% endfor %} @@ -275,7 +275,7 @@ // Pass context data safely to JavaScript const jobTitles = JSON.parse('{{ job_titles|escapejs }}').slice(0, 5); // Take top 5 const jobAppCounts = JSON.parse('{{ job_app_counts|escapejs }}').slice(0, 5); // Take top 5 - + // BAR CHART configuration const ctxBar = document.getElementById('applicationsChart').getContext('2d'); new Chart(ctxBar, { @@ -285,7 +285,7 @@ datasets: [{ label: '{% trans "Applications" %}', data: jobAppCounts, - backgroundColor: 'var(--kaauh-teal)', + backgroundColor: '#00636e', borderColor: 'var(--kaauh-teal-dark)', borderWidth: 1, barThickness: 50 @@ -307,7 +307,7 @@ y: { beginAtZero: true, title: { display: true, text: '{% trans "Total Applications" %}' }, - ticks: { color: '#333333', precision: 0 }, + ticks: { color: '#2222', precision: 0 }, grid: { color: '#e0e0e0' } }, x: { @@ -318,6 +318,7 @@ } }); + // DONUT CHART configuration const ctxDonut = document.getElementById('candidate_donout_chart').getContext('2d'); @@ -330,7 +331,7 @@ label: '{% trans "Candidate Count" %}', data: JSON.parse('{{ candidates_count|safe }}'), backgroundColor: [ - 'var(--kaauh-teal)', // Applied (Primary) + '#00636e', // Applied (Primary) 'rgb(255, 159, 64)', // Exam (Orange) 'rgb(54, 162, 235)', // Interview (Blue) 'rgb(75, 192, 192)' // Offer (Green) -- 2.39.5 From 6447a895c034103cc07b58e45e0bdec7b86c0995 Mon Sep 17 00:00:00 2001 From: Faheed Date: Sun, 26 Oct 2025 18:50:52 +0300 Subject: [PATCH 02/13] small fix --- .../views_frontend.cpython-312.pyc | Bin 24309 -> 24309 bytes recruitment/views_frontend.py | 2 +- templates/jobs/job_detail.html | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/recruitment/__pycache__/views_frontend.cpython-312.pyc b/recruitment/__pycache__/views_frontend.cpython-312.pyc index 9de137fed81c6176bd96e62fc586eba11fb5848e..72a41e00ab1f5f8d86402f7456e11d00019ebf36 100644 GIT binary patch delta 24 ecmeymm+|XfM&8rByj%=G&}jcJWA#Sf7jXb{=m|Ig delta 24 ecmeymm+|XfM&8rByj%=GaN6i!#`2B4FX8}p1_^Nh diff --git a/recruitment/views_frontend.py b/recruitment/views_frontend.py index 2c06fc7..2055fde 100644 --- a/recruitment/views_frontend.py +++ b/recruitment/views_frontend.py @@ -240,7 +240,7 @@ def candidate_detail(request, slug): stage_form = None if request.user.is_staff: stage_form = forms.CandidateStageForm() - + # parsed = JSON(json.dumps(parsed), indent=2, highlight=True, skip_keys=False, ensure_ascii=False, check_circular=True, allow_nan=True, default=None, sort_keys=False) # parsed = json_to_markdown_table([parsed]) return render(request, 'recruitment/candidate_detail.html', { diff --git a/templates/jobs/job_detail.html b/templates/jobs/job_detail.html index a55900d..25b27f2 100644 --- a/templates/jobs/job_detail.html +++ b/templates/jobs/job_detail.html @@ -347,7 +347,7 @@ {% trans "Create New Form Template" %} {% else %} - + {% trans "View Form Template" %} {% endif %} -- 2.39.5 From 465936238e5ace79b408008e67db3068bdce05d7 Mon Sep 17 00:00:00 2001 From: Faheed Date: Sun, 26 Oct 2025 22:09:39 +0300 Subject: [PATCH 03/13] ai scoring spinner fix --- .../__pycache__/models.cpython-312.pyc | Bin 60689 -> 60689 bytes recruitment/models.py | 5 ++ .../email/email_confirmation_message.html | 2 +- templates/recruitment/candidate_detail.html | 84 ++++++++++++++---- templates/recruitment/candidate_list.html | 4 +- .../recruitment/candidate_screening_view.html | 7 +- .../partials/stage_update_modal.html | 2 +- 7 files changed, 80 insertions(+), 24 deletions(-) diff --git a/recruitment/__pycache__/models.cpython-312.pyc b/recruitment/__pycache__/models.cpython-312.pyc index a8ac33d8df1f062e9d365f7f9ac28935112b7ba3..c63aa4945965177ecce4c12aac7888627080198b 100644 GIT binary patch delta 420 zcmWNNPe_w-9EacM{k?6hbQDP|A*)M=AOc|})runUw)7GmOo)hr!YGA^6(n`2@K5WT zMkb5=ZM4oMyA*$X4G@_ddCP4{LD6WSZI zVMh7~;>=2Qe1HYA=|JZLD8rB`v+_{mTg|1>mw%uBU= zn5dH6>qDH@t#1NQq`|j~9xiIh$1S$or0lZ;4>dIQ6&~w`)G0Wpm%f~INVwM?l5%zvZQ zCm_>J=O@IFv+D~C{JOguNFe`*X2esO&5JA6^<)Y>B$Is&k4f}~kV%KVGq}&4edVgY zR!CC4QI;esvm0r&M$fUsB?52RG8O{t{B&FevN(UDL4k`GCZ7PG>3S-Re%_c42>5y0 zDFZv?aJdnrDz^(^GI;!&>%ONz#$S3c*MUoN&PA}q4(~Go9i7j+HRHjBG2F6wu~3If z`mtr9{cpLjaO!s>aF0#PXDVKD+RtQJYm)6#fNnOg{Q^F6y#G1yf@{|+6wGSZNw}!) z_lg!Rd%H-+5;X(L$Bok diff --git a/recruitment/models.py b/recruitment/models.py index 3f7a86e..0e4f3cf 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -647,6 +647,11 @@ class Candidate(Base): return future_meetings or today_future_meetings + # @property + # def time_to_hire(self): + # time_to_hire=self.hired_date-self.created_at + # return time_to_hire + class TrainingMaterial(Base): diff --git a/templates/account/email/email_confirmation_message.html b/templates/account/email/email_confirmation_message.html index fe01de7..ef7f706 100644 --- a/templates/account/email/email_confirmation_message.html +++ b/templates/account/email/email_confirmation_message.html @@ -6,7 +6,7 @@

- {% blocktrans %}Thank you for choosing **KAAUH ATS**. To verify the ownership of your email address, please click the confirmation link below:{% endblocktrans %} + {% blocktrans %}To verify the ownership of your email address, please click the confirmation link below:{% endblocktrans %}

diff --git a/templates/recruitment/candidate_detail.html b/templates/recruitment/candidate_detail.html index 5c33e40..f579c31 100644 --- a/templates/recruitment/candidate_detail.html +++ b/templates/recruitment/candidate_detail.html @@ -11,9 +11,11 @@ /* ================================================= */ :root { --kaauh-teal: #00636e; + --kaauh-teal-light: #007a88; --kaauh-teal-dark: #004a53; --kaauh-border: #eaeff3; --kaauh-primary-text: #343a40; + --kaauh-accent: #ffcc00; } /* Primary Color Overrides */ @@ -179,30 +181,43 @@ - /* ------------------------------------------- */ /* 1. Base Spinner Styling */ /* ------------------------------------------- */ +.ai-loading-container { + /* Flex container to center content and align icon/spinner */ + display: inline-flex; + align-items: center; + gap: 5px; /* Space between robot icon and spinner */ + font-size: 14px; /* Default text size */ + color: var(--kaauh-teal-light); /* Loading text color */ +} + .kaats-spinner { - animation: kaats-spinner-rotate 1.5s linear infinite; /* Faster rotation */ - width: 40px; /* Standard size */ - height: 40px; - display: inline-block; /* Useful for table cells */ + animation: kaats-spinner-rotate 2s linear infinite; /* Slower, more deliberate rotation */ + width: 100px; /* Smaller size to be subtle */ + height: 100px; vertical-align: middle; } .kaats-spinner .path { - stroke: var(--kaauh-teal, #00636e); /* Use Teal color, fallback to dark teal */ + stroke: var(--kaauh-teal-light, #007a88); /* Lighter Teal for the active path */ stroke-linecap: round; - /* Optional: Add a lighter background circle for contrast */ - /* stroke-dashoffset will be reset by the dash animation */ } /* Optional: Background circle for better contrast (similar to Bootstrap) */ .kaats-spinner circle { stroke: var(--kaauh-border, #e9ecef); /* Light gray background */ fill: none; - stroke-width: 5; /* Keep stroke-width on both circles */ + stroke-width: 4; /* Slightly smaller stroke-width */ +} + +/* New: Robot Icon Style and Animation */ +.ai-robot-icon { + font-size: 2rem; /* Slightly larger than text */ + color: var(--kaauh-teal); + animation: ai-pulse 2s infinite ease-in-out; /* Pulsing effect for 'intelligence' */ + transform-origin: center; } /* ------------------------------------------- */ @@ -229,6 +244,22 @@ } } +/* New Keyframe: AI Pulse effect */ +@keyframes ai-pulse { + 0% { + color: var(--kaauh-teal); + transform: scale(1); + } + 50% { + color: var(--kaauh-accent); /* Accent color pulse */ + transform: scale(1.05); + } + 100% { + color: var(--kaauh-teal); + transform: scale(1); + } +} + {% endblock %} @@ -587,7 +618,7 @@ {# ACTIONS CARD #} - @@ -629,14 +668,23 @@ {% include 'recruitment/candidate_resume_template.html' %} {% else %} -
- - - - - AI Score Loading..... -
+
+ +
+ {# Robot Icon (Requires Font Awesome or similar library) #} + + + {# The Spinner #} + + + + + + AI Scoring... +
+ +
{% endif %} diff --git a/templates/recruitment/candidate_list.html b/templates/recruitment/candidate_list.html index 41c2be9..7b4f9df 100644 --- a/templates/recruitment/candidate_list.html +++ b/templates/recruitment/candidate_list.html @@ -254,6 +254,7 @@
+ {% if candidates %}
{# View Switcher - list_id must match the container ID #} @@ -290,13 +291,14 @@ {% endif %} {% else %} - +
+ {% trans "AI Scoring..." %}
{% endif %} diff --git a/templates/recruitment/candidate_screening_view.html b/templates/recruitment/candidate_screening_view.html index b828b8c..630e2c2 100644 --- a/templates/recruitment/candidate_screening_view.html +++ b/templates/recruitment/candidate_screening_view.html @@ -395,19 +395,20 @@
- {% if candidate.is_resume_parsed %} + {% if candidate.is_resume_parsed %} {% if candidate.match_score %} {{ candidate.match_score|default:"0" }}% {% endif %} - {% else %} -
+ {% else %} +
+ {% trans 'AI scoring..' %}
{% endif %} diff --git a/templates/recruitment/partials/stage_update_modal.html b/templates/recruitment/partials/stage_update_modal.html index 9e63894..89d11bf 100644 --- a/templates/recruitment/partials/stage_update_modal.html +++ b/templates/recruitment/partials/stage_update_modal.html @@ -52,7 +52,7 @@ Cancel - -- 2.39.5 From 9eb2b90da6f4df31093a667eabf0b236f92a0d20 Mon Sep 17 00:00:00 2001 From: Faheed Date: Sun, 26 Oct 2025 23:19:31 +0300 Subject: [PATCH 04/13] user registeration done --- .../__pycache__/models.cpython-312.pyc | Bin 60689 -> 60814 bytes .../views_frontend.cpython-312.pyc | Bin 24309 -> 24589 bytes recruitment/models.py | 10 +++--- recruitment/views_frontend.py | 34 +++++++++++------- .../email/email_confirmation_message.txt | 18 ++++++++++ .../email/password_reset_key_message.txt | 27 ++++++++++++++ templates/account/login.html | 22 ++++++------ templates/recruitment/dashboard.html | 2 +- 8 files changed, 84 insertions(+), 29 deletions(-) create mode 100644 templates/account/email/email_confirmation_message.txt create mode 100644 templates/account/email/password_reset_key_message.txt diff --git a/recruitment/__pycache__/models.cpython-312.pyc b/recruitment/__pycache__/models.cpython-312.pyc index c63aa4945965177ecce4c12aac7888627080198b..5f501401d576ea8ed98e2a7f8e990e91a6309a18 100644 GIT binary patch delta 4296 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 U5_KUW=)?_QkAV-Bwy!JT|JDV&4FCWD delta 4215 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 Y4$A!y*2Aw1K%Nz}Zm+bFw|$kr0WFIlr~m)} diff --git a/recruitment/models.py b/recruitment/models.py index 0e4f3cf..4d545ec 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -321,23 +321,23 @@ class JobPosting(Base): 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() + '-sortable_score').count() or 0 @property def screening_candidates_count(self): - return self.all_candidates.filter(stage="Applied").count() + return self.all_candidates.filter(stage="Applied").count() or 0 @property def exam_candidates_count(self): - return self.all_candidates.filter(stage="Exam").count() + return self.all_candidates.filter(stage="Exam").count() or 0 @property def interview_candidates_count(self): - return self.all_candidates.filter(stage="Interview").count() + return self.all_candidates.filter(stage="Interview").count() or 0 @property def offer_candidates_count(self): - return self.all_candidates.filter(stage="Offer").count() + return self.all_candidates.filter(stage="Offer").count() or 0 @property def vacancy_fill_rate(self): diff --git a/recruitment/views_frontend.py b/recruitment/views_frontend.py index 2055fde..4e97d0e 100644 --- a/recruitment/views_frontend.py +++ b/recruitment/views_frontend.py @@ -339,6 +339,8 @@ class TrainingDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView): @login_required def dashboard_view(request): + all_candidates_count=0 + # --- Performance Optimization: Aggregate Data in ONE Query --- # 1. Base Job Query: Get all jobs and annotate with candidate count @@ -393,27 +395,33 @@ def dashboard_view(request): sortable_score__gte=75 ).count() high_potential_ratio = round((high_potential_count / total_candidates) * 100, 1) if total_candidates > 0 else 0 - + jobs=models.JobPosting.objects.all().order_by('internal_job_id') selected_job_pk=request.GET.get('selected_job_pk','') candidate_stage=['APPLIED','EXAM','INTERVIEW','OFFER'] apply_count,exam_count,interview_count,offer_count=[0]*4 if selected_job_pk: - job=jobs.get(pk=selected_job_pk) - apply_count=job.screening_candidates_count - exam_count=job.exam_candidates_count - interview_count=job.interview_candidates_count - offer_count=job.offer_candidates_count - all_candidates_count=job.all_candidates_count + try: + job=jobs.get(pk=selected_job_pk) + apply_count=job.screening_candidates_count + exam_count=job.exam_candidates_count + interview_count=job.interview_candidates_count + offer_count=job.offer_candidates_count + all_candidates_count=job.all_candidates_count + except Exception as e: + print(e) else: #default job - job=jobs.first() - apply_count=job.screening_candidates_count - exam_count=job.exam_candidates_count - interview_count=job.interview_candidates_count - offer_count=job.offer_candidates_count - all_candidates_count=job.all_candidates_count + try: + job=jobs.first() + apply_count=job.screening_candidates_count + exam_count=job.exam_candidates_count + interview_count=job.interview_candidates_count + offer_count=job.offer_candidates_count + all_candidates_count=job.all_candidates_count + except Exception as e: + print(e) candidates_count=[ apply_count,exam_count,interview_count,offer_count ] context = { diff --git a/templates/account/email/email_confirmation_message.txt b/templates/account/email/email_confirmation_message.txt new file mode 100644 index 0000000..5dd0cb4 --- /dev/null +++ b/templates/account/email/email_confirmation_message.txt @@ -0,0 +1,18 @@ +{% load account i18n %} +{% autoescape off %} + +{% blocktrans %}Hello,{% endblocktrans %} + +{% blocktrans %}To verify the ownership of your email address, please click the confirmation link below:{% endblocktrans %} + + +{% trans "Confirm My KAAUH ATS Email" %} +{{ activate_url }} + + +{% blocktrans %}If you did not request this verification, you can safely ignore this email.{% endblocktrans %} + +{% blocktrans %}Alternatively, copy and paste this link into your browser:{% endblocktrans %} +{{ activate_url }} + +{% endautoescape %} \ No newline at end of file diff --git a/templates/account/email/password_reset_key_message.txt b/templates/account/email/password_reset_key_message.txt new file mode 100644 index 0000000..7930487 --- /dev/null +++ b/templates/account/email/password_reset_key_message.txt @@ -0,0 +1,27 @@ +{% load i18n %} +{% load static %} +{% autoescape off %} + +{% trans "Password Reset Request" %} + +{% trans "Hello," %} + +{% blocktrans %}You are receiving this email because you or someone else has requested a password reset for your account at{% endblocktrans %} {{ current_site.name }}. + +------------------------------------------------------ +{% trans "Click Here to Reset Your Password" %} +{{ password_reset_url }} +------------------------------------------------------ + +{% trans "This link is only valid for a limited time." %} + +{% trans "If you did not request a password reset, please ignore this email. Your password will remain unchanged." %} + +{% trans "Thank you," %} +{% trans "KAAUH ATS Team" %} + +--- +{% trans "If the button above does not work, copy and paste the following link into your browser:" %} +{{ password_reset_url }} + +{% endautoescape %} \ No newline at end of file diff --git a/templates/account/login.html b/templates/account/login.html index b7e66c7..5cf52e8 100644 --- a/templates/account/login.html +++ b/templates/account/login.html @@ -1,6 +1,8 @@ -{% load static %} +{% load static i18n %} +{% get_current_language_bidi as LANGUAGE_BIDI %} +{% get_current_language as LANGUAGE_CODE %} - + @@ -146,34 +148,34 @@
-

Sign In

+

{% trans "Sign In" %}

{% csrf_token %}
- - + +
- - + +
- +
diff --git a/templates/recruitment/dashboard.html b/templates/recruitment/dashboard.html index c297668..84d5bea 100644 --- a/templates/recruitment/dashboard.html +++ b/templates/recruitment/dashboard.html @@ -167,7 +167,7 @@

{% trans "High Potential" %}

{{ high_potential_count }}
-
{% trans "Candidates with Score ≥ 75 ({{ high_potential_ratio }}%)" %}
+
{% trans "Candidates with Score ≥ 75%" %} ({{ high_potential_ratio }})
-- 2.39.5 From 12ebc77a5b4b65e856008cf8d221668b7a7d8648 Mon Sep 17 00:00:00 2001 From: Faheed Date: Sun, 26 Oct 2025 23:52:29 +0300 Subject: [PATCH 05/13] time to hire --- templates/recruitment/candidate_detail.html | 10 ++++------ templates/recruitment/candidate_list.html | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/templates/recruitment/candidate_detail.html b/templates/recruitment/candidate_detail.html index f579c31..5d3630f 100644 --- a/templates/recruitment/candidate_detail.html +++ b/templates/recruitment/candidate_detail.html @@ -618,7 +618,7 @@ {# ACTIONS CARD #} -
+
{% trans "Management Actions" %}
-
-
{% trans "Time to Hire" %}
-
- {{candidate.time_to_hire|default:"50 days"}} -
+
+
{% trans "Time to Hire: " %}{{candidate.time_to_hire|default:100}} days
+
diff --git a/templates/recruitment/candidate_list.html b/templates/recruitment/candidate_list.html index 7b4f9df..654524f 100644 --- a/templates/recruitment/candidate_list.html +++ b/templates/recruitment/candidate_list.html @@ -298,7 +298,7 @@ - {% trans "AI Scoring..." %} + {% trans "AI Scoring..." %}
{% endif %} -- 2.39.5 From e3435e36274426556e200f99accb6739fdb1b874 Mon Sep 17 00:00:00 2001 From: Faheed Date: Mon, 27 Oct 2025 12:52:12 +0300 Subject: [PATCH 06/13] update --- templates/jobs/job_list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/jobs/job_list.html b/templates/jobs/job_list.html index c159c7c..c9fbb45 100644 --- a/templates/jobs/job_list.html +++ b/templates/jobs/job_list.html @@ -257,7 +257,7 @@ {% include "includes/_list_view_switcher.html" with list_id="job-list" %} {# 1. TABLE VIEW (Default Active) #} -
+
-- 2.39.5 From d1dda003d6c396bdc8998f5fb1aa183cfec3beaf Mon Sep 17 00:00:00 2001 From: Faheed Date: Mon, 27 Oct 2025 17:43:46 +0300 Subject: [PATCH 07/13] small fix for the form template marked active when the job is made active --- .../__pycache__/urls.cpython-312.pyc | Bin 2656 -> 2656 bytes .../linkedin_service.cpython-312.pyc | Bin 17100 -> 12067 bytes .../__pycache__/models.cpython-312.pyc | Bin 60814 -> 60904 bytes .../__pycache__/signals.cpython-312.pyc | Bin 4406 -> 4406 bytes recruitment/__pycache__/utils.cpython-312.pyc | Bin 20414 -> 20414 bytes recruitment/__pycache__/views.cpython-312.pyc | Bin 92181 -> 92506 bytes recruitment/linkedin_service.py | 174 +++++++++--------- ..._jobposting_linkedin_post_formated_data.py | 18 ++ ..._jobposting_linkedin_post_formated_data.py | 18 ++ ..._jobposting_linkedin_post_formated_data.py | 18 ++ recruitment/models.py | 1 + recruitment/signals.py | 11 +- recruitment/tasks.py | 63 +++++-- recruitment/utils.py | 6 +- recruitment/views.py | 9 + templates/jobs/job_detail.html | 5 + 16 files changed, 220 insertions(+), 103 deletions(-) create mode 100644 recruitment/migrations/0003_jobposting_linkedin_post_formated_data.py create mode 100644 recruitment/migrations/0004_alter_jobposting_linkedin_post_formated_data.py create mode 100644 recruitment/migrations/0005_alter_jobposting_linkedin_post_formated_data.py diff --git a/NorahUniversity/__pycache__/urls.cpython-312.pyc b/NorahUniversity/__pycache__/urls.cpython-312.pyc index 3a455bcac85223e3e61d1dac31fcc69ef96e0329..7ad3c1f00585bcc31d862feff56d2e93f71b0b0b 100644 GIT binary patch delta 20 acmaDL@<4?9G%qg~0}wbA{@=(Q$^`&FiUqC! delta 20 acmaDL@<4?9G%qg~0}!aO{oKeM$^`&CGzB97 diff --git a/recruitment/__pycache__/linkedin_service.cpython-312.pyc b/recruitment/__pycache__/linkedin_service.cpython-312.pyc index 19d53fabe8d1e18fec443a727cba79f0d4da1bfc..7875609da8184188ab8e384a0044253f8317cbe9 100644 GIT binary patch delta 2552 zcmZ`)TWnlM89sBayLV$N;xFtdQ8dEBo$4n7L!yfretYRqWUF? z8rS7JXXy=M#Xi8zl10Lp?*xHcUiB?G)V^7(1Wp*QG0!QND%9E3Mu$d*MnZjbWc1L; zR4I}W;T>_(t*vcci|pYS zW0PdC`9|z{xxEh#Uz0=QjAK)W`D}nMoBc~!ymt^`Kf(b73*iBT2N8whLLa> zVFY0mz(;orIKaDKn3H6tdE0Eru?Zks3LEE_(i3Eozm+}$rv4{=@c1Vw5*F-g)1%>lmn+6Jh;43b=bo|V$=MK z*oCc-g~Y)24;se`z>8xvp>iA{h!zf!)}xX`HD8(zhxQF!{{ zrf&d<@L+&o9p?FPF3)>&`^i&$A@_5r12g|`jL&oGQy9 z**IsjGRU(MQKnBIG2yP;tIo=4*K^oWB%~rjDT?ZDC4>VBKd@P_A~endYg8N9Chj;7 z6Qt=c-HYU6bD-xPqAZm({zENS8i2QPMXJkd!29vh&;q4Jt81;!K*`y!freOJqcT;x zC8}-$Q$wpOR69EfCo0t&3Y8lwH5xjN)%neR`7-W&0pPCH?l<2T|^F{5d|^Kb{B z%bO(e!gfwx>p^iddudmzEjG-$$uhJ%=xxcXo2{U|B+;Iil`^4SjU>IVZq}16gN)v5 z>JyT+kN;}))qUhgO^9EpP*PefT&mm7#O&BElBJHh`su85@MU zB^z%;L-NAJ3EQhV3yf75`>rI7N$fkw8MW2uzJvFLIDFVbl7CM_ou2wUS>~6gE=1nQ zye*$=_I{>c(L%+qo#Kl#gF1Fic7oSthO=i;f$~uU`5QBRqavu7LH8dJNMJaRSMW<= zNVPIi-bMePLh1pYob6kD5|?KXzK5`zee@$VR-M2O0tSWztktkED$svfMRcpkGk$LN zfo}Yygfjnsy3t#++k9FdW0>T)Au+w>))5OYF?ac`b2ktAhgletkQt{b@hF4jI+^;el zj-V{o?DA@52zW^)_V^>g*h|KPXC3OD)5ANC%#-`URwB|Xp>=ihG tEJ!}4{6mEh@}6YACuQE1((g*A&}cH@5%) delta 6680 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 MlAZ?yLPO#I1NbJxng9R* diff --git a/recruitment/__pycache__/models.cpython-312.pyc b/recruitment/__pycache__/models.cpython-312.pyc index 5f501401d576ea8ed98e2a7f8e990e91a6309a18..c1360f1b6b31b7882f741961f5f5236148c0f3bd 100644 GIT binary patch delta 5099 zcmaJ_dsvj$(cf9v1$SLhZmwKpAw)%pphOhK3tmvfC<;nNbX|B~U}0hL-6aOp5|qzN z{2mF8Mx(~yr_m_Z8eWYlTB9cUl8RQ6s@07elQh<*CT(tQQ#E$xED2ltJmrrcGjrz5 znKNh3oOyTShkkcH^fPsuOa=}8WmzAV|L!%@>RvEZZRxe$5Bos~hMR(5F#gaxs*j{U zG^ovw6*Zc|A((4QfCTI^M(c(g3TgAhw@tD5Yg3(T-uM^p;2u*ue>mo=V4z6#l8)?pb^sO zA9Z*J9_bSXPm)JbLxh2`zJpT6kKCbgZT{WnM$*0pH>7Db(=^ifCXF+4)fA4Crjm5Lltnm!#-{mm$e^g*+dACh_?`{8Plw^WkR^Ki`L+O?-h5pSw=;3>41E(JA&UL!n_Uf_( zeS+50D{J1&nd;%b_w{g0ZR>9VIEClUU%(mc7;p;SR_h`xTKGua9<>$Vb9KbPUjTfi z-imnx;CuDO*eL)8^=8}9;d0g)t|m7vFovqN!C&Pd$|;_GokP7=BEMot<)s`{6i$y$iSnAs(| z=`?yxT{~L~`WZwjIoO<=0=MvV?z*(6NZ|1+likV$x)UzqG!`X^P{}2_K5VkBTC&rI zqDj`vImL3z<=I@?`~y1c5f zHN0j-&zcTfby=;gOtzLfYUS#Zy1`^52H#qI(5oM*uzpC7zRRu1)n)E-*Dx(w^CIyL z>;LQd)V%&h)v|hMkNPs%R#`2(T(T?GtABAxyN^CB!<{w@T_FyP$9B|rs16SlN5eRL zxA-lXj2lX}(`_0gb<$1xTKa~r%WZicgs$VIH&eQJ0-VJaa*5kThdjH?0R`&GvVLG( z&*N7(oy>T(JOiv4W}QWfCU@D1kiC%CHr~oz39Jea;UW z;&@I1LkdG0!*dKqhH0!ml_8nI%rbspmE}yWAn+KbR@v+hZ;8;!OnY=S<<4qZ(erSC zO_FFUE~=1rhb+{TRXkbCqkZx3HOrwDT}$Wa`R*yb@!Zl$4YQaGX5ee5@NMz<*{j_O zUlxz9)Zw(bl|mjWVz4qCX19u&k{I}%RXC*;JAMUuk9|*d4Xy7H>vV1%2#*`Hc!}*=}_?J@Lyr&Vw3-`v|E8+~N zX-Hw%gcnxqhSREZWr4r$0txnEXXEtXt32Mlkh8{KLJK{f23+ z0XkLJngu{t=fc{7a2D^b_2|B#(Yx5W?o0SsC8eUjJa3 z*yP`^^tr)?FPLYzf?J>40AJwkX9nYrS}P%mz1)#`wD5s2=MYjIUY z1co(-K|f4vJ_(`fo#qk%GtO*D*Xwz!-aE6aB^mnTZ(GL0_h{N=wwie*v1^e4oVm&| zj<#@9xb%QoPFh9FcAj81lOcdv6T?tu6B)#lagUW5zQd+XXSMeL`vsWQB{&KK*p+{2H$918^HH0ONqd&-Q|hAz<)g0J)skedP7&QUfca5n8Im7g1>T!qi4p3 zJ)>X;9^X?y$q=+R+?$#rN+O;@OT=>ouakgN_FBeyqsn|F!*LD-eJ;@PGd#gCj3JqU zz7e_;hMiI}u|*$Zh-M+fX!X?IW!&MV?VAD6jE(#A;l6rh|3c7>q2XhgcHpZ}ai}6U z$CAtwNDp5rc3q9Yc?bK$ud(W2UdDe<2ygA6Y~!C1V1Z~*Mb z@U~%wW_FR`Wt`R4p>6hNZQP+<)IpGxvQ~Zf7e=60>vxA|>!y%5mvQhf-!o1nN)Zui z`RS<&IT8*xaOjaE)Lwmf#2t{$I@7h-m=uEc*J9x|Zg_27a3h&b@Kd@6NnN% zZh^b%!uA)y`x3u^t@H*Ok4}y{$y2n7fnxzryXc?B(%q;TvEpbdwMDy*CdBuc6rszZ zS;|oMK^)Z%-ug-LhW>Cg3BJa}V{028iK2*xNCQ#-#i5CviW3(G+t})Kw%St@#M>oS z&*1^_l;kq?NQs!lGKMph_pV!%_QPNQqjr)Om)R{rn02KzP>kEp z7K0uioLvN3T=>>}`gCc3D@U(i!n17Xcdj6!mdRxdLRiPta)uS?J{PBR6Iq2j&PlLx zQ)eW-B2k@jkd6~NC&EYgY-h4DlVe=S6Ru-NXQFlzVAVh~KJ2Wd?z8fIHf-8+>^$h! zkj56Z<3a`WZDQq>3}VGveDdM~*p3@7t_bUi+deicl#CeD6=&v2oAl>HB^)pI}cRgi(M&?iF4n;{38dgu#BNxz1y`D zKvCC!@CiU8IxZa$u};1mW^DhDSeJLlc^l03ejNo`7X?vCHo6 z?znW-PPgn*`iYSeXDzi&b;?29cx4dmQ;%KQ3wjURxQ^DVO9DkD)GD9iyH`ij$LfQt z8IS)I9{TvRS9P~+q8a#<=k4a5cuUwWCK*oP@@rFImwNhIlpoy38`op0(|CA2%6Oe; zh~-|!K{u8{7H+$-f}Z0iZV$xNn-TOyJnQCg2zr%ld_tRdrIXW58hn0Z1it#w80$~2 zJlc(l_u9n9BN@)nH03^n@bdusb&!E?si!x6z*N$QI(>iDxs?B7xpya+d20~$Hs!ab z8}_o4;T^T(Rwck*b?leWaC)zP@sSz)vD2Sp+ic&V04Jo`W&UMAjl*kg{ zg;?g_FNP;Dqsl4OI^^-nMVdj&Xj}^j|4Ki7;FEV3^jYZ-vCoHotKt9OPQo2`zxfx! CX{$H@ delta 4753 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 diff --git a/recruitment/__pycache__/utils.cpython-312.pyc b/recruitment/__pycache__/utils.cpython-312.pyc index 7a31e4973e50e67b7327ae821a8caa349cdb5838..3c1ee409550c0176ae9a28a22dd7bb6a211ad2db 100644 GIT binary patch delta 24 ecmdltpK;%OM&8rByj%=GU?2KFWAaAcjs5^)qX-26 delta 24 ecmdltpK;%OM&8rByj%=G@OJl)jJ}P$8~p)mrwI)J diff --git a/recruitment/__pycache__/views.cpython-312.pyc b/recruitment/__pycache__/views.cpython-312.pyc index 5eb571ddafaa0b3c5096352488049b73781ffb20..f6c5db4f7fb3431fede1d79ca57b443b96c44e6b 100644 GIT binary patch delta 7529 zcma)B3tUuX+Mj1;&d6*8R>(*N4ZPm_hvp@68%r1VVrFPM#vNX$VH}fmYZLM{m=N$*w)_%qB$Dijs_vd*p z=Y0>49+P}0B-0y4V~7TS(r&-Dcvj0^)1L~o^Mm=BzYOQaf3(`@pW@8A(ns)_$RqB2HChX zdIu;AY9X$~ofpuRWBSko-1$BH*g!b}FM+>1!n1E*QU-#GmEhhzZcMc3taQrCkN4YXG&ksmHK5rU>52T6ZltFLly{~FC^z*1g@bs(=(0F3YH4nx$Xd5*R+IjtOF2;G{?zdQE3Ma&Dwk z)JG?wGtQxrM|-p!NW;+V~8m<7+0(tf=t?J6yK<2Df9G-3mTqTdQ2}w%Zmt>`k(30{^Jk z$!{sCf{T1}$*{;vXoyO|^~)M*4;y|FtibpDCT! z4~x<;0JWkl0w9&wj-SPowwn6;jEh=Wv(0U9Zf(Lk*m>pPIEds2#;>hSVk^)myNy60 z(4h-oxVyz&-(;(AZEbQi)H~fwBF6@Tegr`T!2}@$_oAUMWJ$|Ho6BAAZga6{;JiFH z)JQ@TK{!8FJ`BwKN_lQte`NTK4VXw{3%i{fQ18<_FgX@Vs$m3s(4#Af_kSjiFRd_| zFHmL#6H(}zDe=`6-_4FmITZzx~V;>uJB7E?v z)QpFW@0f?5GAEuiryMt@>`C8~c+8yguDSS=)XW=--a(PtIpK5NEuA_|nB^OdR?0n> zFyifmocDrauU?Yy@yBl_>ogJLe(=Rib`8Ot4n;**={3iOhE)yKA1e&2O4Fav2Un%& zP8dU~5_KmMrK%Kl_vCgRT;Qpd_dp2WU0EPyVj&-?UsiqwA=oXf?sN3SKfhImY(8ar zl9YqIYpQel0Dv%cRn=9k_H&K;yBYHUy_}u-skQ+4S7|Z)^sES}6jdYD?`Gv{r3pxu zsu^=0*Fph*t?mUV;`hueFx*T5%K1z4ilnJXPJ4F#DnNgHjcueMk7OhHA=_|i8j_3n z*S38D=I#Z#q&5IW%OEdAgMU@wtq_P=Eb{J=(YG*RZ z>;!WN<`T@~wTmZ14gcff@=)@}?xd7z`8SK(3?f!FZ+46cokyZZg2h~Q6iDqz-l<-6 zjFh@G#9c^00s90IHuH&RBbbeV2?}))i5RR7p{CN>&(9YTMOmS;-POPxt!_t)lbxrW z#Dc0NCtB7f`bKng7b~ELi6U+CSWgtW^%-!<*`2cA^&%l7=xsxvsz@LjB2vRQ{`Fxzo<61R_mdTA9Py`_mIL${-Jx8 zbRUvmtE1YU)WUuIgXQ%wgO6>GgKA#eKHTsi3iUd5WqTD^y2+-uV-CY&ZCe+PPU+DUuV2I|mYqFBoBcpN~ zh|c&7{wc^dV1}3yM&Gg~C8`rueA2S|rZ&4($DYI|*2#Tq-h}CV+r2h}gBVM=>AtR< z2qGg%-P~N?9@s}|Vz~SxBc`mVdI~kc$zQl{gdvTr()qXdwZ@1BzE062dxnjcUB~@H z%@o)dF{vp@3P_gjWgZfpy|GX&cRwiD2co$Vyp#lN_Jv~&>33F`2*MF!Z+>-hj! z!8>@4LA2&xzL)3ats-SQK?J6ZcsY=QFVxkxu-V~qi9%MB-Wq~?dCVjE(kn>rQl~$% zPCN2Z;%+85gn;Y*38HDL`NG7mV6(L_+0Oi{no|o_@MtAF)Sm_^*YW8}S$1zOLH<4{ zBokUM3oU}ai5yb$rNhX4N&QK=Uz3Ebc1_*5`GkbBmZ$FGPi+m4>!x@glY>tP&Jdg>_>{*!J!a4pll+EWNqUTLf4Z>j7ft-(NwFs-Yd9NBE?*`% zL0~~S&)7Q9(n5-N5bP(u=von9ur(t~tdU=g(&IZ&?!K*D`WpQVR;!+=2r_&?F5lvt z_T*BZs=LYgFI9wlg4hWjAe-MFF=uZw4o%hG@o-`gHU=MQTvE8@}5rWl;$Yb_u5 z?0~MGMzfWa1)V+pQ-mt?>0EXOF^@Cu37} zMRoP0X_dA~<+dpmv)M(m{)~W*TuiiZ*agxIyRFUMW|wUZEp2#Q`x~iSSFg^^?)O!vnyI=hFF$UcKRr9t}qE*S(4S-RCwY{D4w?1&SBe ztrYVx0r^0iwcb4GMjDQ zdN>ZQilZ^U-C7S%mXc9grye+bo7QlQQaR2G-pGKfyy1;BRDS^Jclh&f)P$y>9upd0 z@wg+Sq*Q#C)mcYuU@#-uVdbU+(LDW46aVCBCN{(LW`>kbHfr&kcrHxje?9JmbUysV zYmmjiJ~1&QlY|8AYRTIjr1AWFC8+V^d;1^h~u<(Bpys#E0u( zJC{FFCw-3%^O+n@+!U5M?8|L*jbM3X@H^@!%?NSh@vluWRm3rZR;f?lTJK)$U#r{s zg43g+m~S|p0b}{$(}mJRbh1s=e_W@fxAMX>o21Fe`B>GT&6iRxQ=7zMD?;W!4av(L z?#0Qho5G2}Bl+C(Nr6{o*ZJ+CA}7k0mGO~Zq=1#r`l5S?$mM0?K0^>hiHp}|7(ISN zQ9}6DFA_t=I-E|-bA0fHvDlsZ3v(ck@4qlzvZ2)_HR8*Cx|mzYf$&GnZCW;m7&-jY zuTo$>H(V@8AU-pbBeAI){Oj?$TJ6PR$&L!6)QuM(lB$Kbt)wfg>dBPhVgyt4Ll=P(xrvr5ZMa$e9GQc<^^w zzqvtOeHuj3=!L*W5Qqb5J5kFCW|Qx`k;2Vf92EVf6IYCsiogv2?-)~d~ID;N&sdW{DGB? z0E1)6hcC?GbhsUOAEm1zYd{v0l~Vv2*XNNEqJ^6b{)Q{JXyIF!sg&qoszszBHY|~| zNOHFFqz+Q7H=I%AIf9C~i{M>?8Om2W$T5pOUd&R_z%nHZm)pSJ7B!Kn=o4k^ufJJo z)5FG)CBz_D?~Rv0F&V||=_&a@B%}m#KOYG>;80?tU^pyQN~0hS<|}nkPy`Li`Y0G< zaFVYU%ENQ+zAn2U8Q zTZX`BGo5vQ-nRjYVJO7r-)QlPt`$;}V#(1?Y&G_I`{yV%Lt!~ID@TUH7-=nrUF!`S z28tAvh&0|%)+#&F;Zaztlx9H2kOwHQYI3)LKtvRyJDeWH=%y>{GH@g>E4wqmVi8;P zTFQAfX^4Zw!^-;^kS{%ozP|NFX2PS|LJGz{?D5fatH9H=Fn*Zi93gPy(v$8Z%Eu#M zKv|Er|87pzTCX4Ad&2w7t*m8{qlvz5;hn(QU}wUo-&QHghWLfMwN2LT>ZY=tmb{Ewp6yQ&abQ6kr^q`wWJLz|q+wkvf- z;0B9wya&*}D-C5(h}UIhV;KxGi<{vns`zdKRXJJ)R_Qr3c*J|P44SouB~;WN#aRK_ zp-YKwB4}2&RX~~aJSy(?UaEj&I?U_fBuIt>%GpU!Cmlk`i{9~*;h+vr2a2H*>XZK$ zdF&I4^8!*n?P3S+^ddbuPBApFekK+Yh`0KOvrtWywIx{L-@TD;}9_-{D;^|idjS8kEle|KmkUY zQUe1sg?W#i3;%zjNv0pRhzwpQUj#wHIBbKJmT8dZeZ2;b;JZLkX2Wgpq7pm@ZjBTB zY&V(weA`r9b07oilt0XYBp9X~m;>b@N69Tgy4N@tIw0s7q^+;8rOJbK*o0!QS_c{W zF1miOOd19HHpq_BV<}Pe(T`0fm_|S!>==ETW9O6xegxcanTaxuB%tFpizi4Tpo20?B@oBjOrpf;ln#q*GJ!ZL z(HTm-3&kGK*5actT3%A`Y;4J*FXvhI_6EE7faYRiW1}5OYyb?lD5vWo-trW2o+hCG zi!oYYY?oqM2m^=gC7KojqnXTTmNJ@&ADek z=eOSLJu7cNBZs^m92}&<-=QhrFaN0d*^uAnNQVRX*4I*aNwtZeX-?p^i6*}GTA)&M z2PCQ06BdJUiz}2&=;!7I=4k%*#8mAZcOW13K@>ke$-px%4FNZAof{YMn8s#wOO44K z_6#0_55c?SgObk_G_DjMrP0_z+|UbtNtK2VJDVigLV4TdVRBe+_(4$KiiE-Wu5tYa z5d-K*S9(7`>LB=Nzcc@Tuj!_jiLT6k?P%9{KYj2f(Hqlm5$pHWWpo=Gv-x+Ehvdci zdA)=Elr26EZM%Y9ll;Ei!K=Y3fyS0#8{+rY72-DXGZReO#NMI2Z9?++P`A#Ou%b|- zVG#o?k=tp&h9q^$i2c?rU`yx<<9%7t%Bv&5sQgDHMAf-+{Q(CwV1z%d0lblZUMG&V zE6;EBGbJbcHM_#y;l7f`jq>w)Q~Z=|v?w{k9l?eSNIs%*Dh+hB%`9s8`|Kkr`vBW8 z+pyj-2PH9}zr2F6{>cAp1*v|`t|)iZ%?if(dA(_V%9buFFu6^CSApq&svw#w2*La_ z{9YOh_>;CsZD#)jo$l|gEz7SnV7g_xiv04Q>HYYF67M?_-;LgUT3s=&8Ge0tjGqe{ zcTAyXOKiQ=psAM@55lPz>yEYEM)ho!HU;W5U2(|yIi2hQI)l4n9U5DXTjF4w;MO8e zbn6f&Epf~EH`%R6%ykb!oZ=2Z%)t=JvUk4?=@Sz-(9GORUpK!;|vcD|+q>T$KyOI@)Yh} z5dK`@12YoX-K4RSKq1iJAbCSwtuAYm#oE@^*A z8G?v55E%KpMWY~?UoXmn5S~#S7ZyP5K!Sh8D9!|)zd4$>6$dI6deDb`PLT;_poa6h zjz3grPY!q;`Z;oF*BVb(elNp z=!$gBn_Qvl_MVIe(!1UKqr~Dsto$4EBy% zTJaggc!QUb*T~rW^0Z1MJA>!8xE!t~yDh00EBET1 zE^C9`$%My0qPZCu>3M_NnB8Vy=4i3oip@F^hgI?b$J$Ygg?O36-ej}5*0kB#Qe-eI z!90Q*g897HF%v5IRY#$L{IRu^Z8a}$Y}boe<@`iru3IrOo(25+{g=D)rZN+VJ z_jNK@LLv${Ac(M8K)i(na}h8>p)Mj3gViEbmzf9o_#&bx%V4uR>zSj?Sj- z;(2&;<*gz?2LY`?ufDOh&f>6P)^}@Sk?Cs`iAC&BL~MTV$_)8VWO~%!udLEREe~B~ z(%(&5>-d;eHS+yPey-lT>emw7&&_MBP{}v084Tro@0t|-CKUc4sc)~D0}Hyy=2qWC zGXF(+1hu!>a5dO0R#&yZAL~%rtoLPCy+PK;%I(WY*FD3N$M*%WOOVpWpz&(>!PM zWce79d20LSNqX4HpV=0x??FbVnLpbe$noUTHt)3LT z24{O+v%~4cCE#T1N$*}^2TD%`MDfn;N%9NGcuIX@`&Mbv!z9~Da2x@*hsTMgx#%^D z&BJ18Wwg_X#m^~}$ZE96%KfWYgD)$xs^w4oYFfsv)B=2|Z6le`Iw7i zBlCz_x?_`+KpEk<_7^19qc40X|MoF69OgrIjtLVE#W;)AbC}oc991UBJ(T2A1cxcq zUZTWw79%N|7C0cYTY(06Bg0qu>pQdM*N}Wim3Q5cAe76y@4&sjYg?!C+W9vONG-Y4jx*dGw}A;Cuk9}}GC4f}HEoe>h$@#<(rvS1`}_C7*Z zeIyf%Aoh#BlFe!U-M$H>|J1}6UIZqfl+2-Q1ciKw;4FgISl?u~;@M!qw5)vP{-LH7 zq`aEo52Phpna%g?PfZIY=06Sh4|U(z&*iVtWU#6nC<@S@BkNQAlV`K)ez6ZU!L3AW zARGU0!B-th!jV%BO@}`I{-M815Ia)Ib^`f6v=)@#vxTjRaGVBWtNgcOKH)3 z_Mc?^3Bd;PD7yItDH>O{+uLyn>Ra3Ks`e#mJVNtf8p*2;$IF+Ivq5zp zo~boZYZ*;Tylb5Fj1Bq@$#Zyva8Bf*M~3jh&+SaOiYz=*#nD$m4o?!0541V^+<5sK zk{_!j6&mBJm5Y9Kg@H;sY8JTL)i_0Wr^uD}Ljzh*2 zwf0Y=0k6h)y;3Ay0{+&kCjQ|oiE;wU$_|}e4+#|0Z1j~?PVpiL?jrb(KUo-w2j%Zx zohc`xT9vAMZKb3?O{u-X?>vcA3yMsTJkMc;SF)L z;2yEcVehcG9L;u?Ne1^&eKZ-Ei$JVUv44oW2(4AGuFdLN?pv|@_@0j^Lmof>aVq5U z;PVsYB6PA_EjhnPqH}!TC%fc3kn^ru@@cj_{1P=tOx+2{yrxg;aJZHy`F1rCIE(N8 zOQQd9ed#Yx8$?c&E#8CId^sFO@SR`wj1;-NMBM!ZdP-cJxU|sNMT&x#`3pk~VhL6f zvyU&okcXYwdtm`&a`>uJUW`^>s5M_buZ@{W4un5q9*|fqG1B?yZ-&DnUiwW=0`XZG zITHK5-nSHQs_pt_s$7o>>1y9M56M-++dk42Ru(d4_*ll45?Mz;^|Mlf9#ZrbkB`M{ z7BTHq!ZL!#Nj#gsbU6pg_z#!ICsmNVl3)(O2vn+ORYX=3%;jZQ(*A!9I(cPCObz*1 zhcR&v7x#dhCH{CNHhDhDM5YUf5{W-R=`Ey3F;a^p9=syn;47S}FH$X66Ab|*#DnOP zvR;CDdY@0FPl6F~i%{YWvlqXFv39QQdCdkz{kz%uw5DoY1TZlF*Pe((_u(LSyc*p*!Eoq1rb}5&l zVWeL4s7pzRf!MT9X-bJTZ}o$|BZ|QnlbqEkbumy>_0!IffAP-qE~&TC<3yt9 z-eZm!IBQ6Bt8c!_h?l8ci-B~#i)f~#$HJJfcA{4itUz!^;ETUWyz~dHAtu2^&*oTI zV3cM`N~j59!fq|0Rtj%c!jmC&a2ctW6Z~|cC^gA2X6$MbH4upV{y(&@lSFqCxRu^y zFh^w(J%nllue`jqojKWB<;!H49QM;@D&t2%X7+$Bhq^0flV}%h989b)+Fs4U15WdF zWydJ!fE7x33gpTgG2%U*$`nxKfOw?wMP-9>F%`DM2Ibyykea-SQmP_%O9@0ou?SM= zQS4*Mz!S4$GQ_}$U3VRc#NKuDf-krmD8>6pBrXdNDT!&2EpzmL!Bd+C+of(wfPK*K zcMloR8wcK~h4Diq--_UW8Pk#O!^9QYh`dBfV%qiF``++HLN~AV{Rw%)+F0u{N0S{t zQbpi9LrcA#i7-B6Wo;(J2W_DU1X|B;G9gz7J<7+~;4tM=_eJ<8%7h=VYt zBL{|>c9CTr@gFB@JsI>U2Xi1x{xvd&dOpd469Ai)rzXM(`2e!YJ*OrjD@w$F%HM%b zDQPs@r#v|sT=1z9oePuXUR18~ROiAXfPKodQ=lAvt3Vzk1RbPM1dX1QJV=#-MPI2e z_=?n`ES(A?^g?H|^1xK6!IAEp3Pa%;1@a+H?`w^coeyK>L+GT#W6y`LB#2ikr{g|x zP}w*gs`33;>6;G2!o<6v82NQn<+DmeA(-XEXt&XmUkJ^TUYsodqNv4?VQ8RwmJ>LX zzZSzZ`3NfR^<>Y0Gg?eTUkXWZR7oj?Me+%hcs%!)!U-)N?n-GnSd)H_Ja(Sq`~fMi zwA_LFyx5ZdjhM%kQ{|AO|08K_Py#9-7o8PWK*pfs#JW$T3{8VLWm^SA%YQ=72cBmt z;FK0#RNU3Dz%anO(pL?|Suc~7*fh1w7l0TqVkq2tawuXMK{dh6p!4TK4hDT_E(|pb z^L{%gUZFHe^b?=L*h%t5a3jETZZ2qL7_NM<09L{=rEDQo3>G_S54rB6ehoY=o?8g1 zNwvhSL%CPi(%OQb2Rj_L2D{5ETU{>PHbyGCMNk;@Iz=HE?wPX)Iw4>$(&m?0gYwQ| z7$N0?GIa^Wc_NlTgsz)jC|C;l9Y=7Q9`lHrMnGQ$8GZd@vkB%9xCrQj52G(TjK1Ko zEd=x>g*{4ejDX(R*~SVtmwWkPZkuq9*Vu^X<`kmv8j-xJ3*wSm4lRZO9 zw1gO~2S#(9(X?hX`Fs;VjE{{PZP!F1im#*rQ<__C?M?RE*=5h+de{u%GbNbvtyUw2 SUBnX!rd+J`RM}yW?!N(%)c-30 diff --git a/recruitment/linkedin_service.py b/recruitment/linkedin_service.py index e275f2d..3f645a4 100644 --- a/recruitment/linkedin_service.py +++ b/recruitment/linkedin_service.py @@ -1,19 +1,18 @@ # jobs/linkedin_service.py import uuid -import re -from html import unescape -from urllib.parse import quote, urlencode + import requests import logging import time from django.conf import settings +from urllib.parse import quote, urlencode logger = logging.getLogger(__name__) # Define constants LINKEDIN_API_VERSION = '2.0.0' LINKEDIN_VERSION = '202409' -MAX_POST_CHARS = 3000 # LinkedIn's maximum character limit for shareCommentary + class LinkedInService: def __init__(self): @@ -162,113 +161,114 @@ class LinkedInService: # ---------------- POSTING UTILITIES ---------------- - def clean_html_for_social_post(self, html_content): - """Converts safe HTML to plain text with basic formatting.""" - if not html_content: - return "" + # def clean_html_for_social_post(self, html_content): + # """Converts safe HTML to plain text with basic formatting.""" + # if not html_content: + # return "" - text = html_content + # text = html_content - # 1. Convert Bolding tags to *Markdown* - text = re.sub(r'(.*?)', r'*\1*', text, flags=re.IGNORECASE) - text = re.sub(r'(.*?)', r'*\1*', text, flags=re.IGNORECASE) + # # 1. Convert Bolding tags to *Markdown* + # text = re.sub(r'(.*?)', r'*\1*', text, flags=re.IGNORECASE) + # text = re.sub(r'(.*?)', r'*\1*', text, flags=re.IGNORECASE) - # 2. Handle Lists: Convert
  • tags into a bullet point - text = re.sub(r'', '\n', text, flags=re.IGNORECASE) - text = re.sub(r']*>', '• ', text, flags=re.IGNORECASE) - text = re.sub(r'
  • ', '\n', text, flags=re.IGNORECASE) + # # 2. Handle Lists: Convert
  • tags into a bullet point + # text = re.sub(r'', '\n', text, flags=re.IGNORECASE) + # text = re.sub(r']*>', '• ', text, flags=re.IGNORECASE) + # text = re.sub(r'
  • ', '\n', text, flags=re.IGNORECASE) - # 3. Handle Paragraphs and Line Breaks - text = re.sub(r'

    ', '\n\n', text, flags=re.IGNORECASE) - text = re.sub(r'
    ', '\n', text, flags=re.IGNORECASE) + # # 3. Handle Paragraphs and Line Breaks + # text = re.sub(r'

    ', '\n\n', text, flags=re.IGNORECASE) + # text = re.sub(r'
    ', '\n', text, flags=re.IGNORECASE) - # 4. Strip all remaining, unsupported HTML tags - clean_text = re.sub(r'<[^>]+>', '', text) + # # 4. Strip all remaining, unsupported HTML tags + # clean_text = re.sub(r'<[^>]+>', '', text) - # 5. Unescape HTML entities - clean_text = unescape(clean_text) + # # 5. Unescape HTML entities + # clean_text = unescape(clean_text) - # 6. Clean up excessive whitespace/newlines - clean_text = re.sub(r'(\n\s*){3,}', '\n\n', clean_text).strip() + # # 6. Clean up excessive whitespace/newlines + # clean_text = re.sub(r'(\n\s*){3,}', '\n\n', clean_text).strip() - return clean_text + # return clean_text - def hashtags_list(self, hash_tags_str): - """Convert comma-separated hashtags string to list""" - if not hash_tags_str: - return ["#HigherEd", "#Hiring", "#UniversityJobs"] + # def hashtags_list(self, hash_tags_str): + # """Convert comma-separated hashtags string to list""" + # if not hash_tags_str: + # return ["#HigherEd", "#Hiring", "#UniversityJobs"] - tags = [tag.strip() for tag in hash_tags_str.split(',') if tag.strip()] - tags = [tag if tag.startswith('#') else f'#{tag}' for tag in tags] + # tags = [tag.strip() for tag in hash_tags_str.split(',') if tag.strip()] + # tags = [tag if tag.startswith('#') else f'#{tag}' for tag in tags] - if not tags: - return ["#HigherEd", "#Hiring", "#UniversityJobs"] + # if not tags: + # return ["#HigherEd", "#Hiring", "#UniversityJobs"] - return tags + # return tags - def _build_post_message(self, job_posting): - """ - Constructs the final text message. - Includes a unique suffix for duplicate content prevention (422 fix). - """ - message_parts = [ - f"🔥 *Job Alert!* We’re looking for a talented professional to join our team.", - f"👉 **{job_posting.title}** 👈", - ] + # def _build_post_message(self, job_posting): + # """ + # Constructs the final text message. + # Includes a unique suffix for duplicate content prevention (422 fix). + # """ + # message_parts = [ + # f"🔥 *Job Alert!* We’re looking for a talented professional to join our team.", + # f"👉 **{job_posting.title}** 👈", + # ] - if job_posting.department: - message_parts.append(f"*{job_posting.department}*") + # if job_posting.department: + # message_parts.append(f"*{job_posting.department}*") - message_parts.append("\n" + "=" * 25 + "\n") + # message_parts.append("\n" + "=" * 25 + "\n") - # KEY DETAILS SECTION - details_list = [] - if job_posting.job_type: - details_list.append(f"💼 Type: {job_posting.get_job_type_display()}") - if job_posting.get_location_display() != 'Not specified': - details_list.append(f"📍 Location: {job_posting.get_location_display()}") - if job_posting.workplace_type: - details_list.append(f"🏠 Workplace: {job_posting.get_workplace_type_display()}") - if job_posting.salary_range: - details_list.append(f"💰 Salary: {job_posting.salary_range}") + # # KEY DETAILS SECTION + # details_list = [] + # if job_posting.job_type: + # details_list.append(f"💼 Type: {job_posting.get_job_type_display()}") + # if job_posting.get_location_display() != 'Not specified': + # details_list.append(f"📍 Location: {job_posting.get_location_display()}") + # if job_posting.workplace_type: + # details_list.append(f"🏠 Workplace: {job_posting.get_workplace_type_display()}") + # if job_posting.salary_range: + # details_list.append(f"💰 Salary: {job_posting.salary_range}") - if details_list: - message_parts.append("*Key Information*:") - message_parts.extend(details_list) - message_parts.append("\n") + # if details_list: + # message_parts.append("*Key Information*:") + # message_parts.extend(details_list) + # message_parts.append("\n") - # DESCRIPTION SECTION - clean_description = self.clean_html_for_social_post(job_posting.description) - if clean_description: - message_parts.append(f"🔎 *About the Role:*\n{clean_description}") + # # DESCRIPTION SECTION + # clean_description = self.clean_html_for_social_post(job_posting.description) + # if clean_description: + # message_parts.append(f"🔎 *About the Role:*\n{clean_description}") + # clean_ - # CALL TO ACTION - if job_posting.application_url: - message_parts.append(f"\n\n---") - # CRITICAL: Include the URL explicitly in the text body. - # When media_category is NONE, LinkedIn often makes these URLs clickable. - message_parts.append(f"🔗 **APPLY NOW:** {job_posting.application_url}") + # # CALL TO ACTION + # if job_posting.application_url: + # message_parts.append(f"\n\n---") + # # CRITICAL: Include the URL explicitly in the text body. + # # When media_category is NONE, LinkedIn often makes these URLs clickable. + # message_parts.append(f"🔗 **APPLY NOW:** {job_posting.application_url}") - # HASHTAGS - hashtags = self.hashtags_list(job_posting.hash_tags) - if job_posting.department: - dept_hashtag = f"#{job_posting.department.replace(' ', '')}" - hashtags.insert(0, dept_hashtag) + # # HASHTAGS + # hashtags = self.hashtags_list(job_posting.hash_tags) + # if job_posting.department: + # dept_hashtag = f"#{job_posting.department.replace(' ', '')}" + # hashtags.insert(0, dept_hashtag) - message_parts.append("\n" + " ".join(hashtags)) + # message_parts.append("\n" + " ".join(hashtags)) - final_message = "\n".join(message_parts) + # final_message = "\n".join(message_parts) - # --- FIX: ADD UNIQUE SUFFIX AND HANDLE LENGTH (422 fix) --- - unique_suffix = f"\n\n| Ref: {int(time.time())}" + # # --- FIX: ADD UNIQUE SUFFIX AND HANDLE LENGTH (422 fix) --- + # unique_suffix = f"\n\n| Ref: {int(time.time())}" - available_length = MAX_POST_CHARS - len(unique_suffix) + # available_length = MAX_POST_CHARS - len(unique_suffix) - if len(final_message) > available_length: - logger.warning("Post message truncated due to character limit.") - final_message = final_message[:available_length - 3] + "..." + # if len(final_message) > available_length: + # logger.warning("Post message truncated due to character limit.") + # final_message = final_message[:available_length - 3] + "..." - return final_message + unique_suffix + # return final_message + unique_suffix # ---------------- MAIN POSTING METHODS ---------------- @@ -279,7 +279,9 @@ class LinkedInService: CRITICAL FIX: Avoids ARTICLE category if not using an image to prevent 402 errors. """ - message = self._build_post_message(job_posting) + message = job_posting.linkedin_post_formated_data + if len(message)>=3000: + message=message[:2900]+"...." # --- FIX FOR 402: Force NONE if no image is present. --- if media_category != "IMAGE": diff --git a/recruitment/migrations/0003_jobposting_linkedin_post_formated_data.py b/recruitment/migrations/0003_jobposting_linkedin_post_formated_data.py new file mode 100644 index 0000000..5e92015 --- /dev/null +++ b/recruitment/migrations/0003_jobposting_linkedin_post_formated_data.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-10-27 10:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0002_candidate_retry'), + ] + + operations = [ + migrations.AddField( + model_name='jobposting', + name='linkedin_post_formated_data', + field=models.JSONField(blank=True, null=True), + ), + ] diff --git a/recruitment/migrations/0004_alter_jobposting_linkedin_post_formated_data.py b/recruitment/migrations/0004_alter_jobposting_linkedin_post_formated_data.py new file mode 100644 index 0000000..d821d60 --- /dev/null +++ b/recruitment/migrations/0004_alter_jobposting_linkedin_post_formated_data.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-10-27 11:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0003_jobposting_linkedin_post_formated_data'), + ] + + operations = [ + migrations.AlterField( + model_name='jobposting', + name='linkedin_post_formated_data', + field=models.CharField(blank=True, null=True), + ), + ] diff --git a/recruitment/migrations/0005_alter_jobposting_linkedin_post_formated_data.py b/recruitment/migrations/0005_alter_jobposting_linkedin_post_formated_data.py new file mode 100644 index 0000000..6bd9476 --- /dev/null +++ b/recruitment/migrations/0005_alter_jobposting_linkedin_post_formated_data.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-10-27 11:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0004_alter_jobposting_linkedin_post_formated_data'), + ] + + operations = [ + migrations.AlterField( + model_name='jobposting', + name='linkedin_post_formated_data', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/recruitment/models.py b/recruitment/models.py index 4d545ec..f651e57 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -129,6 +129,7 @@ class JobPosting(Base): max_length=50, blank=True, help_text="Status of LinkedIn posting" ) linkedin_posted_at = models.DateTimeField(null=True, blank=True) + linkedin_post_formated_data=models.TextField(null=True,blank=True) published_at = models.DateTimeField(db_index=True, null=True, blank=True) # Added index # University Specific Fields diff --git a/recruitment/signals.py b/recruitment/signals.py index 5f6b623..590aed2 100644 --- a/recruitment/signals.py +++ b/recruitment/signals.py @@ -45,6 +45,15 @@ def format_job(sender, instance, created, **kwargs): # If the instance is no longer active, delete the scheduled task existing_schedule.delete() +# @receiver(post_save, sender=JobPosting) +# def update_form_template_status(sender, instance, created, **kwargs): +# if not created: +# if instance.status == "Active": +# instance.form_template.is_active = True +# else: +# instance.form_template.is_active = False +# instance.save() + @receiver(post_save, sender=Candidate) def score_candidate_resume(sender, instance, created, **kwargs): if not instance.is_resume_parsed: @@ -351,4 +360,4 @@ def create_default_stages(sender, instance, created, **kwargs): # required=False, # order=3, # is_predefined=True - # ) \ No newline at end of file + # ) diff --git a/recruitment/tasks.py b/recruitment/tasks.py index d975b10..7edffd7 100644 --- a/recruitment/tasks.py +++ b/recruitment/tasks.py @@ -116,25 +116,55 @@ def format_job_description(pk): job_posting = JobPosting.objects.get(pk=pk) print(job_posting) prompt = f""" - Can you please organize and format this unformatted job description and qualifications into clear, readable sections using headings and bullet points? - Format the Content: You need to convert the clear, formatted job description and qualifications into a 2 blocks of HTML code. - **JOB DESCRIPTION:** - {job_posting.description} + You are a dual-purpose AI assistant specializing in content formatting and social media copywriting for job announcements. - **QUALIFICATIONS:** - {job_posting.qualifications} + **JOB POSTING DATA (Raw Input):** + --- + **JOB DESCRIPTION:** + {job_posting.description} - **STRICT JSON OUTPUT INSTRUCTIONS:** - Output a single, valid JSON object with ONLY the following two top-level keys: + **QUALIFICATIONS:** + {job_posting.qualifications} - 'job_description': 'A HTML containing the formatted job description', - 'job_qualifications': 'A HTML containing the formatted job qualifications', + **BENEFITS:** + {job_posting.benefits} + **APPLICATION INSTRUCTIONS:** + {job_posting.application_instructions} - Do not include any other text except for the JSON output. + **APPLICATION DEADLINE:** + {job_posting.application_deadline} + + **HASHTAGS: for search and reach:** + {job_posting.hash_tags} + --- + + **TASK 1: HTML Formatting (Two Blocks)** + 1. **Format the Job Description:** Organize and format the raw JOB DESCRIPTION and BENEFITS data into clear, readable sections using `

    ` headings and `
      `/`
    • ` bullet points. Encapsulate the entire formatted block within a single `
      `. + 2. **Format the Qualifications:** Organize and format the raw QUALIFICATIONS data into clear, readable sections using `

      ` headings and `
        `/`
      • ` bullet points. Encapsulate the entire formatted block within a single `
        `. + 2. **Format the Requirements:** Organize and format the raw Requirements data into clear, readable sections using `

        ` headings and `
          `/`
        • ` bullet points. Encapsulate the entire formatted block within a single `
          `. + + **TASK 2: LinkedIn Post Creation** + 1. **Write the Post:** Create an engaging, professional, and concise LinkedIn post (maximum 1300 characters) summarizing the opportunity. + 2. **Encourage Action:** The post must have a strong call-to-action (CTA) encouraging applications. + 3. **Use Hashtags:** Integrate relevant industry, role, and company hashtags (including any provided in the raw input) naturally at the end of the post. + + **STRICT JSON OUTPUT INSTRUCTIONS:** + Output a **single, valid JSON object** with **ONLY** the following three top-level key-value pairs. + + * The values for `html_job_description` and `html_qualifications` MUST be the complete, formatted HTML strings (including all tags). + * The value for `linkedin_post` MUST be the complete, final LinkedIn post as a single string not greater than 3000 characters. + + **Output Keys:** + 1. `html_job_description` + 2. `html_qualifications` + 3 `html_job_requirements` + 4. `linkedin_post_data` + + **Do not include any other text, explanation, or markdown outside of the final JSON object.** """ result = ai_handler(prompt) - + print(f"REsults: {result}") if result['status'] == 'error': logger.error(f"AI handler returned error for candidate {job_posting.pk}") print(f"AI handler returned error for candidate {job_posting.pk}") @@ -144,9 +174,10 @@ def format_job_description(pk): data = json.loads(data) print(data) - job_posting.description = data.get('job_description') - job_posting.qualifications = data.get('job_qualifications') - job_posting.save(update_fields=['description', 'qualifications']) + job_posting.description = data.get('html_job_description') + job_posting.qualifications = data.get('html_qualifications') + job_posting.linkedin_post_formated_data=data.get('linkedin_post_data') + job_posting.save(update_fields=['description', 'qualifications','linkedin_post_formated_data']) def ai_handler(prompt): @@ -400,6 +431,8 @@ def handle_reume_parsing_and_scoring(pk): logger.info(f"Successfully scored and saved analysis for candidate {instance.id}") print(f"Successfully scored and saved analysis for candidate {instance.id}") + + def create_interview_and_meeting( candidate_id, job_id, diff --git a/recruitment/utils.py b/recruitment/utils.py index 4b24c1a..af759df 100644 --- a/recruitment/utils.py +++ b/recruitment/utils.py @@ -612,4 +612,8 @@ def update_meeting(instance, updated_data): return {"status": "success", "message": "Zoom meeting updated successfully."} logger.warning(f"Failed to update Zoom meeting {instance.meeting_id}. Error: {result.get('message', 'Unknown error')}") - return {"status": "error", "message": result.get("message", "Zoom meeting update failed.")} \ No newline at end of file + return {"status": "error", "message": result.get("message", "Zoom meeting update failed.")} + + + + diff --git a/recruitment/views.py b/recruitment/views.py index 9d535fa..28c99da 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -357,6 +357,15 @@ def job_detail(request, slug): status_form = JobPostingStatusForm(request.POST, instance=job) if status_form.is_valid(): + job_status=status_form.cleaned_data['status'] + form_template=job.form_template + if job_status=='ACTIVE': + form_template.is_active=True + form_template.save(update_fields=['is_active']) + else: + form_template.is_active=False + form_template.save(update_fields=['is_active']) + status_form.save() # Add a success message diff --git a/templates/jobs/job_detail.html b/templates/jobs/job_detail.html index 25b27f2..952af61 100644 --- a/templates/jobs/job_detail.html +++ b/templates/jobs/job_detail.html @@ -347,9 +347,14 @@ {% trans "Create New Form Template" %} {% else %} + {% if job.form_template.is_active %} {% trans "View Form Template" %} + {% else %} +

          {% trans "This job status is not active, the form will appear once the job is made active"%}

          + {% endif %} + {% endif %}

        -- 2.39.5 From 99643eb08c11a550f7e62cab44b453a93a805ce9 Mon Sep 17 00:00:00 2001 From: Faheed Date: Mon, 27 Oct 2025 17:56:41 +0300 Subject: [PATCH 08/13] missing details --- recruitment/tasks.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/recruitment/tasks.py b/recruitment/tasks.py index 7edffd7..e5a454a 100644 --- a/recruitment/tasks.py +++ b/recruitment/tasks.py @@ -137,12 +137,17 @@ def format_job_description(pk): **HASHTAGS: for search and reach:** {job_posting.hash_tags} + + **APPLICATION URL: for career page only if it is provided** + {job_posting.application_url} --- **TASK 1: HTML Formatting (Two Blocks)** 1. **Format the Job Description:** Organize and format the raw JOB DESCRIPTION and BENEFITS data into clear, readable sections using `

        ` headings and `
          `/`
        • ` bullet points. Encapsulate the entire formatted block within a single `
          `. 2. **Format the Qualifications:** Organize and format the raw QUALIFICATIONS data into clear, readable sections using `

          ` headings and `
            `/`
          • ` bullet points. Encapsulate the entire formatted block within a single `
            `. - 2. **Format the Requirements:** Organize and format the raw Requirements data into clear, readable sections using `

            ` headings and `
              `/`
            • ` bullet points. Encapsulate the entire formatted block within a single `
              `. + 3. **Format the Benefits:** Organize and format the raw Requirements data into clear, readable sections using `

              ` headings and `
                `/`
              • ` bullet points. Encapsulate the entire formatted block within a single `
                `. + 4. **Application Instructions:** Organize and format the raw Requirements data into clear, readable sections using `

                ` headings and `
                  `/`
                • ` bullet points. Encapsulate the entire formatted block within a single `
                  `. + **TASK 2: LinkedIn Post Creation** 1. **Write the Post:** Create an engaging, professional, and concise LinkedIn post (maximum 1300 characters) summarizing the opportunity. @@ -158,8 +163,9 @@ def format_job_description(pk): **Output Keys:** 1. `html_job_description` 2. `html_qualifications` - 3 `html_job_requirements` - 4. `linkedin_post_data` + 3. 'html_benefits' + 4. 'html_application_instructions' + 5. `linkedin_post_data` **Do not include any other text, explanation, or markdown outside of the final JSON object.** """ @@ -176,6 +182,8 @@ def format_job_description(pk): job_posting.description = data.get('html_job_description') job_posting.qualifications = data.get('html_qualifications') + job_posting.benefits=data.get('html_benefits') + job_posting.application_instructions=data.get('html_application_instruction') job_posting.linkedin_post_formated_data=data.get('linkedin_post_data') job_posting.save(update_fields=['description', 'qualifications','linkedin_post_formated_data']) @@ -501,7 +509,7 @@ def handle_zoom_webhook_event(payload): # We rely on the unique 'id' that maps to your ZoomMeeting.meeting_id field. meeting_id_zoom = str(object_data.get('id')) print(meeting_id_zoom) - if not meeting_id_zoom: + if not meeting_id_zoom:job_posting.application_url}" logger.warning(f"Webhook received without a valid Meeting ID: {event_type}") return False -- 2.39.5 From f9aaaeb7888d2ba656da7554e484dbed483f3c53 Mon Sep 17 00:00:00 2001 From: Faheed Date: Mon, 27 Oct 2025 19:33:53 +0300 Subject: [PATCH 09/13] linkedin post content edit mode --- recruitment/__pycache__/forms.cpython-312.pyc | Bin 31422 -> 31802 bytes recruitment/__pycache__/urls.cpython-312.pyc | Bin 12482 -> 12633 bytes recruitment/__pycache__/views.cpython-312.pyc | Bin 92506 -> 93558 bytes recruitment/forms.py | 6 +++- recruitment/tasks.py | 2 +- recruitment/urls.py | 3 +- recruitment/views.py | 27 ++++++++++++++++++ templates/jobs/job_detail.html | 8 ++++++ .../jobs/partials/linkedin_content_form.html | 22 ++++++++++++++ 9 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 templates/jobs/partials/linkedin_content_form.html diff --git a/recruitment/__pycache__/forms.cpython-312.pyc b/recruitment/__pycache__/forms.cpython-312.pyc index cfa60ce958a1bcbf4458f1ca79d640595fc97c8d..4c46da24b7d91601890e0a9bc930bf93b0c22d24 100644 GIT binary patch delta 476 zcmdn@m2uY(M!wU$yj%=G@O9$<455UHd=iXS6V)GaTBY!{Fh*&mSZ@|$RAc61VsK|j zv1nmPv7EesQJ&SQl2g-WvkgmXF4Ic3$%@7DljVFwHeW03Wn`?JoKhUdR?f)4P*gto zK(QQCIpgF&Z+jt8pUk}M)Rchy;u7clypq(slF19a&26MGgx&Isa`k*uOA>EM=YX|j z=EWBPb;YLvB@#DSz@Nv1kwm1 zYEd=HgEY!RG&Vsrrj-6*?EtE5o}5?~uG0q62qKz*8jGqyJg_YwAx*}Y3P2lylmm!z z-u$GDjmaDwSYU^OEd?782b72C(*m+?aoFVMrgK^(iM!wU$yj%=G5D@k&qcMIWp9G`8MD<6U7Abr!j8R%CmYanb)tFf-IW?^| zr?I5wPF6}2+I+Wg10!R_QUW6Cn1GBb5YHUw9Lo0U^~GkH%v~flI5BL)B{8e#j`edRMjyu3QQKSQ3e1{s6HbA diff --git a/recruitment/__pycache__/urls.cpython-312.pyc b/recruitment/__pycache__/urls.cpython-312.pyc index fe6868521816bd68212f222892af85e939954629..5e3fefcced44c35d9b6aef4b7a020635a5538862 100644 GIT binary patch delta 1770 zcmZ{kOKcNI7{|Tq3D{5*1SEktc2Tg`c3#etK=U9bF?ks~v9V(u65}L}^LELjC6JIJ zsw%3gifAg8TSGaJ0tZ*QGqs4N_pk4U$zb8K3u$+=lAN|%nWR?*A#2k@~+b_+(8IPc;FZ7t#*Y+Ql zZfPTD!!7x((gD1ZzgaCC($d&BX*BiZ-=H`5u_w>WPmSO=q(p4_spr{t=~2(Y?ux1G z$bz?dVP+}wo-VE4^w{KLa%OUFS{uwI-_I^ACP%Y#i|M&V@5|!1L;O>bZ97x+=#Vt9 zeYo_IEYJJ!Q%601!X%-&o9 ztLdaQog{c&MJa9(?1*4zHlvS+9}e$SsHl_Y2k>*H*eYw;ePm!pMYG%)z$uqr&d07Znr^uqn*J>GcbDUU z$gIUaw_UCa;AXc@Q*M3URkxnCDaOym^IzSH_Qd4T)hje6bk(D0eJ#d62<1YH%53Mw z{MRIX8@2~|?4+BXbQA9d6}9mpNx)$p67%Uz=i{n}Rk=GVy2O*(e6wY)H4!il0Sa-S zXYR|?eVJStQ&E~{Ab;X5coOgw;8NmfV2%KF1W06BMKgRF@I2sC4;q-Yl3FWsT8rEH z0ziDc{G?V%MR|#)-Q_l@X9ZFkckDil*Mm>_Peu&sr_^uZdfyLqfFL)$XkV8}{g`A>r(FCz1P2)Y76HMs^>7o~OD zCd3w_Kx07bb7^)qLeEA>Lewz;H1Yp*Z4mGX;HI4@Ye~?S1WAdhM}dw3t83{Wr8FmZhjzA|~??MD^h(m3C71(U$xD#bJiW(0p!bz*1~%owa$tM@xh>%t^1I+_MLuG96B z`{^&U8`-TU6*ckRS5U+n(xLn^w)SG7;y zo>w_2n%su)bFY2PCq9$=7t!kzSz)do>gpl=;uUTO+5yzF)y&F+v^+?n6DpeIexL!M zm0L^9+eN)yBqVY#0}TSbuv@@co{;u|Y F{|8%TEaU(H delta 1372 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ΠH){^M+J^FDbP(L063h$RUWAxWw5D|t_`ujFgmjZ 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 elP{|O8g=7@38LQ4Imrq|~ce@fz8(*FhTKRv+! delta 18249 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
                  xkZ;}>iYM|exUD6v0J7{5>L{c*i0Czn@NH z0WfZABX4gJrN)afd523}MZ0C0OGNMO%}v018;oK7JjMjj(2b6NeL7>?I10RD0DN-T ziqdg_O)%C5D$(=eK&z(PH}CR#=s|^UbB98dR}RA}B%Y|NPyDvR(qXH(1O3d}SM38` zuV4~@k-e+7iguBuq4gLwIG&X)kqd57ZJjM5GaE#4Z*GJ5vk2?C2*K`l`W4<7e}Ep~ z1&us_vjn6CdNzHhh%mAjXJ>(_+aELa3Ryx}fNzz13mTr%-zKFUa zlQdBmxmFVu;dQ`VkD*Cx*DjD7P)ds~=V>tB)nyxX*N!nEcd#XBxOpdIl6IFFKk(d) ziGGd7%;gaBdisP}Wi|E2!3?v=Cr9 zz)ApqI*51Z{Nh>Sxh_9-Hx0Dg_|iD;xEKNi4a1BvGW z{Bcy~!^#Q)N&zYW@Ih1J^PX`=8;#O909^OWr2sAf+))}g4tyj$7`fCW%5wQ&fZUri zSa~EevRRat{0I&A10en5REjejy*N_WAPQFFooG;UQi8-mB3MfVN)8+nu9AI+u`N6h z7w-o?^rX%pozrWeS@H|QBi<~*+au35i)97b!uo|J2L8;Yir$)SqDBl~YZWs-OR`vV hFIrK&xILN5pIR%#_-HGY{xnzg?)Qp6Bqd|9{|AC|NACat diff --git a/recruitment/forms.py b/recruitment/forms.py index 6f4e28a..d9989d1 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -629,7 +629,11 @@ class JobPostingStatusForm(forms.ModelForm): widgets = { 'status': forms.Select(attrs={'class': 'form-select'}), } - +class LinkedPostContentForm(forms.ModelForm): + class Meta: + model = JobPosting + fields = ['linkedin_post_formated_data'] + class FormTemplateIsActiveForm(forms.ModelForm): class Meta: model = FormTemplate diff --git a/recruitment/tasks.py b/recruitment/tasks.py index e5a454a..a6d47cb 100644 --- a/recruitment/tasks.py +++ b/recruitment/tasks.py @@ -509,7 +509,7 @@ def handle_zoom_webhook_event(payload): # We rely on the unique 'id' that maps to your ZoomMeeting.meeting_id field. meeting_id_zoom = str(object_data.get('id')) print(meeting_id_zoom) - if not meeting_id_zoom:job_posting.application_url}" + if not meeting_id_zoom: logger.warning(f"Webhook received without a valid Meeting ID: {event_type}") return False diff --git a/recruitment/urls.py b/recruitment/urls.py index d3e418f..1c63b71 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -65,7 +65,8 @@ urlpatterns = [ path('forms/builder//', views.form_builder, name='form_builder'), path('forms/', views.form_templates_list, name='form_templates_list'), path('forms/create-template/', views.create_form_template, name='create_form_template'), - + + path('jobs//edit_linkedin_post_content/',views.edit_linkedin_post_content,name='edit_linkedin_post_content'), path('jobs//candidate_screening_view/', views.candidate_screening_view, name='candidate_screening_view'), path('jobs//candidate_exam_view/', views.candidate_exam_view, name='candidate_exam_view'), path('jobs//candidate_interview_view/', views.candidate_interview_view, name='candidate_interview_view'), diff --git a/recruitment/views.py b/recruitment/views.py index 28c99da..c2d9331 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -33,6 +33,7 @@ from .forms import ( StaffUserCreationForm, MeetingCommentForm, ToggleAccountForm, + LinkedPostContentForm ) from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent @@ -343,6 +344,7 @@ def job_detail(request, slug): offer_count = applicants.filter(stage="Offer").count() status_form = JobPostingStatusForm(instance=job) + linkedin_content_form=LinkedPostContentForm(instance=job) try: # If the related object exists, use its instance data image_upload_form = JobPostingImageForm(instance=job.post_images) @@ -471,6 +473,7 @@ def job_detail(request, slug): 'high_potential_ratio': high_potential_ratio, 'avg_t2i_days': avg_t2i_days, 'avg_t_in_exam_days': avg_t_in_exam_days, + 'linkedin_content_form':linkedin_content_form } return render(request, "jobs/job_detail.html", context) @@ -508,6 +511,30 @@ def job_image_upload(request, slug): return redirect('job_detail', slug=job.slug) +@login_required +def edit_linkedin_post_content(request,slug): + + job=get_object_or_404(JobPosting,slug=slug) + linkedin_content_form=LinkedPostContentForm(instance=job) + if request.method=='POST': + linkedin_content_form=LinkedPostContentForm(request.POST,instance=job) + if linkedin_content_form.is_valid(): + linkedin_content_form.save() + messages.success(request,"Linked post content updated successfully!") + return redirect('job_detail',job.slug) + else: + messages.error(request,"Error update the Linkedin Post content") + return redirect('job_detail',job.slug) + + else: + linkedin_content_form=LinkedPostContentForm() + return redirect('job_detail',job.slug) + + + + + + def kaauh_career(request): active_jobs = JobPosting.objects.select_related( 'form_template' diff --git a/templates/jobs/job_detail.html b/templates/jobs/job_detail.html index 952af61..84939f9 100644 --- a/templates/jobs/job_detail.html +++ b/templates/jobs/job_detail.html @@ -356,6 +356,8 @@ {% endif %} {% endif %} + +

                @@ -404,6 +406,11 @@ {% trans "Error:" %} {{ job.linkedin_post_status }}

              {% endif %} + {% if job.linkedin_post_formated_data %} + + {% endif %}

      @@ -499,6 +506,7 @@ {% include "jobs/partials/image_upload.html" %} +{% include "jobs/partials/linkedin_content_form.html"%}
    • - {% comment %} {% endcomment %} + {% comment %}

    + + + + + + + + + + + + + {% for participant in participants %} + + + + + + + + + + {% endfor %} + +
    {% trans "Name" %}{% trans "Email" %}{% trans "Phone" %}{% trans "Assigned Jobs" %}{% trans "Designation" %}{% trans "Created At" %}{% trans "Actions" %}
    {{ participant.name }}{{ participant.email }}{{ participant.phone|default:"N/A" }} + {# Iterate over the many-to-many relationship (jobs) #} + {% for job in participant.jobs_participating.all %} + + {{ job.title }} + + {% empty %} + {% trans "None Assigned" %} + {% endfor %} + {{ participant.designation|default:"N/A" }}{{ participant.created_at|date:"d-m-Y" }} +
    + + + + {% if user.is_staff %} + + + + + {% endif %} +
    +
    +
    +
    + + {# Card View #} +
    + {% for participant in participants %} +
    +
    +
    + + +

    + {{ participant.email }}
    + {{ participant.phone|default:"N/A" }}
    + {{ participant.designation|default:"N/A" }} +

    + +
    + {% trans "Assigned Jobs:" %}
    + {% for job in participant.jobs.all %} + + {{ job.title }} + + {% empty %} + {% trans "None" %} + {% endfor %} +
    + +
    +
    + + {% trans "View" %} + + {% if user.is_staff %} + + {% trans "Edit" %} + + + {% endif %} +
    +
    +
    +
    +
    + {% endfor %} +
    +
    + + {# Pagination #} + {% include "includes/paginator.html" %} + {% else %} +
    +
    + +

    {% trans "No participants found" %}

    +

    {% trans "Create your first participant record or adjust your filters." %}

    + {% if user.is_staff %} + + {% trans "Add Participant" %} + + {% endif %} +
    +
    + {% endif %} +
    + + + {# Delete Confirmation Modal #} + + +{% endblock %} + + + +{% block customJS %} + +{% endblock %} \ No newline at end of file diff --git a/templates/recruitment/candidate_interview_view.html b/templates/recruitment/candidate_interview_view.html index 29c44e6..3a7cc20 100644 --- a/templates/recruitment/candidate_interview_view.html +++ b/templates/recruitment/candidate_interview_view.html @@ -167,6 +167,8 @@ {% endblock %} {% block content %} + +
    @@ -222,8 +224,13 @@ {% trans "Schedule Interviews" %} - -
    +
    + +
    {% endif %}
    @@ -413,8 +420,47 @@
    +
    + + {% endblock %} {% block customJS %} @@ -516,5 +562,19 @@ } }); }); + + +$(document).ready(function() { + // Check the flag passed from the Django view + var shouldOpenModal = {{ show_modal_on_load|yesno:"true,false" }}; + + // If the view detected an invalid form submission (POST request), open the modal + if (shouldOpenModal) { + // Use the native Bootstrap 5 JS function to show the modal + var myModal = new bootstrap.Modal(document.getElementById('jobAssignmentModal')); + myModal.show(); + } +}); + {% endblock %} -- 2.39.5 From 833044686481e04075d3f82aeeec8bae585b648d Mon Sep 17 00:00:00 2001 From: Faheed Date: Wed, 29 Oct 2025 02:56:24 +0300 Subject: [PATCH 11/13] small fix --- .../linkedin_service.cpython-312.pyc | Bin 12067 -> 12067 bytes templates/jobs/job_detail.html | 3 +-- templates/jobs/job_list.html | 6 +++--- templates/participants/participants_list.html | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/recruitment/__pycache__/linkedin_service.cpython-312.pyc b/recruitment/__pycache__/linkedin_service.cpython-312.pyc index 7875609da8184188ab8e384a0044253f8317cbe9..5f13015508f535e1a1fd6062966d0c7f09b5ece0 100644 GIT binary patch delta 21 bcmZ1+w>XaHG%qg~0}wn8Va(jfBd!ksMYIK? delta 21 bcmZ1+w>XaHG%qg~0}!lB_@A+nM_eBON>2uM diff --git a/templates/jobs/job_detail.html b/templates/jobs/job_detail.html index 36ac548..0bf304c 100644 --- a/templates/jobs/job_detail.html +++ b/templates/jobs/job_detail.html @@ -358,8 +358,7 @@ {% endif %} - Create Participant - +
    diff --git a/templates/jobs/job_list.html b/templates/jobs/job_list.html index c9fbb45..f102ae3 100644 --- a/templates/jobs/job_list.html +++ b/templates/jobs/job_list.html @@ -327,9 +327,9 @@ {# CANDIDATE MANAGEMENT DATA - URLS NEUTRALIZED #} {% if job.all_candidates.count %}{{ job.all_candidates.count }}{% else %}-{% endif %} {% if job.screening_candidates.count %}{{ job.screening_candidates.count }}{% else %}-{% endif %} - {% if job.exam_candidates.count %}{{ job.exam_candidates.count }}{% else %}-{% endif %} - {% if job.interview_candidates.count %}{{ job.interview_candidates.count }}{% else %}-{% endif %} - {% if job.offer_candidates.count %}{{ job.offer_candidates.count }}{% else %}-{% endif %} + {% if job.exam_candidates.count %}{{ job.exam_candidates.count }}{% else %}-{% endif %} + {% if job.interview_candidates.count %}{{ job.interview_candidates.count }}{% else %}-{% endif %} + {% if job.offer_candidates.count %}{{ job.offer_candidates.count }}{% else %}-{% endif %} {% endfor %} diff --git a/templates/participants/participants_list.html b/templates/participants/participants_list.html index 547567d..e955f0b 100644 --- a/templates/participants/participants_list.html +++ b/templates/participants/participants_list.html @@ -142,11 +142,11 @@

    {% trans "Participants List" %}

    - {% comment %} {% if user.is_staff %} + {% if user.is_staff %} {% trans "Add New Participant" %} - {% endif %} {% endcomment %} + {% endif %}
    -- 2.39.5 From 8851e591dafd2cb1fc4b07226914a32235f2de3f Mon Sep 17 00:00:00 2001 From: Faheed Date: Wed, 29 Oct 2025 19:59:56 +0300 Subject: [PATCH 12/13] chnages to dashobard and meetings details --- .../__pycache__/models.cpython-312.pyc | Bin 62223 -> 63964 bytes recruitment/__pycache__/urls.cpython-312.pyc | Bin 13530 -> 13530 bytes .../views_frontend.cpython-312.pyc | Bin 27564 -> 27674 bytes recruitment/migrations/0001_initial.py | 30 +- .../migrations/0002_candidate_retry.py | 18 - ...created_at_remove_profile_slug_and_more.py | 25 + ..._jobposting_linkedin_post_formated_data.py | 18 - ..._jobposting_linkedin_post_formated_data.py | 18 - ..._jobposting_linkedin_post_formated_data.py | 18 - recruitment/migrations/0006_participants.py | 24 - ...s_created_at_participants_slug_and_more.py | 30 - .../0008_rename_job_participants_jobs.py | 18 - .../0009_jobposting_assigned_users.py | 20 - .../0010_remove_jobposting_assigned_users.py | 17 - .../0011_jobposting_internal_participant.py | 20 - .../0012_remove_participants_jobs_and_more.py | 29 - ...obposting_external_participant_and_more.py | 33 - recruitment/models.py | 46 +- recruitment/views_frontend.py | 55 +- templates/meetings/meeting_details.html | 675 +++++++++--------- templates/participants/participants_list.html | 24 +- templates/recruitment/dashboard.html | 140 ++-- 22 files changed, 551 insertions(+), 707 deletions(-) delete mode 100644 recruitment/migrations/0002_candidate_retry.py create mode 100644 recruitment/migrations/0002_remove_profile_created_at_remove_profile_slug_and_more.py delete mode 100644 recruitment/migrations/0003_jobposting_linkedin_post_formated_data.py delete mode 100644 recruitment/migrations/0004_alter_jobposting_linkedin_post_formated_data.py delete mode 100644 recruitment/migrations/0005_alter_jobposting_linkedin_post_formated_data.py delete mode 100644 recruitment/migrations/0006_participants.py delete mode 100644 recruitment/migrations/0007_participants_created_at_participants_slug_and_more.py delete mode 100644 recruitment/migrations/0008_rename_job_participants_jobs.py delete mode 100644 recruitment/migrations/0009_jobposting_assigned_users.py delete mode 100644 recruitment/migrations/0010_remove_jobposting_assigned_users.py delete mode 100644 recruitment/migrations/0011_jobposting_internal_participant.py delete mode 100644 recruitment/migrations/0012_remove_participants_jobs_and_more.py delete mode 100644 recruitment/migrations/0013_remove_jobposting_external_participant_and_more.py diff --git a/recruitment/__pycache__/models.cpython-312.pyc b/recruitment/__pycache__/models.cpython-312.pyc index b4e2d8a063fc9b2b5fd1dca57a48e4e8d306b548..b3fa580085721c185bf20f96e5001b78878100d0 100644 GIT binary patch delta 10376 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 D*Hg9Q delta 9117 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 F`xl~-D(V0L diff --git a/recruitment/migrations/0001_initial.py b/recruitment/migrations/0001_initial.py index d6c2da6..48af4fe 100644 --- a/recruitment/migrations/0001_initial.py +++ b/recruitment/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.7 on 2025-10-23 14:08 +# Generated by Django 5.2.7 on 2025-10-29 11:45 import django.core.validators import django.db.models.deletion @@ -66,6 +66,22 @@ class Migration(migrations.Migration): 'ordering': ['name'], }, ), + migrations.CreateModel( + name='Participants', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Participant Name')), + ('email', models.EmailField(max_length=254, verbose_name='Email')), + ('phone', models.CharField(blank=True, max_length=12, null=True, verbose_name='Phone Number')), + ('designation', models.CharField(blank=True, max_length=100, null=True, verbose_name='Designation')), + ], + options={ + 'abstract': False, + }, + ), migrations.CreateModel( name='Source', fields=[ @@ -211,6 +227,7 @@ class Migration(migrations.Migration): ('offer_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected')], max_length=100, null=True, verbose_name='Offer Status')), ('join_date', models.DateField(blank=True, null=True, verbose_name='Join Date')), ('ai_analysis_data', models.JSONField(default=dict, help_text='Full JSON output from the resume scoring model.', verbose_name='AI Analysis Data')), + ('retry', models.SmallIntegerField(default=3, verbose_name='Resume Parsing Retry')), ('submitted_by_agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='submitted_candidates', to='recruitment.hiringagency', verbose_name='Submitted by Agency')), ], options={ @@ -248,6 +265,7 @@ class Migration(migrations.Migration): ('posted_to_linkedin', models.BooleanField(default=False)), ('linkedin_post_status', models.CharField(blank=True, help_text='Status of LinkedIn posting', max_length=50)), ('linkedin_posted_at', models.DateTimeField(blank=True, null=True)), + ('linkedin_post_formated_data', models.TextField(blank=True, null=True)), ('published_at', models.DateTimeField(blank=True, db_index=True, null=True)), ('position_number', models.CharField(blank=True, help_text='University position number', max_length=50)), ('reporting_to', models.CharField(blank=True, help_text='Who this position reports to', max_length=100)), @@ -257,6 +275,8 @@ class Migration(migrations.Migration): ('cancelled_by', models.CharField(blank=True, help_text='Name of person who cancelled this job', max_length=100, verbose_name='Cancelled By')), ('cancelled_at', models.DateTimeField(blank=True, null=True)), ('hiring_agency', models.ManyToManyField(blank=True, help_text='External agency responsible for sourcing candidates for this role', related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency')), + ('users', models.ManyToManyField(blank=True, help_text='Internal staff involved in the recruitment process for this job', related_name='jobs_assigned', to=settings.AUTH_USER_MODEL, verbose_name='Internal Participant')), + ('participants', models.ManyToManyField(blank=True, help_text='External participants involved in the recruitment process for this job', related_name='jobs_participating', to='recruitment.participants', verbose_name='External Participant')), ('source', models.ForeignKey(blank=True, help_text='The system or channel from which this job posting originated or was first published.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='job_postings', to='recruitment.source')), ], options={ @@ -308,9 +328,17 @@ class Migration(migrations.Migration): name='Profile', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), ('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size])), + ('designation', models.CharField(blank=True, max_length=100, null=True)), + ('phone', models.CharField(blank=True, max_length=12, null=True, verbose_name='Phone Number')), ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), ], + options={ + 'abstract': False, + }, ), migrations.CreateModel( name='SharedFormTemplate', diff --git a/recruitment/migrations/0002_candidate_retry.py b/recruitment/migrations/0002_candidate_retry.py deleted file mode 100644 index 0f405fc..0000000 --- a/recruitment/migrations/0002_candidate_retry.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-26 11:53 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='candidate', - name='retry', - field=models.SmallIntegerField(default=3, verbose_name='Resume Parsing Retry'), - ), - ] diff --git a/recruitment/migrations/0002_remove_profile_created_at_remove_profile_slug_and_more.py b/recruitment/migrations/0002_remove_profile_created_at_remove_profile_slug_and_more.py new file mode 100644 index 0000000..00aaa1e --- /dev/null +++ b/recruitment/migrations/0002_remove_profile_created_at_remove_profile_slug_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 5.2.7 on 2025-10-29 12:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='profile', + name='created_at', + ), + migrations.RemoveField( + model_name='profile', + name='slug', + ), + migrations.RemoveField( + model_name='profile', + name='updated_at', + ), + ] diff --git a/recruitment/migrations/0003_jobposting_linkedin_post_formated_data.py b/recruitment/migrations/0003_jobposting_linkedin_post_formated_data.py deleted file mode 100644 index 5e92015..0000000 --- a/recruitment/migrations/0003_jobposting_linkedin_post_formated_data.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-27 10:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0002_candidate_retry'), - ] - - operations = [ - migrations.AddField( - model_name='jobposting', - name='linkedin_post_formated_data', - field=models.JSONField(blank=True, null=True), - ), - ] diff --git a/recruitment/migrations/0004_alter_jobposting_linkedin_post_formated_data.py b/recruitment/migrations/0004_alter_jobposting_linkedin_post_formated_data.py deleted file mode 100644 index d821d60..0000000 --- a/recruitment/migrations/0004_alter_jobposting_linkedin_post_formated_data.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-27 11:29 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0003_jobposting_linkedin_post_formated_data'), - ] - - operations = [ - migrations.AlterField( - model_name='jobposting', - name='linkedin_post_formated_data', - field=models.CharField(blank=True, null=True), - ), - ] diff --git a/recruitment/migrations/0005_alter_jobposting_linkedin_post_formated_data.py b/recruitment/migrations/0005_alter_jobposting_linkedin_post_formated_data.py deleted file mode 100644 index 6bd9476..0000000 --- a/recruitment/migrations/0005_alter_jobposting_linkedin_post_formated_data.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-27 11:58 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0004_alter_jobposting_linkedin_post_formated_data'), - ] - - operations = [ - migrations.AlterField( - model_name='jobposting', - name='linkedin_post_formated_data', - field=models.TextField(blank=True, null=True), - ), - ] diff --git a/recruitment/migrations/0006_participants.py b/recruitment/migrations/0006_participants.py deleted file mode 100644 index db585d3..0000000 --- a/recruitment/migrations/0006_participants.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-28 12:12 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0005_alter_jobposting_linkedin_post_formated_data'), - ] - - operations = [ - migrations.CreateModel( - name='Participants', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255, verbose_name='Participant Name')), - ('email', models.EmailField(max_length=254, verbose_name='Email')), - ('phone', models.CharField(blank=True, max_length=20, verbose_name='Phone')), - ('designation', models.CharField(blank=True, max_length=100, verbose_name='Designation')), - ('job', models.ManyToManyField(blank=True, related_name='participants', to='recruitment.jobposting')), - ], - ), - ] diff --git a/recruitment/migrations/0007_participants_created_at_participants_slug_and_more.py b/recruitment/migrations/0007_participants_created_at_participants_slug_and_more.py deleted file mode 100644 index fc89f39..0000000 --- a/recruitment/migrations/0007_participants_created_at_participants_slug_and_more.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-28 12:14 - -import django_extensions.db.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0006_participants'), - ] - - operations = [ - migrations.AddField( - model_name='participants', - name='created_at', - field=models.DateTimeField(auto_now_add=True, default=None, verbose_name='Created at'), - preserve_default=False, - ), - migrations.AddField( - model_name='participants', - name='slug', - field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'), - ), - migrations.AddField( - model_name='participants', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), - ), - ] diff --git a/recruitment/migrations/0008_rename_job_participants_jobs.py b/recruitment/migrations/0008_rename_job_participants_jobs.py deleted file mode 100644 index 4bea33f..0000000 --- a/recruitment/migrations/0008_rename_job_participants_jobs.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-28 13:06 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0007_participants_created_at_participants_slug_and_more'), - ] - - operations = [ - migrations.RenameField( - model_name='participants', - old_name='job', - new_name='jobs', - ), - ] diff --git a/recruitment/migrations/0009_jobposting_assigned_users.py b/recruitment/migrations/0009_jobposting_assigned_users.py deleted file mode 100644 index 1ec5f0b..0000000 --- a/recruitment/migrations/0009_jobposting_assigned_users.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-28 16:41 - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0008_rename_job_participants_jobs'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name='jobposting', - name='assigned_users', - field=models.ManyToManyField(blank=True, related_name='assigned_jobs', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/recruitment/migrations/0010_remove_jobposting_assigned_users.py b/recruitment/migrations/0010_remove_jobposting_assigned_users.py deleted file mode 100644 index 3b7548d..0000000 --- a/recruitment/migrations/0010_remove_jobposting_assigned_users.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-28 17:12 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0009_jobposting_assigned_users'), - ] - - operations = [ - migrations.RemoveField( - model_name='jobposting', - name='assigned_users', - ), - ] diff --git a/recruitment/migrations/0011_jobposting_internal_participant.py b/recruitment/migrations/0011_jobposting_internal_participant.py deleted file mode 100644 index e051508..0000000 --- a/recruitment/migrations/0011_jobposting_internal_participant.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-28 20:42 - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0010_remove_jobposting_assigned_users'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name='jobposting', - name='internal_participant', - field=models.ManyToManyField(blank=True, help_text='Internal staff involved in the recruitment process for this job', related_name='internal_participant_jobs', to=settings.AUTH_USER_MODEL, verbose_name='Internal Participant'), - ), - ] diff --git a/recruitment/migrations/0012_remove_participants_jobs_and_more.py b/recruitment/migrations/0012_remove_participants_jobs_and_more.py deleted file mode 100644 index 799ee52..0000000 --- a/recruitment/migrations/0012_remove_participants_jobs_and_more.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-28 21:30 - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0011_jobposting_internal_participant'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.RemoveField( - model_name='participants', - name='jobs', - ), - migrations.AddField( - model_name='jobposting', - name='external_participant', - field=models.ManyToManyField(blank=True, help_text='External participants involved in the recruitment process for this job', related_name='jobs', to='recruitment.participants', verbose_name='External Participant'), - ), - migrations.AlterField( - model_name='jobposting', - name='internal_participant', - field=models.ManyToManyField(blank=True, help_text='Internal staff involved in the recruitment process for this job', related_name='jobs', to=settings.AUTH_USER_MODEL, verbose_name='Internal Participant'), - ), - ] diff --git a/recruitment/migrations/0013_remove_jobposting_external_participant_and_more.py b/recruitment/migrations/0013_remove_jobposting_external_participant_and_more.py deleted file mode 100644 index e830392..0000000 --- a/recruitment/migrations/0013_remove_jobposting_external_participant_and_more.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-28 22:20 - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0012_remove_participants_jobs_and_more'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.RemoveField( - model_name='jobposting', - name='external_participant', - ), - migrations.RemoveField( - model_name='jobposting', - name='internal_participant', - ), - migrations.AddField( - model_name='jobposting', - name='participants', - field=models.ManyToManyField(blank=True, help_text='External participants involved in the recruitment process for this job', related_name='jobs_participating', to='recruitment.participants', verbose_name='External Participant'), - ), - migrations.AddField( - model_name='jobposting', - name='users', - field=models.ManyToManyField(blank=True, help_text='Internal staff involved in the recruitment process for this job', related_name='jobs_assigned', to=settings.AUTH_USER_MODEL, verbose_name='Internal Participant'), - ), - ] diff --git a/recruitment/models.py b/recruitment/models.py index a258a42..7d7f08a 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -28,6 +28,8 @@ class Base(models.Model): class Profile(models.Model): profile_image = models.ImageField(null=True, blank=True, upload_to="profile_pic/",validators=[validate_image_size]) + designation = models.CharField(max_length=100, blank=True,null=True) + phone=models.CharField(blank=True,null=True,verbose_name=_("Phone Number"),max_length=12) user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") def __str__(self): @@ -735,6 +737,41 @@ class ZoomMeeting(Base): def __str__(self): return self.topic + + @property + def get_job(self): + try: + job=self.interview.job.first() + return job + except: + return None + @property + def get_candidate(self): + try: + candidate=self.interview.candidate.first() + return candidate + except: + return None + + @property + def get_external_participants(self): + try: + interview=self.interview.first() + if interview: + return interview.job.participants.all() + return None + except: + return None + @property + def get_users_participants(self): + try: + interview=self.interview.first() + if interview: + return interview.job.users.all() + return None + except: + return None + class MeetingComment(Base): @@ -1267,7 +1304,8 @@ class InterviewSchedule(Base): models.Index(fields=['end_date']), models.Index(fields=['created_by']), ] - + + class ScheduledInterview(Base): """Stores individual scheduled interviews""" @@ -1319,11 +1357,11 @@ class ScheduledInterview(Base): class Participants(Base): """Model to store Participants details""" - name = models.CharField(max_length=255, verbose_name=_("Participant Name")) + name = models.CharField(max_length=255, verbose_name=_("Participant Name"),null=True,blank=True) email= models.EmailField(verbose_name=_("Email")) - phone = models.CharField(max_length=20, blank=True, verbose_name=_("Phone")) + phone = models.CharField(max_length=12,verbose_name=_("Phone Number"),null=True,blank=True) designation = models.CharField( - max_length=100, blank=True, verbose_name=_("Designation") + max_length=100, blank=True, verbose_name=_("Designation"),null=True ) def __str__(self): diff --git a/recruitment/views_frontend.py b/recruitment/views_frontend.py index a7b0b8c..938e150 100644 --- a/recruitment/views_frontend.py +++ b/recruitment/views_frontend.py @@ -336,7 +336,8 @@ class TrainingDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView): success_url = reverse_lazy('training_list') success_message = 'Training material deleted successfully.' - +from django.db.models import F, IntegerField, Count, Avg +from django.db.models.functions import Cast, Coalesce @login_required def dashboard_view(request): all_candidates_count=0 @@ -362,39 +363,39 @@ def dashboard_view(request): # Assuming 'match_score' is a direct IntegerField/FloatField on the Candidate model # (based on the final, optimized version of handle_reume_parsing_and_scoring) + # The path to your score: ai_analysis_data['analysis_data']['match_score'] + SCORE_PATH = 'ai_analysis_data__analysis_data__match_score' - # Average Match Score (Overall Quality) - candidates_with_score = models.Candidate.objects.filter( - # Filter only candidates that have been parsed/scored + # --- The Annotate Step --- + candidates_with_score_query = models.Candidate.objects.filter( is_resume_parsed=True ).annotate( - score_as_text=KeyTextTransform( - 'match_score', - KeyTextTransform('scoring_data', F('ai_analysis_data')) + # 1. Use Coalesce to handle cases where the score might be missing or NULL + # (It defaults the value to 0 if missing). + # 2. Use Cast to convert the JSON value (which is often returned as text/string by the DB) + # into a proper IntegerField so we can perform math on it. + annotated_match_score=Coalesce( + Cast(SCORE_PATH, output_field=IntegerField()), + 0 ) - ).annotate( - # Cast the extracted text score to a FloatField so AVG() can operate on it. - sortable_score=Cast('score_as_text', output_field=FloatField()) ) + # Now calculate the average match score + avg_match_score_result = candidates_with_score_query.aggregate( + avg_score=Avg('annotated_match_score') + )['avg_score'] + avg_match_score = round(avg_match_score_result or 0, 1) - # 2b. AGGREGATE using the newly created 'sortable_score' field - avg_match_score_result = candidates_with_score.aggregate( - avg_score=Avg('sortable_score') - )['avg_score'] + hight_potential_count=0 + # --- The Filter Step for High Potential Candidates --- + candidates_with_score_gte_75 = candidates_with_score_query.filter( + annotated_match_score__gte=75 + ) + high_potential_count=candidates_with_score_gte_75.count() + high_potential_ratio = round((hight_potential_count / total_candidates) * 100, 1) if total_candidates > 0 else 0 - avg_match_score = round(avg_match_score_result or 0, 1) - - # 2c. Use the annotated QuerySet for other metrics - - # Scored Candidates Ratio (Now simpler, as we filtered the QuerySet) - total_scored = candidates_with_score.count() - scored_ratio = round((total_scored / total_candidates) * 100, 1) if total_candidates > 0 else 0 - - # High Potential Candidates (Filter the annotated QuerySet) - high_potential_count = candidates_with_score.filter( - sortable_score__gte=75 - ).count() - high_potential_ratio = round((high_potential_count / total_candidates) * 100, 1) if total_candidates > 0 else 0 + # Scored Candidates Ratio + total_scored_candidates = candidates_with_score_query.count() + scored_ratio = round((total_scored_candidates / total_candidates) * 100, 1) if total_candidates > 0 else 0 jobs=models.JobPosting.objects.all().order_by('internal_job_id') selected_job_pk=request.GET.get('selected_job_pk','') diff --git a/templates/meetings/meeting_details.html b/templates/meetings/meeting_details.html index 2d3761a..8df9080 100644 --- a/templates/meetings/meeting_details.html +++ b/templates/meetings/meeting_details.html @@ -3,199 +3,199 @@ {% block customCSS %} {% endblock %} @@ -203,251 +203,263 @@ body { {% block content %}
    - {# --- TOP BAR / BACK BUTTON --- #} -
    + {# --- TOP BAR / BACK BUTTON & ACTIONS (EDIT/DELETE) --- #} +
    + {# Back Button #} {% trans "Back to Meetings" %} + + {# Edit and Delete Buttons #} +
    + + {% trans "Edit Meeting" %} + + {# DELETE MEETING FORM #} +
    + {% csrf_token %} + +
    +
    -
    + {# ========================================================= #} + {# --- SECTION 1: PROMINENT TOP DETAILS & JOIN INFO --- #} + {# ========================================================= #} +
    - {# --- LEFT COLUMN (COMMENTS & INTERNAL CONTEXT) - Takes 50% of the screen #} -
    - - {# --- 1. INTERNAL NOTES / DESCRIPTION CARD (New CRM Feature) --- #} - {% if meeting.description %} -
    -
    -

    {% trans "Internal Context" %}

    -

    {% trans "Meeting agenda, purpose, or interview details for internal team use." %}

    -
    -

    {{ meeting.description|safe }}

    -
    + {# --- LEFT HALF: MAIN TOPIC & JOB CONTEXT --- #} +
    +
    +

    + + {{ meeting.topic|default:"[Meeting Topic N/A]" }} + + {{ meeting.status|title|default:'N/A' }} + +

    +
    + + {# JOB CONTEXT DETAILS (Simple Divs) #} +
    +

    {% trans "Interview Detail" %}

    +
    +
    {% trans "Job Title" %}:
    {{ meeting.get_job.title|default:"N/A" }}
    + + +
    {% trans "Job Type" %}:
    {{ meeting.get_job.job_type|default:"N/A" }}
    - {% endif %} +
    - {# --- 2. Comments Section (Now in the Left Column) --- #} -
    + {# --- RIGHT HALF: ZOOM LINK / ACTIONS --- #} +
    +
    +

    {% trans "Connection Details" %}

    +
    +
    {% trans "Date & Time" %}:
    {{ meeting.start_time|date:"M d, Y H:i"|default:"N/A" }}
    +
    {% trans "Duration" %}:
    {{ meeting.duration|default:"N/A" }} {% trans "minutes" %}
    +
    {% trans "Meeting ID" %}:
    {{ meeting.meeting_id|default:"N/A" }}
    +
    {% trans "Host Email" %}:
    {{ meeting.host_email|default:"N/A" }}
    + {% if meeting.join_url %} +
    +
    {% trans "Copied!" %}
    + +
    +
    + {% trans "Join URL" %}: + {{ meeting.join_url }} +
    + +
    +
    + {% endif %} +
    +
    +
    + +
    + + {# ========================================================= #} + {# --- SECTION 2: PERSONNEL TABLES --- #} + {# ========================================================= #} +
    + + + {# --- PARTICIPANTS TABLE --- #} +
    +
    +

    {% trans "Assigned Participants" %}

    + + + + + + + + + + + + + {% for participant in meeting.get_external_participants %} + + + + + + + + {% endfor %} + {% for participant in meeting.get_users_participants %} + + + + + + + + {% endfor %} + + +
    {% trans "Name" %}{% trans "Role/Designation" %}{% trans "Email" %}{% trans "Phone Number" %}{% trans "Source Type" %}
    {{participant.name}}{{participant.designation}}{{participant.email}}{{participant.phone}}{% trans "External Participants" %}
    {{participant.name}}{{participant.designation}}{{participant.email}}{{participant.phone}}{% trans "System User" %}
    +
    +
    +
    + + {# ========================================================= #} + {# --- SECTION 3: COMMENTS (CORRECTED) --- #} + {# ========================================================= #} +
    + +
    +
    - {% trans "Comments" %} ({{ meeting.comments.count }}) + {% trans "Comments" %} ({% if meeting.comments %}{{ meeting.comments.count }}{% else %}0{% endif %})
    - {% if user.is_authenticated %} - - {% endif %}
    -
    + + {# 1. COMMENT DISPLAY & IN-PAGE EDIT FORMS #} +
    {% if meeting.comments.all %} {% for comment in meeting.comments.all|dictsortreversed:"created_at" %} -
    -
    -
    - {{ comment.author.get_full_name|default:comment.author.username }} - {% if comment.author != user %} - {% trans "Comment" %} - {% endif %} -
    - {{ comment.created_at|date:"M d, Y P" }} -
    -
    -

    {{ comment.content|safe }}

    -
    - -
    -
    + +
    - {# --- RIGHT COLUMN (MAIN DETAILS & JOIN INFO) - Takes 50% of the screen #} -
    -
    - - {# --- CRM ASSOCIATED RECORD CARD (Elevated Importance) --- #} - {% if meeting.interview %} -
    -
    -
    - -
    -
    {% trans "Associated Record" %}
    - - - {{ meeting.interview.candidate.name }} - - - {{ meeting.interview.job_position }} + {# 2. NEW COMMENT SUBMISSION (Remains the same) #} +
    {% trans "Add a New Comment" %}
    + {% if user.is_authenticated %} +
    + {% csrf_token %} + {% if comment_form %} + {{ comment_form.as_p }} + {% else %} +
    + +
    -
    -
    -
    - {% endif %} - - {# --- 1. MAIN DETAILS CARD --- #} -
    - {# --- CONSOLIDATED HEADER --- #} -
    -
    -
    -

    - - - - - {{ meeting.topic }} -

    -
    - - {{ meeting.status|title }} - -
    -
    -
    -
    - - {# --- CONNECTION DETAIL BODY (Renamed from Core Details) --- #} -
    -

    {% trans "Connection Details" %}

    -
    -
    {% trans "Meeting ID" %}:
    {{ meeting.meeting_id }}
    -
    {% trans "Start Time" %}:
    {{ meeting.start_time|date:"M d, Y H:i" }}
    -
    {% trans "Duration" %}:
    {{ meeting.duration }} {% trans "minutes" %}
    -
    {% trans "Timezone" %}:
    {{ meeting.timezone|default:"UTC" }}
    -
    {% trans "Host Email" %}:
    {{ meeting.host_email|default:"N/A" }}
    -
    -
    - - {# --- ACTION BAR AT THE BOTTOM OF THE MAIN CARD --- #} - + + {% else %} +

    {% trans "You must be logged in to add a comment." %}

    + {% endif %}
    - - {# --- 2. JOIN INFO CARD (Separate from details, but in the same column) --- #} - {% if meeting.join_url %} -
    -
    -

    {% trans "Join Information" %}

    - - - {% trans "Join Meeting Now" %} - - -
    - {# Message should not be display: none; but opacity: 0; for smooth transition #} -
    {% trans "Copied!" %}
    - -
    -
    - {% trans "Join URL" %}: - {{ meeting.join_url }} -
    - -
    -
    - - {% if meeting.password %} -
    -
    {% trans "Password" %}:
    -
    {{ meeting.password }}
    -
    - {% endif %} -
    -
    - {% endif %} -
    -
    -
    - - {# --- API RESPONSE CARD (Full width, hidden by default) --- #} - {% if meeting.zoom_gateway_response %} - - {% endif %} - -
    - -{# MODALS (KEEP OUTSIDE OF THE MAIN LAYOUT ROWS) #} -{% comment %} {% include 'modals/delete_modal.html' with item_name="Meeting" delete_url_name='delete_meeting' %} {% endcomment %} - + {% endblock %} {% block customJS %} -- 2.39.5 From 8da8d89433c65311a8147059351a68d2e58356ce Mon Sep 17 00:00:00 2001 From: Faheed Date: Thu, 30 Oct 2025 04:07:56 +0300 Subject: [PATCH 13/13] dashboard --- .../__pycache__/settings.cpython-312.pyc | Bin 8201 -> 8201 bytes .../__pycache__/urls.cpython-312.pyc | Bin 2656 -> 2998 bytes NorahUniversity/settings.py | 6 +- recruitment/__pycache__/admin.cpython-312.pyc | Bin 11907 -> 12213 bytes recruitment/__pycache__/forms.cpython-312.pyc | Bin 33591 -> 53711 bytes .../__pycache__/models.cpython-312.pyc | Bin 63964 -> 83794 bytes .../__pycache__/signals.cpython-312.pyc | Bin 4406 -> 5969 bytes recruitment/__pycache__/urls.cpython-312.pyc | Bin 13530 -> 18794 bytes recruitment/__pycache__/views.cpython-312.pyc | Bin 94735 -> 142367 bytes .../views_frontend.cpython-312.pyc | Bin 27674 -> 45108 bytes recruitment/forms.py | 56 +++- recruitment/migrations/0001_initial.py | 91 +++++- ...created_at_remove_profile_slug_and_more.py | 25 -- ...red_date_source_custom_headers_and_more.py | 48 --- .../0004_alter_integrationlog_method.py | 18 - ...itted_by_agency_candidate_hiring_agency.py | 18 - ...agencyaccesslink_agencymessage_and_more.py | 129 -------- ..._candidate_source_candidate_source_type.py | 24 -- ...e_remove_candidate_source_type_and_more.py | 32 -- ...y_agencymessage_recipient_user_and_more.py | 59 ---- .../0010_remove_agency_message_model.py | 16 - recruitment/models.py | 6 +- recruitment/views.py | 51 +-- recruitment/views_frontend.py | 268 ++++++++++----- templates/recruitment/dashboard.html | 307 +++++++++++------- .../recruitment/partials/_guage_chart.html | 51 +++ .../recruitment/partials/stats_cards.html | 112 +++++++ 27 files changed, 709 insertions(+), 608 deletions(-) delete mode 100644 recruitment/migrations/0002_remove_profile_created_at_remove_profile_slug_and_more.py delete mode 100644 recruitment/migrations/0003_candidate_hired_date_source_custom_headers_and_more.py delete mode 100644 recruitment/migrations/0004_alter_integrationlog_method.py delete mode 100644 recruitment/migrations/0005_rename_submitted_by_agency_candidate_hiring_agency.py delete mode 100644 recruitment/migrations/0006_agencyjobassignment_agencyaccesslink_agencymessage_and_more.py delete mode 100644 recruitment/migrations/0007_candidate_source_candidate_source_type.py delete mode 100644 recruitment/migrations/0008_remove_candidate_source_remove_candidate_source_type_and_more.py delete mode 100644 recruitment/migrations/0009_agencymessage_priority_agencymessage_recipient_user_and_more.py delete mode 100644 recruitment/migrations/0010_remove_agency_message_model.py create mode 100644 templates/recruitment/partials/_guage_chart.html create mode 100644 templates/recruitment/partials/stats_cards.html diff --git a/NorahUniversity/__pycache__/settings.cpython-312.pyc b/NorahUniversity/__pycache__/settings.cpython-312.pyc index a88295350cc19e3ac2818151a90794266869738e..ef70d24783e736e696063246d75a7968373dc1de 100644 GIT binary patch delta 21 acmeBl=yc#Y&CAQh00KTtnHzcj$^!s5q6HoR delta 21 bcmeBl=yc#Y&CAQh00bNi|1vi6{FMg)JFx}T diff --git a/NorahUniversity/__pycache__/urls.cpython-312.pyc b/NorahUniversity/__pycache__/urls.cpython-312.pyc index 7ad3c1f00585bcc31d862feff56d2e93f71b0b0b..0e668c617a37eecd172142e80c966685c855fef2 100644 GIT binary patch delta 1231 zcmZ{k&ui0Q7{~LbX_Gchf6UEpwR6?|>S|4gY$9_^QHRBi9mG=(D_h^JHYTkk>16fL z9mSixhxsQAI#HO2M-Se-bWj);L`6Zo$iRaaQQ!A%L*~#>@_e7q^L?K8&D*9QdSArq zZzL(e;Hx*smhSr*<`YSrtFJ4*&v6X1#XtrEoiTs{3|3(Q$ywcFa0*AYNB0^&#YZ)# z^M;@ZhF|g1+^Yu+Q4y*3=|MwMB&sj+ddLVXVVVef#E2?Uz_5%OZFJXGdek0m+}V*E zq5sjhWa2-yfC{uU%7A#14iliL`m~JO>?lpfra@7FLA<0N50lX?Fv<&1B01UweU}6n zCK^T>pwrc0I}uX>Mrp0*pS5Fn6O*U~PI&})93|Np3bP?RyC3r@G{O=dM|UyLQ=Y*5 z0;R!H9kLfJRc?RnpE~=cCb2{CK=?yn? z)Q;>NdQD=O?qo7y(Q&vD(3Po9YeUvXxsbJ2tUK9R%Qk0-%9r3=*0NXZs+Da=h#^mL2h}9na=T>N6>}ro4pKum ztquNFoVE07aYn1G=82edrghDEjjgPrXTI@zsx6vo!K{{SLoM4T`Fc(KPBLu~YAfYp zMJ`mzYo?cwkV|Z{v}*be+p4B+nLOzh2#&elHJQ|8@(87ZiT??hc5k=3UXt+n$VIiR znz$A~%lru-q8gt7LA1qR2VwM?9}AlOWbOdD#St$&OpxCQd1DAD`ZfPrqDhaSNC<{i$tvKRv&f bo^LKLx48U4b|B1diU;S>FaI)k4zKbDsu~AV delta 848 zcmZvaON$dh5XZack)B5~N!I8tyU7wCc`YFX5fv2?5nT|(gP`<5*v+&t!%S8?Sznj+ zrUx(D2d{gTcw6zd7f*^`KmtBMJa~)v0kpa|!$`0hrhfnGe^(Vvzn%Y9*!q~sI0)9c zyMJz9w-EX+MZD&w@cW&P&`abaABQNy6l2L+s6{%}73-l9nbcHlgsF&75?Rzz+6?W; zp^oBI=tgOpR!qW7blRhy5>}Xvax{mLhFESi?Mw44A5`LLlIYtE2UXF=iz$qx<@uL+ zT$ftpu(?Ehmzlvz;*Tk4f~VvXKMh?hIM@_7aOE)ZGgABB2=6&Z+Fpb#@w0MIY~%gl z%PZg9e|`I4rk41G4-(=Z0B*m~BrX6R6?Mgnz{kW@#YN!b;(_92;FDroaT)lOc(3>n z@EI{td<6KcSkRZZx*jdFGECyB`KoHB+U!iRew0CI>UWqPoR9A!iIBaJsF+^Y<6D@i zaQR|vNg|Y3O;k*?B_1{!D?jCC1CPynJ$8Sv^VE2wiCt2=QW?9Pb@*m)7_t74!+S$x z*ALeE-HldfqkoGVGUFwgA@8ko^IzL04+lI28BW9rt5k&7jK+oO3DvIEWqrnb9d3&| z))IEaGpmSQ@yfcC^5nUk_+?c^)qYZJgOrb9unYf*jxqj$N}o{~zOOqU>{V<}YK5%! O*qt=RHTR~`0`FgxT%NH2 diff --git a/NorahUniversity/settings.py b/NorahUniversity/settings.py index 2333ee5..3eb57de 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/__pycache__/admin.cpython-312.pyc b/recruitment/__pycache__/admin.cpython-312.pyc index 9901ab4daa7d6d7792052a0b4d8c020b1fdc6d49..86a871d863cc43ee5e660b7aa6ef660f064e8499 100644 GIT binary patch delta 3586 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 US>Xy-bD66-Fz_AM%HF*H0t!>t2LJ#7 diff --git a/recruitment/__pycache__/forms.cpython-312.pyc b/recruitment/__pycache__/forms.cpython-312.pyc index 8e0f6cc8719d67d1b7a80f8e0306567bab27302e..a61fc29cef4e9b0a8242fc431fb56b5038dc85bb 100644 GIT binary patch literal 53711 zcmeIb33yvaekX{N1OX5L3En42iXuUZAa&o8DT%s8$(H1U-8P!ygD6Nm)DNH}!ji3Y z+-YjsiK#f zW@q>JufjV3K9cg0{Wg<*{8*^>>RQ&YMtbhGkVWHiC>lY50!sFjH82*NC%GT-#SWT-R4OT;Eqe?CbLlH}o|OH}*9SH}y3QujpGb z+}zhZys~fQa7$kcE6+L9I_&TBXUcBtYeQQKhT8ku@mn~wYPh4XgQdBK0>hnsoroV0 z+^-n>x`cy*2fwRLhJ6O1=oN!d95S92CbH@L&zH1hOyA}-U{XEc9y>x`ByIUrX38j1(2=FK<;Faen7S@1Nj7lYzO43Wgxp5 zWCtJvp;D#xyV%oCJncgMrCPt6L9Pbmnq}U!he56d19A@_ zm-|4>-ykB*AC zce8|0G&X{h^`rA<>^F?`|>F9}SO=P+`3v8L3(x$X!!gP!4@4D z0nCMP=;HB#Ga+GYC?pl^!pqK`2oHxOx0>(3aQ|RP%0DKKo(==Zm3i;((c$6HNK`78 zvkOXrXyoXaI1rMY`@)j(85ozm{1+v6M)pE#^$1~f^7?1O0{WO9Wj+P^rAHLm2eCy_?bzr@Xn^`izVZ+u^f31- zg0C5G7@~5D;jBrCDiKYpMR}-1Vu<|{qhLInuRKyB6UNg1&@^v3O;5!!@mke2rpsK9J|J;5A-!L9B7(pFe zHauTxFkCT4mFDU-=$c7>soVlfnrNO=QiM#BT4 zTV}}==#q><$<{w~{!ITVq*4F*d{-Z>?L0F&9O^vXeKoPAG!h>P7%5ruzjM*jjj|_&;+PtCA$YAu07(f9L`ux5p#7_K3p4_U5Tk*J@ zz`78iCO>tTrrb>lchftkzdii+aKgRz>i)E&@U@NCHYSUA|Mtl{8-DwFM3Ro(am(&G z;xt*d^fWM z_cPyYX$rT?k?&*##bFi*(j#lE=ziBSw=tmO|z} z%OLp1-!S%B@#XT)x^%ub#*APQtZ$m%%agzO60rHYuzKUjB-n%k!Ty-#Is_-iTy_og zODz;!k6o%eyHvsRW?o;xc#*&Oky{-%fg_=4zdv8HjP!%laqwXhjEfEfPaX3Sm6U-$amy_31E}NxNj4BVGdd)ML_Q1%)yKR_2`nqh zI(FbALdUU{yTowlv`Gj{<`8I!PJ2Gyej4Rv^*4BFM*YbG-1vDm$S}ei?H~#1H@IAjlB(Fjt zUNc`d{4#L%WnbQ!X2V)W7T!jF$~(iFnDmf`s|mC72e0P|6kX zR^2j`as|sG5DK9krB<;k>Xl=_X?gGE%Q!^y+_uQ`f<>O&7kTbj*E$Y z?-l9Pvv`qmyjjmnmG+hhr9v6rkoh|AI(W(=eV0i!fZq3)_ez%Y-~t~pwzcETs2B#T z6zv~+WE&VA>KGO})+({heA?0y1{_r|5QzjC!di49i%$Xck%!~EGogWV9bq;p3uRnN zv_5F^GhsmpjevWzu$8tb!vzFoFhMGzN1U9~Jk5(RjydKy-hUzF$w8G24u_&=Mg_^vZdltS2gAU; zp@5=QCevyg@{2@)iA2DGgW=o>6IVY{P)UyuMbUFUUj4!TPG~9Ij%Wo zZT0DfHFCTz-MC7Q*K6bRd4{?b_nbwOE2n#Boogx9F|%XVxg|TcY9=u2+?@3+UfgmU zfV=2nd)&Wu*0~LZ6&6o+&e&$1t64l=+E4E@rK<$brw%TGC$WD<}!ft{D~ctK(#K&1T8rGcR_0g?*w3T0t5#@7(b@QFT-^6l(hjJk-G zYUZ8#c%1UQh2W~;L$^2O_9fiDlzT%PiAZ@{6W-PtA?|HWde^4BTN2(av)*m@s{HY`L$HM8O2orwr*{09imyKr?ERBxH zT!J#-_ZVI?4VVTE1E%MjAZwy&rC?+kTIEF}##c+0hPZBewb^30Vn#{kJ%;B&8D23J zfD$Sz8GRmhm;?VFlm`&hIEY2v4WmU10lu&i5rpAZ{bsoNO#GE zyk_t~GJ?y@w<12VG)z*gX2G=6q_P?NHz*LZudFIn){-b|NtLxH%Gzg+B+Ir;IPN;i zK5Si^YTcY@-F&BMwsl9c_(-C4$8_WSjX!Mte(N8%O|H4I>H4N`Y>Bt-nAjaJJ~EeQ zD6g6{-8Wf__8C8{se9|-&4bgCnO(`6&Q#6DM9s$8n$7?70MGrSwSU-=+;J>^;^YTA zPTs5Qhej<~ZS?XHU3 zs{ZcdN`tr8`15&>p`zhqgAuQ!iM#5F_lw`R#M^q3ReR$ld+(bm;ZdXl?fhQpZlmS< z`Nb4AbUSwEnf@@ZVt1W2i*_fJNB?v0AYimRe)+{{cZ_a%+!F%S9qlTL#C#P@G}6Vk zs4{Qm{FjY_nPgO|z*>`Hp%j9Eh?#z^nR`p-K9Ao+9+Mxd+YP>WblAs6AY+4>K!$ix z8pz@>iU7$60gIHMA^9Y`O6ZB_kuH82f!`{U{GH?@Koe}tGBGTZ#>r!l4QDD+yox{` zgc&M}R`ZxAci8X# zdp~eA{&eMrqN&Zc|*ZAt$Q+|(qTE0JgQ_B+ody7o;Rj#sWqI6Km7cfRjT ztUW^aoe5{xhwkdQt(vXb{vz=Uc&qqD3W&~=@`FKPbO1ytePift&h(31@#EOfXk*6A zvJMl!M46PX5e2#>B2xTi3aCjUwN@(3P@Dsyp`l<9sQD_MMu-e}f>WN|h>8?6P{Is? zpTAc<-X}oyBa9TT{kiI z+RN8ozP;l8x*5Ry=ezS?v!D2<8tOtE~2qH#w~( zAJ-IFs~@iPSZnSZ5ODsMsy={!<)Rgjt~nCwYUQX*u4sf@(In%IJ~NXbLJ;0(VG=|u zMyy3DR1{`=F+j5lSHx<6Zd^*NrlLp^oT>C4yHltzc4w854!g?|cM17fX*T&zD0nl! zk3_k)v&F2bS%ClnD&FZ(ph=8YDBmrU%O%4xG1=qhaxamqB&%}lCR0Pt61mEBauo^1 zVPj59K1Vpgi(Yk(m-tH`6=w|qE)BFv=Ch-xz!ZkXNR-iyQUT<&nW**r8K@Q|YiPJX zJR}vKVQS4Fr;(N0esHo-bLK%_I5r%5WZz3g`FcS?KPotkqF7Y29^*yzu%hywV!$!1 z0HnzBoq!NBR+zcrs!@Nw_*>{Mk=AGNJqo@Cc^b8eB|R$U4_M25BV)s-LSoFppRr~#$>1Z#GD93V1d?38CPY!7>1thi{e?Lz zpGf0ae1U=>1sw>8m4?)t3-j1|C)rt_aFJf#arR2_6v5LR<;y`Q#n@^{OBr=U&BAU8A#A=hIjROW&%$S%0T4@$7T)zH`ZEhZ3E` zvy~%htfMDy1g{5gzmPa_GXC`0MSp3iCD|IgnXiR zqNJB*SF~1+K?Z4q-VA+Bm*%;Bk>`#@o;w$LUbx6}*CNl|i#+!%^1LYPc|C830k(Eg zq8HdDc$I~`1Z^eS1=qV7REhpojMb*{fhBEmPHoVxVJ7vk%BW}SQIN}ai4!drQ{m_wy}zlFXM-=Pjef63(SZ_+~w z2r)*;VguyuLsJP+mk1uNs>AMJ90mm4yqS_m*CA5{5}PX;`Bd4>R;nOvI9W| zr>Bc6Co3oNK6HAoKN&9%BuhK*n@tQ`+YE&zlV8lmqDalgkG;37xpkaI@tlgFj@M8Y zKcHI*{u2KY65bHAwEv;S1|G$LfWs0*)+bYj(Hf+DDyGD(G3i^tme4{+*WCY(AbUh; zwf&FBmI%f-%}cNs%x*wX7!@HO?1jRbF!n+hpEAvVE~E~A1;%u+(d$&ON+B!;lsB;2 zImcao_oI@WF5!ea%q60u=feY%9WzOc2FdPEvI%2c1ut1oP~syyMynY0FhfF!}(zN+&;ZktHP++QF>TWDWT!dR`B{0VaYNBl>wBA_`^O?2-%(7hQ-H$(nbR8Fs&3aF zCEUd>Eo-mQ_GXrtMd&ReRM-W2t5Bcb`ctH9D6K!E+kZ;|(NZxNGc*qQGS0ElMqkmr+iRn2sq!D{?2dOcFh!43h{TuKOG{qW3syqY^JFY1202nssi>kSFo_ z&SY&@eA|&(XYX8rHJ3mU-$%_BP$560hZOvu_*bbACs82?9@ZeZPce=@2=a4AA0@e# z-wSXE(X({Tc`zqGD-K~RxI$52&9*FoQDBGyft4cc%CK0}eZ#}^wc5{fB{b_>mF^hJ~UKq}E+7Uf|pE2?z ztq_SU5$TgN$$`q!FN&mUb=L$LQ&rNy{}mMXNifw)*2PIoWs{d@ zVzZtt6uU4}HS1ZIu37=rN}+4sZYcE7<4f-p#hcb88`h^AHq1k{xaq^H76vg_RQPy! zN}{$?fbF6hJmn%hh2WvjOLz(a$5RBUkEfuz&th{H%S9T^C2ibIc%v-xm_|$!y98!qIWbrQCw)rHy{d{lMrLwit{v z3v0?i z+lb`GJcFbZ7Vcb6BspO{GKyV#BZI-{sN@-azz%Cyi4@dU;bS75c|Ca&`B9~gH+rn{g`xuiPEC=B%Ac`r-;KDGw+m|pjQUMSx?_bWA^3%DJA?$$P$UB;S(yws zRV$EF=@~b7OsxNdl-yCJu;IaIT$dPu)OE66>mnm;_AB+r*WH ztD5kw2>^YD_xB!nx?@e(n)MxBU8`fhJ<5>q?LN~V7FYT9V2=rHEagp#)g0-+6doQM z&gkjrWtmQ;J(4btX#X?S={^OZi45Y$6o5!Fi1QTuEd@WP;J;Due^EfZmiS*O`0ogM z{mxvfk%qp8YBbaBmna}Po>V~c3r->aIXzrdX7Qhpx`t5B+X#qEtRmC<{EW^`mP{}M z`Tg9ueifO|myvP3tdZlXS?2SV%`i=H-gKrbS5hvknR1yc$+Et_eJb?U(9IzwiLCX_ z^==p`)OA417Q7i`1-2m9I!dZzxdO;FdUKQ|H6dwzCYR07&^2xSw)<^2B~`W}X(J^y zPz`HWP4&EW^d{drum(wMDXG?dzrxVGZo22&N8e^i6?MF6TajvMrkd1uO@&f*or$_m zDz&^KUB4M}6)M<*n%WIjey(z4)mitzYU!e+DmSb$e4SHcZ@ql;WtK#n3O3L?ebk7o z>U3+WV>dLcnYN~Us}sJ}@(Z`)g`26=2KR&Gre5O`v@(gM8Ldo$gyn@g5LC!gQNYWW zp0X&ioozFa8B`e*zk-R>bU|4l4pBTx)q`V(Ng6ooB5--x<&BeZe15N#_f&YiUpT|2 zOw7%1Xkoe#Ceu`!Low&^{xKo!>lXV@h5Olz!UmTpQaIuHHHy7L0aGLWFBH3sKu`2h zo`W4^dXlW6BQGEm=t-1Q%>-I~$lxD(FSS!Y*zT@QZ{ zhfR~y4_oc@hCT9=HfVT$>|A@lz~J#Q+Ad36NBv7vd+rI;fVLKsDlYeW414Ss7cogw z6cvBZ2BobTBUM>ku4#g5qazw$EsYWgNU zzPP@I5+!3(Y+q0Rz!@K*L|{bUFs$WB2L$0XGn;N{YQbi$;ZvhS%=VKdp9=ZF)erZ# zuJU7N`1ufkLqfoBQ;j*rUq#{K>+}XL29Ho-T*cBzNgEN!HgmMpOJ=+rW_ZBK@{sg# zOKCT^+*D9@tS)DG=A_@CQA)4BYDkw%P15*3LU? zXWMrsy+;%6JEwQQzxIclzrXp9w@&W9ap3xaZybuZ@0{2Z_a6O=^6E)*y1eSfOV?kT z9{6#2+r6s#R8?o9sxw{DKW8>p4;b$ojHL*zhKd@jZzbhq_&K#FV~$x7w^jVz$AyNH zW5x)5*6)?>swP8Agjs`}79%#uEuU?W*Qh(DFx-jZ%(>pp+$;JG(>d@VVyS9rskiw+ z9iU$^YTuxgVmY_xF-lQnaufscMfd0Nyw&~J{6Mq%Fq4qPQjA`b@I;*|_uBdk)vc*w?pAnLlt; zYf_&~7C#ktK9zQty!PU?7r%ZbZsW#@`s0h%{?D+8{=YE3mK_>Go9P z+C<~pWaIi|-G)2GAJlEte8?A)#lg5U2pjGc+wTE$*SM{5AwpEviXVKoZ&|9&WO-Gf z9SqQgayaM zq1d`(G`T}Qh_k_O^>h1eAEdwXCSO_D9J^ET*8Gf^SjkI|#HEE2O&> zDS)dpLxe0LnL+lthn8oJB!{DLK58nk_831dE3j66T$X38{n%r%HvO&BX7zH(9OWOt zzjD#Hpli;DK-KQ(*+h?v&!zsluX@_Jd z(OeLFGY3Pxpp?IWQdCQLkyJApzLJ{48`eoN)~nU1!S7QSkw^zTqpNnpn)cL8ymT#( zgA!ElElY0EEOSnQB&iiC_&q9+NIo(WGFj_BHXz`|QL=uQZUDU2rek?wClhMs0mWJ+ z5B03Xx~7$|O=s^>RdZzEk)4kQRn7$`4{oUD6gaqUFvDJ(TsM^F z_F+aX$4m(M4nRUNY$LK{#*mB^%013)t{r}ebZei;6{@rEALjb1I z3OQvqGP<-6L~DMlz`nu5hdB$W$hyeuS< zXn4PYybzLn{^lX7<*VMt`OJQJ#ZsgzS-U#E{rIf&#GE59R|9t|KNmAiQ;QjTVid!- z==L21TI!%4%@Zm318Qd#QW& z$i0+cftp9f85$ikGx_*LZmV?4v#F!E&j#0vcBxSDLP&!k_HXPkrS^6EB4MIJIr1Ef zfXM{XWFBv_z~Rs-z%m?&(zbQtN&9_A$axGE<~tP{=tpLj4>J9Jf`L{5{+xUQleoif zH$1|8^klh4Y>D^|9zWX4cI&NT?Z*Y^OWOL>fvL~YLH3_j)4%b4`eSe^kdWI zmWk^S7^>J$qu}2m0JF4<;s+7LY(3ZqOqPMY{sP@$WCj#G-~fW}AY@UF9sZ1xY@rc> znMUUBoq;1>1Ds154CG5PLMZDM!vmAWYJih+zp$T=PI}V`xWBT(L66uk*&!!@!U=x*LRn)rvlbcV zeufHZX)RC^W<541WYv@0=}GEj#tY?;`DV@~kbej2Ok92B?98l3)4<5q{2RtaOHduK zf%1XTI8@y*poP$fmFi?l@O&u2v6%xZZx^Eu-Av_Os`$*5y=bz94?e+c!d?+B zLs7y<)b*0>cz<+ElJ)T)>K+j5iT)4-7^J76mJ#B;eKGw-cRk~w`lg4&7 z)3{gIh-@iMN=&{mu$Yrn&ZNP`XOD=qYKQ@RL#dcNnz7X{s904yPK}*D9TK^*wdWN4 zdz=e0AvS;F;!l({hjz4Z%AlQMY>jK97%h2_V|P=07X`{|MY6yWdnnjT!9D~!y9-{BycJMFEXbz5=Nn9R<_xx<#3lEA4 za){>4;(~@43^aoo(fm&k$sw98nLU3dc>FSFJbqay6J2_QpPnxccmSYfGV@LljCP)1 z8**O(L4}kA*=XsN3Y0Ml9KiNT1tUXagKUS1NZKZBDOHtdG^fM~3a(MWG-+!Ih;O-q z%O%SD5~b0`EAc7<-Q8Ao%4@aDYCxtTt7#C>imFdTc5Fy>>_~L%NOkliI(pJ&Rq5i| z`&L6>uW`;`4rCk~O&(6z8h&hRV}mS95KLV_<8qD~_UQ$|8eLf->9tf|=dz+tgF_X! zg*hM5x*_qKdd1z;ZDv(i2cI1yQ9NLBc1o1YH0`OLpsu(ksF(}qw97)QRlg?}LN-#L zu0wH<&XrfE%3BlVt*P==iSku5FC@#iO*rqa=t{a*zkPV>_^+8KcYnh+eK_G>9k;Fi zyARx}nM!K9c)i8%$)$FvhEfHi;Es$9MZ=&0cQb^<=pMeYn1&3TInoa8GMQ86+lS>W zY^+IM`Bqy3b%us2KT5aXLV*iNb24BMDR_kc$Ou3{nx~68CreXBor$8(bWv5Rs4-F0 zm?~;X6tyIa{7@LA-0cZ>d%CC;hI)*8vKvY|CfBFDs}kN-@IF{%CwdCLg=Mh9{F1z@ zhaVPuh}=gozrt#@|Ee7UJ1mJa)8L^7x@F>7krz>@K92gdjsY{tU9g^S@ydPu=as;45S*!0lkpoI&;!zw$;DG=~UK*0J7mk+H}mxx7G7KI8bqID^& zDAa*nbEH>Li{i=(!8B;6 z1X!BajWn;zryTL3720XqE7nrS5sRvR$O3Db-V{&|9BX38Y`{&s zO3|C1`ncP-d|#eIUXg;m*?kEwT_$S-?@LP7@5_IM%4u;Zu#>L%fziV#g)Zi4+pH&! z04r*QQ$|#T!I4WJeK_ps*1*}=H-x1;aGspEfQb(5LIDH_4uShix4%-*2jK!dqZdM$ z;FL;X&U*TE?x7(k{6=v%-v3b@0#M<0vdGOGgutGE6VJ1HBhxO!TnSRtk;P{dW)dm> zs&wDQnNKTsQ1e^J&79Uq1?T%C5s;O_YBjn>XQGj&Z<(PngvuRLzbvdhFf@veYR5$u zDm0@=`k$lPEJlnAMjDhjOC!_U;Ed$ih3E36XHApk9+c2-&Ls<b;tMs3`i!*=6|84% z!%B`d=zm~N_%o0b^cjeu*BISQw663@t(z3$Hz00j4Ah0eZq2UQ9V0F`7>y_ zC_UzvWJlnx=$*L8g-itGDE(fs2zghmpTd)?88kNxd2mHp zRCc5KdiB)tWKqL(L!xL!s;Dhd)Rrt-HIaAETY6)|^$k;jnZ}vZN$>gz+g*2gy1W+7 zVcq2umUOXqBLAMN__d*HLn&87!qqUdDeh`Wy4Iy!TNAFWv##yws=BGN>HMkjc)1_B zoZiVVUpqXp4<;!v>|G?=k6qize5GQ-y>NQaFlEz&EKV4ij|cpcy+vyA6uhqLu4@@D zk0#aY`yz1M`=d$q{k{ks^R0B*q?%&`4ud)Og11&hTBN4QlLh%@9umTI+}UIpIDYmZ zZ)TFpPw7{l=CG(TVPNO??uGB+JmUMp(Hxv~0(upa&p@UGui|{wq(P@~Ky(?s%Ez7! z$2~*{x5^f^FH;iv)#oB8LKYZ_SJ_Y$ui|-Z5&TFm8>I1e5m|_`cb8ndc_C!_t%;7}UW<0?TgxXYPTcWaUW^1ByBc8a@ z6*Z}fK%ycr6a8-NRxDYu1y2j{lz#3yiqcNcYX`0!c=eF#Q23Vd#KJwo)TJ3GY+Sm+ zdgSfwZV~AA%hYz(LDX7(dC4H9P;iJE{2VnHl(DmBq9^5qGtHW5VbQS8HAhMcm5>=Xf($A;F*-On6vDY-|>-CND$6RDlV{`} zh!JS@s%2JgX&FKtEDv51ge)I#>AkhUMvJJwWvH=hsLKWF3z9Yv+k<^Q>EecXQKOb1 z(`S0VMx9B)7!^QYh}&c}5ux71`;?Ni+WtxVv_$O%V1wvbWC6=VUy-i4JqXmVr1mCv zwzQa53VI>Mx{8EV<5qv(qe{KE@?OV*WmJs?Zj=^id2@Q-jM^>ZJn1!b){E4aZGcri zM%82$$(BkchbBWehOQ6IwA|hv_jk{h?ONz4NtZeOW%^DO#85VyN%Wl{l>FF$fPW`S z{!HJgl>8)wgk!K7%s5b3@S*Sub}h}DR_PYR`@R~x9PyGc+p36@8!_jh5##RYd&RF) zJE;+x3D4HLZy@0-3kG>-yr^sWLH;@wO2IE=5AxMC$XD|>QSxW%yb9_o*|_FFgqyd0 z%KocetSr`r%)(BGam%q|)9b~5idKG&I)Qi-8l5mrgI;B_cKJ@6qE4jXm$Eyto;tCf zcOoT!rcNxyD#`VwJ+ODCjg1R>P^MPq_M!=&e5uG*OS8p-ys%a2zL<~L0 z$%cIKZHME}gc55{&sGjT=F;&d)s%v-;|1b%y4yzephx;2s~5>y`OES;3^KM@#;<^f zyVLDv?s@5}#yTj>Ho+yw`|`PQe8;X2rpqJPhMcX~kO|qjpP>=igFx$wZ83bIdzczc ziJS|TQv6W9<(&(d;9g+iR($<3;g<9$kV8CEaRdmQ9fyVjgiSJLbq$wdyi>-!eKIlm3M7>8&<_O!zx6e!Y&&d6>n|m~n@-u)jm-5zL>abqFJC{zcMZGno zDqHrxM9+SYf}c|G`xNw3z?Q-eiY=<9`xfT*H)&CP0+9@|TI8s>HM!=|_1Zn;Td@6F z-9uisQg#Q;wr#IPVh3&8#y)qO;tPo-tws{t%+8W3R!)x5_DGhrQO;FCxhhs5>GJi< zFteVj#j;Fh+9ssr8MK+`P`dRn(5AOpkZL!Skip{c^Ijg??X3mae>;aqpD*7TP@>l4-HaRuG9j6sk~8BsNF#z>upe5) zZ!|5q8pZ27n4)~W{TU_DqWeg8Dz#SppD6V=Dfl)8GZb)DEjvw)FIVD6l=J^U0Q(b? z3N7wTYMfGI6i_?3e`q_!NWA}vd`nd>Kq#{6_s?kMkx<^}tNP72cUBDBEiG=rwS??}(#M_Bv)ya6tNwWSbc`bG= z_Vvp+H;8tp`tN8BSKf#%=#tX(snP7%WP8Oh{aQRX7c{gAbE0=*ggouU7c@0gRCe&i@Y&v|Gtr^D&}p0M0ei_> zYWq=BOXx)E@GZ)78joAKeP%)QL;adp6}S zPz`C`%Vw|Nq1==)dkr96MyzI$HGoy@V_F3M0vWLg=)0G2cs}%&O*LQNHobNx^xbo} z&fRHB26o)}aw5>3XzIRK)*N4XC|Pzm?m0}9dCii`#YwhYY)yH0CA_<4y?fXMKbfq0 zI$rYhPrcboivqhl;obfIr60cdeViw`JLx^1^7bXXeY4&#YIEOh?ZCzz=E5T7ZcMlv zlWrU}yXp3N1WPOz{{}7p49mr8c+k;XB+RcsqrXQPpfW~Uat%ZOrR zp&s=@CKFHz8kagIGBd>UVl~PT*B*S78R`6ZrDn7@YvH6tb1nYo4k9>YFtXlw9%f$NkYb}Ox<9EzB_|YU8+k0W2^9|&O0qQFJ zNX7xJO@`QoXR*#j1}AGlU>oyOAK4Mu1{4~)bY9$1fC@+Ik@IInLdF|nev|PdT9jfPoGIsS{uX+IS>1(wa)WRo z$h~~{wT@jjBER@e3K}WU&b;^V0H(Ecu3wNoQ^cl~tZ2ksSY8lFs+JstNveb?74PldRxYX&o*>+f#{v@eGy zBUVDC#8$uvNg4g}C|bs42z^9$!99o*?m^TUqG+Zxu0X7@5@iNtL{sGSgE2~eQ!Jlk zMDQ^E$R-PmMDm#sYtUWaH3bB+T!YwJ}O8 znU}d}C`3>+hC*>!s<b~dSQ+Tks{)hx z8uuWFn0t_xSx+MwSd>q=CN566n12vxAdQ8G;28wj4*kMF*sQDnw!~pA=R$SLJb--4Mcq%=++hTtUZ}3@UbIb+sbK+G3`ymjafY6g z9C;3okij#Ptgl;IvYw&x&%&b+Oae0*vjyxjy){R?`X?8#YZbB(2`{>xZ~>g&_B)ti zF)_SZRr0bjNzIaT;armf=HpkHbPQ5YWDd{03*N3TuBUB>+2!ep)~zIjhu>aJEd@6n zcF08H$}x=Wu1H3+?Cvv*(%E}lUHK2`(qMN_qU-GoKxji;PfVfFSQsER>zl zGG&+K=ofIZN|4LP_o1i7-=SbX1w9nd$I^3(susz-uX3$ff^rN|z~tL26mfF45F4DS zIwqOC8|IYn)4aSzdB0DwA5icV-A+>M4-rUtyx}niW`mD3*yQ20Sa#T|X!jzq!aW&J zyFOAcJ}Nj6@o`-(cH1(m0NOu*cKa3+bCU&cu9%z2wf~$Fn4Yhhs?|@orzluj%>4%d zuBYYnzXO)U+)cD0Qa)U%yeZdWAnmQ6Tz_L5sN9RU3*#$y%zAgG>pF9Nn379T)rV=_ z>Zy~d+O9-x7ZWv;=hRIk^Je~5RW~8G_rwu1=m$dYhQgdR?+^y?n@{d>~}z2*`{J6n6CEqC#q#mP|5uY7jxBdTLzTEo7jn6_=)%WWViCYGHy5TW7+oV!FMDi(IhJwP^9M>F^JqcUYtgUI@W~f_{kU(c2Ygg(h)-py~kE4Xqo}Z-Y&Bs12|Lgt2Fw4i2|DOl6hqG zBIkB8`zwf6h|nS?+o*dvpCi|Skvg&V#fGE~@M#*ITTmicA2Ru+!`V_59f^vLnbBm$ zjtSWMR!lwr-kI+X-5Pp#-nlbb z`NV|luA_2ln1IDh&abF|AT$Dkh`&3_r)Dvx?5 zc~ZI=c`}zq8wMf0uG~$TAJ}JN60hQzFqJE|vFg9B6Zd&D-5?eDjp0)X)edn*vHD z;q&DZ4%+}*B3?yF>fV{mh6SoYtaizPAElB0F8~L;_|x*5>ld$IdHv-HXS%31RfK&q z4b!d3qV|bAc!^56>JqNHsj;N1dBXhHj-u-WKP_)dm9I&ZuSu0}PLyxPmYM0bKzgHuD0*4zqS6|jlhP5r4OJXtAH#fUAE!&h2MGc zdoSJ|hClE<#+GzTXR2jaqGeaIWlwx%4@5Mj`;2pDb9wQ7gSn^}MlK$#c-3s0E!@CA zjmBu)KKwh(I5AEE`ySt-)OQe&nnONo_+L{p1vS(g?MPrUnjH@<4J4yMFu&K>2pP?O zqZKY&T~=$|Lszl2=Dq;|7t&Df0c2DzOC~nA4UtbHq+vptdyC7Acqi42iujb%ag*Ru$YSm;}rpuc@D9mTf zcCi8M^Wir-<%9j;M|A5ObzsVuF!&;TVNsKaBs7I_ZaT@8Gb{LW~z9D^B{%GWA_MVNsV%3M0kz?3&G zbE2RkHeJ*V?1cCu40qsSHYJIj-HSb^UcQX6gE_&|MMlPkhQvQXqIj1AHdW7}#v-{9 zf!M}rS}Et&1)%Uqu3{AUo$v5dx2NHTD?V6 z|2_)-+ykr^Ig{mRT~(4b1+$aDDK2A)5^H5lG9zbTvP37 z8uqN+`tscAeE)#fd8d9srzj zDa2JT9(@|(Dsx;VG+(EUIJB~$Pt}1HNSWEF2>6eWof-}U?ei_0u>%U-)t}i%LYx5h zy<;=7Ps7NzGRL+!v=6L8hYxhd*l@_#?!$KX@BqXUr$_x+Z6fIxU326m@spEdpJ@My zMX)Z|uc1f2fhhX~<Wpxl-^vGc{9sf?tai-#puvYG9AhZ&;|PwxUT1c zBQJ+81qG^H3SCM(U=CesS?E6VGK?03O8$k`8c3Db9m8Vh+NrJ)U`nZL0;xLEgk^|6 z1WAk7rJfC~n)z$wB$#cgLp{y&eC>0=1{jP1x^zn^&_ZiUv1^}am7*9U;-FfmrUr^3 zqGk(0p{fxQ)cNUF>jdgkaSEuR9f^+0xw5tp6lL@v{{cWT?=W*9iPO|NPs6aW1LEC& zKBs?%WUlcb`I^Yo9(q%pk%=C*&_{jBVxb`twc7_6#YrDYygA804H~%{2~MJp((-dgA|1`^(i6naZMDJz=SwhN-TwZK!+Z(sNv{`r}OaG&y3yi0G)i2EtXZZHW3); zlh~D6B!dQKQ=+J%$yl#;C>Y)aEUC#uM>1jDoODI zp<=RYeQer^@_^GJmqH9cMP)IC}kP zs-z`R(gGf?5n@<#qYEq?T+xKSK6pRhP+0!}veQN;b3OQ@b^q(mKiQc&^g`m$3#mgw z;*bzO7>b8Z$4dt1%qEhxnu>}a&;i+p9BDS;9SJX0bq;(6{3=ppPe@c( zskncLTzd&Y=1iA3porw6I%^)#XTreuad%x#KCF4GV{MN!1vqRa%8Aef#T+Pt<7Mt| zd1az#<@BXwQP)Hs+#P-BE+r%Pso)3hRm_OPCX(yb17G8rwK zC}ezRmdiS2WYb!aqrxNCyTD41~R`r%q;FN+Rqhuj77 zOEkF^@tm9nv!F6HF2|6YWL)e58EaAvfMBrSwyX~?{Wj7*ubfN9H5tEOme|Up&>!P~ z3rrd~++uup2-{c$ft}2b?d#yShlI#p@g;nv6WC84MTyLDmu6*W`ZHQ?nUitJ3LCva zY~$}fcI@zho;}Qky;RuEkG3C)o&Z5773}Wr-E&}1_lX|KlR3R`MAp$^-#^DSQWZ{> z7DHq*8)V`DCNl^!KUDIxVh+Z>#wvwCKCw5Wue*v$$Clw(soiZfS7;iO(#~6PeIpF_ z#wNEVy&V%az76H&>o2Fu+Y;q%6Sh~K>4xTS*S}q#YS@@)*f?REwbk9LY<#Q!W__x% zD^b}65ZApG9jO&t6Dzi+R`euR^bk-(y1JfzE1>I3J4;?Wa_z{gM?Zwav7^_HrkooR z&JDB9P2?euJFpYKg32$ejbj$m{|S)%n|zBp;vMP;Jd(9>e@)30{1Ik_YB7EMp=Bk> zhY-vk;}+8{7)$%y*0P6 zc4N+rQ2hiwKLIjn7aAlUq+mCKSmqo)R)k!*WIF~m9QlpS+KodCANAWDv1vI=?BAD_ z#2Bn3a3JV1b`r#L=88F>NOFz9Xmi!|c7vQ(-?fP^898M{I^#U49NaKNd?A%RUE)p)AbO;O2e_Mg}_ z20(0(WQ`|s(tj#E!ld4km8DBIc|6cLQck`PG04Z)Q|OskEwjGJj62d_`-s5jB5xrO zYhI8HBaIOn*CT_l8-ml8@P$wyR&-$G0&RX&hISxUl^L_FOkp&{j}v1=eGCUTad7Q; z4}f`Kn1tC7Fw-!m*(DA_A}5k8i)2SEQ$0pn;cQ-}d=JVEl}Z^P?3!Si$&;uKl5-C# z0psu4QmcC;Xv0JlCJSZ}JxIyjK@U)on=BZAjH^ zNz`pgH>^z8HGC0e#Y&|svYr?9YA7%&6Ac{Pn&G)F+sbie?DJeC|Qy6 zb|t)Ba1n;;cuM1CYhW?SsKM>YvK?{Hj{8=mJYtRqf2X@^SG(Z{?QOeD%`nTMeP)`o zYNzRz798?i zv?*P^4%YH3?XZ^jIA8|j_Ds5xu6jC5q%glQ?`qHeV$D%(jT2*>^I|!>kh4vj)Bg2g zp3#$cb^pVzYBEbfz%e1!XaJQ_E?Q&hnp=TD)iNyRj%(cF$4R%xAXy6!gwOZmFc5}N z+9+haJF_!p)+Rt6vYRa}eAe4#sovb5X1$$!GJa|wTROxo^RcC;i_j5}kl_i=zcoP$ ziN2x>7<;kJMR3!ug@n$+f+I^De&?f7ZJVigxN5<|kBU`9j*<+iEaPHIQ?kyvCO!gxfr20fY}kK^VoalOiemJs_^|1u zm_Pv?tMyeH(!`oUg;0dUnx&#G6L}w&G)?x9z=4g*weU$RI~powZirL`0gd-Ap01-H zpwv!`Y1JuGO*^)P?7X>?6&yg)8roJ;<9<+Il6_WIulUDQ#hhxx8NWfK;44&Z!db5v z?pkmPH;&l+n9kYcO(Y2Yu~~z0kUKN)nd$2F<$*b&e}=(E&YDnRc}E1DV*mP_L{D5gY{x1Pn7YE$H5u zmqTYg1#iS`Cw8IMTt7zSW6YTWJqd5)k*rUJPsgt1P6esfzFVy(o0^hSGvgwXmAs2b zmT(s8!b|@-jVS72(xowvxWJh4BENQ*k&Y9yY2@G&?ihxa=%ZUkIhY|{5uc)G4DGn_ z3hq1ILotEiat1ec{=vKi!CO?7tAL=v?f+1FSQRWcn-fLNsUm-($PepH&0$q#cU|td zr#b0bIbUEYZoPW=zSH3FPc*0OtqFT8(8!IN>owf)Gg;J>lab*lY(jBDqwP)0Ibp?EjQvtd{HSclY@o} zKg`TaF`~J}QVK|{B9baiq{K?I%VIY@{7nje zK*0|wkSO?%6jajKt*7Q%7&ILUD|wXja_Je)?PX`Hu``9(Svc$v7v?{lyWA#U(991p z^QX)F!7@jpY}Yh1%VpLgOeDZaEVjC{A(t~fB-=Lh`q)rthuDNUf^rR!4n$x3jIkRK^UNC% zoj+kLH0I&02d^KTHz4|;&QQ`cz5R~yePPynC~ok~TiT3krh48wdh_VK0nzzqjg@M( z5S`y(Ha5%kMsywxY?wOvR`4b&f#|%w*;o>~ zBxWM$rg?jw+9*WlU1sBI`SplW>Ge}%Z@qjI4M3E`XR1f=p1mexx!gjE&p%~3Yc$sI z12pFi6rb-iuQyi7U5DuW1>=~}STWu6?W1p_nG~NtZ{CS76uNQt`q_B{qVtDMRchZM TI=`jJ*v1?BUkr#c^5Fjg>PDgH delta 8387 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 Dii=Ly diff --git a/recruitment/__pycache__/signals.cpython-312.pyc b/recruitment/__pycache__/signals.cpython-312.pyc index 03bb073025b0e82c01564e613a6b202d4bfc754e..44189bec21bca975ea4b8c6473535c2b04b66a41 100644 GIT binary patch delta 2043 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 Zi1Uc)45RB+j3RcEW3Si{`2c)5_&*I-|3m-) delta 1910 zcmZuxZA?>F81B72kQOXBp$MfGDpFQdIxJtdl#kNNhuF$Tv9?U0v>;VS(7B1CZ2no7 zWeM&iTjQ1t6St7exGHWlS!T@kXW5qQo_Ld!ZSrGgKbFNT*JRlbw;wy_y#6q81m5R; z`aJJ*-kuxoAMZ#%&lP=dG8O39&y_rR?!m50Mdk8^=Waazi%~)t6*yUK5H-r{`o)3; zy*@wvE?(8Uu(7C=^;yxoMSq$1TQ6TTexm1*I^88n)}^H6`>Fi&2g3J8(LCN#T#uv0 z<~EeR3a+PX^6E3wAEi+G172!XxWWxTHo9p<)0kpCB{n}kGK8cdz-Zq`#W{9P^Z)Mk|b^D ztm3z1Lszw-vuJ2kE|z~nhRl}C5o)R@rutl^ihP=WRK}^j_BJj*D$wN_Gq$XmRyaw8 zGq;Hw3Wc(XDT>jRGt8 zu4N@iOC6-tu@+L1SKG>FH+d``VX%qWs#Pje1knuS<pLprBJ~xXnkiptrX{;b$u7m+r=ouCRTjz~ z;2Aj5zS>M{e5A&w1Y#Hu^L(Mz4l6(V4#0hY#~oI< zu6;P#=`i#%2Rmc{f6d5#AR(-EZd(omIVg*7+;KOLcFmHmSxu*#mxkrPbh^9|SsMb= zD3}^^&D0$s?uarjwupc_25M6-MO#Kl%ZL&?rJ|R$L!id~UtNxaoB-K-J3@Ui;)^L$ zB6=FsB&d6GLE020O;KfHnQzuDYbh|yfT8_%igrwqjw!bOoYW`Q@0Yb%FzwTe?^wHf zn$`tKT|n7Cp@z|<*315r*yZ$r?7thNfe8|rP-f1kXi@72H3Vu{85*NQ3uI`49kD1y z#1Uh%76#Wr9P73jhJXy?*SZ~s5g?=ZIwKJvQGB1-4g-l{+3zsKfh6z?{?Zqxfh6I~ zJ^a3hiF;UyiW^OVI>XhBbtU!Q{H6IduZmi=Sx}A&rE(=g%}!!=Dy~iyb!l@TrtuZO z*>DWVaeT*LR(Ar(OK_ZXt(MjYNqtb+pHR`XHV^6tH;$n+g#^>Ey1KU zdz{+5#O9^89%AdcXA6ECR?)b29$Xjn_;`=aunepVAM2@F?iO#iM}G9=(Jaxr0a7=h z3?)^R(t1D*fa+Xpr#nMrXGn=0RneRl1a%P9-D{_*tB<()l(3jR0BQ)-&bwyX9Vgv! zB_W<_7*uH(HwMZKBf!RRI8f#~1mp-G%v5C?sKrMtK57XPOYokBy+Ef_l+Y%?HHq+d leKzA1umr-77@G!`M7X@)W=sK_5zI6z*z1KS-(oNQ{{U?-IpzQW diff --git a/recruitment/__pycache__/views.cpython-312.pyc b/recruitment/__pycache__/views.cpython-312.pyc index 317da3143c8b0263210869aca23a14f6950fb8f6..1b785c1aae650cb3860e8a7bfd5549fce1a20e5e 100644 GIT binary patch delta 56154 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 IBJgbeZ%R7?c>n+a delta 20616 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%0D=U@`p){M5GLrijPC< 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 O-yZpUh*L&bxBm}gAllIY delta 10681 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 Q$Kt0C(>veUr9k)lU&a8+UjP6A diff --git a/recruitment/forms.py b/recruitment/forms.py index 495db3e..590a0ea 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -11,7 +11,7 @@ from .models import ( ZoomMeeting, Candidate,TrainingMaterial,JobPosting, FormTemplate,InterviewSchedule,BreakTime,JobPostingImage, Profile,MeetingComment,ScheduledInterview,Source,HiringAgency, - AgencyJobAssignment, AgencyAccessLink + AgencyJobAssignment, AgencyAccessLink,Participants ) # from django_summernote.widgets import SummernoteWidget from django_ckeditor_5.widgets import CKEditor5Widget @@ -1145,3 +1145,57 @@ class AgencyLoginForm(forms.Form): raise ValidationError('Invalid access token.') return cleaned_data + + + + + +#participants form +class ParticipantsForm(forms.ModelForm): + """Form for creating and editing Participants""" + + class Meta: + model = Participants + fields = ['name', 'email', 'phone', 'designation'] + widgets = { + 'name': forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Enter participant name', + 'required': True + }), + 'email': forms.EmailInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Enter email address', + 'required': True + }), + 'phone': forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Enter phone number' + }), + 'designation': forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Enter designation' + }), + # 'jobs': forms.CheckboxSelectMultiple(), + } + + +class ParticipantsSelectForm(forms.ModelForm): + """Form for selecting Participants""" + + participants=forms.ModelMultipleChoiceField( + queryset=Participants.objects.all(), + widget=forms.CheckboxSelectMultiple, + required=False, + label=_("Select Participants")) + + users=forms.ModelMultipleChoiceField( + queryset=User.objects.all(), + widget=forms.CheckboxSelectMultiple, + required=False, + label=_("Select Users")) + + class Meta: + model = JobPosting + fields = ['participants','users'] # No direct fields from Participants model + \ No newline at end of file diff --git a/recruitment/migrations/0001_initial.py b/recruitment/migrations/0001_initial.py index 45877f8..1e9fde3 100644 --- a/recruitment/migrations/0001_initial.py +++ b/recruitment/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.7 on 2025-10-29 11:45 +# Generated by Django 5.2.7 on 2025-10-29 18:04 import django.core.validators import django.db.models.deletion @@ -100,6 +100,11 @@ class Migration(migrations.Migration): ('integration_version', models.CharField(blank=True, help_text='Version of the integration protocol', max_length=50, verbose_name='Integration Version')), ('last_sync_at', models.DateTimeField(blank=True, help_text='Timestamp of the last successful synchronization', null=True, verbose_name='Last Sync At')), ('sync_status', models.CharField(blank=True, choices=[('IDLE', 'Idle'), ('SYNCING', 'Syncing'), ('ERROR', 'Error'), ('DISABLED', 'Disabled')], default='IDLE', max_length=20, verbose_name='Sync Status')), + ('sync_endpoint', models.URLField(blank=True, help_text='Endpoint URL for sending candidate data (for outbound sync)', null=True, verbose_name='Sync Endpoint')), + ('sync_method', models.CharField(blank=True, choices=[('POST', 'POST'), ('PUT', 'PUT')], default='POST', help_text='HTTP method for outbound sync requests', max_length=10, verbose_name='Sync Method')), + ('test_method', models.CharField(blank=True, choices=[('GET', 'GET'), ('POST', 'POST')], default='GET', help_text='HTTP method for connection testing', max_length=10, verbose_name='Test Method')), + ('custom_headers', models.TextField(blank=True, help_text='JSON object with custom HTTP headers for sync requests', null=True, verbose_name='Custom Headers')), + ('supports_outbound_sync', models.BooleanField(default=False, help_text='Whether this source supports receiving candidate data from ATS', verbose_name='Supports Outbound Sync')), ], options={ 'verbose_name': 'Source', @@ -217,7 +222,7 @@ class Migration(migrations.Migration): ('is_potential_candidate', models.BooleanField(default=False, verbose_name='Potential Candidate')), ('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')), ('applied', models.BooleanField(default=False, verbose_name='Applied')), - ('stage', models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer')], db_index=True, default='Applied', max_length=100, verbose_name='Stage')), + ('stage', models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer'), ('Hired', 'Hired')], db_index=True, default='Applied', max_length=100, verbose_name='Stage')), ('applicant_status', models.CharField(blank=True, choices=[('Applicant', 'Applicant'), ('Candidate', 'Candidate')], default='Applicant', max_length=100, null=True, verbose_name='Applicant Status')), ('exam_date', models.DateTimeField(blank=True, null=True, verbose_name='Exam Date')), ('exam_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=100, null=True, verbose_name='Exam Status')), @@ -225,10 +230,12 @@ class Migration(migrations.Migration): ('interview_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=100, null=True, verbose_name='Interview Status')), ('offer_date', models.DateField(blank=True, null=True, verbose_name='Offer Date')), ('offer_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected')], max_length=100, null=True, verbose_name='Offer Status')), + ('hired_date', models.DateField(blank=True, null=True, verbose_name='Hired Date')), ('join_date', models.DateField(blank=True, null=True, verbose_name='Join Date')), ('ai_analysis_data', models.JSONField(default=dict, help_text='Full JSON output from the resume scoring model.', verbose_name='AI Analysis Data')), ('retry', models.SmallIntegerField(default=3, verbose_name='Resume Parsing Retry')), - ('submitted_by_agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='submitted_candidates', to='recruitment.hiringagency', verbose_name='Submitted by Agency')), + ('hiring_source', models.CharField(blank=True, choices=[('Public', 'Public'), ('Internal', 'Internal'), ('Agency', 'Agency')], default='Public', max_length=255, null=True, verbose_name='Hiring Source')), + ('hiring_agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='candidates', to='recruitment.hiringagency', verbose_name='Hiring Agency')), ], options={ 'verbose_name': 'Candidate', @@ -238,6 +245,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='JobPosting', fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), @@ -255,7 +263,7 @@ class Migration(migrations.Migration): ('application_url', models.URLField(blank=True, help_text='URL where candidates apply', null=True, validators=[django.core.validators.URLValidator()])), ('application_deadline', models.DateField(db_index=True)), ('application_instructions', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)), - ('internal_job_id', models.CharField(editable=False, max_length=50, primary_key=True, serialize=False)), + ('internal_job_id', models.CharField(editable=False, max_length=50)), ('created_by', models.CharField(blank=True, help_text='Name of person who created this job', max_length=100)), ('status', models.CharField(choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], db_index=True, default='DRAFT', max_length=20)), ('hash_tags', models.CharField(blank=True, help_text='Comma-separated hashtags for linkedin post like #hiring,#jobopening', max_length=200, validators=[recruitment.validators.validate_hash_tags])), @@ -315,6 +323,31 @@ class Migration(migrations.Migration): name='job', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.jobposting', verbose_name='Job'), ), + migrations.CreateModel( + name='AgencyJobAssignment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('max_candidates', models.PositiveIntegerField(help_text='Maximum candidates agency can submit for this job', verbose_name='Maximum Candidates')), + ('candidates_submitted', models.PositiveIntegerField(default=0, help_text='Number of candidates submitted so far', verbose_name='Candidates Submitted')), + ('assigned_date', models.DateTimeField(auto_now_add=True, verbose_name='Assigned Date')), + ('deadline_date', models.DateTimeField(help_text='Deadline for agency to submit candidates', verbose_name='Deadline Date')), + ('is_active', models.BooleanField(default=True, verbose_name='Is Active')), + ('status', models.CharField(choices=[('ACTIVE', 'Active'), ('COMPLETED', 'Completed'), ('EXPIRED', 'Expired'), ('CANCELLED', 'Cancelled')], default='ACTIVE', max_length=20, verbose_name='Status')), + ('deadline_extended', models.BooleanField(default=False, verbose_name='Deadline Extended')), + ('original_deadline', models.DateTimeField(blank=True, help_text='Original deadline before extensions', null=True, verbose_name='Original Deadline')), + ('admin_notes', models.TextField(blank=True, help_text='Internal notes about this assignment', verbose_name='Admin Notes')), + ('agency', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='job_assignments', to='recruitment.hiringagency', verbose_name='Agency')), + ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agency_assignments', to='recruitment.jobposting', verbose_name='Job')), + ], + options={ + 'verbose_name': 'Agency Job Assignment', + 'verbose_name_plural': 'Agency Job Assignments', + 'ordering': ['-created_at'], + }, + ), migrations.CreateModel( name='JobPostingImage', fields=[ @@ -327,17 +360,11 @@ class Migration(migrations.Migration): name='Profile', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), - ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), ('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size])), ('designation', models.CharField(blank=True, max_length=100, null=True)), ('phone', models.CharField(blank=True, max_length=12, null=True, verbose_name='Phone Number')), ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), ], - options={ - 'abstract': False, - }, ), migrations.CreateModel( name='SharedFormTemplate', @@ -364,7 +391,7 @@ class Migration(migrations.Migration): ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), ('action', models.CharField(choices=[('REQUEST', 'Request'), ('RESPONSE', 'Response'), ('ERROR', 'Error'), ('SYNC', 'Sync'), ('CREATE_JOB', 'Create Job'), ('UPDATE_JOB', 'Update Job')], max_length=20, verbose_name='Action')), ('endpoint', models.CharField(blank=True, max_length=255, verbose_name='Endpoint')), - ('method', models.CharField(blank=True, max_length=10, verbose_name='HTTP Method')), + ('method', models.CharField(blank=True, max_length=50, verbose_name='HTTP Method')), ('request_data', models.JSONField(blank=True, null=True, verbose_name='Request Data')), ('response_data', models.JSONField(blank=True, null=True, verbose_name='Response Data')), ('status_code', models.CharField(blank=True, max_length=10, verbose_name='Status Code')), @@ -452,6 +479,28 @@ class Migration(migrations.Migration): 'ordering': ['-created_at'], }, ), + migrations.CreateModel( + name='AgencyAccessLink', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('unique_token', models.CharField(editable=False, max_length=64, unique=True, verbose_name='Unique Token')), + ('access_password', models.CharField(help_text='Password for agency access', max_length=32, verbose_name='Access Password')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('expires_at', models.DateTimeField(help_text='When this access link expires', verbose_name='Expires At')), + ('last_accessed', models.DateTimeField(blank=True, null=True, verbose_name='Last Accessed')), + ('access_count', models.PositiveIntegerField(default=0, verbose_name='Access Count')), + ('is_active', models.BooleanField(default=True, verbose_name='Is Active')), + ('assignment', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='access_link', to='recruitment.agencyjobassignment', verbose_name='Assignment')), + ], + options={ + 'verbose_name': 'Agency Access Link', + 'verbose_name_plural': 'Agency Access Links', + 'ordering': ['-created_at'], + 'indexes': [models.Index(fields=['unique_token'], name='recruitment_unique__f91e76_idx'), models.Index(fields=['expires_at'], name='recruitment_expires_954ed9_idx'), models.Index(fields=['is_active'], name='recruitment_is_acti_4b0804_idx')], + }, + ), migrations.CreateModel( name='FieldResponse', fields=[ @@ -502,6 +551,26 @@ class Migration(migrations.Migration): model_name='candidate', index=models.Index(fields=['created_at'], name='recruitment_created_73590f_idx'), ), + migrations.AddIndex( + model_name='agencyjobassignment', + index=models.Index(fields=['agency', 'status'], name='recruitment_agency__491a54_idx'), + ), + migrations.AddIndex( + model_name='agencyjobassignment', + index=models.Index(fields=['job', 'status'], name='recruitment_job_id_d798a8_idx'), + ), + migrations.AddIndex( + model_name='agencyjobassignment', + index=models.Index(fields=['deadline_date'], name='recruitment_deadlin_57d3b4_idx'), + ), + migrations.AddIndex( + model_name='agencyjobassignment', + index=models.Index(fields=['is_active'], name='recruitment_is_acti_93b919_idx'), + ), + migrations.AlterUniqueTogether( + name='agencyjobassignment', + unique_together={('agency', 'job')}, + ), migrations.AddIndex( model_name='jobposting', index=models.Index(fields=['status', 'created_at', 'title'], name='recruitment_status_8b77aa_idx'), diff --git a/recruitment/migrations/0002_remove_profile_created_at_remove_profile_slug_and_more.py b/recruitment/migrations/0002_remove_profile_created_at_remove_profile_slug_and_more.py deleted file mode 100644 index 00aaa1e..0000000 --- a/recruitment/migrations/0002_remove_profile_created_at_remove_profile_slug_and_more.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-29 12:41 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='profile', - name='created_at', - ), - migrations.RemoveField( - model_name='profile', - name='slug', - ), - migrations.RemoveField( - model_name='profile', - name='updated_at', - ), - ] diff --git a/recruitment/migrations/0003_candidate_hired_date_source_custom_headers_and_more.py b/recruitment/migrations/0003_candidate_hired_date_source_custom_headers_and_more.py deleted file mode 100644 index 7c999ad..0000000 --- a/recruitment/migrations/0003_candidate_hired_date_source_custom_headers_and_more.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 5.2.4 on 2025-10-26 13:27 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0002_candidate_retry'), - ] - - operations = [ - migrations.AddField( - model_name='candidate', - name='hired_date', - field=models.DateField(blank=True, null=True, verbose_name='Hired Date'), - ), - migrations.AddField( - model_name='source', - name='custom_headers', - field=models.TextField(blank=True, help_text='JSON object with custom HTTP headers for sync requests', null=True, verbose_name='Custom Headers'), - ), - migrations.AddField( - model_name='source', - name='supports_outbound_sync', - field=models.BooleanField(default=False, help_text='Whether this source supports receiving candidate data from ATS', verbose_name='Supports Outbound Sync'), - ), - migrations.AddField( - model_name='source', - name='sync_endpoint', - field=models.URLField(blank=True, help_text='Endpoint URL for sending candidate data (for outbound sync)', null=True, verbose_name='Sync Endpoint'), - ), - migrations.AddField( - model_name='source', - name='sync_method', - field=models.CharField(blank=True, choices=[('POST', 'POST'), ('PUT', 'PUT')], default='POST', help_text='HTTP method for outbound sync requests', max_length=10, verbose_name='Sync Method'), - ), - migrations.AddField( - model_name='source', - name='test_method', - field=models.CharField(blank=True, choices=[('GET', 'GET'), ('POST', 'POST')], default='GET', help_text='HTTP method for connection testing', max_length=10, verbose_name='Test Method'), - ), - migrations.AlterField( - model_name='candidate', - name='stage', - field=models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer'), ('Hired', 'Hired')], db_index=True, default='Applied', max_length=100, verbose_name='Stage'), - ), - ] diff --git a/recruitment/migrations/0004_alter_integrationlog_method.py b/recruitment/migrations/0004_alter_integrationlog_method.py deleted file mode 100644 index e4ab1d0..0000000 --- a/recruitment/migrations/0004_alter_integrationlog_method.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.4 on 2025-10-26 13:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0003_candidate_hired_date_source_custom_headers_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='integrationlog', - name='method', - field=models.CharField(blank=True, max_length=50, verbose_name='HTTP Method'), - ), - ] diff --git a/recruitment/migrations/0005_rename_submitted_by_agency_candidate_hiring_agency.py b/recruitment/migrations/0005_rename_submitted_by_agency_candidate_hiring_agency.py deleted file mode 100644 index 48ea4b0..0000000 --- a/recruitment/migrations/0005_rename_submitted_by_agency_candidate_hiring_agency.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.4 on 2025-10-26 14:37 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0004_alter_integrationlog_method'), - ] - - operations = [ - migrations.RenameField( - model_name='candidate', - old_name='submitted_by_agency', - new_name='hiring_agency', - ), - ] diff --git a/recruitment/migrations/0006_agencyjobassignment_agencyaccesslink_agencymessage_and_more.py b/recruitment/migrations/0006_agencyjobassignment_agencyaccesslink_agencymessage_and_more.py deleted file mode 100644 index 8c1c20c..0000000 --- a/recruitment/migrations/0006_agencyjobassignment_agencyaccesslink_agencymessage_and_more.py +++ /dev/null @@ -1,129 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-26 14:51 - -import django.db.models.deletion -import django_extensions.db.fields -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0005_rename_submitted_by_agency_candidate_hiring_agency'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='AgencyJobAssignment', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), - ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), - ('max_candidates', models.PositiveIntegerField(help_text='Maximum candidates agency can submit for this job', verbose_name='Maximum Candidates')), - ('candidates_submitted', models.PositiveIntegerField(default=0, help_text='Number of candidates submitted so far', verbose_name='Candidates Submitted')), - ('assigned_date', models.DateTimeField(auto_now_add=True, verbose_name='Assigned Date')), - ('deadline_date', models.DateTimeField(help_text='Deadline for agency to submit candidates', verbose_name='Deadline Date')), - ('is_active', models.BooleanField(default=True, verbose_name='Is Active')), - ('status', models.CharField(choices=[('ACTIVE', 'Active'), ('COMPLETED', 'Completed'), ('EXPIRED', 'Expired'), ('CANCELLED', 'Cancelled')], default='ACTIVE', max_length=20, verbose_name='Status')), - ('deadline_extended', models.BooleanField(default=False, verbose_name='Deadline Extended')), - ('original_deadline', models.DateTimeField(blank=True, help_text='Original deadline before extensions', null=True, verbose_name='Original Deadline')), - ('admin_notes', models.TextField(blank=True, help_text='Internal notes about this assignment', verbose_name='Admin Notes')), - ('agency', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='job_assignments', to='recruitment.hiringagency', verbose_name='Agency')), - ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agency_assignments', to='recruitment.jobposting', verbose_name='Job')), - ], - options={ - 'verbose_name': 'Agency Job Assignment', - 'verbose_name_plural': 'Agency Job Assignments', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='AgencyAccessLink', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), - ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), - ('unique_token', models.CharField(editable=False, max_length=64, unique=True, verbose_name='Unique Token')), - ('access_password', models.CharField(help_text='Password for agency access', max_length=32, verbose_name='Access Password')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('expires_at', models.DateTimeField(help_text='When this access link expires', verbose_name='Expires At')), - ('last_accessed', models.DateTimeField(blank=True, null=True, verbose_name='Last Accessed')), - ('access_count', models.PositiveIntegerField(default=0, verbose_name='Access Count')), - ('is_active', models.BooleanField(default=True, verbose_name='Is Active')), - ('assignment', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='access_link', to='recruitment.agencyjobassignment', verbose_name='Assignment')), - ], - options={ - 'verbose_name': 'Agency Access Link', - 'verbose_name_plural': 'Agency Access Links', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='AgencyMessage', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), - ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), - ('subject', models.CharField(max_length=200, verbose_name='Subject')), - ('message', models.TextField(verbose_name='Message')), - ('message_type', models.CharField(choices=[('INFO', 'Information'), ('WARNING', 'Warning'), ('EXTENSION', 'Deadline Extension'), ('GENERAL', 'General')], default='GENERAL', max_length=20, verbose_name='Message Type')), - ('is_read', models.BooleanField(default=False, verbose_name='Is Read')), - ('read_at', models.DateTimeField(blank=True, null=True, verbose_name='Read At')), - ('assignment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='recruitment.agencyjobassignment', verbose_name='Assignment')), - ('recipient_agency', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to='recruitment.hiringagency', verbose_name='Recipient Agency')), - ('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_agency_messages', to=settings.AUTH_USER_MODEL, verbose_name='Sender')), - ], - options={ - 'verbose_name': 'Agency Message', - 'verbose_name_plural': 'Agency Messages', - 'ordering': ['-created_at'], - }, - ), - migrations.AddIndex( - model_name='agencyjobassignment', - index=models.Index(fields=['agency', 'status'], name='recruitment_agency__491a54_idx'), - ), - migrations.AddIndex( - model_name='agencyjobassignment', - index=models.Index(fields=['job', 'status'], name='recruitment_job_id_d798a8_idx'), - ), - migrations.AddIndex( - model_name='agencyjobassignment', - index=models.Index(fields=['deadline_date'], name='recruitment_deadlin_57d3b4_idx'), - ), - migrations.AddIndex( - model_name='agencyjobassignment', - index=models.Index(fields=['is_active'], name='recruitment_is_acti_93b919_idx'), - ), - migrations.AlterUniqueTogether( - name='agencyjobassignment', - unique_together={('agency', 'job')}, - ), - migrations.AddIndex( - model_name='agencyaccesslink', - index=models.Index(fields=['unique_token'], name='recruitment_unique__f91e76_idx'), - ), - migrations.AddIndex( - model_name='agencyaccesslink', - index=models.Index(fields=['expires_at'], name='recruitment_expires_954ed9_idx'), - ), - migrations.AddIndex( - model_name='agencyaccesslink', - index=models.Index(fields=['is_active'], name='recruitment_is_acti_4b0804_idx'), - ), - migrations.AddIndex( - model_name='agencymessage', - index=models.Index(fields=['assignment', 'is_read'], name='recruitment_assignm_4f518d_idx'), - ), - migrations.AddIndex( - model_name='agencymessage', - index=models.Index(fields=['recipient_agency', 'is_read'], name='recruitment_recipie_427b10_idx'), - ), - migrations.AddIndex( - model_name='agencymessage', - index=models.Index(fields=['sender'], name='recruitment_sender__97dd96_idx'), - ), - ] diff --git a/recruitment/migrations/0007_candidate_source_candidate_source_type.py b/recruitment/migrations/0007_candidate_source_candidate_source_type.py deleted file mode 100644 index 5f83b42..0000000 --- a/recruitment/migrations/0007_candidate_source_candidate_source_type.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-27 11:42 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0006_agencyjobassignment_agencyaccesslink_agencymessage_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='candidate', - name='source', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='candidates', to='recruitment.source', verbose_name='Source'), - ), - migrations.AddField( - model_name='candidate', - name='source_type', - field=models.CharField(blank=True, choices=[('Public', 'Public'), ('Internal', 'Internal'), ('Agency', 'Agency')], default='Public', max_length=255, null=True, verbose_name='Source'), - ), - ] diff --git a/recruitment/migrations/0008_remove_candidate_source_remove_candidate_source_type_and_more.py b/recruitment/migrations/0008_remove_candidate_source_remove_candidate_source_type_and_more.py deleted file mode 100644 index 591252c..0000000 --- a/recruitment/migrations/0008_remove_candidate_source_remove_candidate_source_type_and_more.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-27 11:44 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0007_candidate_source_candidate_source_type'), - ] - - operations = [ - migrations.RemoveField( - model_name='candidate', - name='source', - ), - migrations.RemoveField( - model_name='candidate', - name='source_type', - ), - migrations.AddField( - model_name='candidate', - name='hiring_source', - field=models.CharField(blank=True, choices=[('Public', 'Public'), ('Internal', 'Internal'), ('Agency', 'Agency')], default='Public', max_length=255, null=True, verbose_name='Hiring Source'), - ), - migrations.AlterField( - model_name='candidate', - name='hiring_agency', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='candidates', to='recruitment.hiringagency', verbose_name='Hiring Agency'), - ), - ] diff --git a/recruitment/migrations/0009_agencymessage_priority_agencymessage_recipient_user_and_more.py b/recruitment/migrations/0009_agencymessage_priority_agencymessage_recipient_user_and_more.py deleted file mode 100644 index 5b27532..0000000 --- a/recruitment/migrations/0009_agencymessage_priority_agencymessage_recipient_user_and_more.py +++ /dev/null @@ -1,59 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-27 20:26 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0008_remove_candidate_source_remove_candidate_source_type_and_more'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name='agencymessage', - name='priority', - field=models.CharField(choices=[('LOW', 'Low'), ('MEDIUM', 'Medium'), ('HIGH', 'High'), ('URGENT', 'Urgent')], default='MEDIUM', max_length=10, verbose_name='Priority'), - ), - migrations.AddField( - model_name='agencymessage', - name='recipient_user', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='received_agency_messages', to=settings.AUTH_USER_MODEL, verbose_name='Recipient User'), - ), - migrations.AddField( - model_name='agencymessage', - name='send_email', - field=models.BooleanField(default=False, verbose_name='Send Email Notification'), - ), - migrations.AddField( - model_name='agencymessage', - name='send_sms', - field=models.BooleanField(default=False, verbose_name='Send SMS Notification'), - ), - migrations.AddField( - model_name='agencymessage', - name='sender_agency', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to='recruitment.hiringagency', verbose_name='Sender Agency'), - ), - migrations.AddField( - model_name='agencymessage', - name='sender_type', - field=models.CharField(choices=[('ADMIN', 'Admin'), ('AGENCY', 'Agency')], default='ADMIN', max_length=10, verbose_name='Sender Type'), - ), - migrations.AlterField( - model_name='agencymessage', - name='sender', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sent_agency_messages', to=settings.AUTH_USER_MODEL, verbose_name='Sender'), - ), - migrations.AddIndex( - model_name='agencymessage', - index=models.Index(fields=['sender_type', 'created_at'], name='recruitment_sender__14b136_idx'), - ), - migrations.AddIndex( - model_name='agencymessage', - index=models.Index(fields=['priority', 'created_at'], name='recruitment_priorit_80d9f1_idx'), - ), - ] diff --git a/recruitment/migrations/0010_remove_agency_message_model.py b/recruitment/migrations/0010_remove_agency_message_model.py deleted file mode 100644 index f042dcf..0000000 --- a/recruitment/migrations/0010_remove_agency_message_model.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-29 10:59 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0009_agencymessage_priority_agencymessage_recipient_user_and_more'), - ] - - operations = [ - migrations.DeleteModel( - name='AgencyMessage', - ), - ] diff --git a/recruitment/models.py b/recruitment/models.py index 1bfb364..5e80ec2 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -108,7 +108,7 @@ class JobPosting(Base): ) # Internal Tracking - internal_job_id = models.CharField(max_length=50, primary_key=True, editable=False) + internal_job_id = models.CharField(max_length=50, editable=False) created_by = models.CharField( max_length=100, blank=True, help_text="Name of person who created this job" @@ -363,6 +363,10 @@ class JobPosting(Base): def offer_candidates_count(self): return self.all_candidates.filter(stage="Offer").count() or 0 + @property + def hired_candidates_count(self): + return self.all_candidates.filter(stage="Hired").count() or 0 + @property def vacancy_fill_rate(self): total_positions = self.open_positions diff --git a/recruitment/views.py b/recruitment/views.py index 7231631..b2d40cf 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -17,7 +17,8 @@ from django.urls import reverse from django.conf import settings from django.utils import timezone from django.db.models import FloatField,CharField, DurationField -from django.db.models.functions import Cast +from django.db.models import F, IntegerField, Count, Avg, Sum, Q, ExpressionWrapper, fields +from django.db.models.functions import Cast, Coalesce, TruncDate from django.db.models.fields.json import KeyTextTransform from django.db.models.expressions import ExpressionWrapper from django.db.models import Count, Avg, F,Q @@ -38,7 +39,9 @@ from .forms import ( AgencyCandidateSubmissionForm, AgencyLoginForm, AgencyAccessLinkForm, - AgencyJobAssignmentForm + AgencyJobAssignmentForm, + LinkedPostContentForm, + ParticipantsSelectForm ) from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent from rest_framework import viewsets @@ -332,6 +335,8 @@ def edit_job(request, slug): return render(request, "jobs/edit_job.html", {"form": form, "job": job}) +SCORE_PATH = 'ai_analysis_data__analysis_data__match_score' +HIGH_POTENTIAL_THRESHOLD=75 @login_required def job_detail(request, slug): """View details of a specific job""" @@ -391,29 +396,31 @@ def job_detail(request, slug): # --- 2. Quality Metrics (JSON Aggregation) --- # Filter for candidates who have been scored and annotate with a sortable score - candidates_with_score = applicants.filter(is_resume_parsed=True).annotate( - # Extract the score as TEXT - score_as_text=KeyTextTransform( - 'match_score', - KeyTextTransform('resume_data', F('ai_analysis_data')) - ) + # candidates_with_score = applicants.filter(is_resume_parsed=True).annotate( + # # Extract the score as TEXT + # score_as_text=KeyTextTransform( + # 'match_score', + # KeyTextTransform('resume_data', F('ai_analysis_data')) + # ) + # ).annotate( + # # Cast the extracted text score to a FloatField for numerical operations + # sortable_score=Cast('score_as_text', output_field=FloatField()) + # ) + candidates_with_score = applicants.filter( + is_resume_parsed=True ).annotate( - # Cast the extracted text score to a FloatField for numerical operations - sortable_score=Cast('score_as_text', output_field=FloatField()) + annotated_match_score=Coalesce( + Cast(SCORE_PATH, output_field=IntegerField()), + 0 + ) ) + total_candidates=applicants.count() + avg_match_score_result = candidates_with_score.aggregate(avg_score=Avg('annotated_match_score'))['avg_score'] + avg_match_score = round(avg_match_score_result or 0, 1) + high_potential_count = candidates_with_score.filter(annotated_match_score__gte=HIGH_POTENTIAL_THRESHOLD).count() + high_potential_ratio = round( (high_potential_count / total_candidates) * 100, 1 ) if total_candidates > 0 else 0 - # Aggregate: Average Match Score - avg_match_score_result = candidates_with_score.aggregate( - avg_score=Avg('sortable_score') - )['avg_score'] - avg_match_score = round(avg_match_score_result or 0, 1) - - # Metric: High Potential Count (Score >= 75) - high_potential_count = candidates_with_score.filter( - sortable_score__gte=75 - ).count() - high_potential_ratio = round((high_potential_count / total_applicant) * 100, 1) if total_applicant > 0 else 0 - + # --- 3. Time Metrics (Duration Aggregation) --- # Metric: Average Time from Applied to Interview (T2I) diff --git a/recruitment/views_frontend.py b/recruitment/views_frontend.py index 567e7e1..f0b25cc 100644 --- a/recruitment/views_frontend.py +++ b/recruitment/views_frontend.py @@ -339,119 +339,227 @@ class TrainingDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView): success_url = reverse_lazy('training_list') success_message = 'Training material deleted successfully.' -from django.db.models import F, IntegerField, Count, Avg -from django.db.models.functions import Cast, Coalesce +from django.db.models import F, IntegerField, Count, Avg, Sum, Q, ExpressionWrapper, fields +from django.db.models.functions import Cast, Coalesce, TruncDate +from django.contrib.auth.decorators import login_required +from django.shortcuts import render +from django.utils import timezone +from datetime import timedelta +import json + +# IMPORTANT: Ensure 'models' correctly refers to your Django models file +# Example: from . import models + +# --- Constants --- +SCORE_PATH = 'ai_analysis_data__analysis_data__match_score' +HIGH_POTENTIAL_THRESHOLD = 75 +MAX_TIME_TO_HIRE_DAYS = 90 +TARGET_TIME_TO_HIRE_DAYS = 45 # Used for the template visualization + + @login_required def dashboard_view(request): - all_candidates_count=0 - # --- Performance Optimization: Aggregate Data in ONE Query --- + selected_job_pk = request.GET.get('selected_job_pk') + today = timezone.now().date() + + # --- 1. BASE QUERYSETS & GLOBAL METRICS (UNFILTERED) --- + + all_jobs_queryset = models.JobPosting.objects.all().order_by('-created_at') + all_candidates_queryset = models.Candidate.objects.all() - # 1. Base Job Query: Get all jobs and annotate with candidate count - jobs_with_counts = models.JobPosting.objects.annotate( - candidate_count=Count('candidates') - ).order_by('-candidate_count') + # Global KPI Card Metrics + total_jobs_global = all_jobs_queryset.count() + total_participants = models.Participants.objects.count() + total_jobs_posted_linkedin = all_jobs_queryset.filter(linkedin_post_id__isnull=False).count() + + # Data for Job App Count Chart (always for ALL jobs) + job_titles = [job.title for job in all_jobs_queryset] + job_app_counts = [job.candidates.count() for job in all_jobs_queryset] - total_jobs = jobs_with_counts.count() - total_candidates = models.Candidate.objects.count() + # --- 2. TIME SERIES: GLOBAL DAILY APPLICANTS --- + + # Group ALL candidates by creation date + global_daily_applications_qs = all_candidates_queryset.annotate( + date=TruncDate('created_at') + ).values('date').annotate( + count=Count('pk') + ).order_by('date') - job_titles = [job.title for job in jobs_with_counts] - job_app_counts = [job.candidate_count for job in jobs_with_counts] + global_dates = [item['date'].strftime('%Y-%m-%d') for item in global_daily_applications_qs] + global_counts = [item['count'] for item in global_daily_applications_qs] - average_applications = round(jobs_with_counts.aggregate( - avg_apps=Avg('candidate_count') - )['avg_apps'] or 0, 2) - # 5. New: Candidate Quality & Funnel Metrics + # --- 3. FILTERING LOGIC: Determine the scope for scoped metrics --- + + candidate_queryset = all_candidates_queryset + job_scope_queryset = all_jobs_queryset + interview_queryset = models.ScheduledInterview.objects.all() + + current_job = None + if selected_job_pk: + # Filter all base querysets + candidate_queryset = candidate_queryset.filter(job__pk=selected_job_pk) + interview_queryset = interview_queryset.filter(job__pk=selected_job_pk) + + try: + current_job = all_jobs_queryset.get(pk=selected_job_pk) + job_scope_queryset = models.JobPosting.objects.filter(pk=selected_job_pk) + except models.JobPosting.DoesNotExist: + pass - # Assuming 'match_score' is a direct IntegerField/FloatField on the Candidate model - # (based on the final, optimized version of handle_reume_parsing_and_scoring) - # The path to your score: ai_analysis_data['analysis_data']['match_score'] - SCORE_PATH = 'ai_analysis_data__analysis_data__match_score' + # --- 4. TIME SERIES: SCOPED DAILY APPLICANTS --- - # --- The Annotate Step --- - candidates_with_score_query = models.Candidate.objects.filter( + # Only run if a specific job is selected + scoped_dates = [] + scoped_counts = [] + if selected_job_pk: + scoped_daily_applications_qs = candidate_queryset.annotate( + date=TruncDate('created_at') + ).values('date').annotate( + count=Count('pk') + ).order_by('date') + + scoped_dates = [item['date'].strftime('%Y-%m-%d') for item in scoped_daily_applications_qs] + scoped_counts = [item['count'] for item in scoped_daily_applications_qs] + + + # --- 5. SCOPED CORE AGGREGATIONS (FILTERED OR ALL) --- + + total_candidates = candidate_queryset.count() + + candidates_with_score_query = candidate_queryset.filter( is_resume_parsed=True ).annotate( - # 1. Use Coalesce to handle cases where the score might be missing or NULL - # (It defaults the value to 0 if missing). - # 2. Use Cast to convert the JSON value (which is often returned as text/string by the DB) - # into a proper IntegerField so we can perform math on it. annotated_match_score=Coalesce( Cast(SCORE_PATH, output_field=IntegerField()), 0 ) ) - # Now calculate the average match score - avg_match_score_result = candidates_with_score_query.aggregate( - avg_score=Avg('annotated_match_score') - )['avg_score'] - avg_match_score = round(avg_match_score_result or 0, 1) - hight_potential_count=0 - # --- The Filter Step for High Potential Candidates --- - candidates_with_score_gte_75 = candidates_with_score_query.filter( - annotated_match_score__gte=75 + # A. Pipeline & Volume Metrics (Scoped) + total_active_jobs = job_scope_queryset.filter(status="ACTIVE").count() + last_week = timezone.now() - timedelta(days=7) + new_candidates_7days = candidate_queryset.filter(created_at__gte=last_week).count() + + open_positions_agg = job_scope_queryset.filter(status="ACTIVE").aggregate(total_open=Sum('open_positions')) + total_open_positions = open_positions_agg['total_open'] or 0 + average_applications_result = job_scope_queryset.annotate( + candidate_count=Count('candidates', distinct=True) + ).aggregate(avg_apps=Avg('candidate_count'))['avg_apps'] + average_applications = round(average_applications_result or 0, 2) + + + # B. Efficiency & Conversion Metrics (Scoped) + hired_candidates = candidate_queryset.filter( + Q(offer_status="Accepted") | Q(stage='HIRED'), + join_date__isnull=False ) - high_potential_count=candidates_with_score_gte_75.count() - high_potential_ratio = round((hight_potential_count / total_candidates) * 100, 1) if total_candidates > 0 else 0 + time_to_hire_query = hired_candidates.annotate( + time_diff=ExpressionWrapper( + F('join_date') - F('created_at__date'), + output_field=fields.DurationField() + ) + ).aggregate(avg_time_to_hire=Avg('time_diff')) + avg_time_to_hire_days = ( + time_to_hire_query.get('avg_time_to_hire').days + if time_to_hire_query.get('avg_time_to_hire') else 0 + ) + + applied_count = candidate_queryset.filter(stage='Applied').count() + advanced_count = candidate_queryset.filter(stage__in=['Exam', 'Interview', 'Offer']).count() + screening_pass_rate = round( (advanced_count / applied_count) * 100, 1 ) if applied_count > 0 else 0 + offers_extended_count = candidate_queryset.filter(stage='Offer').count() + offers_accepted_count = candidate_queryset.filter(offer_status='Accepted').count() + offers_accepted_rate = round( (offers_accepted_count / offers_extended_count) * 100, 1 ) if offers_extended_count > 0 else 0 + filled_positions = offers_accepted_count + vacancy_fill_rate = round( (filled_positions / total_open_positions) * 100, 1 ) if total_open_positions > 0 else 0 - # Scored Candidates Ratio + + # C. Activity & Quality Metrics (Scoped) + current_year, current_week, _ = today.isocalendar() + meetings_scheduled_this_week = interview_queryset.filter( + interview_date__week=current_week, interview_date__year=current_year + ).count() + avg_match_score_result = candidates_with_score_query.aggregate(avg_score=Avg('annotated_match_score'))['avg_score'] + avg_match_score = round(avg_match_score_result or 0, 1) + high_potential_count = candidates_with_score_query.filter(annotated_match_score__gte=HIGH_POTENTIAL_THRESHOLD).count() + high_potential_ratio = round( (high_potential_count / total_candidates) * 100, 1 ) if total_candidates > 0 else 0 total_scored_candidates = candidates_with_score_query.count() - scored_ratio = round((total_scored_candidates / total_candidates) * 100, 1) if total_candidates > 0 else 0 - - jobs=models.JobPosting.objects.all().order_by('internal_job_id') - selected_job_pk=request.GET.get('selected_job_pk','') - candidate_stage=['APPLIED','EXAM','INTERVIEW','OFFER'] - apply_count,exam_count,interview_count,offer_count=[0]*4 + scored_ratio = round( (total_scored_candidates / total_candidates) * 100, 1 ) if total_candidates > 0 else 0 - if selected_job_pk: - try: - job=jobs.get(pk=selected_job_pk) - apply_count=job.screening_candidates_count - exam_count=job.exam_candidates_count - interview_count=job.interview_candidates_count - offer_count=job.offer_candidates_count - all_candidates_count=job.all_candidates_count - except Exception as e: - print(e) - - else: #default job - try: - job=jobs.first() - apply_count=job.screening_candidates_count - exam_count=job.exam_candidates_count - interview_count=job.interview_candidates_count - offer_count=job.offer_candidates_count - all_candidates_count=job.all_candidates_count - except Exception as e: - print(e) - candidates_count=[ apply_count,exam_count,interview_count,offer_count ] + # --- 6. CHART DATA PREPARATION --- + + # A. Pipeline Funnel (Scoped) + stage_counts = candidate_queryset.values('stage').annotate(count=Count('stage')) + stage_map = {item['stage']: item['count'] for item in stage_counts} + candidate_stage = ['Applied', 'Exam', 'Interview', 'Offer', 'HIRED'] + candidates_count = [ + stage_map.get('Applied', 0), stage_map.get('Exam', 0), stage_map.get('Interview', 0), + stage_map.get('Offer', 0), filled_positions + ] + + # --- 7. GAUGE CHART CALCULATION (Time-to-Hire) --- + + current_days = avg_time_to_hire_days + rotation_percent = current_days / MAX_TIME_TO_HIRE_DAYS if MAX_TIME_TO_HIRE_DAYS > 0 else 0 + rotation_degrees = rotation_percent * 180 + rotation_degrees_final = round(min(rotation_degrees, 180), 1) # Ensure max 180 degrees + + + # --- 8. CONTEXT RETURN --- + context = { - 'total_jobs': total_jobs, + # Global KPIs + 'total_jobs_global': total_jobs_global, + 'total_participants': total_participants, + 'total_jobs_posted_linkedin': total_jobs_posted_linkedin, + + # Scoped KPIs + 'total_active_jobs': total_active_jobs, 'total_candidates': total_candidates, + 'new_candidates_7days': new_candidates_7days, + 'total_open_positions': total_open_positions, 'average_applications': average_applications, - - # Chart Data - 'job_titles': json.dumps(job_titles), - 'job_app_counts': json.dumps(job_app_counts), - - # New Analytical Metrics (FIXED) - 'avg_match_score': avg_match_score, + 'avg_time_to_hire_days': avg_time_to_hire_days, + 'screening_pass_rate': screening_pass_rate, + 'offers_accepted_rate': offers_accepted_rate, + 'vacancy_fill_rate': vacancy_fill_rate, + 'meetings_scheduled_this_week': meetings_scheduled_this_week, + 'avg_match_score': avg_match_score, 'high_potential_count': high_potential_count, 'high_potential_ratio': high_potential_ratio, 'scored_ratio': scored_ratio, - 'current_job_id':selected_job_pk, - 'jobs':jobs, - 'all_candidates_count':all_candidates_count, - 'candidate_stage':json.dumps(candidate_stage), - 'candidates_count':json.dumps(candidates_count) - ,'my_job':job + + # Chart Data + 'candidate_stage': json.dumps(candidate_stage), + 'candidates_count': json.dumps(candidates_count), + 'job_titles': json.dumps(job_titles), + 'job_app_counts': json.dumps(job_app_counts), + # 'source_volume_chart_data' is intentionally REMOVED + # Time Series Data + 'global_dates': json.dumps(global_dates), + 'global_counts': json.dumps(global_counts), + 'scoped_dates': json.dumps(scoped_dates), + 'scoped_counts': json.dumps(scoped_counts), + 'is_job_scoped': bool(selected_job_pk), + # Gauge Data + 'gauge_max_days': MAX_TIME_TO_HIRE_DAYS, + 'gauge_target_days': TARGET_TIME_TO_HIRE_DAYS, + 'gauge_rotation_degrees': rotation_degrees_final, + + # UI Control + 'jobs': all_jobs_queryset, + 'current_job_id': selected_job_pk, + 'current_job': current_job, } + return render(request, 'recruitment/dashboard.html', context) + + @login_required def candidate_offer_view(request, slug): """View for candidates in the Offer stage""" diff --git a/templates/recruitment/dashboard.html b/templates/recruitment/dashboard.html index 9dee733..208acd4 100644 --- a/templates/recruitment/dashboard.html +++ b/templates/recruitment/dashboard.html @@ -36,7 +36,7 @@ padding: 1.25rem; border-bottom: 1px solid var(--kaauh-border); background-color: #f8f9fa; - display: flex; /* Ensure title and filter are aligned */ + display: flex; justify-content: space-between; align-items: center; } @@ -107,14 +107,11 @@ padding: 2rem; } - /* Bootstrap Overrides (Optional, for full consistency) */ - .btn-primary { - background-color: var(--kaauh-teal); - border-color: var(--kaauh-teal); - } - .btn-primary:hover { - background-color: var(--kaauh-teal-dark); - border-color: var(--kaauh-teal-dark); + /* Funnel Specific Styles */ + #candidate_funnel_chart { + max-height: 400px; + width: 100%; + margin: 0 auto; } @@ -126,66 +123,60 @@

    {% trans "Recruitment Analytics" %}

    {# -------------------------------------------------------------------------- #} - {# STATS CARDS SECTION #} + {# JOB FILTER SECTION #} {# -------------------------------------------------------------------------- #} -
    - -
    -
    -

    {% trans "Total Jobs" %}

    -
    -
    {{ total_jobs }}
    -
    {% trans "Active & Drafted Positions" %}
    -
    - -
    -
    -

    {% trans "Total Candidates" %}

    -
    -
    {{ total_candidates }}
    -
    {% trans "All Profiles in ATS" %}
    -
    - -
    -
    -

    {% trans "Avg. Apps per Job" %}

    -
    -
    {{ average_applications|floatformat:1 }}
    -
    {% trans "Efficiency Metric" %}
    -
    - -
    -
    -

    {% trans "Avg. Match Score" %}

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

    {% trans "High Potential" %}

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

    {% trans "Scored Profiles" %}

    -
    -
    {{ scored_ratio|floatformat:1 }}%
    -
    {% trans "Percent of profiles processed by AI" %}
    +
    +
    +

    + + {% if current_job %} + {% trans "Data Scope: " %} **{{ current_job.title }}** + {% else %} + {% trans "Data Scope: All Jobs" %} + {% endif %} +

    + {# Job Filter Dropdown #} +
    + + +
    + {# -------------------------------------------------------------------------- #} + {# STATS CARDS SECTION (12 KPIs) #} + {# -------------------------------------------------------------------------- #} + {% include 'recruitment/partials/stats_cards.html' %} + + + {# -------------------------------------------------------------------------- #} - {# CHARTS SECTION (Using a row/col layout for structure) #} + {# CHARTS SECTION #} {# -------------------------------------------------------------------------- #}
    - - {# BAR CHART - Application Volume #} + {# AREA CHART - Daily Candidate Applications Trend (Global Chart) #}
    +
    +
    +

    + + {% trans "Daily Candidate Applications Trend" %} +

    +
    +
    + +
    +
    +
    + {# BAR CHART - Application Volume (Global Chart) #} +

    @@ -199,51 +190,56 @@

    - {# HORIZONTAL BAR CHART - Candidate Pipeline Status (NOW FUNNEL EFFECT) #} -
    + {# FUNNEL CHART - Candidate Pipeline Status (Scoped Chart) #} +

    - - {% trans "Candidate Pipeline Status for job: " %} + + {% if current_job %} + {% trans "Pipeline Funnel: " %} **{{ current_job.title }}** + {% else %} + {% trans "Total Pipeline Funnel (All Jobs)" %} + {% endif %}

    - {{my_job}} - - {# Job Filter Dropdown - Consistent with Card Header Layout #} -
    - - -
    -
    - {# Changed ID to reflect the funnel appearance #}
    + + {# GAUGE CHART - Average Time-to-Hire (Avg. Days) #} +
    +
    +
    +

    {% trans "Time-to-Hire Target Check" %}

    +
    +
    +
    + {% include "recruitment/partials/_guage_chart.html" %} +
    +
    +
    +
    + +
    + + + - {% endblock %} \ No newline at end of file diff --git a/templates/recruitment/partials/_guage_chart.html b/templates/recruitment/partials/_guage_chart.html new file mode 100644 index 0000000..d85fdf5 --- /dev/null +++ b/templates/recruitment/partials/_guage_chart.html @@ -0,0 +1,51 @@ +{% load i18n %} + + +{# Use variables directly from the context #} +
    + {{ avg_time_to_hire_days|default:0 }} {% trans "Days" %} +
    + +
    +
    +
    + + {# Inject the final, calculated degrees directly into the style attribute #} +
    +
    +
    +
    + +
    + {% trans "Target:" %} **{{ gauge_target_days }}** {% trans "Days" %} | {% trans "Max Scale:" %} {{ gauge_max_days }} {% trans "Days" %} +
    \ No newline at end of file diff --git a/templates/recruitment/partials/stats_cards.html b/templates/recruitment/partials/stats_cards.html new file mode 100644 index 0000000..5ce4227 --- /dev/null +++ b/templates/recruitment/partials/stats_cards.html @@ -0,0 +1,112 @@ +{%load i18n %} +{# -------------------------------------------------------------------------- #} + {# STATS CARDS SECTION (12 KPIs) #} + {# -------------------------------------------------------------------------- #} +
    + + {# GLOBAL - 1. Total Jobs (System) #} +
    +
    +

    {% trans "Total Jobs" %}

    +
    +
    {{ total_jobs_global }}
    +
    {% trans "All Active & Drafted Positions (Global)" %}
    +
    + + {# SCOPED - 2. Total Active Jobs #} +
    +
    +

    {% trans "Active Jobs" %}

    +
    +
    {{ total_active_jobs }}
    +
    {% trans "Currently Open Requisitions (Scoped)" %}
    +
    + + {# SCOPED - 3. Total Candidates #} +
    +
    +

    {% trans "Total Candidates" %}

    +
    +
    {{ total_candidates }}
    +
    {% trans "Total Profiles in Current Scope" %}
    +
    + + {# SCOPED - 4. Open Positions #} +
    +
    +

    {% trans "Open Positions" %}

    +
    +
    {{ total_open_positions }}
    +
    {% trans "Total Slots to be Filled (Scoped)" %}
    +
    + + {# GLOBAL - 5. Total Participants #} +
    +
    +

    {% trans "Total Participants" %}

    +
    +
    {{ total_participants }}
    +
    {% trans "Total Recruiters/Interviewers (Global)" %}
    +
    + + {# GLOBAL - 6. Total LinkedIn Posts #} +
    +
    +

    {% trans "LinkedIn Posts" %}

    +
    +
    {{ total_jobs_posted_linkedin }}
    +
    {% trans "Total Job Posts Sent to LinkedIn (Global)" %}
    +
    + +
    +
    +

    {% trans "New Apps (7 Days)" %}

    +
    +
    {{ new_candidates_7days }}
    +
    {% trans "Incoming applications last week" %}
    +
    + +
    +
    +

    {% trans "Avg. Apps per Job" %}

    +
    +
    {{ average_applications|floatformat:1 }}
    +
    {% trans "Average Applications per Job (Scoped)" %}
    +
    + + {# --- Efficiency & Quality Metrics --- #} + +
    +
    +

    {% trans "Time-to-Hire" %}

    +
    +
    {{ avg_time_to_hire_days }}
    +
    {% trans "Avg. Days (Application to Hired)" %}
    +
    + +
    +
    +

    {% trans "Avg. Match Score" %}

    +
    +
    {{ avg_match_score|floatformat:1 }}
    +
    {% trans "Average AI Score (Current Scope)" %}
    +
    + +
    +
    +

    {% trans "High Potential" %}

    +
    +
    {{ high_potential_count }}
    +
    {% trans "Score ≥ 75% Profiles" %} ({{ high_potential_ratio|floatformat:1 }}%)
    +
    + +
    +
    +

    {% trans "Meetings This Week" %}

    +
    +
    {{ meetings_scheduled_this_week }}
    +
    {% trans "Scheduled Interviews (Current Week)" %}
    +
    + +
    + -- 2.39.5