From 916cbc4fcf725b09d805bf5677259f4ff4586faa Mon Sep 17 00:00:00 2001 From: ismail Date: Sun, 12 Oct 2025 13:30:16 +0300 Subject: [PATCH] more update and add qcluster --- .../__pycache__/settings.cpython-313.pyc | Bin 5232 -> 5654 bytes NorahUniversity/settings.py | 226 +----- recruitment/__pycache__/admin.cpython-313.pyc | Bin 11440 -> 11458 bytes .../__pycache__/models.cpython-313.pyc | Bin 44705 -> 45244 bytes .../__pycache__/signals.cpython-313.pyc | Bin 11075 -> 6443 bytes recruitment/__pycache__/urls.cpython-313.pyc | Bin 5754 -> 6220 bytes recruitment/__pycache__/utils.cpython-313.pyc | Bin 17282 -> 17282 bytes recruitment/__pycache__/views.cpython-313.pyc | Bin 36759 -> 43206 bytes .../views_frontend.cpython-313.pyc | Bin 19821 -> 19780 bytes recruitment/admin.py | 2 +- .../0004_candidate_applicant_status.py | 18 + recruitment/models.py | 13 +- recruitment/signals.py | 137 +--- recruitment/tasks.py | 155 ++++ .../__pycache__/form_filters.cpython-313.pyc | Bin 2515 -> 3046 bytes recruitment/templatetags/form_filters.py | 82 +- recruitment/urls.py | 9 +- recruitment/views.py | 163 +++- recruitment/views_frontend.py | 2 +- templates/forms/form_submission_details.html | 147 ++-- .../forms/form_template_all_submissions.html | 371 +++++++++ .../forms/form_template_submissions_list.html | 17 +- templates/forms/form_wizard.html | 21 +- templates/includes/candidate_modal_body.html | 21 + templates/jobs/job_detail.html | 55 +- .../candidate_tier_management.html | 724 ++++++++++++++++++ 26 files changed, 1719 insertions(+), 444 deletions(-) create mode 100644 recruitment/migrations/0004_candidate_applicant_status.py create mode 100644 recruitment/tasks.py create mode 100644 templates/forms/form_template_all_submissions.html create mode 100644 templates/includes/candidate_modal_body.html create mode 100644 templates/recruitment/candidate_tier_management.html diff --git a/NorahUniversity/__pycache__/settings.cpython-313.pyc b/NorahUniversity/__pycache__/settings.cpython-313.pyc index b6f4ada70ad7bcd970a163cc9981a567d5a0e7a9..48f3c69beda3231ee409a1985d04f28aa7fe401a 100644 GIT binary patch delta 588 zcmXv~zi-n(6n4&b9RJD>Hz^5Cr7oxhp@?)rf()cdADWftCr_uwXMQSUzec~A!M zA>CVL8Q<)IQrmw6KEz@L{}RUoi6pvDjA75#5Bw|fZ$LQ}D{I%w*XXK*-rvM&EbmRl z!}tm<|Dx$t;#sdHXZ`omh&>|eX05i{)a$KXxU;bxax}*L6JwaLO1?`dx zyht&=K#vkjxW4VUgd5q|C7`=!ewArzn$8%4^8mp$Eo6^f!eiv(2~jNLxo)^FvPP~G zMlzL-0A7zM8xa768`#4h;hjF^EXjl$8WtkLp^cv+><|H?&ZILyXDKAnv8@qCjzc8J zc!uDq%nhsHFtsT8Co{tSnRbNPcWMv|@ z=Tdl|GEjO)`=Vkn&Sy{2wuB`Oyi87S_#KT0kAE<^sl=$oAe9bMT9C>F>1?3of`!E( zn+w$Z&-7CCl1$A6Hn}iWn0ztF6@udOOyDw7gfB*bXEkZ2O1b!KxuAkqB7RHj{R8%@ BuC4$8 delta 149 zcmbQH^Fc%XGcPX}0}wbIex4C7&cN^(#DM`eDC2Y7M)i0mmUJdfhs}qWW-&5KY>s7N zW!jv>wx5Zoiqp`@T+aXq4KpX3bN*r!oovKa&M3Qi71vy5CQa+je*7z#Co>8Ma}@)P jV+7*jn8|U%0ql}OObvV=%rAj#G>NK;W9OB zOMnWCmTtZ&^PG`!>*RxS&lz`2UMcUuxNY()`D5H$L9#(0V%ua}DT&FK6~q|xHosEn zVPxDtxm+ofZ7)b}@8nBL^{gOf@(#>mcU!$u~7}7_U!u)BGX;b{g0ou+i5idub^%KHr?D<-`O4 Dyxd~@ delta 307 zcmX>UxgnDGGcPX}0}#w;d!Dg(Bd?+iBl~218Li3LGF5Dgf#O9=Houm6&d9a}$i5{v znNdM}@_V_{jN2#gmUm#>I{Aa7lQ4omOI{B=E9%J<8KMFmJjQb{cDuuG`0g3OK z{6wjq6~s)QC@4C)R(TKGF_8H2$@VIW0uUyMe*#3DoLr!CgzX53d1SJc>NLh9n=h*R zFtS|&iCmhjtX|4^Y4Sx?$;q44mvi3&3Elt^Hz%iPnCO9-w?TZ6OqHl(K|xMta$;VI zf@fY@eo<~>NoIbYCR5Q(kdix-_bbV>T>~+%O@6JB!+33Sgyt6ku#>^|gRQ(aIYvvF L@!94EEhi=bn@wij diff --git a/recruitment/__pycache__/models.cpython-313.pyc b/recruitment/__pycache__/models.cpython-313.pyc index 7de510d782a45d32a2b42e26ea8b9d65c33c7315..1236c5d99bd4bc5078e1e9a0d396dc65e516cfef 100644 GIT binary patch delta 4300 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 Z#^&S8LYpOs?ef2_px^6D@x$Y<{~Jn3V&VV* delta 3906 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 diff --git a/recruitment/__pycache__/signals.cpython-313.pyc b/recruitment/__pycache__/signals.cpython-313.pyc index 530e3d0bd289545b8e713b9a9b34ac4766373eb8..3a9c25cfbdb9f6604c5e7da686b8bec2893ae361 100644 GIT binary patch delta 1084 zcmaJ7fK6rNeH*ZX4|NbIC_DRE2)TBP~_iUd^~wN&blH?mr+Rv;oR!|t%z#@-EY z)>P#Z4pm!D6s;k}LBJtX!{|z!DJ#n$ zv$jRJS%y?u@D%r{1t;h6GA>d)y6L@Odyb>A>KxDgFLgwA6YZ_f2vn+ zSvReF6Q}Nc{D5Z!FVWzPp z9)unk>CdhDq>d&Vhftn0CdIfEwe)W5r|CMvtezLZ_4ySx?Bm2FEZ_n@Law&{-O+Q} z+3~C-i-}dw4F5|O1##QI45Paa?CYUeVTrc{vUpiFx4KvJ20}4?{lt+~O;Xw*_IY1` zObg_2fUGdcVatqwCkuJkbk~irDy|C$BrQ8J%IFsYlM;_I*WbazhJ8K{pP^08oVbKy zyFe*@LaLi&dXvm-l4F~s{STS%J|OeYjDwGi#(kr)cKU%ab3^-=8sFP%pKQ?C9i>Fg f-2zdLZ%ec@MidjNg$-)&D6UGYk~@*ZQ40S74=oN- literal 11075 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 diff --git a/recruitment/__pycache__/urls.cpython-313.pyc b/recruitment/__pycache__/urls.cpython-313.pyc index cce2bd915df6eb42c6a9c9e3e2cc673b37572f92..f3436fb6ed11b4a575bad585a1b79dcb9f954391 100644 GIT binary patch delta 1100 zcmZvaOH30{6ozNY@KUJs6$q;wC(0lKQ6vJft=g#@R|I9hf{Iq>=YJW5~ zo+0360N!~SNZf6#<4sSM@ON=1#I~JMr61T(%?0~?r zeAZEJ{a2w;g-$VPCc&oIw2<#x7E%u!UM?++tlX)H(u+JNusp}m_c->xqijBIal)Y; zs6pO&E5oPY6sV8DrdE>h?KHRKcVu%5DK4AkGU>lFiUBPb$-7kk2t6gpB$ES`;c^Fx z&_@Xx`gDaq+3(oh)!H?#a_8}+|1FDG;hpdw7L#0tb1(d3wxT@16@1J*X!FmFxG~#^IIm_;sF+qq>i29MdCi4(mgG4?J(`duwpUD(Vca;wOC#EPm1VLTxZ0bs2hxG2p~a`?j^RH2|yu zY4RqZ7{!IXLG0O!TG<%^&WIEj0F_hRLGhUZEw+O@eZ=|;YnQPP)&oZLh9o|VSl?p( zZ0vyb-$pZ3v#Aa0MbibSw5b#78rDs$?@Wl8~#jYKTVy@zxef-*o*Xmh87u2jueXK@&hs% zfw)+8@;)IkNe@PTrkgAr9WoQ@u8W&o6gSyneUZie1GC5Ew?ayc9+L%xB~3wUKJqZI za$ew6UJ$xGZeiRG9greku!I0uLS{zlbveC@a(WwLF0wd%5cHTFCoB!rUnMMU305Wr zRwlnd^tz(yMMcvKHa-_wd_PEo%0!-F|$weY65E~4@rl_spyRPAWQN#U$U&2L}#1DpG8;rnmdK>tz8-!jo z2)z(fa*?IbXr_si=kcW-Lu;>LMGBtvup~Xyjn@=y*YhW@ng~1qW`{bCrQ4C z92*MA-=W(#)RP}Z2RAgx$@kHjEmqy{fb|LnmxO&Ey|%@o_y8#HqyOGAR{bKpet{vC zbHL98e4fzQ2*Z8})IS0I0pJsWtLUNjtyuoA+uJqo1ECk-W8~br8S6f{wN@c0`T+gI zRy+A3dU>l|a}_wg0{CC_?$(uLPPAy-o$|W{Th#1((D6qA(4VDK7gdP6J1IabDOOx#l7CZD8sRW2U6&eh?|GKY~tc`xO?~2L>Ql2M;DM~5Cv}5lDpC#;j=%ayBvjJ;J$pu4QQg#Jy zv4*H~P(hM^MlSz|gdow={>QNQ)d%f9!KO(VZn!b-FfyeQHTZpOGc@(y{?Mp5;K@)y z8M_i-HNYBxR)9MI)&jJl7Y;U%9nt?f_;2!}MojriSIdp|4f#VM#^>>dSPrXaEog3} zQCwUl=*y9&CL4?uCNrH87k~{iGo=xRbDV!T$gy_dD+VY*dk;60-3T2%S%C8+^QiC} z6phcHMAvE^`MvVR9=8OXf`>xIIVEU{H7nc_TGB$Ixw?Fvbg?%m?af7w92+pGFoUdx z>^7pVjV45ms8JqQP);kG(;9=aUXv)3Aly<~m7&Ys1o~}`88!Ch>(m*^3b!1ct29U4 ziVR8F3tkJ4o1k;t61UPVqcwHXW^e*OrG!fxUbE59jRd@7ls6Edvf@GEq@uilLsW5$wr!>0gEi9FmZw~N4PR8 zaTld~umzqh3&d2G3RA_FfLvIduH3A00*w+WF3S-k$MoUnq#&!i26krYlb9%3`{*nL%Dx#_QS=x{jEx zWBy27cTYmMKc?Hydx!bMN8`G&@l9VS(8a?m3zuKEP)Yn(V7sQ2sdGM8sfEy$`;>t-G)snW$0Rk8J)Z@ zUr5h$#(Z>{&OwcwnKUAjF`*t#OXi^iTnU+v{)XFQgKWUnBeTAQ`4VT|fd0yrDum*@ z2~~!wR>ArcYl={oH{qcvy_hj9!;Rnt$#5=B)>I3#{4x4|$hr}jSvSBjkRp^GVS%f& zO3#uNLgmV6VRys87txuWGmuV6Xj1fB|mBQsZ5jLv7*&dOrOyQ1zU;Ke;Yq=?g!{YFF&}d^p?{5fG4e@;2{^@z;v})N@_v7usR6n7e2rZKoes3 z5<2pbT>*Ar=}$e>Keb*5{GeZtSs-g8Qd+iHH8WW)!zEC@`U!&Y%7wbBaLZ&w zCs|JfalWT}@Iu+Of>xyIie95A7xQO|YnszZ5KC2>R$kA!LL5e`YOx24h6I&v1+8!^ zWkgW5YXFo4)txe|UWNau#nNt|)vI?A#AcZnZx3+cCeW!4vv~~`=iQ1GrrpHm2p!;R z{!Lv)R~@%iYuvJ2g4Q-k7cHQOUi+d$w9ZY^WQ(MfpmRlOIf9zEvxV#Qndqf$0hQH; z3{{g=WQrF>?>bK#agouwwcQR}EzAXr?641n$J>FGXwf>xYzXRhy%Jc8vi9+#W^`v* zg+6hVN%Jtzg%xHBJ7IF`Fq++(T%t*_NG+$&^2+V?P+bu@CN*821 z9Q$^Mb%l!_1k+i`~dK9Da40;|?%f}+4Ze-6NhN_gDY z#iB@b4?XP6Rct0ZzZAVTS+in6cD_y9vOPP$=$WLu9<~}fd&XeHQUbLUXQ&!l9yGcQ zCSuD*f(Yh(vs2>SB8NBM=)#uYY!;0a;WpCBS{;^V+_8xuH0TeGaz~!qh^M9mCX=R` zktVX-0zpd1IV5xWcvKrqBE7~;dVUGcR204EScHTMc43-tv)(&|?{evE^f0$)b8W_;% z=^q{qhCcM+eijFg=wNdr_~%1-XP?H9(vFURe2*YMTy{4eg|&v5fngS(hd@3ha`@^f zp|f!{71k^n#-)~Jj1+`;h6K}xm|!R)TrfGM6&NFeI=?Ta4l+kPLJypnz##Y-hE;CC zARiT0PzTuIAu24RLc>%zZ-X}&91dkUPNnkF#xqB~Lm(HHkZCe6Cs= zmC6Oah$rKBSf3R~!AZrU#33(x$m8XfMyJ&4Mn(dDEDfI;AWq61n1#V%(otTH!nTWv z5-y?qI8OpBgoNmlj@vvkUH_W*1;C&asT7dg7q@;cv`K7ZDixs4#L*Aq8 zKLQJ^B^JI%;=PRx-e8(iMy%4av{*f%$gps{6EB{G+YNl?BG`w);735Ha)1d1{9Gty z!1p@hjHh#({SV+g1^`DR@lbAN_rv=I0AV*N*e&>vK%ALnhP?iuKR6&ps~=VaUM3J^ z98#zc>=U5;?*YCCFaZ$2kWvrh%QPXQmkeaT4?VhcRY*&C5{7&i+rXJIpx>S_p}%fb zt|-n-;*Ih4-0NqKuv2*0BY3;W{TKs2K&S|f4gGV2Gjc3$TsyuMA1F)(CkIX+I(6vu z@TuXcp&2?}&=SvEJ-+R#f>_ZuXNs0B#ckKAF$$|xC$z{#5$$QT3IkP9Oaa}61PY#?7oCzd~>te-q z(+|w{$BWm;i#AMbyQDQIw6-_3w#j?=${q8E=J)cu8NU3$f_C7N$;#VS&H7@d4ZL;( zmY!>yJaYE_Gxx_My6U9WdA9XTYrP-VRRXDw6rsu_d7xwWhHqLFDZ<(*=-93C{cR%0bw2M_ZDEMLi8G=#tt=hQvV?K!t6u4}xh zBSnt0HD_uPMOCq)s_8r8MJ*HCvYm_E!n?;Je;b)9kLxRwl}!tVvbRmPWL49Gx#Fgt z@iiOgdVaOE5{h_GIntC)Epvs+-1Udh<;Ciyap__@)hWckoqR zZyCBFIJ(DmlLJ4ldp=T!J>^?=@fAI98Ft?=pWyp%?6qlsYh$L`8S7i7)?{Pr$@T?v z-A(;f6UB8`wS>Vwc{r}WF8wyo?3iie*Z1*N{cjn3X(J+&1AK*N!La|TiZB-^Or2wQkD&;D{L<(+b|;;0^W3{E|d|c`Uztx|h!{=kr^VMr*?8j2WHNg}l+p z8yl~w-j|=JCcF-O^Ph(W^wyHd2u%am5Qe{lWggsE^^`8Z zhbzGTC3^VbSqU=-`hN+K0GJ234Db%XhX6TH>5TvsfD@nspbDTCpaY;A0Iu>_xFurY zLP99_?0FP<)SmM%xQ+h^?k~|FJo*lKTOq0VY()u%jU*$%KX=K)B-u@VepD(Ydr864 F{|AGQe`o*z delta 1993 zcmai!du&s66vumR*GnH;hqy7=)^&rycHIbdFmMqzCNfzGlL-u#(sgCM+q#ree(jz? zF>z!2!(@;nQI{ArAjStMUR@lbyy8%!CJ@A=7w7ejQ2(#HRV=chB#8 z9=~($?+zZ5UfwQQM-vjvI{xb!9BK6I*lL{wp}jfpNKk^urNOucP!?!=^tbas!96p3 zVE~IAj^qKZXZr}B67~~5!xtR6a6o&{@hZR(t*qdbfj>=otQg>=<}K=%rkvoisL|UT zY4kL+Kv>ToGcz;ULh?Q;pcT#QlVA`}6n_riAePLroFY3pvg3H7Bo9u1xS$Q-3v4ef zln;^lFn&~;1821#ORHeg5Vu6*g5Gs1OXEu=j@6Rp0y-DYgo|3oLWco{@!iE~c^Alj zo-`60ruAzaQFDl?KDFK-RDEUKd4x8G@!I0{ynb4a3QfIV4fs5f_K?c{AnTumUkSev zuHd$ECtScY<>eL;*+te%INSLY4B-K+#uY<-<@;7q~$MJ zU|1o_Gch_8yH23|cjXjrSIKsb@CyeoTF6CWGO)`WD=HLu%#H__Wo^1dN{KC}eLABu zk)_f)m#~vS3RX-Y|ETN@h18%g7UM56->;O|320X|Lewyh9xcETPuNY-vgM}rYdwCS zGHz@{@Tt#}e3aQFS#R_x9>k7{PtjIgq8Ds}4c(1d=8Z{_B+m8(i5i8DDheBuIRVx2Kj; zOvZz*X)s+Ic9{Uq z?)LZc6U}e3EfJ|-A@}k3_L4lK##jcwkIkmXL~mWB*&FcuYo%nYkWhrLH`(xTQ$8%! zt~7mRNOf>)N5YJ7^V)U(NJRB{yb%_r1>1yT)&|Zmf@QlwqQz2vJ?c|S}+MWu1T}&U0e|Rx#T;m)l-*CMfzae zNDn(GgC@z9-Fk!^$|kW=ld_D1+N3qOp~qq&5kQTH_%J%UbP_g7vLy z(#P(*Xqu>Byh`HgCgM-6*|K=`7NN22k$GZYbYHxbi+EMh#Bjvhpn8}Z4)OD-VI}_F zdy~&A=6BuSmiDypu!hxuS{LCi0Wa?%dye)+e$g+}T|+CO#@r_>TR|KB_+^{JyoxX1 z()0Up7&p07;;+lP2~&3#f3WV@L#s4b}UbzrI+@naYXe2H7MRXrlSpNs$z@v z(@F;V)G(FcD h0LIqIr?jG2I2d?@CYxv*u`@9oVq$UPSe&b^1OVAb7VrQ7 delta 110 zcmX>yi}CF&M&8f7yj%=GuxZEhjJ}P$xf+amldCm8@U!SMnlgeU7#Q+cqNOJ%X=*cu zOzzW6XXKmwO*4&!4Ja`=S}Txo;^ehjQ37lXd@2jV7I<9d)opOQ!!0 Li<8J=7i}c~&T<|4 diff --git a/recruitment/admin.py b/recruitment/admin.py index 92200c8..bfcf6cc 100644 --- a/recruitment/admin.py +++ b/recruitment/admin.py @@ -143,7 +143,7 @@ class JobPostingAdmin(admin.ModelAdmin): @admin.register(Candidate) class CandidateAdmin(admin.ModelAdmin): - list_display = ['full_name', 'job', 'email', 'phone', 'stage', 'applied', 'created_at'] + list_display = ['full_name', 'job', 'email', 'phone', 'stage', 'applied','is_resume_parsed', 'created_at'] list_filter = ['stage', 'applied', 'created_at', 'job__department'] search_fields = ['first_name', 'last_name', 'email', 'phone'] readonly_fields = ['slug', 'created_at', 'updated_at'] diff --git a/recruitment/migrations/0004_candidate_applicant_status.py b/recruitment/migrations/0004_candidate_applicant_status.py new file mode 100644 index 0000000..866ba2e --- /dev/null +++ b/recruitment/migrations/0004_candidate_applicant_status.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.6 on 2025-10-11 14:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0003_candidate_is_resume_parsed_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='candidate', + name='applicant_status', + field=models.CharField(blank=True, choices=[('Applicant', 'Applicant'), ('Candidate', 'Candidate')], default='Applicant', max_length=100, null=True, verbose_name='Applicant Status'), + ), + ] diff --git a/recruitment/models.py b/recruitment/models.py index d470e25..a6df23c 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -267,6 +267,10 @@ class Candidate(Base): ACCEPTED = "Accepted", _("Accepted") REJECTED = "Rejected", _("Rejected") + class ApplicantType(models.TextChoices): + APPLICANT = "Applicant", _("Applicant") + CANDIDATE = "Candidate", _("Candidate") + # Stage transition validation constants STAGE_SEQUENCE = { "Applied": ["Exam", "Interview", "Offer"], @@ -298,7 +302,14 @@ class Candidate(Base): choices=Stage.choices, verbose_name=_("Stage"), ) - + applicant_status = models.CharField( + choices=ApplicantType.choices, + default="Applicant", + max_length=100, + null=True, + blank=True, + verbose_name=_("Applicant Status"), + ) exam_date = models.DateField(null=True, blank=True, verbose_name=_("Exam Date")) exam_status = models.CharField( choices=ExamStatus.choices, diff --git a/recruitment/signals.py b/recruitment/signals.py index 2c6edb1..2649cb0 100644 --- a/recruitment/signals.py +++ b/recruitment/signals.py @@ -1,136 +1,21 @@ -from . import models -from django.urls import reverse +import logging from django.db import transaction from django.dispatch import receiver +from django_q.tasks import async_task from django.db.models.signals import post_save -from .models import FormField,FormStage,FormTemplate +from .models import FormField,FormStage,FormTemplate,Candidate -# @receiver(post_save, sender=models.Candidate) -# def parse_resume(sender, instance, created, **kwargs): -# if instance.resume and not instance.summary: -# from .utils import extract_summary_from_pdf,match_resume_with_job_description -# summary = extract_summary_from_pdf(instance.resume.path) -# if 'error' not in summary: -# instance.summary = summary -# instance.save() - - # match_resume_with_job_description - -import logging logger = logging.getLogger(__name__) -import os -from .utils import extract_text_from_pdf,score_resume_with_openrouter -import asyncio -@receiver(post_save, sender=models.Candidate) +@receiver(post_save, sender=Candidate) def score_candidate_resume(sender, instance, created, **kwargs): - if instance.is_resume_parsed: - return - try: - # Get absolute file path - file_path = instance.resume.path - if not os.path.exists(file_path): - logger.warning(f"Resume file not found: {file_path}") - return - - resume_text = extract_text_from_pdf(file_path) - # if not resume_text: - # instance.scoring_error = "Could not extract text from resume." - # instance.save(update_fields=['scoring_error']) - # return - job_detail=str(instance.job.description)+str(instance.job.qualifications) - prompt1 = f""" - You are an expert resume parser and summarizer. Given a resume in plain text format, extract and organize the following key-value information into a clean, valid JSON object: - - full_name: Full name of the candidate - current_title: Most recent or current job title - location: City and state (or country if outside the U.S.) - contact: Phone number and email (as a single string or separate fields) - linkedin: LinkedIn profile URL (if present) - github: GitHub or portfolio URL (if present) - summary: Brief professional profile or summary (1–2 sentences) - education: List of degrees, each with: - institution - degree - year - gpa (if provided) - relevant_courses (as a list, if mentioned) - skills: Grouped by category if possible (e.g., programming, big data, visualization), otherwise as a flat list of technologies/tools - experience: List of roles, each with: - company - job_title - location - start_date and end_date (or "Present" if applicable) - key_achievements (as a list of concise bullet points) - projects: List of notable projects (if clearly labeled), each with: - name - year - technologies_used - brief_description - Instructions: - - Be concise but preserve key details. - Normalize formatting (e.g., “Jun. 2014” → “2014-06”). - Omit redundant or promotional language. - If a section is missing, omit the key or set it to null/empty list as appropriate. - Output only valid JSON—no markdown, no extra text. - Now, process the following resume text: - {resume_text} - """ - result = score_resume_with_openrouter(prompt1) - prompt = f""" - You are an expert technical recruiter. Your task is to score the following candidate for the role of a Senior Data Analyst based on the provided job criteria. - - **Job Criteria:** - {job_detail} - - **Candidate's Extracted Resume Json:** - \"\"\" - {result} - \"\"\" - - **Your Task:** - Provide a response in strict JSON format with the following keys: - 1. 'match_score': A score from 0 to 100 representing how well the candidate fits the role. - 2. 'strengths': A brief summary of why the candidate is a strong fit, referencing specific criteria. - 3. 'weaknesses': A brief summary of where the candidate falls short or what criteria are missing. - 4. 'criteria_checklist': An object where you rate the candidate's match for each specific criterion (e.g., {{'Python': 'Met', 'AWS': 'Not Mentioned'}}). - - - Only output valid JSON. Do not include any other text. - """ - - result1 = score_resume_with_openrouter(prompt) - - instance.parsed_summary = str(result) - - # Update candidate with scoring results - instance.match_score = result1.get('match_score') - instance.strengths = result1.get('strengths', '') - instance.weaknesses = result1.get('weaknesses', '') - instance.criteria_checklist = result1.get('criteria_checklist', {}) - - instance.is_resume_parsed = True - - # Save only scoring-related fields to avoid recursion - instance.save(update_fields=[ - 'match_score', 'strengths', 'weaknesses', - 'criteria_checklist','parsed_summary', 'is_resume_parsed' - ]) - - logger.info(f"Successfully scored resume for candidate {instance.id}") - - except Exception as e: - # error_msg = str(e)[:500] # Truncate to fit TextField - # instance.scoring_error = error_msg - # instance.save(update_fields=['scoring_error']) - logger.error(f"Failed to score resume for candidate {instance.id}: {e}") - - -# @receiver(post_save,sender=models.Candidate) -# def trigger_scoring(sender,intance,created,**kwargs): - - + if not instance.is_resume_parsed: + logger.info(f"Scoring resume for candidate {instance.pk}") + async_task( + 'recruitment.tasks.handle_reume_parsing_and_scoring', + instance.pk, + # hook='myapp.tasks.email_sent_callback' # Optional callback + ) @receiver(post_save, sender=FormTemplate) def create_default_stages(sender, instance, created, **kwargs): diff --git a/recruitment/tasks.py b/recruitment/tasks.py new file mode 100644 index 0000000..abfa760 --- /dev/null +++ b/recruitment/tasks.py @@ -0,0 +1,155 @@ +import os +import json +import logging +import requests +from PyPDF2 import PdfReader +from recruitment.models import Candidate + +logger = logging.getLogger(__name__) + +OPENROUTER_API_KEY ='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' +OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free' + +if not OPENROUTER_API_KEY: + logger.warning("OPENROUTER_API_KEY not set. Resume scoring will be skipped.") + +def extract_text_from_pdf(file_path): + print("text extraction") + text = "" + try: + with open(file_path, "rb") as f: + reader = PdfReader(f) + for page in reader.pages: + text += (page.extract_text() or "") + except Exception as e: + logger.error(f"PDF extraction failed: {e}") + raise + return text.strip() + +def ai_handler(prompt): + print("model call") + response = requests.post( + url="https://openrouter.ai/api/v1/chat/completions", + headers={ + "Authorization": f"Bearer {OPENROUTER_API_KEY}", + "Content-Type": "application/json", + }, + data=json.dumps({ + "model": OPENROUTER_MODEL, + "messages": [{"role": "user", "content": prompt}], + }, + ) + ) + res = {} + print(response.status_code) + if response.status_code == 200: + res = response.json() + content = res["choices"][0]['message']['content'] + try: + + content = content.replace("```json","").replace("```","") + + res = json.loads(content) + + except Exception as e: + print(e) + + # res = raw_output["choices"][0]["message"]["content"] + else: + print("error response") + return res + +def handle_reume_parsing_and_scoring(pk): + logger.info(f"Scoring resume for candidate {pk}") + try: + instance = Candidate.objects.get(pk=pk) + file_path = instance.resume.path + if not os.path.exists(file_path): + logger.warning(f"Resume file not found: {file_path}") + return + + resume_text = extract_text_from_pdf(file_path) + job_detail= f"{instance.job.description} {instance.job.qualifications}" + resume_parser_prompt = f""" + You are an expert resume parser and summarizer. Given a resume in plain text format, extract and organize the following key-value information into a clean, valid JSON object: + + full_name: Full name of the candidate + current_title: Most recent or current job title + location: City and state (or country if outside the U.S.) + contact: Phone number and email (as a single string or separate fields) + linkedin: LinkedIn profile URL (if present) + github: GitHub or portfolio URL (if present) + summary: Brief professional profile or summary (1–2 sentences) + education: List of degrees, each with: + institution + degree + year + gpa (if provided) + relevant_courses (as a list, if mentioned) + skills: Grouped by category if possible (e.g., programming, big data, visualization), otherwise as a flat list of technologies/tools + experience: List of roles, each with: + company + job_title + location + start_date and end_date (or "Present" if applicable) + key_achievements (as a list of concise bullet points) + projects: List of notable projects (if clearly labeled), each with: + name + year + technologies_used + brief_description + Instructions: + + Be concise but preserve key details. + Normalize formatting (e.g., “Jun. 2014” → “2014-06”). + Omit redundant or promotional language. + If a section is missing, omit the key or set it to null/empty list as appropriate. + Output only valid JSON—no markdown, no extra text. + Now, process the following resume text: + {resume_text} + """ + resume_parser_result = ai_handler(resume_parser_prompt) + resume_scoring_prompt = f""" + You are an expert technical recruiter. Your task is to score the following candidate for the role of a Senior Data Analyst based on the provided job criteria. + + **Job Criteria:** + {job_detail} + + **Candidate's Extracted Resume Json:** + \"\"\" + {resume_parser_result} + \"\"\" + + **Your Task:** + Provide a response in strict JSON format with the following keys: + 1. 'match_score': A score from 0 to 100 representing how well the candidate fits the role. + 2. 'strengths': A brief summary of why the candidate is a strong fit, referencing specific criteria. + 3. 'weaknesses': A brief summary of where the candidate falls short or what criteria are missing. + 4. 'criteria_checklist': An object where you rate the candidate's match for each specific criterion (e.g., {{'Python': 'Met', 'AWS': 'Not Mentioned'}}). + + + Only output valid JSON. Do not include any other text. + """ + + resume_scoring_result = ai_handler(resume_scoring_prompt) + + instance.parsed_summary = str(resume_parser_result) + + # Update candidate with scoring results + instance.match_score = resume_scoring_result.get('match_score') + instance.strengths = resume_scoring_result.get('strengths', '') + instance.weaknesses = resume_scoring_result.get('weaknesses', '') + instance.criteria_checklist = resume_scoring_result.get('criteria_checklist', {}) + + instance.is_resume_parsed = True + + # Save only scoring-related fields to avoid recursion + instance.save(update_fields=[ + 'match_score', 'strengths', 'weaknesses', + 'criteria_checklist','parsed_summary', 'is_resume_parsed' + ]) + + logger.info(f"Successfully scored resume for candidate {instance.id}") + + except Exception as e: + logger.error(f"Failed to score resume for candidate {instance.id}: {e}") diff --git a/recruitment/templatetags/__pycache__/form_filters.cpython-313.pyc b/recruitment/templatetags/__pycache__/form_filters.cpython-313.pyc index a5b4be1d95a3ea5aa300882d7da19d8b2edd9430..c7fbf5f7ce4beb6f0b1aa2def5dbee529bd028cd 100644 GIT binary patch literal 3046 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/upload_image_simple/', views.job_image_upload, name='job_image_upload'), + path('job//upload_image_simple/', views.job_image_upload, name='job_image_upload'), path('jobs//update/', views.edit_job, name='job_update'), # path('jobs//delete/', views., name='job_delete'), path('jobs//', views.job_detail, name='job_detail'), @@ -32,7 +32,7 @@ urlpatterns = [ path('candidates//delete/', views_frontend.CandidateDeleteView.as_view(), name='candidate_delete'), path('candidate//view/', views_frontend.candidate_detail, name='candidate_detail'), path('candidate//update-stage/', views_frontend.candidate_update_stage, name='candidate_update_stage'), - + # Training URLs path('training/', views_frontend.TrainingListView.as_view(), name='training_list'), @@ -64,11 +64,14 @@ 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//candidate-tiers/', views.candidate_tier_management_view, name='candidate_tier_management'), + path('htmx//candidate_criteria_view/', views.candidate_criteria_view_htmx, name='candidate_criteria_view_htmx'), # path('forms/form//submit/', views.submit_form, name='submit_form'), # path('forms/form//', views.form_wizard_view, name='form_wizard'), - path('forms//submissions//', views.form_submission_details, name='form_submission_details'), + path('forms//submissions//', views.form_submission_details, name='form_submission_details'), path('forms/template//submissions/', views.form_template_submissions_list, name='form_template_submissions_list'), + path('forms/template//all-submissions/', views.form_template_all_submissions, name='form_template_all_submissions'), # path('forms//', views.form_preview, name='form_preview'), # path('forms//submit/', views.form_submit, name='form_submit'), diff --git a/recruitment/views.py b/recruitment/views.py index d83b636..a4da922 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -991,13 +991,38 @@ def form_template_submissions_list(request, slug): ) -def form_submission_details(request, template_id, submission_id): +def form_template_all_submissions(request, template_id): + """Display all submissions for a form template in table format""" + template = get_object_or_404(FormTemplate, id=template_id) + print(template) + # Get all submissions for this template + submissions = FormSubmission.objects.filter(template=template).order_by("-submitted_at") + + # Get all fields for this template, ordered by stage and field order + fields = FormField.objects.filter(stage__template=template).select_related('stage').order_by('stage__order', 'order') + + # Pagination + paginator = Paginator(submissions, 10) # Show 10 submissions per page + page_number = request.GET.get("page") + page_obj = paginator.get_page(page_number) + + return render( + request, + "forms/form_template_all_submissions.html", + { + "template": template, + "page_obj": page_obj, + "fields": fields, + }, + ) + + +def form_submission_details(request, template_id, slug): """Display detailed view of a specific form submission""" # Get the form template and verify ownership - template = get_object_or_404(FormTemplate, id=template_id, created_by=request.user) - + template = get_object_or_404(FormTemplate, id=template_id) # Get the specific submission - submission = get_object_or_404(FormSubmission, id=submission_id, template=template) + submission = get_object_or_404(FormSubmission, slug=slug, template=template) # Get all stages with their fields stages = template.stages.prefetch_related("fields").order_by("order") @@ -1192,3 +1217,133 @@ def schedule_interviews_view(request, job_id): "interviews/schedule_interviews.html", {"form": form, "break_formset": break_formset, "job": job}, ) + + +def candidate_tier_management_view(request, slug): + """ + Manage candidate tiers and stage transitions + """ + job = get_object_or_404(JobPosting, slug=slug) + + # Get all candidates for this job, ordered by match score (descending) + candidates = job.candidates.all().order_by("-match_score") + + # Get tier categorization parameters + tier1_count = int(request.GET.get("tier1_count", 100)) + + # Categorize candidates into tiers + tier1_candidates = candidates[:tier1_count] if tier1_count > 0 else [] + remaining_candidates = candidates[tier1_count:] if tier1_count > 0 else [] + + if len(remaining_candidates) > 0: + # Tier 2: Next 50% of remaining candidates + tier2_count = max(1, len(remaining_candidates) // 2) + tier2_candidates = remaining_candidates[:tier2_count] + tier3_candidates = remaining_candidates[tier2_count:] + else: + tier2_candidates = [] + tier3_candidates = [] + + # Handle form submissions + if request.method == "POST": + # Update tier categorization + if "update_tiers" in request.POST: + tier1_count = int(request.POST.get("tier1_count", 100)) + messages.success(request, f"Tier categorization updated. Tier 1: {tier1_count} candidates") + return redirect("candidate_tier_management", slug=slug) + + # Update individual candidate stages + elif "update_stage" in request.POST: + candidate_id = request.POST.get("candidate_id") + new_stage = request.POST.get("new_stage") + candidate = get_object_or_404(Candidate, id=candidate_id, job=job) + + if candidate.can_transition_to(new_stage): + old_stage = candidate.stage + candidate.stage = new_stage + candidate.save() + messages.success(request, f"Updated {candidate.name} from {old_stage} to {new_stage}") + else: + messages.error(request, f"Cannot transition {candidate.name} from {candidate.stage} to {new_stage}") + + # Update exam status + elif "update_exam_status" in request.POST: + candidate_id = request.POST.get("candidate_id") + exam_status = request.POST.get("exam_status") + exam_date = request.POST.get("exam_date") + candidate = get_object_or_404(Candidate, id=candidate_id, job=job) + + if candidate.stage == "Exam": + candidate.exam_status = exam_status + if exam_date: + candidate.exam_date = exam_date + candidate.save() + messages.success(request, f"Updated exam status for {candidate.name}") + else: + messages.error(request, f"Can only update exam status for candidates in Exam stage") + + # Bulk stage update + elif "bulk_update_stage" in request.POST: + selected_candidates = request.POST.getlist("selected_candidates") + new_stage = request.POST.get("bulk_new_stage") + updated_count = 0 + + for candidate_id in selected_candidates: + candidate = get_object_or_404(Candidate, id=candidate_id, job=job) + if candidate.can_transition_to(new_stage): + candidate.stage = new_stage + candidate.save() + updated_count += 1 + + messages.success(request, f"Updated {updated_count} candidates to {new_stage} stage") + + # Mark individual candidate as Candidate + elif "mark_as_candidate" in request.POST: + candidate_id = request.POST.get("candidate_id") + candidate = get_object_or_404(Candidate, id=candidate_id, job=job) + + if candidate.applicant_status == "Applicant": + candidate.applicant_status = "Candidate" + candidate.save() + messages.success(request, f"Marked {candidate.name} as Candidate") + else: + messages.info(request, f"{candidate.name} is already marked as Candidate") + + # Mark all Tier 1 candidates as Candidates + elif "mark_as_candidates" in request.POST: + updated_count = 0 + for candidate in tier1_candidates: + if candidate.applicant_status == "Applicant": + candidate.applicant_status = "Candidate" + candidate.save() + updated_count += 1 + + if updated_count > 0: + messages.success(request, f"Marked {updated_count} Tier 1 candidates as Candidates") + else: + messages.info(request, "All Tier 1 candidates are already marked as Candidates") + + # Group candidates by current stage for display + stage_groups = { + "Applied": candidates.filter(stage="Applied"), + "Exam": candidates.filter(stage="Exam"), + "Interview": candidates.filter(stage="Interview"), + "Offer": candidates.filter(stage="Offer"), + } + + context = { + "job": job, + "tier1_candidates": tier1_candidates, + "tier2_candidates": tier2_candidates, + "tier3_candidates": tier3_candidates, + "stage_groups": stage_groups, + "tier1_count": tier1_count, + "total_candidates": candidates.count(), + } + + return render(request, "recruitment/candidate_tier_management.html", context) + +def candidate_criteria_view_htmx(request, pk): + candidate = get_object_or_404(Candidate, pk=pk) + print(candidate) + return render(request, "includes/candidate_modal_body.html", {"candidate": candidate}) \ No newline at end of file diff --git a/recruitment/views_frontend.py b/recruitment/views_frontend.py index 7451276..5427f7b 100644 --- a/recruitment/views_frontend.py +++ b/recruitment/views_frontend.py @@ -223,7 +223,7 @@ def candidate_detail(request, slug): stage_form = forms.CandidateStageForm(candidate=candidate) # 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]) + # parsed = json_to_markdown_table([parsed]) return render(request, 'recruitment/candidate_detail.html', { 'candidate': candidate, 'parsed': parsed, diff --git a/templates/forms/form_submission_details.html b/templates/forms/form_submission_details.html index 0b38326..4846c18 100644 --- a/templates/forms/form_submission_details.html +++ b/templates/forms/form_submission_details.html @@ -9,8 +9,8 @@ @@ -51,62 +51,79 @@
Responses
- {% get_all_responses_flat stage_responses as all_responses %} - {% if all_responses %} -
- - - - - - - - - - {% for response in all_responses %} - - - - - - {% endfor %} - -
Field LabelResponse ValueFile
- {{ response.field_label }} - {% if response.required %} - * - {% endif %} - - {% if response.uploaded_file %} - File: {{ response.uploaded_file.name }} - {% elif response.value %} - {% if response.field_type == 'checkbox' and response.value|length > 0 %} -
- {% for val in response.value %} - {{ val }} - {% endfor %} -
- {% elif response.field_type == 'radio' or response.field_type == 'select' %} - {{ response.value }} - {% else %} -

