From c5c7963df55c31b13324e5ac377850f4170b809a Mon Sep 17 00:00:00 2001 From: ismail Date: Tue, 7 Oct 2025 16:23:58 +0300 Subject: [PATCH] update the form builder --- .../__pycache__/settings.cpython-313.pyc | Bin 4955 -> 5152 bytes NorahUniversity/settings.py | 4 +- recruitment/__pycache__/admin.cpython-313.pyc | Bin 11317 -> 11317 bytes .../erp_integration_service.cpython-313.pyc | Bin 13176 -> 13176 bytes recruitment/__pycache__/forms.cpython-313.pyc | Bin 16944 -> 18777 bytes .../linkedin_service.cpython-313.pyc | Bin 11109 -> 11109 bytes .../__pycache__/models.cpython-313.pyc | Bin 36795 -> 37614 bytes .../__pycache__/signals.cpython-313.pyc | Bin 5567 -> 10965 bytes recruitment/__pycache__/urls.cpython-313.pyc | Bin 5954 -> 6071 bytes recruitment/__pycache__/views.cpython-313.pyc | Bin 27573 -> 28898 bytes .../views_frontend.cpython-313.pyc | Bin 19511 -> 19442 bytes .../views_integration.cpython-313.pyc | Bin 8310 -> 8310 bytes recruitment/forms.py | 45 +- recruitment/linkedin_service.py | 79 +- ...ield_max_files_formfield_multiple_files.py | 23 + .../0020_delete_job.cpython-313.pyc | Bin 658 -> 658 bytes ...ource_description_and_more.cpython-313.pyc | Bin 4812 -> 4812 bytes ...2_alter_source_trusted_ips.cpython-313.pyc | Bin 961 -> 961 bytes ...g_application_url_and_more.cpython-313.pyc | Bin 1265 -> 1265 bytes ...ieldresponse_slug_and_more.cpython-313.pyc | Bin 4902 -> 4902 bytes recruitment/models.py | 26 +- recruitment/signals.py | 304 ++++++- recruitment/urls.py | 1 + recruitment/views.py | 30 +- templates/forms/create_form_template.html | 208 +++++ templates/forms/form_builder.html | 800 +++++++++++------- templates/forms/form_templates_list.html | 149 +++- 27 files changed, 1249 insertions(+), 420 deletions(-) create mode 100644 recruitment/migrations/0025_formfield_max_files_formfield_multiple_files.py create mode 100644 templates/forms/create_form_template.html diff --git a/NorahUniversity/__pycache__/settings.cpython-313.pyc b/NorahUniversity/__pycache__/settings.cpython-313.pyc index 8662d9d2012d1de5b120753dc444a0f57d29341e..345c317d215461bcf974bcc9c6626617c4b4a710 100644 GIT binary patch delta 272 zcmcbuwm^gLGcPX}0}xyke44=|K9Ns?F=nHBAd^6_b&*A|O_61=ZIM+vlcwF~8m7}+ zDYy76%*?aOi!&=za}7-mOsW*a1AO8g0}S;7{X9%_BJ^$m^8^$dW}(8|KVz(7AMKdD$hCo?ZQH6=4oKRGccCn+>VK<9vnOvV zv-2$>A5TATR~JvecxN9^SHF;WPnTO_=wiXH&Oxprw?yIMK|mcruFfIxp+TOL*9io3 t6$1Un2*kxPo4Ex&nWP)|Kd>=~sa;?Yx*=(EfkEtsg!@GX(V~2y5&)qUP-OrB delta 74 zcmZ3Waa)b=GcPX}0}yl^dz=v}Hjz()F<_&5AQMYElcx3N4yMywOqv#(xAUelPmUK1 Z=E?=CWCY@3kIj1oJ(<`W`HQlFTmT8g6B7Uc diff --git a/NorahUniversity/settings.py b/NorahUniversity/settings.py index bc1e0ff..3a40e03 100644 --- a/NorahUniversity/settings.py +++ b/NorahUniversity/settings.py @@ -212,8 +212,6 @@ CELERY_RESULT_SERIALIZER = 'json' CELERY_TIMEZONE = 'UTC' - - LINKEDIN_CLIENT_ID = '867jwsiyem1504' LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw==' -LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/jobs/linkedin/callback/' +LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/jobs/linkedin/callback/' \ No newline at end of file diff --git a/recruitment/__pycache__/admin.cpython-313.pyc b/recruitment/__pycache__/admin.cpython-313.pyc index 149ef4d021f0e08e211a99cd955015875ba06858..87dcd283ed4b296bc5b5ea1bc57b7bf4928b4f1c 100644 GIT binary patch delta 20 acmdlQu{DDGGcPX}0}#Z%d$N&RO9uc*4hDVz delta 20 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 Q(~cZV9ZCI5U?N}Ve@`CIt^fc4 delta 2897 zcmai0eQXp(6z^QG*SpeQdtQOIv}I{~z}glnVkuC*=?Bo3kMbo!j^%D?7nj{SyQOI% zKm)-T5)v382GsZ!V?hyF6%~ztAPI>^qXrXJ6GJrNFHvF)ME}tF&Gu-)Q!eSRZ{EE3 zX6E@i%9d)DOQ1$QIt`U(HSbq=*-zn*3_`U z!YErng8Acxehk;=-lH!P`aG@=-=i-U`h2c0xJO?i^bxLqXp}zW?~2NW>?N{?_*t2+ z-cfvmK7TRaAG_`^W`FysT*U`}j@fR~owb&#s|j1qqA;K(fC+$7Ksh^^^)iuJV_+E> z#|{Oq1ZF`!+gY2v*VkCY<+W~*s!Gx{^nO~w$3w&oB`hnR&=t!{9}^MESUBfE#dI#Z zfj%wSrP_1~^dUeV0EE&y=Hx66G(c=}{>q7xjaYw(&}RG#(uXlq4X6Rs0%il60QDT) zytucQgsSWDICt&jT8nU4NJZc%OrvajZk2bqC>x9wJ43m1NP9V-8W`K5_DPF+dUP$J z*qUibowm}YQcy^z0wx1w4jpn{+8yP?U>X}-5|uL{&ICLLzz&5g6}*c5i2Xmo^zgD2 z*%!k}A1P;2q_A)+9F_ubLf6;xB+pT6WNJ)MnVsl1wS;QX6|kt|AP4D6$g2RW+3`rM z0*9hq0Kr6%OOp57?-A1E{1Ay}@&y~mo+oYW*Rdr{2t~I75&(p7BMHTb+f*?u4Yb5< zlP00}?=Uq(_KS7hkfHX)EkRf}>ljx?8XSFGkZi(nWg_SYG>pGaM2i%{Eekl8yTt0lK z`o*2n%kPjPk%lo|8f?wh)fD}EBsl>3X^fqbL%GOUh zJhntdMcd7a8G&N`=_YKlI9rQ@t8hlV0n3gDtPF-6Af*5W=CU$v&^aeGJ8Ez!?s1V6kc0ra|Atz}LQrzQu)s zymf|Vt4TCQTeXH=-K<@T-mB8Ha6ZYdR&N`@Iar`E$h+#J49jYl!5Mc-qgX{-0W59bSj4*^~kM$%Sc1P0Rp=)lyw z^N>W7e;(2c9Nf@SzRhJuPp?g%fies5A8jEx{D{5oT%Y!xFVz@6dE^Y)cNuQjl$*7b zD%)-P0p!3MtzSuYiyRH)djn||HFs%7LKVpx$n8{mlbW=MD%&(gq>Vof+Ys#v;BLvf zLCxU(8h!~(h*OE@1SMMVuNuAm=p{1Cxj6GMAs1Oo{knh&uQv8dePwkIM9N|KH4yo5 zGx=-4TddM$rGiPc!TGJez~|NLB^H>SnBj%tzHX+j8uV*~667K%FR_(SpuR012Z@JZ zr)IxMX0bVQej#Vs)VT|auZ^-q6^>Z$pUb#qVdE`cNP&5crFR#j+1zf_I+=7O+cK|| zzgoNJy+#6}Bpeo7(9#0J+^mGIDh6$X98!|WxJs!>`L~qVorvnWAZjNE3u))QG}TOR zFlWKueQ2iqF+?KdaM~*C<5Qy^!mAj55BL%AGvEfG9U?^OPvFDL>Sdp|PV_f_o6*c}wyqrWN5)|Up delta 73 zcmaDF_B4#|GcPX}0}#wO`Z!~&#zww&en#KT)A-jjF=lOc6N(gItk}Fj=@1*^-_6OI ZT8xYull!zz@K-T2ihN=Kkwv0Fy#T2r7P0^U diff --git a/recruitment/__pycache__/models.cpython-313.pyc b/recruitment/__pycache__/models.cpython-313.pyc index 0a696c750c5e4485452fd1fd606795b684c46549..c673a1c5ee53944078f2cc258d731f6dc8cb9f43 100644 GIT binary patch delta 9663 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 diff --git a/recruitment/__pycache__/signals.cpython-313.pyc b/recruitment/__pycache__/signals.cpython-313.pyc index 6369bd07181631a1251473436dd13ace2d80ece7..df8b0cd633c1060f77bc5bdfd9539192876cd510 100644 GIT binary patch literal 10965 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 T#8-81_&oJAJZLwWj@ADkApHKy delta 599 zcmZ8dJ8KkC6uxI(J2TmteW)bt%3^|ICNbMZHlTtiTg+6+pja$~VP{B2_LXyYBLQKP zN(&nq#1vMs62T@vKrGzOB87+pTUn(`*j_wWw~3dT?|$ETeD`p^Kl;3EPb|x1bPm7% z+Aht$w&#XNq~0)}`QR}Z5UvR>d6FxmOkDOAPjOXL|Hf5ML#?50*uvxDSzrx)75>H0 zAP?0u(WHLOn{l(4buF}}9yZzynd+gTZal7Bv=PeBtVzCJVyn{>=C@SGI2Fo))aZ42 zf0GBh5eXfmW)$y6cq*QzfY$DYQHv)B@C5dg6w1&%{teG^H?x#SOF&LQo*=b*0rDu^ z0MsRLai==`TymPumrj3T=pOLM=!EjahyoSdgSGZ5pXc1cfbH#RBoh z!GmTm96Z#6k&B6jgB;P5CLU%^8%eln;&F_gj0b0D@ZiD8?wg(OdB1nQZ03FVZAAPb ziuD3rbHBgby!}*^ESEvX^YCw|KN3ntpha8On?61#KaJ zZ?Ra`3(b;6JFAzQGx2Ha0Q4J9b)Q1!4~YBfFI8wG0}stZYT@-Z(S0FjGg>W%y&* zA43BZ7)}z2u~xQr%xw-~e z@EBm@b?sUWWfUu;Xe^CkhKvJ7r%bxtwqrF~6x^bq$Pk7}GEHMvUFHvX>^hxe#`JLv zQsRa(iIqt-vw&fdWOz>YKX{(w9LE79#|(J_%M&Pd3&SOnr$;oqeN@9t$KAdycH9zg z@K<9-a1aLv(FHd3GLKu~RM|acgpxRvJlMc+maKB9aTs~+GWtev-^hnH4ChFR!!-_v z4qV1?4iD#0aTCKWa+lNll*Z|0kal8*H;TPcl$gSBnmpimoj12vtwv`*?(9cHDGX=G cLryC?@VIRoe6y){c--O^+dOK}F4K$p2k4LjtpET3 delta 711 zcmXAn%}*0S6u@`sl#kW|?RK}VU6uw5wAkG$E=diVf=TTXNrPf90!2;GDh9E{Z-JP2 z=*0xxnDizbj5S_RPVMEc}V1~ z!c{=muxgmGpi|{caDZ?z=u~+@`GIne@{lqt4iTHByg~U=>?1q7qAb%hF7fn`*!JLi zZAWWt06LFnNx*?POz4%Ab{aNs_DUhw0@<396vDD3FHlit`N!);Tg`%+ZLI(*;#)kG zw;fpxZ;jbP3WSt($^bNlmzi8)nTh(KJu(3!6RmY8^DdLC=08c?yvG(57MX2{_GAer zOV$#(<|bZa_5rhz+L#^HA*x&BGk`AQhs-`=R@rvju?)mAR(2N9W&D`gC(P2@=RhUD ii+J-XZ9cEN7V)Z diff --git a/recruitment/__pycache__/views.cpython-313.pyc b/recruitment/__pycache__/views.cpython-313.pyc index 0273c215d8111963a128551fe6536375e588b872..dc13da4d3a8a5fa258dc22706fb43fb0de11d90a 100644 GIT binary patch delta 7170 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 delta 1459 zcmZuwS!`QX5WVx3*nVD;##tQ4j-M?z&FYXkOWmw)k~#~8;G2hnEH$-=LsODh`W%Vn zgA|F6AVd{1s@fkuir}Z}qGAvdNR_G<(TX2bMF>&>p&}B6eo%^#_(0;`=R8G)TJo8> zcV^Da+?n^s8Tj-pcpkdl3Sqy<rorZv_st*m0&w-s33R2+;_Ys7yP`r{sEPQaFMz4$Qc+C% z@m#gn;gMM;T{6=IA%N{o-sL(g`Hu9GXXU)mM)HD}jM@k}u2JBTlfofZf|E69a zJ5skX`<|V%rD>AI-mrZ&TltvU$~BvZP_HI2tcc6SD)<<9R*S1BeO%}^UeH3hWnylP z_><3H&c-g%N?KLqGiz>ZtqfMj<^MDBV0PP#+1A>Gi~3?Ei*C)OxjO_}<0r@Lx;@is zCD7~<(z$)GI*PJfQ!c#dt4l{15gRa2e@t9-Ke7|GQ|?Z&7#U^5Q`uja=SY}jViviL zWDl~Q9Bg|76FyzhlysbTv#dKaR#HNPnv+k+%C0`TdUb4lLz!-iQgc{-+f!BfO6G+S zp#j~IbVRr19L$kEF+}E|G5lJ1G^@`p?nn7iin60GIIR_5{Pay|02;IJ>t2T!9T~X?`(hoac_$;_Yw|AI7_wPgBS9ZjaHz zu8rq7oX680-OZmdbpvw7O)lLz!#Hlt%;uJ|^9y<79CsUJkY^tnQSq;i=D=;Heao*d zv%3C1Nwy^acamcz{O0KILLuJyq43mNL#m&A-pSxvd$Ng%>$RR#Lb~2q&-5E*$!aFP zXh=3Q@nuDnV?yTGuZ(%boA#bDhj>c_rr&bNV;-#Ew*qS6^1gQks4paYRsp*4cJGVI zIxE89pkk`87uKLm$=;j=&w${Z#k` zygyh0V=Uw?_yJEvx|B`s$YBh>N)Es#Iu9&B+T>h2FkgC?n>nP}cb*U8k>N@V4ef%v zjPMnhHvMi4y`yY#KMvEF8E%6u{C;;$;e+9;Lg*K6xyPZFLyp6*9Dd_)AKyr|$Pbu0 xQ}`%F1^5I@#z#W@+0Aav9?bg8D$Eqd13We!gntSv<09Bb08YT4leji<Ue!P);8#4etf(A1H diff --git a/recruitment/migrations/__pycache__/0023_alter_jobposting_application_url_and_more.cpython-313.pyc b/recruitment/migrations/__pycache__/0023_alter_jobposting_application_url_and_more.cpython-313.pyc index 5049dfb72a86436b610f0354e08c3b8b562eb6d3..fbfeec0f848455d689aacf089b6e1d97a8b55a66 100644 GIT binary patch delta 20 acmey!`H_?RGcPX}0}#Z%d$N)H84CbMDhBKT delta 20 acmey!`H_?RGcPX}0}v!#dAyPP84CbLtOnKq diff --git a/recruitment/migrations/__pycache__/0024_fieldresponse_created_at_fieldresponse_slug_and_more.cpython-313.pyc b/recruitment/migrations/__pycache__/0024_fieldresponse_created_at_fieldresponse_slug_and_more.cpython-313.pyc index 714a9d768b1e287bd8502740585690825e7b55e4..f5967821110625cb9c7ce0b57ccdfad4582ba3fd 100644 GIT binary patch delta 19 ZcmZ3cwoHxdGcPX}0}#Z%+sGv&3;;F@1n&R< delta 19 ZcmZ3cwoHxdGcPX}0}v>@+{h&(3;;BQ1g!u7 diff --git a/recruitment/models.py b/recruitment/models.py index 973898d..2619a0b 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _ from django_extensions.db.fields import RandomCharField from django.core.exceptions import ValidationError from django_countries.fields import CountryField +from django.urls import reverse class Base(models.Model): created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created at')) @@ -167,6 +168,12 @@ class JobPosting(Base): return self.application_deadline < timezone.now().date() return False + def publish(self): + self.status = 'PUBLISHED' + self.published_at = timezone.now() + self.application_url = reverse('form_wizard', kwargs={'slug': self.form_template.slug}) + self.save() + class Candidate(Base): class Stage(models.TextChoices): @@ -339,6 +346,7 @@ class FormTemplate(Base): return sum(stage.fields.count() for stage in self.stages.all()) + class FormStage(Base): """ Represents a stage/section within a form template @@ -402,15 +410,20 @@ class FormField(Base): default=5, help_text="Maximum file size in MB (default: 5MB)" ) + multiple_files = models.BooleanField( + default=False, + help_text="Allow multiple files to be uploaded" + ) + max_files = models.PositiveIntegerField( + default=1, + help_text="Maximum number of files allowed (when multiple_files is True)" + ) class Meta: ordering = ['order'] verbose_name = 'Form Field' verbose_name_plural = 'Form Fields' - def __str__(self): - return f"{self.stage.name} - {self.label}" - def clean(self): # Validate options for selection fields if self.field_type in ['select', 'radio', 'checkbox']: @@ -427,11 +440,18 @@ class FormField(Base): self.file_types = '.pdf,.doc,.docx' if self.max_file_size <= 0: raise ValidationError("Max file size must be greater than 0") + if self.multiple_files and self.max_files <= 0: + raise ValidationError("Max files must be greater than 0 when multiple files are allowed") + if not self.multiple_files: + self.max_files = 1 else: # Clear file settings for non-file fields self.file_types = '' self.max_file_size = 0 + self.multiple_files = False + self.max_files = 1 + # Validate order if self.order < 0: raise ValidationError("Order must be a positive integer") diff --git a/recruitment/signals.py b/recruitment/signals.py index 69f2b2d..912989d 100644 --- a/recruitment/signals.py +++ b/recruitment/signals.py @@ -1,6 +1,9 @@ -from django.db.models.signals import post_save -from django.dispatch import receiver from . import models +from django.urls import reverse +from django.db import transaction +from django.dispatch import receiver +from django.db.models.signals import post_save +from .models import FormField,FormStage,FormTemplate # @receiver(post_save, sender=models.Candidate) # def parse_resume(sender, instance, created, **kwargs): @@ -19,8 +22,6 @@ import os from .utils import extract_text_from_pdf,score_resume_with_openrouter import asyncio - - @receiver(post_save, sender=models.Candidate) def score_candidate_resume(sender, instance, created, **kwargs): # Skip if no resume or OpenRouter not configured @@ -105,9 +106,9 @@ def score_candidate_resume(sender, instance, created, **kwargs): Only output valid JSON. Do not include any other text. """ - - result1 = score_resume_with_openrouter(prompt) - + + result1 = score_resume_with_openrouter(prompt) + instance.parsed_summary = str(result) # Update candidate with scoring results @@ -115,8 +116,8 @@ def score_candidate_resume(sender, instance, created, **kwargs): instance.strengths = result1.get('strengths', '') instance.weaknesses = result1.get('weaknesses', '') instance.criteria_checklist = result1.get('criteria_checklist', {}) - - + + # Save only scoring-related fields to avoid recursion instance.save(update_fields=[ @@ -131,10 +132,291 @@ def score_candidate_resume(sender, instance, created, **kwargs): # 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): - \ No newline at end of file + +@receiver(post_save, sender=FormTemplate) +def create_default_stages(sender, instance, created, **kwargs): + """ + Create default resume stages when a new FormTemplate is created + """ + if created: # Only run for new templates, not updates + with transaction.atomic(): + # Stage 1: Contact Information + contact_stage = FormStage.objects.create( + template=instance, + name='Contact Information', + order=0, + is_predefined=True + ) + FormField.objects.create( + stage=contact_stage, + label='Full Name', + field_type='text', + required=True, + order=0, + is_predefined=True + ) + FormField.objects.create( + stage=contact_stage, + label='Email Address', + field_type='email', + required=True, + order=1, + is_predefined=True + ) + FormField.objects.create( + stage=contact_stage, + label='Phone Number', + field_type='phone', + required=True, + order=2, + is_predefined=True + ) + FormField.objects.create( + stage=contact_stage, + label='Address', + field_type='text', + required=False, + order=3, + is_predefined=True + ) + FormField.objects.create( + stage=contact_stage, + label='Resume Upload', + field_type='file', + required=True, + order=4, + is_predefined=True, + file_types='.pdf,.doc,.docx', + max_file_size=5 + ) + + # 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 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 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 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/urls.py b/recruitment/urls.py index 4d7514d..a1c2404 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -57,6 +57,7 @@ urlpatterns = [ 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'), path('forms/form//', views.form_wizard_view, name='form_wizard'), path('forms/form//submit/', views.submit_form, name='submit_form'), diff --git a/recruitment/views.py b/recruitment/views.py index 2f26c1b..b671f81 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -7,8 +7,9 @@ from datetime import datetime from django.views import View from django.db.models import Q from django.urls import reverse +from django.conf import settings from django.utils import timezone -from .forms import ZoomMeetingForm,JobPostingForm +from .forms import ZoomMeetingForm,JobPostingForm,FormTemplateForm from rest_framework import viewsets from django.contrib import messages from django.core.paginator import Paginator @@ -20,8 +21,8 @@ from django.shortcuts import get_object_or_404, render, redirect from django.views.generic import CreateView,UpdateView,DetailView,ListView from .utils import create_zoom_meeting, delete_zoom_meeting, update_zoom_meeting from django.views.decorators.csrf import ensure_csrf_cookie - import logging + logger=logging.getLogger(__name__) @@ -321,6 +322,7 @@ def linkedin_callback(request): access_token=service.get_access_token(code) request.session['linkedin_access_token']=access_token request.session['linkedin_authenticated']=True + settings.LINKEDIN_IS_CONNECTED = True messages.success(request,'Successfully authenticated with LinkedIn!') except Exception as e: logger.error(f"LinkedIn authentication error: {e}") @@ -685,10 +687,11 @@ def load_form_template(request, template_id): 'id': template.id, 'name': template.name, 'description': template.description, + 'is_active': template.is_active, + 'job': template.job_id if template.job else None, 'stages': stages } }) - def form_templates_list(request): """List all form templates for the current user""" query = request.GET.get('q', '') @@ -703,13 +706,32 @@ def form_templates_list(request): paginator = Paginator(templates, 10) # Show 10 templates per page page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) - + form = FormTemplateForm() + form.fields['job'].queryset = JobPosting.objects.filter(form_template__isnull=True) context = { 'templates': page_obj, 'query': query, + 'form': form } return render(request, 'forms/form_templates_list.html', context) + +def create_form_template(request): + """Create a new form template""" + if request.method == 'POST': + form = FormTemplateForm(request.POST) + if form.is_valid(): + template = form.save(commit=False) + template.created_by = request.user + template.save() + + messages.success(request, f'Form template "{template.name}" created successfully!') + return redirect('form_builder', template_id=template.id) + else: + form = FormTemplateForm() + + return render(request, 'forms/create_form_template.html', {'form': form}) + @require_http_methods(["GET"]) def list_form_templates(request): """List all form templates for the current user""" diff --git a/templates/forms/create_form_template.html b/templates/forms/create_form_template.html new file mode 100644 index 0000000..03688e3 --- /dev/null +++ b/templates/forms/create_form_template.html @@ -0,0 +1,208 @@ +{% extends 'base.html' %} +{% load static i18n %} +{% load crispy_forms_tags %} + +{% block title %}Create Form Template - ATS{% endblock %} + +{% block customCSS %} + +{% endblock %} + +{% block content %} +
+
+

