From b7d0dc8257535418ecfdac6b16a5799a542d3e9a Mon Sep 17 00:00:00 2001 From: Faheed Date: Sun, 26 Oct 2025 18:27:09 +0300 Subject: [PATCH 01/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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 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 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/10] 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/10] 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