{{ response.value|linebreaksbr }}

- {% endif %} - {% else %} - Not provided - {% endif %} -
- {% if response.uploaded_file %} - - Download - - {% endif %} -
-
- {% else %} -
-

No responses found for this submission.

-
- {% endif %} + {% with submission=submission %} + {% get_all_responses_flat submission as flat_responses %} + + {% if flat_responses %} +
+ + + + + {% for response in flat_responses %} + + {% endfor %} + + + + + + {% for response in flat_responses %} + + {% endfor %} + + + + {% for response in flat_responses %} + + {% endfor %} + + + + {% for response in flat_responses %} + + {% endfor %} + + +
Field Label{{ response.field_label }}
Response Value + {% if response.uploaded_file %} +
+ {{ response.uploaded_file.name }} + + + +
+ {% elif response.value %} + {% if response.field_type == 'checkbox' and response.value|length > 0 %} +
+ {% for val in response.value %} + {{ val }} + {% endfor %} +
+ {% elif response.field_type == 'radio' or response.field_type == 'select' %} + {{ response.value }} + {% else %} +

{{ response.value|linebreaksbr }}

+ {% endif %} + {% else %} + Not provided + {% endif %} +
Stage + {{ response.stage_name|default:"N/A" }} +
Required + {% if response.required %} + Yes + {% else %} + No + {% endif %} +
+
+ {% else %} +
+

