From ef8616c08827bf7bc615d2820ace4719742cceb7 Mon Sep 17 00:00:00 2001 From: ismail Date: Tue, 21 Oct 2025 17:22:32 +0300 Subject: [PATCH] conflict issue --- .../__pycache__/settings.cpython-313.pyc | Bin 8455 -> 8441 bytes NorahUniversity/settings.py | 19 +- recruitment/__pycache__/admin.cpython-313.pyc | Bin 12160 -> 12148 bytes .../linkedin_service.cpython-313.pyc | Bin 16545 -> 16545 bytes .../__pycache__/models.cpython-313.pyc | Bin 60864 -> 60891 bytes .../__pycache__/signals.cpython-313.pyc | Bin 6466 -> 3126 bytes recruitment/__pycache__/urls.cpython-313.pyc | Bin 11420 -> 11908 bytes recruitment/__pycache__/views.cpython-313.pyc | Bin 93485 -> 93422 bytes recruitment/admin.py | 2 +- recruitment/linkedin_service.py | 62 +-- recruitment/models.py | 3 +- recruitment/signals.py | 461 +++++++++--------- recruitment/tasks.py | 190 ++------ recruitment/urls.py | 11 +- recruitment/views.py | 27 +- templates/forms/form_list.html | 8 +- templates/forms/form_templates_list.html | 8 +- templates/forms/form_wizard.html | 6 +- templates/jobs/job_list.html | 4 +- txt | 1 + 20 files changed, 367 insertions(+), 435 deletions(-) create mode 100644 txt diff --git a/NorahUniversity/__pycache__/settings.cpython-313.pyc b/NorahUniversity/__pycache__/settings.cpython-313.pyc index b3d3954123eaa66a740736630f674d78cc4500c4..b45cc7587cf0fda178d031e7c62b3364c3b40a2e 100644 GIT binary patch delta 933 zcmX|8Nmmm=5T2fdun13h#5@m1)(8PI37bS^UnSxaKUYLFNKA$$36%jv#kkdAu#my^{aIQ{CYuj{L+>gvAT|GTf`M@dPM!e9Q~pYg7@B_WY< z!;S5zK&9pg2y&we)d9g+V_~g@o`CS%X*+iK1y%SJ)P)6A!a-H2r)sEF!@L$=G;jqE z8fgcbsE(~34qiS@B`-9ntTw=pfM2CXG*i<=AzJEvn-qo1`p`-mm+`X&&_>N@rxs4w zxvsZ96+Y2nHmMz%V7n?uP!wv%F4~FRe4;&J!PprV=+b1L4s=rxJ+uqGv>Sc22mREE z0qSDw<`z8|q+SeBABMSkKSpSPZ4i5D2%|KN5RG6T?Zp`O;{Xn#kmoqWtPtT0U)Ny( zM>HFb;uwzO1hbPYjcY1Su}x^jG>X%h#2Mb5Ma1$`IA{53L@j?FG0VrHOYe732s4=F zG3LS|z>C?uHCBShN+QKM3Kqf{DG-O!R=^00iO@s{i<-LOvBU>j#s#e4A}%q%j8$Ag z23IvZuHiavV9nw;aSOL`M{>WNU;3_=hkM*guKSveBM&qi?ZZPFi>cm6kv5?yMcJ>m zwt~{li6lkhiwS)xN@?9#FFM=! zS(O&7n&$U%UBu0k&Nlw(k#n5!n=>e4X7!fG&Sx^^IRm#%8G162CNe*4=@eMTuB}0#-Q6f}H5h$uu7Fh#i5|%(vKtKcxxWt1yi@}U!2qd8sz&-B!%2er- z`U_fl@x@2Kz!!}#@(q?{QNGxB1m{+tb8dG}_w9Sv`|l4F{wyqXD*ScCen$sC6b416 z92KZU6?SV5pP*f+MvYG})>>F+VZBdy?Np8iub>LA0(VGIB^*?RM%oRPs#({d3C&!g z79OgDM)hnBaPabKD!Ji<%F#x&pw+8V6WXYGA|LGyo^6W4Wj*kd#$~*0K6FqEI;oWt z0vqwRRpAj`X0zIv+2dE`2#P{}?4=HL^ND&wg0VLwuuqeHI?+b~^ivlGXb<+&UJOz< z4p0wUFSpo-A?m|0^<#va58xo}XB$M24q%jqFh;{TL?bwiBRGl>@_CM9tnz^jU)MN} zij_P{kvUAAdzSb7MSyK=Yb=k>+Z+6Eq`BLL%cadjPCW;ot zbat(%S`5WxFd-o+!P+jea#zyarX#l{v6PPJ@pR5%#3lbro0stg32`%6JS<}7YH?G| z|5~}N=|m(kW~4UKWmO`prny#Z2*Vt8wewFmTxS@+y8((SW zXOa?0ozjM$a@%M%`>FIxp7@h}xwA}c+3|II*;iL(_WiEULO8R_6&Yb)=Q@7@@_hk| diff --git a/NorahUniversity/settings.py b/NorahUniversity/settings.py index 705d22e..5c00084 100644 --- a/NorahUniversity/settings.py +++ b/NorahUniversity/settings.py @@ -135,9 +135,9 @@ WSGI_APPLICATION = 'NorahUniversity.wsgi.application' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'haikal_db', - 'USER': 'faheed', - 'PASSWORD': 'Faheed@215', + 'NAME': 'norahuniversity', + 'USER': 'norahuniversity', + 'PASSWORD': 'norahuniversity', 'HOST': '127.0.0.1', 'PORT': '5432', } @@ -148,7 +148,7 @@ DATABASES = { # 'ENGINE': 'django.db.backends.sqlite3', # 'NAME': BASE_DIR / 'db.sqlite3', # } -# } +# } # Password validation # https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators @@ -171,12 +171,12 @@ AUTH_PASSWORD_VALIDATORS = [ ] -ACCOUNT_LOGIN_METHODS = ['email'] +ACCOUNT_LOGIN_METHODS = ['email'] ACCOUNT_SIGNUP_FIELDS = ['email*', 'password1*', 'password2*'] - -ACCOUNT_UNIQUE_EMAIL = True -ACCOUNT_EMAIL_VERIFICATION = 'none' -ACCOUNT_USER_MODEL_USERNAME_FIELD = None + +ACCOUNT_UNIQUE_EMAIL = True +ACCOUNT_EMAIL_VERIFICATION = 'none' +ACCOUNT_USER_MODEL_USERNAME_FIELD = None ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True @@ -274,7 +274,6 @@ LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw==' LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/jobs/linkedin/callback/' - Q_CLUSTER = { 'name': 'KAAUH_CLUSTER', 'workers': 8, diff --git a/recruitment/__pycache__/admin.cpython-313.pyc b/recruitment/__pycache__/admin.cpython-313.pyc index c293cd48af2f11e70f5356eac722810f3439c8ae..ad3e681e0dc8ce8e211b8f28a677cae6a31af6d9 100644 GIT binary patch delta 508 zcmZpO{}RXhnU|M~0SK(>zGqZ#&$-sf~abhx)>Pfc4Am-u87geV*9tIm(Bq%lcET7@zGio8cS3r`N zfrKV=(ca0r>PC#qCnu;Y$bp24K$2Db&iT2ysd*&|&iQ#IKw6U#sBoUTHrqvzl8cki zs~0g|-0Y;`!^n0MByw}|6wOk`o0GY;4sbsJ3ETq__a|S~GSLGwAAg5BmCCy6Dis(d3Up zmh{7-;wDdVNo}Fgnk7om!8gQ@4qfWx2NXoS=X!_#{h!}C_ndq0)zIv)cByHuM{*t0 z^_#hM?X{Pxk@6;=IIj2Bh-nRvjaVr=>wQ5q4?XC^KVO*^@WbCpY25nHr9TP0q>RjM zdFEvwv!5l+lEJ>_$89i6qxRy{k0ib_U4o&Hi)FSe$mq{JUle6g`ET~V-b=KEhEOMM zU@-KTw$N>KiA`DJG+`*@L(W)pZ}15nGOr$0)qi1T;l6#zFVJ4OkWiBb>SInd`?&1Nvuk|?zhpB_P_%u0^ z{&b%)IAJ+Ou`TIV_MGE0)FgCq%JvUFH-|)pZ3V9q<5a=-#Q&iB0rg&Wc{NcDsNkSI LLKpT``|RU;PEe#l diff --git a/recruitment/__pycache__/linkedin_service.cpython-313.pyc b/recruitment/__pycache__/linkedin_service.cpython-313.pyc index 75e7844056c375605cdc06137c3b19e816fcbb8e..e84214f042256427d0ad3b5168681e380477741d 100644 GIT binary patch delta 24 ecmZ43$hfePk@qt%FBbz4^!9wu5ZTB(#Q^|Zq6dip delta 24 ecmZ43$hfePk@qt%FBbz4m}P&@__&dGiUR;#+z0Le diff --git a/recruitment/__pycache__/models.cpython-313.pyc b/recruitment/__pycache__/models.cpython-313.pyc index d45fe48bcf80144ed2ef4183e60ceb8cb7a87919..fbd3902ebaab9bf7390f8c7a8ee26384ca504722 100644 GIT binary patch delta 1475 zcmYk6eNa?o6vp?t7gp5PSxOlf!~{_c6HUeZNKh1n1!P?U{6MtC#DakqCq*z7#tBUn zIc0!_#gvUwshqG}Fhgt@bTB0G13xG&EhI(2{7R>@+aKQh$1~@5p7-v|d(LzB$BXL4 zi>jf=VCbilXI0U?)U1PsGkuZASNkq@U7r*j7w)MGa(MFCLgMCl>TEOOB1C737pd_= zh0PkCB^;&U*}~Bp4ik>iaJcYX4bKr?AFF9ZNW-GxNa1-JHVa!d93>p5;b`GkH5?-x zui?4E2^x+~3O2?iY8sZLV4h{{@7{l$%wmJCORsyz$ca8>Kqq(m9s_#0+HW?X=Z7Ok z0sU#Xe=FSifxik2;>V->fWhn=5D$3sn$Z=2AA5}%4){|@U;{>rdXmrBS_C?)`(qDa zyrbg!ak0Q;er|jqFpY*z@}#5*tq5^e(G%Md>a4|{k1AD{!tT=_0cJ9U?0}hfha|comMoDb zGDhUWLWL38qJ1K3kU;g3D@DhccVQtln$tyJj;cU1wME&G%rViaDr)IaYz}I9yu}x& zr}%j_Xd-uO9hyZw`ERQXxsDRtvl$mc>8oM zN^{D?VCBN{?^L;CSjBZ<4ZryLXh+=Ve^II+m%G1=0*WbkZyJiZa_>Z-jDFpBTb$;~ z0Sd~w{cC@qiu&v?5G~sO1a{hfz>XtiK6qGi<5G1&?slarS;0A)UR{Wv`DC>b^5UgK zjgY|X`QF75lFGxYM2FcA$Q4hEUgo@-JQdySdUS(|SE#Nvg*uKI6A) z;V9p24~94sfBQy717G`HmhcySdc6ak^!^Qt{IguTAq#2X_CK$v#uOLKMA5)rowMkT Jrl!uG{{T5Z{kQ-C delta 1483 zcmYk5c~F&Q6vprKeSn}LIFbosxUYd&Xf6q2AY3lo3l|U>0ZBAbLE)knE(2VbESJQS zA{x#}W+;ik$%-Wlx3of(5nRJH)Nskb$YrQ=X>#uS$1~6GyzjT1_ndS2viiei)v?vl zv8zVb*ahv$^U560IpBT1;Sg_|6*<*SWAcp*67`D=7M&dDAE|ZIjIo@SWtt)!V#RL@ z>#R6HIMj**g~P0Ps<7UQgMOSJ-PrG~mvwM-~DDd8p?=U9Zx!k2t~p1@zc8yUx(M_{L`^X|gBvD+0_(|4AV9Jk0BkxTYz zrbTIUOp7z=`M5Rm!{+h%s`6(qer?h-Kug2?a-ro)zZiSyDNE-{&$C^rKs%7;22X~8 zih?s018B6lH%-(QBAS}C^F)I~b|RK;g`|oab%lth$2t?@IXN_0MHQVjEJqcGg%1FZ zP+CM4exV0OH=1hv0(Jbr*cVDYC?cT`4~yywoTswseJMP;1y?MNW2e0ey#5vgt#B2c730$9AB#(``%YL!~hxv~)&3{_c!;#y!VB7RN#H_J9qCC)h%9P^d?o zJ7vtg1ZN6QaHsi+Pw{f6%X#O%;`ZP6hFSOEL77QMF_~>PoXfpaqao1`n{j>`kKLk)qQ6E*JoSoL!184chhFstlE#8zU+OVk{ z_i5e7)%b%0H_rvy=s<3cT;r%M$K-6=mTI7#%RW(nF7(yb6w&_MYVi_1+;$aCRF_w2 z*KYY{7X4A|!6n-_D$>($?HsHkf@bXsLg%1%phcLfFf^uSP{j0PNBCt7^40~NHtv=Y@cr>sI6$fNus zPV$rT1O<&W{m@37=H^4rkO$kpJ_TIins04jcURSDmbuXN3KzOsF#~sLVr8i$eNXfX z7gU*5JmP-eXRDYe$p-$|?9!&`G;JYt=U(f-xSI~i diff --git a/recruitment/__pycache__/signals.cpython-313.pyc b/recruitment/__pycache__/signals.cpython-313.pyc index fc601be53b0495bef0b3c2911021a8d1a360c910..a1a22633916589c3e52b2577adc8474a6a7f6ace 100644 GIT binary patch delta 864 zcmY*WT}TvB6ux(6XLokoSy6Z0S$ET2P1Z5nP_mMKsFZ>v3pa&G*v8q}ZPeY-J7b7} z_0U5-NNg9OB6?2fO^6;-f}kgT2xSn$mmWh|O6VavcPpwh+;6^nzH<&U-&yN=0>kef2Wcd z=xmmK65t;C>26JAhEUr?Pw*3JMjS$23q|F(g@7<03;wt6A=1?iwa2~VH)Spb0ki0^3l*$?te=! zVohFIOz0a&tZh}TmrR##?;e%3(8|PN>zD6Aa&3~pUPLf z2Z4}w`b94GuszP#rl)iHu5`}Qt1zdEB*jYDSfVoMWbnIfG1xpZ1g7(lbG@2l6-}M6 z?UH&14Gcs8RjB$+A}paWhyu8>Zj~I^bQYFSFl@FR<_8!;pV!4|j>EkLj;-UBwYp}E z6LhD(yJLR_qhRa0oZRKX-%oP#qn!M12cYmp|B-XPj@v0&qP}P< QbOGZaOLp#Yj8;ef1pGe5mH+?% literal 6466 zcmb_g&2QVt6(4<*lqfrnqB!w~D1RkN9Xnk+AI|O;O`T-dA5Bt^7d<3^K#OCUYl+kj zsl?$a$LwiO3j`?8dwWVx1^OTK7VUN+flaUrv=?6r2ianQp894;%Ff0~6e*NAoO$!+ z@y+kOnR$;dWLZLR{bBAuTYo==(7#wCxMF_g#bc;^jWEKY6-3!Hyb@Xt)3Czok(J1D zlty_yx)NKC(>SlkRuans6`&r+i4}2KqSA7bCYMt*wJcM4IZe|Fx`Wn6gV*gp$IzYr z^C7QIkKBnuTeu@*VIhpgljyw9NC*6$6z`D^^c*dR3dvo@ccTz;lQvZiOD);DX+Tq; zq(t<)ghE|Xt@}o)XsgyXw8WZe*+omeOLp1Y3o%!`Zqn*?om4chify$_+!Uj(lWMJ^ z+QbzXRYTJ?R+W~`jawi?H_9L`ZV^Kxv=s3bW{)V`Nw{A;g5qoRS!fNOKfg}<1Z@GK z+u@*hEwD#GfF}-tg!lo1UrnRAaDbqKZi@j24j;~c-hYdN%J3^hZa5>jWKx)iLWxzc z5@pk*N-3DA;(P!g(zN=&g_$7E<*%4-OG@062_-` zk|Elx1k z9`6~?#CyOqfhPdZ-qvdZPjVT(tk(pd;_HO1*Y4NJDZXbgy9!;{Rluil!CR}d2%q^~ z2>)Us(dDi&&37KAiP(iEB0l>bc8wW4>+f7r7dlAz9AB;bJJ-9PY!dz@m(k0qB;gPE zitlCDknr4l*fr+yc^}VY7kDP|g`V+Dz6U&$_+rm^Ch;ZipY2^3FRlHhQiy=NO6HB?bMGQq%402C7 zm>ej&rTCGnHL#+}wW>Uxj z-5{DHUDv5)D>oQN)FwN2A@0Vw3T~{TZjg#AZL(~oXy309mg#&^*I_grhPTiWC>(KB z^##xbYs=SI+^k&DGziQsH%3@utC*_hMmO~e*=2FO(>GJoHm7Gav&4&?f$+VbPHNGt)>Pv@H-sZW zR!D6=))B55npew#x;4|V^bNfNW5GwJlq;nzo!liXX|min-qtG>%jsVyr7c4*sTIYe zxI$@*l(sj_okGMpc8i*2s#dEk7h6@0a-I2`D_sW~xo6UC%gL;pW`&98f=tM$;BAG( zTaZqeH8vy5P2hU9s?z&T241xZP5@Mtf0x{tJT{0byqgymio8}le-DP?5#g40JV_fc_lr{oG!zPV~kfWc?+}X zt)xD0D7+{jkieIGLO*HyhEGXZ+?ak#h7^}U>Nhi`mmmy=sC@EX%VL=!%V&d&W|?Nk zJmri2rsMO{PoZxRZtD_Q;{p0fLPFnDd9W$xzL9g^$@zZ|Pd10oHipkOhvyr^^Q{y4 z*0D3K?D5v<*q(?+&hMevNc!P#e@r1c+f0o#QX|dOL?boP%8s|PW38cl>%@r9E+cvJ zIZDXsANvnu&c6%uWNT=wW62Zm!kpu3jCQ6osIfo6*3fioG`DwzPg8_x4x-%Y=ICr= zbhbHqp)q=agLkS8UhX1*mrFmqc?fuut?cR6&}8extBPa}ld5zob@ZUddv7ucs4)I6 z%u2_=f#g%KVa_0>(9E4{^xuOi4dU4IhHpR4Y5xvBJO%Zn1B? zH$nl-ozD)KJNNsx=K;6j^uyIdU>p0H ztGR=}-|OQ(EPxMZ(a6**8$jTcZ`}Z0j{$XSML#{5kg}<##d<5!F zfSHv;^o8B$%xk>o)qMq+K?g8%pz@nM9+*ALb;$gBCqBd&Gws>E#TcEd0G8`~a=`5E zR-H@^m|^W7w*CB67EpTZiJaeyz)A7FoQE^wfBeY#8xC!<-7S3>2 z)INt;!<^AJytB10XzNx@wM$!+1%7VYqZYsLJ^}M@x9(uTx-nBVH3DB+{V%SRg${1q zBl7c@AJvqlYm|KzrR)%N#WJy1y|2PTvB-kqVv+Wj92A4{2wkXNBjT) diff --git a/recruitment/__pycache__/urls.cpython-313.pyc b/recruitment/__pycache__/urls.cpython-313.pyc index d758325a516ffc8b05f773c9cb39d75983f9ee9e..c867c4d3311e99b23d0af8899b08bd651fca3218 100644 GIT binary patch delta 1903 zcmai!U2Kz87{}M{6gNmmVC!gC-1^<_%YwFS(0z6-A2Q~kD|X#S*U@fNItJ|qqaABZ zjM;^fk=G~}TukI+Xkun|vpcvX@xu3s7e|RVUYqD4ChCPIo^u{$EFtdzZd6sZS$m$@_xvvM5JS1>^yiIVY_#jGuh{HUv$>hP zcXB?HO{H_*j(m20`p8^*=2A8}mre+`u~74v~dWWlmM*paz7x3!&2 zs*6`;XOpR|sy?yNg8To8HN00t22x6v#K&(rCMoUjXoIvbbPY~$T7B?bBo<1VgY+yyNuP! z*x^k-4GckGNFBKZ(fMgF`klIc1{`r*tu=Y;@h}6#ed^eC>2%gEy6LoDJlph9i?p2D@A` zm`3@)<+R9&V;Z@B@4A5=?1zK>YUCV{6dM+m;4+KeKfB~nR=Qon9dy4cWDsQ*<%Zi_ zlI^ZwyVurr^gs{}1fSFciLtA^8-G*_0xla@{It3Ss$0}IjsrQtt|2U7rFGR$?VVun zR6}tfBdmyY38{a5p0-7yEvk-Q0dkd=!bIQuMS4_Yj48qwW7$|Z)OHLUhe07*fGS%u{iiROhX9>CDf?}AlKLlqyx%uQ+>r-Xac5@QSLRlDk5lxlyA9-qKPS*=K6|ZG;zh<>~f5tnG|y^H{3E%Qxlk)*7ZQT*g0e= z<#KavDUD`I2KTR|KbgHbyA}Y_#-@>+m&CjhrG`c@G^*ZiAU!OD@Er;3Z|SJf3r4T% zmoMW5gjoUAO81(MdV0XqqaKwn;|$V^k}5k|_EgN`HwC4krFOcAW)X|K9~)@z2=tDq zlk$|VBV9t;``AzW641xba0y6>m5>IBocPeDj34|J?f~jqu#@@Br7+C}iv@XRaHjhA)0�FvBA6&HI!g50aCCc z^+2Rl0_~w3dLZqgmRdEg=mDv!>LZs(`#)5pRH}Ne`p`r<^wLJ1nSU>}cLx0dzd-}U$ z*le0S?QZ>{RKMWrtR3RZ$P9~NL=2;R(0g5B{gEqvahn?x|8CUlf4b@&d#0MwXQblb z)q^CpXkgI_4M6%?*`7&nb~{VCgxxkF3Adv`gr8TJm@AD+*GcS){$SB)ExQjEL zCjsi}2UoulnE{eu_mS=+4HUApHwL{iV|EM3J(fcnDlIYV$L&_JuqW+}LY|`h*6tI| zu1Oiz;yvI_!hF`)QLT_H7S_rgIaZg;| zJi;`>;8~3J&p`i-F~1AsK3hP#i1czHO1tCG9XC?B=OrcPa4B2Dz_NbpQp=z`&4-v( zPO{V!0Z+sjO97cu>xfGf?8EZZ^Q=1=bej^|sig|=- zJcB21rrr>EL&lJNC>D?|B5fBpBMMs77)t|LU`t5vBJId$sec6gBgXYbAWLi=X(oku zplGGx1cVbtT5jS7;!ULCVt`&r!4LMs!t4Ug6WzDy+qwwbqE>3((~S6mHR#3Orw%%D0gD z$8zMt@c{+nEzj#i z*}~8+d)6i3{KZZw*#tUJVa2;q_|9TOgt^nec~9^Og%+G0H%P56_#VZC&16r__XXmH+~i0R;B zU7Snh;+;HUBEtuT+MvWRE}?r>oTQ?3xYPsKPMIX95!chw-{0=F_;G!Ftk69_;w7d~ zD`o3eps~T(+fLrEYCKsX-=KyGvz{;|226=3O#K6<{v`7grnG=54J#YQO11|;3lCv~ z*NU@es)5IAw|c7VOraXhjcKaaK{Vs!#vutRaR`m0l;txj(O?cvTt(%3Cw*&U-!iliY*=lmA9pf3=0x<+ zK=e?&_{2asi4jj`@ICtmG{eyKWN&tl20ho~aO3`gxa&zRKZjv#jFg!QHXOZ9r(zj9 zgWJy9(Dal=*}Q^6i_T5;ytAGxWn@4hxdy?x5F*uXJJFS8M!z!<~N0_++2RP z6i9#`5pkX7q+7(fVh6A;yUT{HO}+H*j^N^$66i(FfHabaShO`8Lk}=a3G*upkd4XB zRxo3BbKHn%w>gA)psSnMnK;s2eC9FcJWcRf0-Yp<_v80)^_(H(=hM>&G5ELUxMFj+ zRaGJVX;p^X+}Wp;e622923y5;#z?#u>wXMJue!)d9$#^p*IrT~mfNdp=9HIKRh3rw z#7`I;)smrojeP#Oq~mz8p(WkYOlIwTt*5NUOZv=iG83EeaEpv9pPnH+MyE^h{L@*` ziV5rg%X$GF*vD)MHl%rUS?MON49>#1RO;TY3I2GO7K?AvX{cvzb&aR2s7p&DiZTU1 zZbhq}e&Av=fhr50&54rEB!Pe(XzX*8*yPa$b(es0%Tb!#gMrYoq(SMzo0C^3*N>BX zJ`=*hUUW#GFMo@$Y>$|l!W>}7WaP$h?+w?I$wA&q4Q7G;LuqR8G-Ni{jeSBc_FO0Rs>@5Y7|C zK?#xar_YT8*n!a-v&paC+>N;e-`qHjKTgWAL)N^olude-R&NJ|009SxyQoZW4(*1j zYR^1xQBj3h>=ngw+_`C(UfH4-sYwOi+~hLdzi}K3DrdBg=RYUnLHXs@IyRQlE4M)6 zq4?6vPVoBGj6*g%;C-CEImx6fDJMp`xV(=WH#^Bv+vd19C4%x3^h+bk)Q%xroctF= zJR?uq@-hc!@Y`2M@t25#kyBpF6(avd&Ay}XZwil-1@SVV#~+DXw+)5!cy3!v{B^oS zPme<<{-#3|A$M+1cl^2mA?Tni*CDWUO>ns; z=j9en9$%C*VWxPOdf%Wxzece)m2Oi*!=p9c8gFsY+=?1swfG~|Z=x1=h*T9qB)eTY zmIUGW^gbIrBk$Um&Iz*bO6nxE{sTuW|Fc=AdzsWgA}3S5(KUiGNd83Li5 z&*0`eedNZQKEAVA{_vl3UFNDg8z4`9;m%JwLMlBkz`=V~{yA#+h|o&OTF8d$(n2lV zQA?EZJB~1#{cCkFFN8SxzmJB)Abd?;+962wJ;>$zi=HqUZc3*e&{vug13AR~xfr+z zKl+!&!YpHOiIVnBg7eVi*Y<}E0*$|U5V@9+r+*Mkft!BSV0eQS{Ir~Vr9Ejd%o>!b zVg=pabOPjRtlT|`o8Ad=o#v@dNCczb?S!E`IQ&h+;1a-0e{CiNz@pInRpBL2$J+ae z`=r8=P)$;68wn#|mOo?^dg^j4{*h!9ptJBy$U`uJmKLj%Avf2#)?RU_6= zI{L4exQW6`6t+;HmvC_#1^P=R(*7*cE-KQ_Cem&u{_LMs0z28TY!)(aa13keV5!(q k;J21SJj6tS(2>TlA=lVK03Edm=}^Ru$NUd1faC1{09M?1lmGw# delta 4292 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.*?)', 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) - + 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) - + # 4. Strip all remaining, unsupported HTML tags clean_text = re.sub(r'<[^>]+>', '', 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() - + 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"] - + 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] @@ -213,10 +213,10 @@ class LinkedInService: 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}*") - + message_parts.append(f"*{job_posting.department}*") + message_parts.append("\n" + "=" * 25 + "\n") # KEY DETAILS SECTION @@ -229,7 +229,7 @@ class LinkedInService: 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) @@ -239,7 +239,7 @@ class LinkedInService: clean_description = self.clean_html_for_social_post(job_posting.description) if clean_description: message_parts.append(f"🔎 *About the Role:*\n{clean_description}") - + # CALL TO ACTION if job_posting.application_url: message_parts.append(f"\n\n---") @@ -255,17 +255,17 @@ class LinkedInService: if len(message_parts)>=3000: message_parts=message_parts[0:2980]+"........" - + return "\n".join(message_parts) - + def _send_ugc_post(self, person_urn, job_posting, media_category="NONE", media_list=None): """ New private method to handle the final UGC post request (text or image). This eliminates the duplication between create_job_post and create_job_post_with_image. """ - + message = self._build_post_message(job_posting) - + url = "https://api.linkedin.com/v2/ugcPosts" headers = { 'Authorization': f'Bearer {self.access_token}', @@ -280,7 +280,7 @@ class LinkedInService: "shareMediaCategory": media_category, } } - + if media_list: specific_content["com.linkedin.ugc.ShareContent"]["media"] = media_list @@ -295,7 +295,7 @@ class LinkedInService: response = requests.post(url, headers=headers, json=payload, timeout=60) response.raise_for_status() - + post_id = response.headers.get('x-restli-id', '') post_url = f"https://www.linkedin.com/feed/update/{quote(post_id)}/" if post_id else "" @@ -309,13 +309,13 @@ class LinkedInService: def create_job_post_with_image(self, job_posting, image_file, person_urn, asset_urn): """Step 3: Creates the final LinkedIn post payload with the image asset.""" - + # Prepare the media list for the _send_ugc_post helper media_list = [{ "status": "READY", "media": asset_urn, "description": {"text": job_posting.title}, - "originalUrl": job_posting.application_url, + "originalUrl": job_posting.application_url, "title": {"text": "Apply Now"} }] @@ -371,7 +371,7 @@ class LinkedInService: except Exception as e: logger.error(f"Image post failed, falling back to text: {e}") # Force fallback to text-only if image posting fails - has_image = False + has_image = False # === FALLBACK TO PURE TEXT POST (shareMediaCategory: NONE) === # Use the single helper method here diff --git a/recruitment/models.py b/recruitment/models.py index 72d4dca..91a1fb6 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -226,6 +226,7 @@ class JobPosting(Base): parts.append(self.location_country) return ", ".join(parts) if parts else "Not specified" + @property def is_expired(self): """Check if application deadline has passed""" if self.application_deadline: @@ -305,7 +306,7 @@ class JobPosting(Base): @property def offer_candidates(self): return self.all_candidates.filter(stage="Offer") - + #counts @property diff --git a/recruitment/signals.py b/recruitment/signals.py index 3795b13..e27c531 100644 --- a/recruitment/signals.py +++ b/recruitment/signals.py @@ -7,10 +7,17 @@ from .models import FormField,FormStage,FormTemplate,Candidate,JobPosting logger = logging.getLogger(__name__) -# @receiver(post_save, sender=JobPosting) -# def create_form_for_job(sender, instance, created, **kwargs): -# if created: -# FormTemplate.objects.create(job=instance, is_active=True, name=instance.title) +@receiver(post_save, sender=JobPosting) +def format_job(sender, instance, created, **kwargs): + if created: + FormTemplate.objects.create(job=instance, is_active=True, name=instance.title) + async_task( + 'recruitment.tasks.format_job_description', + instance.pk, + # hook='myapp.tasks.email_sent_callback' # Optional callback + ) + + @receiver(post_save, sender=Candidate) def score_candidate_resume(sender, instance, created, **kwargs): if not instance.is_resume_parsed: @@ -75,238 +82,246 @@ def create_default_stages(sender, instance, created, **kwargs): order=4, is_predefined=True ) + # FormField.objects.create( + # stage=contact_stage, + # label='National ID / Iqama Number', + # field_type='text', + # required=False, + # order=5, + # is_predefined=True + # ) FormField.objects.create( stage=contact_stage, label='Resume Upload', field_type='file', required=True, - order=5, + order=6, is_predefined=True, file_types='.pdf,.doc,.docx', max_file_size=1 ) - # Stage 2: Resume Objective - objective_stage = FormStage.objects.create( - template=instance, - name='Resume Objective', - order=1, - is_predefined=True - ) - FormField.objects.create( - stage=objective_stage, - label='Career Objective', - field_type='textarea', - required=False, - order=0, - is_predefined=True - ) + # # Stage 2: Resume Objective + # objective_stage = FormStage.objects.create( + # template=instance, + # name='Resume Objective', + # order=1, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=objective_stage, + # label='Career Objective', + # field_type='textarea', + # required=False, + # order=0, + # is_predefined=True + # ) - # Stage 3: Education - education_stage = FormStage.objects.create( - template=instance, - name='Education', - order=2, - is_predefined=True - ) - FormField.objects.create( - stage=education_stage, - label='Degree', - field_type='text', - required=True, - order=0, - is_predefined=True - ) - FormField.objects.create( - stage=education_stage, - label='Institution', - field_type='text', - required=True, - order=1, - is_predefined=True - ) - FormField.objects.create( - stage=education_stage, - label='Location', - field_type='text', - required=False, - order=2, - is_predefined=True - ) - FormField.objects.create( - stage=education_stage, - label='Graduation Date', - field_type='date', - required=False, - order=3, - is_predefined=True - ) + # # Stage 3: Education + # education_stage = FormStage.objects.create( + # template=instance, + # name='Education', + # order=2, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=education_stage, + # label='Degree', + # field_type='text', + # required=True, + # order=0, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=education_stage, + # label='Institution', + # field_type='text', + # required=True, + # order=1, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=education_stage, + # label='Location', + # field_type='text', + # required=False, + # order=2, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=education_stage, + # label='Graduation Date', + # field_type='date', + # required=False, + # order=3, + # is_predefined=True + # ) - # Stage 4: Experience - experience_stage = FormStage.objects.create( - template=instance, - name='Experience', - order=3, - is_predefined=True - ) - FormField.objects.create( - stage=experience_stage, - label='Position Title', - field_type='text', - required=True, - order=0, - is_predefined=True - ) - FormField.objects.create( - stage=experience_stage, - label='Company Name', - field_type='text', - required=True, - order=1, - is_predefined=True - ) - FormField.objects.create( - stage=experience_stage, - label='Location', - field_type='text', - required=False, - order=2, - is_predefined=True - ) - FormField.objects.create( - stage=experience_stage, - label='Start Date', - field_type='date', - required=True, - order=3, - is_predefined=True - ) - FormField.objects.create( - stage=experience_stage, - label='End Date', - field_type='date', - required=True, - order=4, - is_predefined=True - ) - FormField.objects.create( - stage=experience_stage, - label='Responsibilities & Achievements', - field_type='textarea', - required=False, - order=5, - is_predefined=True - ) + # # Stage 4: Experience + # experience_stage = FormStage.objects.create( + # template=instance, + # name='Experience', + # order=3, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=experience_stage, + # label='Position Title', + # field_type='text', + # required=True, + # order=0, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=experience_stage, + # label='Company Name', + # field_type='text', + # required=True, + # order=1, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=experience_stage, + # label='Location', + # field_type='text', + # required=False, + # order=2, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=experience_stage, + # label='Start Date', + # field_type='date', + # required=True, + # order=3, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=experience_stage, + # label='End Date', + # field_type='date', + # required=True, + # order=4, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=experience_stage, + # label='Responsibilities & Achievements', + # field_type='textarea', + # required=False, + # order=5, + # is_predefined=True + # ) - # Stage 5: Skills - skills_stage = FormStage.objects.create( - template=instance, - name='Skills', - order=4, - is_predefined=True - ) - FormField.objects.create( - stage=skills_stage, - label='Technical Skills', - field_type='checkbox', - required=False, - order=0, - is_predefined=True, - options=['Programming Languages', 'Frameworks', 'Tools & Technologies'] - ) + # # Stage 5: Skills + # skills_stage = FormStage.objects.create( + # template=instance, + # name='Skills', + # order=4, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=skills_stage, + # label='Technical Skills', + # field_type='checkbox', + # required=False, + # order=0, + # is_predefined=True, + # options=['Programming Languages', 'Frameworks', 'Tools & Technologies'] + # ) - # Stage 6: Summary - summary_stage = FormStage.objects.create( - template=instance, - name='Summary', - order=5, - is_predefined=True - ) - FormField.objects.create( - stage=summary_stage, - label='Professional Summary', - field_type='textarea', - required=False, - order=0, - is_predefined=True - ) + # # Stage 6: Summary + # summary_stage = FormStage.objects.create( + # template=instance, + # name='Summary', + # order=5, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=summary_stage, + # label='Professional Summary', + # field_type='textarea', + # required=False, + # order=0, + # is_predefined=True + # ) - # Stage 7: Certifications - certifications_stage = FormStage.objects.create( - template=instance, - name='Certifications', - order=6, - is_predefined=True - ) - FormField.objects.create( - stage=certifications_stage, - label='Certification Name', - field_type='text', - required=False, - order=0, - is_predefined=True - ) - FormField.objects.create( - stage=certifications_stage, - label='Issuing Organization', - field_type='text', - required=False, - order=1, - is_predefined=True - ) - FormField.objects.create( - stage=certifications_stage, - label='Issue Date', - field_type='date', - required=False, - order=2, - is_predefined=True - ) - FormField.objects.create( - stage=certifications_stage, - label='Expiration Date', - field_type='date', - required=False, - order=3, - is_predefined=True - ) + # # Stage 7: Certifications + # certifications_stage = FormStage.objects.create( + # template=instance, + # name='Certifications', + # order=6, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=certifications_stage, + # label='Certification Name', + # field_type='text', + # required=False, + # order=0, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=certifications_stage, + # label='Issuing Organization', + # field_type='text', + # required=False, + # order=1, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=certifications_stage, + # label='Issue Date', + # field_type='date', + # required=False, + # order=2, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=certifications_stage, + # label='Expiration Date', + # field_type='date', + # required=False, + # order=3, + # is_predefined=True + # ) - # Stage 8: Awards and Recognitions - awards_stage = FormStage.objects.create( - template=instance, - name='Awards and Recognitions', - order=7, - is_predefined=True - ) - FormField.objects.create( - stage=awards_stage, - label='Award Name', - field_type='text', - required=False, - order=0, - is_predefined=True - ) - FormField.objects.create( - stage=awards_stage, - label='Issuing Organization', - field_type='text', - required=False, - order=1, - is_predefined=True - ) - FormField.objects.create( - stage=awards_stage, - label='Date Received', - field_type='date', - required=False, - order=2, - is_predefined=True - ) - FormField.objects.create( - stage=awards_stage, - label='Description', - field_type='textarea', - required=False, - order=3, - is_predefined=True - ) \ No newline at end of file + # # Stage 8: Awards and Recognitions + # awards_stage = FormStage.objects.create( + # template=instance, + # name='Awards and Recognitions', + # order=7, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=awards_stage, + # label='Award Name', + # field_type='text', + # required=False, + # order=0, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=awards_stage, + # label='Issuing Organization', + # field_type='text', + # required=False, + # order=1, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=awards_stage, + # label='Date Received', + # field_type='date', + # required=False, + # order=2, + # is_predefined=True + # ) + # FormField.objects.create( + # stage=awards_stage, + # label='Description', + # field_type='textarea', + # required=False, + # order=3, + # is_predefined=True + # ) \ No newline at end of file diff --git a/recruitment/tasks.py b/recruitment/tasks.py index a972cba..3c3adeb 100644 --- a/recruitment/tasks.py +++ b/recruitment/tasks.py @@ -26,11 +26,23 @@ except ImportError: logger = logging.getLogger(__name__) OPENROUTER_API_KEY ='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' -# OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free' -OPENROUTER_MODEL = 'openai/gpt-oss-20b:free' +OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free' + +# OPENROUTER_MODEL = 'openai/gpt-oss-20b:free' # OPENROUTER_MODEL = 'openai/gpt-oss-20b' # OPENROUTER_MODEL = 'mistralai/mistral-small-3.2-24b-instruct:free' +# from google import genai + +# client = genai.Client(api_key="AIzaSyDkwYmvRe5ieTjQi1ClSzD5z5roTwaFsmY") + +# def google_ai(text): +# response = client.models.generate_content( +# model="gemini-2.5-flash", contents=text +# ) +# return response + + if not OPENROUTER_API_KEY: logger.warning("OPENROUTER_API_KEY not set. Resume scoring will be skipped.") @@ -100,6 +112,43 @@ def extract_text_from_document(file_path): else: raise ValueError(f"Unsupported file type: {file_ext}. Only .pdf and .docx files are supported.") +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} + + **QUALIFICATIONS:** + {job_posting.qualifications} + + **STRICT JSON OUTPUT INSTRUCTIONS:** + Output a single, valid JSON object with ONLY the following two top-level keys: + + 'job_description': 'A HTML containing the formatted job description', + 'job_qualifications': 'A HTML containing the formatted job qualifications', + + + Do not include any other text except for the JSON output. + """ + result = ai_handler(prompt) + + 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}") + return + data = result['data'] + if isinstance(data, str): + 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']) + + def ai_handler(prompt): print("model call") response = requests.post( @@ -133,143 +182,6 @@ def ai_handler(prompt): return {"status": "error", "data": response.json()} -# def handle_reume_parsing_and_scoring(pk): -# from django.db import transaction - -# logger.info(f"Scoring resume for candidate {pk}") -# instance = Candidate.objects.get(pk=pk) -# try: -# file_path = instance.resume.path -# with transaction.atomic(): -# 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 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. 'years_of_experience': The total number of years of professional experience mentioned in the resume as a numerical value (e.g., 6.5). -# 5. 'criteria_checklist': An object where you rate the candidate's match for each specific criterion (e.g., {{'Python': 'Met', 'AWS': 'Not Mentioned'}}). -# 6. 'criteria_checklist': An object where you rate the candidate's match for each specific criterion (e.g., {{'Python': 'Met', 'AWS': 'Not Mentioned'}}). -# 7. 'category': Based on the content provided, determine the most fitting professional field or category for the individual. (e.g., {{"category" : "Data Science"}}) only output the category name and no other text example ('Software Development', 'correct') , ('Software Development and devops','wrong'). -# 8. 'most_recent_job_title': The candidate's most recent or current professional job title. -# 9. 'recommendation': Provide a recommendation for the candidate (e.g., {{"recommendation": " -# Conclusion and Minor Considerations -# Overall Assessment: Highly Recommended Candidate. - -# [Candidate] is an exceptionally strong candidate for this role. His proven track record with the core technology stack (Django, Python, Docker, CI/CD) and relevant experience in large-scale, high-impact enterprise projects (Telecom BPM/MDM) make him an excellent technical fit. His fluency in Arabic and English directly addresses a major non-negotiable requirement. - -# The only minor area not explicitly mentioned is the mentoring aspect, but his senior level of experience and technical breadth strongly suggest he possesses the capability to mentor junior engineers. - -# The hiring manager should move forward with this candidate with high confidence. -# ."}}). -# 10. 'top_3_keywords': A list of the three most dominant and relevant technical skills or technologies from the resume that match the job criteria. -# 11. 'job_fit_narrative': A single, concise sentence summarizing the core fit. -# 12. 'language_fluency': A list of languages and their fluency levels mentioned. -# 13. 'screening_stage_rating': A standardized rating (e.g., "A - Highly Qualified", "B - Qualified"). -# 14. 'min_req_met_bool': Boolean (true/false) indicating if all non-negotiable minimum requirements are met. -# 15. 'soft_skills_score': A score (0-100) for inferred non-technical skills like leadership and communication. -# 16. 'experience_industry_match': A score (0-100) for the relevance of the candidate's industry experience. - -# Only output valid JSON. Do not include any other text. -# """ - -# resume_scoring_result = ai_handler(resume_scoring_prompt) - -# print(resume_scoring_result) - -# instance.parsed_summary = str(resume_parser_result) - - -# # Core Scores -# instance.set_field('match_score', resume_scoring_result.get('match_score', 0)) # Set default for int -# instance.set_field('years_of_experience', resume_scoring_result.get('years_of_experience', 0.0)) # Set default for float -# instance.set_field('soft_skills_score', resume_scoring_result.get('soft_skills_score', 0)) -# instance.set_field('experience_industry_match', resume_scoring_result.get('experience_industry_match', 0)) - -# # Screening & Funnel -# instance.set_field('min_req_met_bool', resume_scoring_result.get('min_req_met_bool', False)) # Set default for bool -# instance.set_field('screening_stage_rating', resume_scoring_result.get('screening_stage_rating', 'N/A')) -# instance.set_field('most_recent_job_title', resume_scoring_result.get('most_recent_job_title', 'N/A')) -# instance.set_field('top_3_keywords', resume_scoring_result.get('top_3_keywords', [])) # Set default for list - -# # Summaries & Narrative -# instance.set_field('strengths', resume_scoring_result.get('strengths', '')) -# instance.set_field('weaknesses', resume_scoring_result.get('weaknesses', '')) -# instance.set_field('job_fit_narrative', resume_scoring_result.get('job_fit_narrative', '')) -# instance.set_field('recommendation', resume_scoring_result.get('recommendation', '')) - -# # Structured Data -# instance.set_field('criteria_checklist', resume_scoring_result.get('criteria_checklist', {})) # Set default for dict -# instance.set_field('language_fluency', resume_scoring_result.get('language_fluency', [])) # Set default for list - -# instance.set_field('category', resume_scoring_result.get('category', 'Uncategorized')) # Use 'category' key - -# instance.is_resume_parsed = True - -# instance.save(update_fields=['ai_analysis_data', 'is_resume_parsed','parsed_summary']) - -# logger.info(f"Successfully scored resume for candidate {instance.id}") - -# except Exception as e: -# instance.is_resume_parsed = False -# instance.save(update_fields=['is_resume_parsed']) -# logger.error(f"Failed to score resume for candidate:{instance.pk} {e}") - def safe_cast_to_float(value, default=0.0): """Safely converts a value (int, float, or string) to a float.""" if isinstance(value, (int, float)): diff --git a/recruitment/urls.py b/recruitment/urls.py index a30f8dd..3940ee8 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -62,7 +62,7 @@ urlpatterns = [ # Form Preview URLs # path('forms/', views.form_list, name='form_list'), path('forms/builder/', views.form_builder, name='form_builder'), - path('forms/builder//', views.form_builder, name='form_builder'), + 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'), @@ -82,8 +82,8 @@ urlpatterns = [ path('htmx//candidate_update_status/', views.candidate_update_status, name='candidate_update_status'), - path('forms/form//submit/', views.submit_form, name='submit_form'), - path('forms/form//', views.form_wizard_view, name='form_wizard'), + 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/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'), @@ -96,6 +96,11 @@ urlpatterns = [ # path('api/forms/save/', views.save_form_builder, name='save_form_builder'), # path('api/forms//load/', views.load_form, name='load_form'), # path('api/forms//update/', views.update_form_builder, name='update_form_builder'), + path('api/templates/', views.list_form_templates, name='list_form_templates'), + path('api/templates/save/', views.save_form_template, name='save_form_template'), + path('api/templates//', views.load_form_template, name='load_form_template'), + path('api/templates//delete/', views.delete_form_template, name='delete_form_template'), + path('jobs//calendar/', views.interview_calendar_view, name='interview_calendar'), path('jobs//calendar/interview//', views.interview_detail_view, name='interview_detail'), diff --git a/recruitment/views.py b/recruitment/views.py index 1d3e901..4d1d2fb 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -347,7 +347,6 @@ def edit_job(request, slug): def job_detail(request, slug): """View details of a specific job""" job = get_object_or_404(JobPosting, slug=slug) - # Get all candidates for this job, ordered by most recent applicants = job.candidates.all().order_by("-created_at") @@ -633,12 +632,12 @@ def application_success(request,slug): @ensure_csrf_cookie @login_required -def form_builder(request, template_id=None): +def form_builder(request, template_slug=None): """Render the form builder interface""" context = {} - if template_id: + if template_slug: template = get_object_or_404( - FormTemplate, id=template_id, created_by=request.user + FormTemplate, slug=template_slug, created_by=request.user ) context['template']=template context["template_id"] = template.id @@ -762,7 +761,7 @@ def load_form_template(request, template_id): def form_templates_list(request): """List all form templates for the current user""" query = request.GET.get("q", "") - templates = FormTemplate.objects.filter(created_by=request.user) + templates = FormTemplate.objects.filter() if query: templates = templates.filter( @@ -802,7 +801,7 @@ def create_form_template(request): @require_http_methods(["GET"]) def list_form_templates(request): """List all form templates for the current user""" - templates = FormTemplate.objects.filter(created_by=request.user).values( + templates = FormTemplate.objects.filter().values( "id", "name", "description", "created_at", "updated_at" ) return JsonResponse({"success": True, "templates": list(templates)}) @@ -812,26 +811,25 @@ def list_form_templates(request): @require_http_methods(["DELETE"]) def delete_form_template(request, template_id): """Delete a form template""" - template = get_object_or_404(FormTemplate, id=template_id, created_by=request.user) + template = get_object_or_404(FormTemplate, id=template_id) template.delete() return JsonResponse( {"success": True, "message": "Form template deleted successfully!"} ) -def form_wizard_view(request, template_id): +def form_wizard_view(request, template_slug): """Display the form as a step-by-step wizard""" - template = get_object_or_404(FormTemplate, pk=template_id, is_active=True) + template = get_object_or_404(FormTemplate, slug=template_slug, is_active=True) job_id = template.job.internal_job_id job=template.job - is_limit_exceeded=job.is_application_limit_reached + is_limit_exceeded = job.is_application_limit_reached if is_limit_exceeded: messages.error( request, 'Application limit reached: This job is no longer accepting new applications. Please explore other available positions.' ) return redirect('job_detail_candidate',slug=job.slug) - if job.is_expired: messages.error( request, @@ -842,14 +840,15 @@ def form_wizard_view(request, template_id): return render( request, "forms/form_wizard.html", - {"template_id": template_id, "job_id": job_id}, + {"template_slug": template_slug, "job_id": job_id}, ) @require_POST -def submit_form(request, template_id): +def submit_form(request, template_slug): """Handle form submission""" - template = get_object_or_404(FormTemplate, id=template_id) + template = get_object_or_404(FormTemplate, slug=template_slug) + job = template.job if request.method == "POST": try: with transaction.atomic(): diff --git a/templates/forms/form_list.html b/templates/forms/form_list.html index e77425c..9421e68 100644 --- a/templates/forms/form_list.html +++ b/templates/forms/form_list.html @@ -74,17 +74,17 @@