+ Create Form Template +

+ + Back to Templates + +
+ +
+
+
+
+

New Form Template

+
+
+
+ {% csrf_token %} + {{ form|crispy }} +
+ + Cancel + + +
+
+
+
+
+
+
+ +{% if messages %} + {% for message in messages %} + + {% endfor %} +{% endif %} +{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/templates/forms/form_builder.html b/templates/forms/form_builder.html index 842bc4e..a3217a6 100644 --- a/templates/forms/form_builder.html +++ b/templates/forms/form_builder.html @@ -12,21 +12,23 @@ --primary: #004a53; /* Deep Teal/Cyan for main actions */ --primary-light: #00b4d8; /* Brighter Aqua/Cyan */ --secondary: #005a78; /* Darker Teal for hover/accent */ + --success: #00cc99; /* Bright Greenish-Teal for success */ + --success: #005a78; /* Bright Greenish-Teal for success */ - + /* Neutral Colors (Kept for consistency) */ --light: #f4fcfc; /* Very light off-white (slightly blue tinted) */ --dark: #212529; /* Near black text */ --gray: #6c757d; /* Standard gray text */ --light-gray: #e0f0f4; /* Lighter background for hover/disabled */ --border: #c4d7e0; /* Lighter, softer border color */ - + /* Structural Variables (Kept exactly the same) */ --shadow: 0 4px 6px rgba(0, 0, 0, 0.1); --radius: 8px; --transition: all 0.3s ease; } - /* All other structural and component styles below remain the same, + /* All other structural and component styles below remain the same, but will automatically adopt the new colors defined above. */ * { margin: 0; @@ -670,18 +672,20 @@ - +
+

File Settings

+
+ + +
+
+ + +
+
+
+ + +
+ Enable this to allow uploading multiple files for this field. +
+
+ + + Only applicable when multiple files are allowed. +
+
@@ -1156,97 +1182,109 @@ // API Functions async function saveFormTemplate() { - const formData = { - name: state.formName, - description: state.formDescription, - is_active: state.formActive, - template_id: state.templateId, // Include template_id for updates - stages: state.stages.map(stage => ({ - name: stage.name, - predefined: stage.predefined, - fields: stage.fields.map(field => ({ - type: field.type, - label: field.label, - placeholder: field.placeholder || '', - required: field.required || false, - options: field.options || [], - fileTypes: field.fileTypes || '', - maxFileSize: field.maxFileSize || 5, - predefined: field.predefined - })) - })) - }; + const formData = { + name: state.formName, + description: state.formDescription, + is_active: state.formActive, + template_id: state.templateId, + stages: state.stages.map(stage => ({ + name: stage.name, + predefined: stage.predefined, + fields: stage.fields.map(field => ({ + type: field.type, + label: field.label, + placeholder: field.placeholder || '', + required: field.required || false, + options: field.options || [], + fileTypes: field.fileTypes || '', + maxFileSize: field.maxFileSize || 5, + predefined: field.predefined + })) + })) + }; - try { - const response = await fetch(djangoConfig.saveUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': djangoConfig.csrfToken, - 'X-Requested-With': 'XMLHttpRequest' - }, - body: JSON.stringify(formData) - }); + // If there's a job_id in the Django context, include it + if (djangoConfig.jobId) { + formData.job = djangoConfig.jobId; + } - const result = await response.json(); + try { + const response = await fetch(djangoConfig.saveUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': djangoConfig.csrfToken, + 'X-Requested-With': 'XMLHttpRequest' + }, + body: JSON.stringify(formData) + }); - if (result.success) { - alert('Form template saved successfully! Template ID: ' + result.template_id); - // Update templateId for future saves (important for new templates) - state.templateId = result.template_id; - } else { - alert('Error saving form template: ' + result.error); - } - } catch (error) { - console.error('Error:', error); - alert('Error saving form template. Please try again.'); - } + const result = await response.json(); + + if (result.success) { + state.templateId = result.template_id; + window.location.href = "{% url 'form_templates_list' %}"; + + } else { + alert('Error saving form template: ' + result.error); } - + } catch (error) { + console.error('Error:', error); + alert('Error saving form template. Please try again.'); + } +} // Load existing template if editing async function loadExistingTemplate() { - if (djangoConfig.loadUrl) { - try { - const response = await fetch(djangoConfig.loadUrl); - const result = await response.json(); + if (djangoConfig.loadUrl) { + try { + const response = await fetch(djangoConfig.loadUrl); + const result = await response.json(); + if (result.success) { + const templateData = result.template; + // Set form settings + state.formName = templateData.name || 'Untitled Form'; + state.formDescription = templateData.description || ''; + state.formActive = templateData.is_active !== false; - if (result.success) { - const templateData = result.template; - // Set form settings - state.formName = templateData.name || 'Untitled Form'; - state.formDescription = templateData.description || ''; - state.formActive = templateData.is_active !== false; // Default to true if not set + // Update form title + elements.formTitle.textContent = state.formName; + elements.formName.value = state.formName; + elements.formDescription.value = state.formDescription; + elements.formActive.checked = state.formActive; - // Update form title - elements.formTitle.textContent = state.formName; - elements.formName.value = state.formName; - elements.formDescription.value = state.formDescription; - elements.formActive.checked = state.formActive; + // Set stages (this is where your actual stages come from) + state.stages = templateData.stages; + state.templateId = templateData.id; - // Set stages - state.stages = templateData.stages; - state.templateId = templateData.id; - // Update next IDs to avoid conflicts - let maxFieldId = 0; - let maxStageId = 0; - templateData.stages.forEach(stage => { - maxStageId = Math.max(maxStageId, stage.id); - stage.fields.forEach(field => { - maxFieldId = Math.max(maxFieldId, field.id); - }); - }); - state.nextFieldId = maxFieldId + 1; - state.nextStageId = maxStageId + 1; - state.currentStage = 0; - renderStageNavigation(); - renderCurrentStage(); - } - } catch (error) { - console.error('Error loading template:', error); - alert('Error loading template data.'); - } + // Update next IDs to avoid conflicts + let maxFieldId = 0; + let maxStageId = 0; + templateData.stages.forEach(stage => { + maxStageId = Math.max(maxStageId, stage.id); + stage.fields.forEach(field => { + maxFieldId = Math.max(maxFieldId, field.id); + }); + }); + state.nextFieldId = maxFieldId + 1; + state.nextStageId = maxStageId + 1; + state.currentStage = 0; + + // Now show the form content + elements.formStage.style.display = 'block'; + elements.emptyState.style.display = 'none'; + + renderStageNavigation(); + renderCurrentStage(); } + } catch (error) { + console.error('Error loading template:', error); + elements.formTitle.textContent = 'Error Loading Template'; + elements.emptyState.style.display = 'block'; + elements.emptyState.innerHTML = '

Error loading template data.

'; + elements.formStage.style.display = 'none'; } + } +} // DOM Rendering Functions (same as before) function renderStageNavigation() { @@ -1319,164 +1357,255 @@ } function createFieldElement(field, index) { - const fieldDiv = document.createElement('div'); - fieldDiv.className = `form-field ${state.selectedField && state.selectedField.id === field.id ? 'selected' : ''}`; - fieldDiv.dataset.fieldId = field.id; - fieldDiv.dataset.fieldIndex = index; - const fieldHeader = document.createElement('div'); - fieldHeader.className = 'field-header'; - fieldHeader.innerHTML = ` -
- - ${field.label || field.type.charAt(0).toUpperCase() + field.type.slice(1)} - ${field.required ? ' *' : ''} -
-
-
- -
- ${!field.predefined ? `
- -
` : ''} -
- `; - const fieldContent = document.createElement('div'); - fieldContent.className = 'field-content'; - fieldContent.innerHTML = ` - - `; - // Add field input based on type - if (field.type === 'text' || field.type === 'email' || field.type === 'phone' || field.type === 'date') { - const input = document.createElement('input'); - input.type = 'text'; - input.className = 'field-input'; - input.placeholder = field.placeholder || 'Enter value'; - input.disabled = true; - fieldContent.appendChild(input); - } else if (field.type === 'textarea') { - const textarea = document.createElement('textarea'); - textarea.className = 'field-input'; - textarea.rows = 3; - textarea.placeholder = field.placeholder || 'Enter text'; - textarea.disabled = true; - fieldContent.appendChild(textarea); - } else if (field.type === 'file') { - const fileUpload = document.createElement('div'); - fileUpload.className = 'file-upload-area'; - fileUpload.innerHTML = ` -
- -
-
-

Drag & drop your resume here or click to browse

-
-
-

Supported formats: ${field.fileTypes || '.pdf, .doc, .docx'} (Max ${field.maxFileSize || 5}MB)

-
- - `; - if (field.uploadedFile) { - const uploadedFile = document.createElement('div'); - uploadedFile.className = 'uploaded-file'; - uploadedFile.innerHTML = ` -
- -
-
${field.uploadedFile.name}
-
${formatFileSize(field.uploadedFile.size)}
-
+ const fieldDiv = document.createElement('div'); + fieldDiv.className = `form-field ${state.selectedField && state.selectedField.id === field.id ? 'selected' : ''}`; + fieldDiv.dataset.fieldId = field.id; + fieldDiv.dataset.fieldIndex = index; + + const fieldHeader = document.createElement('div'); + fieldHeader.className = 'field-header'; + fieldHeader.innerHTML = ` +
+ + ${field.label || field.type.charAt(0).toUpperCase() + field.type.slice(1)} + ${field.required ? ' *' : ''} +
+
+
+ +
+ ${!field.predefined ? `
+ +
` : ''} +
+ `; + + const fieldContent = document.createElement('div'); + fieldContent.className = 'field-content'; + fieldContent.innerHTML = ` + + `; + + // Add field input based on type + if (field.type === 'text' || field.type === 'email' || field.type === 'phone' || field.type === 'date') { + const input = document.createElement('input'); + input.type = 'text'; + input.className = 'field-input'; + input.placeholder = field.placeholder || 'Enter value'; + input.disabled = true; + fieldContent.appendChild(input); + } else if (field.type === 'textarea') { + const textarea = document.createElement('textarea'); + textarea.className = 'field-input'; + textarea.rows = 3; + textarea.placeholder = field.placeholder || 'Enter text'; + textarea.disabled = true; + fieldContent.appendChild(textarea); + } else if (field.type === 'file') { + const fileUpload = document.createElement('div'); + fileUpload.className = 'file-upload-area'; + fileUpload.innerHTML = ` +
+ +
+
+

Drag & drop your ${field.label.toLowerCase()} here or click to browse

+
+
+

Supported formats: ${field.fileTypes || '.pdf, .doc, .docx'} (Max ${field.maxFileSize || 5}MB)

+ ${field.multipleFiles ? `

Multiple files allowed (Max ${field.maxFiles || 1} files)

` : ''} +
+ + `; + + // Show uploaded files + if (field.uploadedFiles && field.uploadedFiles.length > 0) { + field.uploadedFiles.forEach((file, fileIndex) => { + const uploadedFile = document.createElement('div'); + uploadedFile.className = 'uploaded-file'; + uploadedFile.innerHTML = ` +
+ +
+
${file.name}
+
${formatFileSize(file.size)}
- - `; - fileUpload.appendChild(uploadedFile); - } - fieldContent.appendChild(fileUpload); - } else if (field.type === 'select') { - const select = document.createElement('select'); - select.className = 'field-input'; - select.disabled = true; - field.options.forEach(option => { - const optionEl = document.createElement('option'); - optionEl.textContent = option; - select.appendChild(optionEl); - }); - fieldContent.appendChild(select); - } else if (field.type === 'radio' || field.type === 'checkbox') { - const optionsDiv = document.createElement('div'); - optionsDiv.className = 'field-options'; - field.options.forEach((option, idx) => { - const optionItem = document.createElement('div'); - optionItem.className = 'option-item'; - optionItem.innerHTML = ` - - - `; - optionsDiv.appendChild(optionItem); - }); - fieldContent.appendChild(optionsDiv); - } - fieldDiv.appendChild(fieldHeader); - fieldDiv.appendChild(fieldContent); - // Add event listeners - fieldDiv.addEventListener('click', (e) => { - if (!e.target.closest('.edit-field') && !e.target.closest('.remove-field') && - !e.target.closest('.remove-file-btn')) { - selectField(field); - } +
+ + `; + fileUpload.appendChild(uploadedFile); }); - const editBtn = fieldDiv.querySelector('.edit-field'); - if (editBtn) { - editBtn.addEventListener('click', (e) => { - e.stopPropagation(); - selectField(field); - }); - } - const removeBtn = fieldDiv.querySelector('.remove-field'); - if (removeBtn) { - removeBtn.addEventListener('click', (e) => { - e.stopPropagation(); - removeField(parseInt(removeBtn.dataset.fieldIndex)); - }); - } - const removeFileBtn = fieldDiv.querySelector('.remove-file-btn'); - if (removeFileBtn) { - removeFileBtn.addEventListener('click', (e) => { - e.stopPropagation(); - const fieldId = parseInt(fieldDiv.dataset.fieldId); - const stage = state.stages[state.currentStage]; - const field = stage.fields.find(f => f.id === fieldId); - if (field) { - field.uploadedFile = null; - renderCurrentStage(); - } - }); - } - // Make draggable - fieldDiv.draggable = true; - fieldDiv.addEventListener('dragstart', (e) => { - state.draggedFieldIndex = parseInt(fieldDiv.dataset.fieldIndex); - e.dataTransfer.setData('text/plain', 'reorder'); - e.dataTransfer.effectAllowed = 'move'; - }); - fieldDiv.addEventListener('dragover', (e) => { - e.preventDefault(); - }); - fieldDiv.addEventListener('drop', (e) => { - e.preventDefault(); - const targetIndex = parseInt(fieldDiv.dataset.fieldIndex); - dropField(targetIndex); - }); - return fieldDiv; } + fieldContent.appendChild(fileUpload); + } else if (field.type === 'select') { + const select = document.createElement('select'); + select.className = 'field-input'; + select.disabled = true; + field.options.forEach(option => { + const optionEl = document.createElement('option'); + optionEl.textContent = option; + select.appendChild(optionEl); + }); + fieldContent.appendChild(select); + } else if (field.type === 'radio' || field.type === 'checkbox') { + const optionsDiv = document.createElement('div'); + optionsDiv.className = 'field-options'; + field.options.forEach((option, idx) => { + const optionItem = document.createElement('div'); + optionItem.className = 'option-item'; + optionItem.innerHTML = ` + + + `; + optionsDiv.appendChild(optionItem); + }); + fieldContent.appendChild(optionsDiv); + } + + fieldDiv.appendChild(fieldHeader); + fieldDiv.appendChild(fieldContent); + + // Add event listeners + fieldDiv.addEventListener('click', (e) => { + if (!e.target.closest('.edit-field') && !e.target.closest('.remove-field') && + !e.target.closest('.remove-file-btn')) { + selectField(field); + } + }); + + const editBtn = fieldDiv.querySelector('.edit-field'); + if (editBtn) { + editBtn.addEventListener('click', (e) => { + e.stopPropagation(); + selectField(field); + }); + } + + const removeBtn = fieldDiv.querySelector('.remove-field'); + if (removeBtn) { + removeBtn.addEventListener('click', (e) => { + e.stopPropagation(); + removeField(parseInt(removeBtn.dataset.fieldIndex)); + }); + } + + const removeFileBtns = fieldDiv.querySelectorAll('.remove-file-btn'); + removeFileBtns.forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const fileIndex = parseInt(btn.dataset.fileIndex); + const fieldId = parseInt(fieldDiv.dataset.fieldId); + const stage = state.stages[state.currentStage]; + const field = stage.fields.find(f => f.id === fieldId); + if (field && field.uploadedFiles) { + field.uploadedFiles.splice(fileIndex, 1); + renderCurrentStage(); + } + }); + }); + + // Make draggable + fieldDiv.draggable = true; + fieldDiv.addEventListener('dragstart', (e) => { + state.draggedFieldIndex = parseInt(fieldDiv.dataset.fieldIndex); + e.dataTransfer.setData('text/plain', 'reorder'); + e.dataTransfer.effectAllowed = 'move'; + }); + + fieldDiv.addEventListener('dragover', (e) => { + e.preventDefault(); + }); + + fieldDiv.addEventListener('drop', (e) => { + e.preventDefault(); + const targetIndex = parseInt(fieldDiv.dataset.fieldIndex); + dropField(targetIndex); + }); + + // Add file input event listener + const fileInput = fieldDiv.querySelector('.file-input'); + if (fileInput) { + fileInput.addEventListener('change', (e) => { + handleFileUpload(e, field); + }); + + // Make the file upload area clickable + const fileUploadArea = fieldDiv.querySelector('.file-upload-area'); + if (fileUploadArea) { + fileUploadArea.addEventListener('click', () => { + fileInput.click(); + }); + } + } + + return fieldDiv; +} +function handleFileUpload(event, field) { + const files = Array.from(event.target.files); + if (files.length === 0) return; + + // Validate file count for multiple files + if (field.multipleFiles) { + const maxFiles = field.maxFiles || 1; + if (files.length > maxFiles) { + alert(`You can only upload ${maxFiles} files for this field.`); + return; + } + } else if (files.length > 1) { + // For single file fields, only take the first file + files.splice(1); + } + + // Validate each file + const validFiles = []; + const allowedTypes = (field.fileTypes || '.pdf,.doc,.docx').split(',').map(type => type.trim().toLowerCase()); + const maxFileSize = field.maxFileSize || 5; + + for (const file of files) { + // Validate file type + const fileType = '.' + file.name.split('.').pop().toLowerCase(); + if (!allowedTypes.includes(fileType)) { + alert(`Invalid file type for ${file.name}. Allowed types: ${field.fileTypes || '.pdf, .doc, .docx'}`); + return; + } + + // Validate file size + const fileSizeMB = file.size / (1024 * 1024); + if (fileSizeMB > maxFileSize) { + alert(`File ${file.name} exceeds ${maxFileSize}MB limit.`); + return; + } + + validFiles.push(file); + } + + // Store the files + if (field.multipleFiles) { + // Initialize or update the uploadedFiles array + if (!field.uploadedFiles) { + field.uploadedFiles = []; + } + field.uploadedFiles = [...validFiles]; + } else { + // Single file - store as array with one file for consistency + field.uploadedFiles = [validFiles[0]]; + } + + // Re-render the current stage to show uploaded files + renderCurrentStage(); +} + function showFieldEditor(field) { elements.fieldEditor.style.display = 'flex'; elements.fieldLabel.value = field.label || ''; @@ -1499,30 +1628,51 @@ } function renderOptionsEditor(field) { - elements.optionsList.innerHTML = ''; - field.options.forEach((option, index) => { - const optionInput = document.createElement('div'); - optionInput.className = 'option-input'; - optionInput.innerHTML = ` - - - `; - elements.optionsList.appendChild(optionInput); - const input = optionInput.querySelector('input'); - const removeBtn = optionInput.querySelector('.remove-option'); - input.addEventListener('input', () => { - field.options[index] = input.value; - }); - removeBtn.addEventListener('click', () => { - if (field.options.length > 1) { - field.options.splice(index, 1); - renderOptionsEditor(field); + elements.optionsList.innerHTML = ''; + field.options.forEach((option, index) => { + const optionInput = document.createElement('div'); + optionInput.className = 'option-input'; + optionInput.innerHTML = ` + + + `; + elements.optionsList.appendChild(optionInput); + + const input = optionInput.querySelector('input'); + const removeBtn = optionInput.querySelector('.remove-option'); + + input.addEventListener('input', () => { + field.options[index] = input.value; + }); + + removeBtn.addEventListener('click', () => { + if (field.options.length > 1) { + field.options.splice(index, 1); + renderOptionsEditor(field); + } + }); + }); + + // Add event listener for multiple files checkbox if this is a file field + if (field.type === 'file') { + const multipleFilesCheckbox = elements.multipleFiles; + if (multipleFilesCheckbox) { + multipleFilesCheckbox.addEventListener('change', function() { + elements.maxFiles.disabled = !this.checked; + if (!this.checked) { + elements.maxFiles.value = 1; + // Update the field configuration + if (state.selectedField) { + state.selectedField.maxFiles = 1; } - }); + } }); } + } +} + // Event Handlers (same as before, but updated saveForm function) function selectField(field) { @@ -1668,29 +1818,33 @@ } function drop(event) { - event.preventDefault(); - event.target.classList.remove('drag-over'); - if (state.draggedField) { - const newField = { - id: state.nextFieldId++, - type: state.draggedField.type, - label: state.draggedField.label, - placeholder: '', - required: false, - options: state.draggedField.type === 'select' || state.draggedField.type === 'radio' || state.draggedField.type === 'checkbox' - ? ['Option 1', 'Option 2'] - : [], - fileTypes: state.draggedField.type === 'file' ? '.pdf,.doc,.docx' : '', - maxFileSize: state.draggedField.type === 'file' ? 5 : 0, - predefined: false, - uploadedFile: null - }; - state.stages[state.currentStage].fields.push(newField); - selectField(newField); - state.draggedField = null; - renderCurrentStage(); - } - } + event.preventDefault(); + event.target.classList.remove('drag-over'); + + if (state.draggedField) { + const newField = { + id: state.nextFieldId++, + type: state.draggedField.type, + label: state.draggedField.label, + placeholder: '', + required: false, + options: state.draggedField.type === 'select' || state.draggedField.type === 'radio' || state.draggedField.type === 'checkbox' + ? ['Option 1', 'Option 2'] + : [], + fileTypes: state.draggedField.type === 'file' ? '.pdf,.doc,.docx' : '', + maxFileSize: state.draggedField.type === 'file' ? 5 : 0, + multipleFiles: state.draggedField.type === 'file' ? false : undefined, + maxFiles: state.draggedField.type === 'file' ? 1 : undefined, + predefined: false, + uploadedFiles: state.draggedField.type === 'file' ? [] : undefined + }; + + state.stages[state.currentStage].fields.push(newField); + selectField(newField); + state.draggedField = null; + renderCurrentStage(); + } +} function dropField(targetIndex) { if (state.draggedFieldIndex !== null && state.draggedFieldIndex !== targetIndex) { @@ -1790,15 +1944,23 @@ // Initialize Application function init() { // Initialize form title - elements.formTitle.textContent = state.formName; + elements.formTitle.textContent = 'Loading...'; - renderStageNavigation(); - renderCurrentStage(); - initEventListeners(); - // Load existing template if editing - if (djangoConfig.loadUrl) { - loadExistingTemplate(); - } + // Hide the form stage initially to prevent flickering + elements.formStage.style.display = 'none'; + elements.emptyState.style.display = 'block'; + elements.emptyState.innerHTML = '