No responses found for this submission.

+
+ {% endif %} + {% endwith %}
@@ -119,6 +136,8 @@ border-top: none; font-weight: 600; color: #495057; + vertical-align: top; + white-space: nowrap; } .table td { vertical-align: top; @@ -126,5 +145,17 @@ .response-value { max-width: 300px; } +.table th:first-child, +.table td:first-child { + background-color: #f8f9fa; + font-weight: 600; +} +.table-striped > tbody > tr:nth-of-type(odd) > td { + background-color: rgba(0, 0, 0, 0.02); +} +.table-bordered th, +.table-bordered td { + border: 1px solid #dee2e6; +} {% endblock %} diff --git a/templates/forms/form_template_all_submissions.html b/templates/forms/form_template_all_submissions.html new file mode 100644 index 0000000..9b0e498 --- /dev/null +++ b/templates/forms/form_template_all_submissions.html @@ -0,0 +1,371 @@ +{% extends 'base.html' %} +{% load static i18n form_filters %} +{% load partials %} + +{% block title %}All Submissions for {{ template.name }} - ATS{% endblock %} + +{% block customCSS %} + +{% endblock %} + +{% block content %} +
+ + +
+
+
+

+ + {% trans "All Submissions for" %}: {{ template.name }} +

+ Template ID: #{{ template.id }} +
+ + {% trans "Back to Submissions" %} + +
+
+ {% if page_obj.object_list %} +
+ + + + + + + + {% for field in fields %} + + {% endfor %} + + + + {% for submission in page_obj %} + + + + + + {% for field in fields %} + {% get_field_response_for_submission submission field as response %} + + {% endfor %} + + {% endfor %} + +
{% trans "Submission ID" %}{% trans "Applicant Name" %}{% trans "Applicant Email" %}{% trans "Submitted At" %}{{ field.label }}
{{ submission.id }}{{ submission.applicant_name|default:"N/A" }}{{ submission.applicant_email|default:"N/A" }}{{ submission.submitted_at|date:"M d, Y H:i" }} + {% if response %} + {% if response.uploaded_file %} +
+ + + + +
+ {% elif response.value %} + {% if response.field.field_type == 'checkbox' and response.value|length > 0 %} +
+ {% for val in response.value|to_list %} + {{ val }} + {% endfor %} +
+ {% elif response.field.field_type == 'radio' or response.field.field_type == 'select' %} + {{ response.value }} + {% else %} +