Loading form template...

'; + + // Only render navigation if we have a template to load + if (djangoConfig.loadUrl) { + loadExistingTemplate(); + } else { + // For new templates, show empty state + elements.formTitle.textContent = 'New Form Template'; + elements.formStage.style.display = 'block'; + renderStageNavigation(); + renderCurrentStage(); + } } // Start the application diff --git a/templates/forms/form_templates_list.html b/templates/forms/form_templates_list.html index 3f0dbfc..0194a88 100644 --- a/templates/forms/form_templates_list.html +++ b/templates/forms/form_templates_list.html @@ -1,5 +1,5 @@ {% extends 'base.html' %} -{% load static i18n %} +{% load static i18n crispy_forms_tags %} {% block title %}Form Templates - ATS{% endblock %} @@ -13,7 +13,7 @@ --kaauh-teal-dark: #004a53; --kaauh-border: #eaeff3; --kaauh-primary-text: #343a40; - --kaauh-gray-light: #f8f9fa; + --kaauh-gray-light: #f8f9fa; } /* --- Typography and Color Overrides --- */ @@ -25,7 +25,7 @@ border-color: var(--kaauh-teal); color: white; font-weight: 600; - padding: 0.375rem 0.75rem; + padding: 0.375rem 0.75rem; border-radius: 0.5rem; transition: all 0.2s ease; display: inline-flex; @@ -37,7 +37,7 @@ border-color: var(--kaauh-teal-dark); transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.15); - color: white; + color: white; } /* Secondary Button Style (for Edit/Preview) */ @@ -69,39 +69,41 @@ background-color: white; transition: transform 0.2s, box-shadow 0.2s; } - + /* Template Card Hover Effect (Consistent with job list card hover) */ .template-card { height: 100%; } .template-card:hover { - transform: translateY(-2px); + transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0,0,0,0.1) !important; } - + /* Card Header Theming */ .card-header { /* FIX: Use !important to override default white/light backgrounds from Bootstrap */ - background-color: var(--kaauh-teal-dark) !important; + background-color: var(--kaauh-teal-dark) !important; border-bottom: 1px solid var(--kaauh-border); color: white !important; /* Base color for header text */ font-weight: 600; padding: 1rem 1.25rem; border-radius: 0.75rem 0.75rem 0 0; } - + /* Ensure all elements within the header are visible */ .card-header h3 { - color: white !important; + color: white !important; font-weight: 700; } .card-header .fas { - color: white !important; + color: white !important; } .card-header .small { color: rgba(255, 255, 255, 0.7) !important; } + /* Stats Theming */ + /* --- Content Styles (Stats, Description) --- */ .stat-value { font-size: 1.5rem; @@ -116,14 +118,13 @@ .card-description { min-height: 60px; color: var(--kaauh-primary-text); - margin-bottom: 1rem; } - /* --- Form/Search Input Theming (Matching Job List) --- */ - .form-control-search { - box-shadow: none; + /* Search Input Theming */ + .form-control { + border-radius: 0.5rem 0 0 0.5rem; border-color: var(--kaauh-border); - border-radius: 0 0.5rem 0.5rem 0; + border-radius: 0 0.5rem 0.5rem 0; } .form-control-search:focus { border-color: var(--kaauh-teal); @@ -146,8 +147,8 @@ --bs-btn-hover-bg: #dc3545; --bs-btn-hover-color: white; } - - /* --- Empty State Theming --- */ + + /* Empty State Theming */ .empty-state { text-align: center; padding: 3rem 1rem; @@ -159,7 +160,7 @@ .empty-state i { font-size: 3.5rem; margin-bottom: 1rem; - color: var(--kaauh-teal-dark); + color: var(--kaauh-teal-dark); } .empty-state .btn-main-action .fas { color: white !important; @@ -188,23 +189,23 @@