{{ response.value|linebreaksbr|truncatewords:10 }}

+ {% endif %} + {% else %} + Not provided + {% endif %} + {% else %} + Not provided + {% endif %} +
+
+ + + {% if page_obj.has_other_pages %} +
+
+ {% blocktrans with start=page_obj.start_index end=page_obj.end_index total=page_obj.paginator.count %} + Showing {{ start }} to {{ end }} of {{ total }} results. + {% endblocktrans %} +
+ +
+ {% endif %} + {% else %} +
+ +

{% trans "No Submissions Found" %}

+

+ {% trans "There are no submissions for this form template yet." %} +

+ + {% trans "Back to Submissions" %} + +
+ {% endif %} +
+
+
+{% endblock %} diff --git a/templates/forms/form_template_submissions_list.html b/templates/forms/form_template_submissions_list.html index 77edba2..11df1ba 100644 --- a/templates/forms/form_template_submissions_list.html +++ b/templates/forms/form_template_submissions_list.html @@ -200,9 +200,14 @@ Template ID: #{{ template.id }} - - {% trans "Back to Templates" %} - +
{% if page_obj.object_list %} @@ -231,10 +236,10 @@ {{ submission.applicant_email|default:"N/A" }} {{ submission.submitted_at|date:"M d, Y H:i" }} - + {% trans "View Details" %} - + {% endfor %} @@ -260,7 +265,7 @@

diff --git a/templates/forms/form_wizard.html b/templates/forms/form_wizard.html index 73de014..3705ad6 100644 --- a/templates/forms/form_wizard.html +++ b/templates/forms/form_wizard.html @@ -881,17 +881,28 @@ formData.append('csrfmiddlewaretoken', csrfToken); // Add field responses - state.stages.forEach(stage => { + state.stages.forEach(stage => { stage.fields.forEach(field => { const value = state.formData[field.id]; - if (value !== undefined && value !== null) { - if (field.type === 'file' && value instanceof File) { + + // Always include the field, even if it's empty + if (field.type === 'file') { + if (value instanceof File) { formData.append(`field_${field.id}`, value); - } else if (field.type === 'checkbox') { + } else { + // Include empty file field + formData.append(`field_${field.id}`, ''); + } + } else if (field.type === 'checkbox') { + // For checkboxes, send empty array if no selection + if (Array.isArray(value) && value.length > 0) { formData.append(`field_${field.id}`, JSON.stringify(value)); } else { - formData.append(`field_${field.id}`, value); + formData.append(`field_${field.id}`, JSON.stringify([])); } + } else { + // For other field types, send the value or empty string + formData.append(`field_${field.id}`, value || ''); } }); }); diff --git a/templates/includes/candidate_modal_body.html b/templates/includes/candidate_modal_body.html new file mode 100644 index 0000000..6b32d24 --- /dev/null +++ b/templates/includes/candidate_modal_body.html @@ -0,0 +1,21 @@ +{% load i18n %} + +
+ + +
+
+ + +
+
+ +
    + {% for key, value in candidate.criteria_checklist.items %} +
  • + {{ key }} + {{ value|yesno:"Yes,No" }} +
  • + {% endfor %} +