{% trans "Form Templates" %}

- - {% trans "Create New Template" %} - +
{# Search/Filter Area - Matching Job List Structure #}
Search Templates
-
- + +
@@ -214,7 +215,7 @@ - + {# Show Clear button if search is active #} {% if query %} @@ -236,13 +237,14 @@

{{ template.name }}

-
+ {{ template.job }} +
{{ template.created_at|date:"M d, Y" }} {{ template.updated_at|timesince }} {% trans "ago" %}
- + {# Content area - includes stats and description #}
@@ -263,7 +265,7 @@ {% endif %}

- + {# Action area - visually separated with pt-2 border-top #}
@@ -336,7 +338,33 @@ {% endif %}
-{% include 'includes/delete_modal.html' %} +{% include 'includes/delete_modal.html' %} + + + {% endblock %} {% block customJS %} @@ -387,7 +415,7 @@ window.location.href = query ? `?q=${encodeURIComponent(query)}` : '{% url "form_templates_list" %}'; } }); - + // Bind search form submit to the main button click event for consistency document.querySelector('.filter-buttons button[type="submit"]').addEventListener('click', function(e) { // Prevent default submission to handle URL construction correctly @@ -415,18 +443,18 @@ e.preventDefault(); if (!templateToDelete) return; - - // This CSRF token selector assumes it's present in your base template or form - const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; + + // This relies on 'csrfToken' being defined somewhere, which is typical for Django templates. + const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; try { // NOTE: Update this URL to match your actual Django API endpoint for deletion - const response = await fetch(`/api/templates/${templateToDelete}/delete/`, { + const response = await fetch(`/api/templates/${templateToDelete}/delete/`, { method: 'DELETE', headers: { 'X-CSRFToken': csrfToken, 'X-Requested-With': 'XMLHttpRequest', - 'Content-Type': 'application/json' + 'Content-Type': 'application/json' } }); @@ -473,5 +501,50 @@ document.getElementById('deleteModal').addEventListener('hidden.bs.modal', function() { templateToDelete = null; }); + + // Handle create template form submission + document.getElementById('createTemplateForm').addEventListener('submit', async function(e) { + e.preventDefault(); + + const form = e.target; + const formData = new FormData(form); + + try { + const response = await fetch(form.action, { + method: 'POST', + body: formData, + headers: { + 'X-Requested-With': 'XMLHttpRequest', + } + }); + + const result = await response.json(); + + if (response.ok && result.success) { + // Show success toast + createToast(result.message || 'Template created successfully!'); + + // Close modal + bootstrap.Modal.getInstance(document.getElementById('createTemplateModal')).hide(); + + // Clear form + form.reset(); + + // Redirect to form builder with new template ID + if (result.template_id) { + window.location.href = `{% url 'form_builder' %}${result.template_id}/`; + } else { + // Fallback to template list if no ID is returned + window.location.reload(); + } + } else { + // Show error toast + createToast('Error: ' + (result.message || 'Could not create template.'), 'error'); + } + } catch (error) { + console.error('Error:', error); + createToast('An error occurred while creating the template.', 'error'); + } + }); -{% endblock %} \ No newline at end of file +{% endblock %}