+
\ No newline at end of file diff --git a/templates/jobs/job_detail.html b/templates/jobs/job_detail.html index 28ba0d1..4b9ce4e 100644 --- a/templates/jobs/job_detail.html +++ b/templates/jobs/job_detail.html @@ -136,7 +136,7 @@ } .right-column-tabs .nav-link { padding: 0.9rem 1rem; - font-size: 0.95rem; + font-size: 0.95rem; font-weight: 600; color: var(--kaauh-primary-text); border-radius: 0; @@ -409,11 +409,11 @@ {% trans "Edit Job" %} - + - + @@ -472,9 +472,9 @@ {% trans "View All Applicants" %} ({{ total_candidates }}) - + {% endif %} - + @@ -551,6 +551,43 @@ {% endif %} + + {# Applicant Form Management (Content from old card) #} +
{% trans "Form Management" %}
+
+

+ {% trans "Manage the custom application forms associated with this job posting." %} +

+ + + {% trans "Create New Form" %} + + + + {% trans "View All Existing Forms" %} + + + + {% trans "Create Candidate" %} + + + {% trans "Manage Tiers" %} + +
+ + + {# TAB 3: INTERNAL INFO CONTENT #} +
+
{% trans "Internal Information" %}
+
+

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

+

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

+

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

+ {% if job.reporting_to %} +

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

+ {% endif %} +
+
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/recruitment/candidate_tier_management.html b/templates/recruitment/candidate_tier_management.html new file mode 100644 index 0000000..42d10f5 --- /dev/null +++ b/templates/recruitment/candidate_tier_management.html @@ -0,0 +1,724 @@ +{% extends 'base.html' %} +{% load static i18n %} + +{% block title %}Candidate Tier Management - {{ job.title }} - ATS{% endblock %} + +{% block customCSS %} + +{% endblock %} + +{% block content %} +
+ + + +
+
+ {% csrf_token %} +
+
+ + +
+
+ +
+
+
+
+ + +
+

{% trans "Bulk Stage Update" %}

+
+ {% csrf_token %} +
+
+ + +
+
+ +
+
+
+
+ + + {% comment %}
+ {% for stage_name, stage_candidates in stage_groups.items %} +
+
+ {{ stage_name }} + {{ stage_candidates.count }} +
+
+ {% for candidate in stage_candidates %} +
+
+ + +
+
+ {% empty %} +

{% trans "No candidates in this stage" %}

+ {% endfor %} +
+
+ {% endfor %} +
{% endcomment %} + + +

{% trans "Candidate Tiers" %}

+ + + + + +
+ +
+ {% if tier1_candidates %} +
+ +
+
+ + + + + + + + + + + + + {% for candidate in tier1_candidates %} + + + + + + + + + {% endfor %} + +
{% trans "Name" %}{% trans "Contact" %}{% trans "AI Score" %}{% trans "Status" %}{% trans "Stage" %}{% trans "Actions" %}
+
{{ candidate.name }}
+
+
+ Email: {{ candidate.email }}
+ Phone: {{ candidate.phone }}
+
+
+ {{ candidate.match_score|default:"0" }} + + + {{ candidate.get_applicant_status_display }} + + + + {{ candidate.get_stage_display }} + + {% if candidate.stage == "Exam" and candidate.exam_status %} +
+ {{ candidate.get_exam_status_display }} + {% endif %} +
+ +
+
+ {% else %} +

{% trans "No candidates in Tier 1" %}

+ {% endif %} +
+ + +
+ {% if tier2_candidates %} +
+ + + + + + + + + + + + + {% for candidate in tier2_candidates %} + + + + + + + + + {% endfor %} + +
{% trans "Name" %}{% trans "Contact" %}{% trans "AI Score" %}{% trans "Status" %}{% trans "Stage" %}{% trans "Actions" %}
+
{{ candidate.name }}
+
+
+ Email: {{ candidate.email }}
+ Phone: {{ candidate.phone }}
+
+
+ {{ candidate.match_score|default:"0" }} + + + {{ candidate.get_applicant_status_display }} + + + + {{ candidate.get_stage_display }} + + {% if candidate.stage == "Exam" and candidate.exam_status %} +
+ {{ candidate.get_exam_status_display }} + {% endif %} +
+ +
+ {% if candidate.applicant_status == 'Applicant' %} + + {% endif %} + {% for next_stage in candidate.get_available_stages %} + + {% endfor %} + {% if candidate.stage == "Exam" %} + + {% endif %} +
+ + +
+
+ {% else %} +

{% trans "No candidates in Tier 2" %}

+ {% endif %} +
+ + +
+ {% if tier3_candidates %} +
+ + + + + + + + + + + + + {% for candidate in tier3_candidates %} + + + + + + + + + {% endfor %} + +
{% trans "Name" %}{% trans "Contact" %}{% trans "AI Score" %}{% trans "Status" %}{% trans "Stage" %}{% trans "Actions" %}
+
{{ candidate.name }}
+
+
+ Email: {{ candidate.email }}
+ Phone: {{ candidate.phone }}
+
+
+ {{ candidate.match_score|default:"0" }} + + + {{ candidate.get_applicant_status_display }} + + + + {{ candidate.get_stage_display }} + + {% if candidate.stage == "Exam" and candidate.exam_status %} +
+ {{ candidate.get_exam_status_display }} + {% endif %} +
+ +
+ {% if candidate.applicant_status == 'Applicant' %} + + {% endif %} + {% for next_stage in candidate.get_available_stages %} + + {% endfor %} + {% if candidate.stage == "Exam" %} + + {% endif %} +
+ + +
+
+ {% else %} +

{% trans "No candidates in Tier 3" %}

+ {% endif %} +
+
+
+ + +{% for candidate in tier1_candidates|add:tier2_candidates|add:tier3_candidates %} +{% if candidate.stage == "Exam" %} + +{% endif %} +{% endfor %} + + +{% endblock %} + +{% block customJS %} + + +{% endblock customJS %}