diff --git a/NorahUniversity/__pycache__/settings.cpython-313.pyc b/NorahUniversity/__pycache__/settings.cpython-313.pyc index 988431b..b611091 100644 Binary files a/NorahUniversity/__pycache__/settings.cpython-313.pyc and b/NorahUniversity/__pycache__/settings.cpython-313.pyc differ diff --git a/NorahUniversity/__pycache__/urls.cpython-313.pyc b/NorahUniversity/__pycache__/urls.cpython-313.pyc index 624686a..f3db1f1 100644 Binary files a/NorahUniversity/__pycache__/urls.cpython-313.pyc and b/NorahUniversity/__pycache__/urls.cpython-313.pyc differ diff --git a/NorahUniversity/settings.py b/NorahUniversity/settings.py index 249db2c..baaf0bb 100644 --- a/NorahUniversity/settings.py +++ b/NorahUniversity/settings.py @@ -67,7 +67,7 @@ MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -196,7 +196,6 @@ SOCIALACCOUNT_PROVIDERS = { } } - ZOOM_ACCOUNT_ID = 'HoGikHXsQB2GNDC5Rvyw9A' ZOOM_CLIENT_ID = 'brC39920R8C8azfudUaQgA' ZOOM_CLIENT_SECRET = 'rvfhjlbID4ychXPOvZ2lYsoAC0B0Ny2L' @@ -215,7 +214,6 @@ CELERY_TASK_SERIALIZER = 'json' 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/' \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 index feeaaa2..1e9728d 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/recruitment/__pycache__/admin.cpython-313.pyc b/recruitment/__pycache__/admin.cpython-313.pyc index 6bb62fc..a4f7fc8 100644 Binary files a/recruitment/__pycache__/admin.cpython-313.pyc and b/recruitment/__pycache__/admin.cpython-313.pyc differ diff --git a/recruitment/__pycache__/forms.cpython-313.pyc b/recruitment/__pycache__/forms.cpython-313.pyc index 62636ce..ffffc9f 100644 Binary files a/recruitment/__pycache__/forms.cpython-313.pyc and b/recruitment/__pycache__/forms.cpython-313.pyc differ diff --git a/recruitment/__pycache__/models.cpython-313.pyc b/recruitment/__pycache__/models.cpython-313.pyc index 4b04a43..7de510d 100644 Binary files a/recruitment/__pycache__/models.cpython-313.pyc and b/recruitment/__pycache__/models.cpython-313.pyc differ diff --git a/recruitment/__pycache__/signals.cpython-313.pyc b/recruitment/__pycache__/signals.cpython-313.pyc index fcfcc91..530e3d0 100644 Binary files a/recruitment/__pycache__/signals.cpython-313.pyc and b/recruitment/__pycache__/signals.cpython-313.pyc differ diff --git a/recruitment/__pycache__/urls.cpython-313.pyc b/recruitment/__pycache__/urls.cpython-313.pyc index 11edbba..f8eb656 100644 Binary files a/recruitment/__pycache__/urls.cpython-313.pyc and b/recruitment/__pycache__/urls.cpython-313.pyc differ diff --git a/recruitment/__pycache__/utils.cpython-313.pyc b/recruitment/__pycache__/utils.cpython-313.pyc index 4e08a67..13274c0 100644 Binary files a/recruitment/__pycache__/utils.cpython-313.pyc and b/recruitment/__pycache__/utils.cpython-313.pyc differ diff --git a/recruitment/__pycache__/views.cpython-313.pyc b/recruitment/__pycache__/views.cpython-313.pyc index 2121db6..151d83e 100644 Binary files a/recruitment/__pycache__/views.cpython-313.pyc and b/recruitment/__pycache__/views.cpython-313.pyc differ diff --git a/recruitment/__pycache__/views_frontend.cpython-313.pyc b/recruitment/__pycache__/views_frontend.cpython-313.pyc index 16dcc88..7a1340c 100644 Binary files a/recruitment/__pycache__/views_frontend.cpython-313.pyc and b/recruitment/__pycache__/views_frontend.cpython-313.pyc differ diff --git a/recruitment/forms.py b/recruitment/forms.py index 3363fb2..8317b4b 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -469,4 +469,18 @@ class InterviewScheduleForm(forms.ModelForm): def clean_working_days(self): working_days = self.cleaned_data.get('working_days') # Convert string values to integers - return [int(day) for day in working_days] \ No newline at end of file + return [int(day) for day in working_days] + + +class JobPostingCancelReasonForm(forms.ModelForm): + class Meta: + model = JobPosting + fields = ['cancel_reason'] +class JobPostingStatusForm(forms.ModelForm): + class Meta: + model = JobPosting + fields = ['status'] +class FormTemplateIsActiveForm(forms.ModelForm): + class Meta: + model = FormTemplate + fields = ['is_active'] \ No newline at end of file diff --git a/recruitment/linkedin.py b/recruitment/linkedin.py index b67abde..dae3bfc 100644 --- a/recruitment/linkedin.py +++ b/recruitment/linkedin.py @@ -2,12 +2,13 @@ import requests LINKEDIN_API_BASE = "https://api.linkedin.com/v2" + class LinkedInService: def __init__(self, access_token): self.headers = { - 'Authorization': f'Bearer {access_token}', - 'X-Restli-Protocol-Version': '2.0.0', - 'Content-Type': 'application/json' + "Authorization": f"Bearer {access_token}", + "X-Restli-Protocol-Version": "2.0.0", + "Content-Type": "application/json", } def post_job(self, organization_id, job_data): @@ -17,10 +18,10 @@ class LinkedInService: "lifecycleState": "PUBLISHED", "specificContent": { "com.linkedin.ugc.ShareContent": { - "shareCommentary": {"text": job_data['text']}, - "shareMediaCategory": "NONE" + "shareCommentary": {"text": job_data["text"]}, + "shareMediaCategory": "NONE", } }, - "visibility": {"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"} + "visibility": {"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"}, } - return requests.post(url, json=data, headers=self.headers) \ No newline at end of file + return requests.post(url, json=data, headers=self.headers) diff --git a/recruitment/migrations/0001_initial.py b/recruitment/migrations/0001_initial.py index d0381aa..27ae747 100644 --- a/recruitment/migrations/0001_initial.py +++ b/recruitment/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.6 on 2025-10-08 15:48 +# Generated by Django 5.2.6 on 2025-10-09 10:10 import django.core.validators import django.db.models.deletion @@ -213,6 +213,7 @@ class Migration(migrations.Migration): ('last_name', models.CharField(max_length=255, verbose_name='Last Name')), ('email', models.EmailField(max_length=254, verbose_name='Email')), ('phone', models.CharField(max_length=20, verbose_name='Phone')), + ('address', models.TextField(max_length=200, verbose_name='Address')), ('resume', models.FileField(upload_to='resumes/', verbose_name='Resume')), ('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')), ('applied', models.BooleanField(default=False, verbose_name='Applied')), @@ -311,6 +312,14 @@ class Migration(migrations.Migration): name='job', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.jobposting', verbose_name='Job'), ), + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), + ], + ), migrations.CreateModel( name='SharedFormTemplate', fields=[ @@ -374,6 +383,7 @@ class Migration(migrations.Migration): name='ScheduledInterview', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), ('interview_date', models.DateField(verbose_name='Interview Date')), ('interview_time', models.TimeField(verbose_name='Interview Time')), ('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], default='scheduled', max_length=20)), @@ -384,5 +394,8 @@ class Migration(migrations.Migration): ('schedule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interviews', to='recruitment.interviewschedule')), ('zoom_meeting', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='interview', to='recruitment.zoommeeting')), ], + options={ + 'abstract': False, + }, ), ] diff --git a/recruitment/migrations/0002_candidate_address.py b/recruitment/migrations/0002_candidate_address.py deleted file mode 100644 index c9713a1..0000000 --- a/recruitment/migrations/0002_candidate_address.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-08 17:46 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='candidate', - name='address', - field=models.TextField(default='', max_length=200, verbose_name='Address'), - preserve_default=False, - ), - ] diff --git a/recruitment/migrations/0002_jobposting_cancel_reason_jobposting_cancelled_at_and_more.py b/recruitment/migrations/0002_jobposting_cancel_reason_jobposting_cancelled_at_and_more.py new file mode 100644 index 0000000..91b6321 --- /dev/null +++ b/recruitment/migrations/0002_jobposting_cancel_reason_jobposting_cancelled_at_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.6 on 2025-10-09 10:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='jobposting', + name='cancel_reason', + field=models.TextField(blank=True, help_text='Reason for canceling the job posting', verbose_name='Cancel Reason'), + ), + migrations.AddField( + model_name='jobposting', + name='cancelled_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='jobposting', + name='cancelled_by', + field=models.CharField(blank=True, help_text='Name of person who cancelled this job', max_length=100, verbose_name='Cancelled By'), + ), + migrations.AlterField( + model_name='jobposting', + name='status', + field=models.CharField(blank=True, choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], default='DRAFT', max_length=20, null=True), + ), + ] diff --git a/recruitment/migrations/0003_candidate_is_resume_parsed_and_more.py b/recruitment/migrations/0003_candidate_is_resume_parsed_and_more.py new file mode 100644 index 0000000..959dd78 --- /dev/null +++ b/recruitment/migrations/0003_candidate_is_resume_parsed_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.6 on 2025-10-09 12:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0002_jobposting_cancel_reason_jobposting_cancelled_at_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='candidate', + name='is_resume_parsed', + field=models.BooleanField(default=False, verbose_name='Resume Parsed'), + ), + migrations.AlterField( + model_name='formtemplate', + name='is_active', + field=models.BooleanField(default=False, help_text='Whether this template is active'), + ), + ] diff --git a/recruitment/migrations/0003_scheduledinterview_slug.py b/recruitment/migrations/0003_scheduledinterview_slug.py deleted file mode 100644 index 364117e..0000000 --- a/recruitment/migrations/0003_scheduledinterview_slug.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-08 17:47 - -import django_extensions.db.fields -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0002_candidate_address'), - ] - - operations = [ - migrations.AddField( - model_name='scheduledinterview', - name='slug', - field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'), - ), - ] diff --git a/recruitment/migrations/0027_profile.py b/recruitment/migrations/0027_profile.py deleted file mode 100644 index d2b58c8..0000000 --- a/recruitment/migrations/0027_profile.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-08 13:01 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0026_interviewschedule_scheduledinterview'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Profile', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/recruitment/migrations/__pycache__/0001_initial.cpython-311.pyc b/recruitment/migrations/__pycache__/0001_initial.cpython-311.pyc deleted file mode 100644 index a76b82e..0000000 Binary files a/recruitment/migrations/__pycache__/0001_initial.cpython-311.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0001_initial.cpython-312.pyc b/recruitment/migrations/__pycache__/0001_initial.cpython-312.pyc deleted file mode 100644 index 0a882a2..0000000 Binary files a/recruitment/migrations/__pycache__/0001_initial.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0001_initial.cpython-313.pyc b/recruitment/migrations/__pycache__/0001_initial.cpython-313.pyc index 492600e..dfcea98 100644 Binary files a/recruitment/migrations/__pycache__/0001_initial.cpython-313.pyc and b/recruitment/migrations/__pycache__/0001_initial.cpython-313.pyc differ diff --git a/recruitment/migrations/__pycache__/0002_trainingmaterial.cpython-311.pyc b/recruitment/migrations/__pycache__/0002_trainingmaterial.cpython-311.pyc deleted file mode 100644 index 5cad7cd..0000000 Binary files a/recruitment/migrations/__pycache__/0002_trainingmaterial.cpython-311.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0002_trainingmaterial.cpython-312.pyc b/recruitment/migrations/__pycache__/0002_trainingmaterial.cpython-312.pyc deleted file mode 100644 index a68d9e1..0000000 Binary files a/recruitment/migrations/__pycache__/0002_trainingmaterial.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0002_trainingmaterial.cpython-313.pyc b/recruitment/migrations/__pycache__/0002_trainingmaterial.cpython-313.pyc deleted file mode 100644 index de3f533..0000000 Binary files a/recruitment/migrations/__pycache__/0002_trainingmaterial.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0003_candidate_updated_at_job_updated_at_and_more.cpython-311.pyc b/recruitment/migrations/__pycache__/0003_candidate_updated_at_job_updated_at_and_more.cpython-311.pyc deleted file mode 100644 index 699d7a5..0000000 Binary files a/recruitment/migrations/__pycache__/0003_candidate_updated_at_job_updated_at_and_more.cpython-311.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0003_candidate_updated_at_job_updated_at_and_more.cpython-312.pyc b/recruitment/migrations/__pycache__/0003_candidate_updated_at_job_updated_at_and_more.cpython-312.pyc deleted file mode 100644 index 6b5a791..0000000 Binary files a/recruitment/migrations/__pycache__/0003_candidate_updated_at_job_updated_at_and_more.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0003_candidate_updated_at_job_updated_at_and_more.cpython-313.pyc b/recruitment/migrations/__pycache__/0003_candidate_updated_at_job_updated_at_and_more.cpython-313.pyc deleted file mode 100644 index f8739c4..0000000 Binary files a/recruitment/migrations/__pycache__/0003_candidate_updated_at_job_updated_at_and_more.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0004_remove_candidate_status_candidate_applied.cpython-311.pyc b/recruitment/migrations/__pycache__/0004_remove_candidate_status_candidate_applied.cpython-311.pyc deleted file mode 100644 index 7843763..0000000 Binary files a/recruitment/migrations/__pycache__/0004_remove_candidate_status_candidate_applied.cpython-311.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0004_remove_candidate_status_candidate_applied.cpython-312.pyc b/recruitment/migrations/__pycache__/0004_remove_candidate_status_candidate_applied.cpython-312.pyc deleted file mode 100644 index 5c2f438..0000000 Binary files a/recruitment/migrations/__pycache__/0004_remove_candidate_status_candidate_applied.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0004_remove_candidate_status_candidate_applied.cpython-313.pyc b/recruitment/migrations/__pycache__/0004_remove_candidate_status_candidate_applied.cpython-313.pyc deleted file mode 100644 index 1403683..0000000 Binary files a/recruitment/migrations/__pycache__/0004_remove_candidate_status_candidate_applied.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0005_zoommeeting.cpython-312.pyc b/recruitment/migrations/__pycache__/0005_zoommeeting.cpython-312.pyc deleted file mode 100644 index f5fcc8a..0000000 Binary files a/recruitment/migrations/__pycache__/0005_zoommeeting.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0005_zoommeeting.cpython-313.pyc b/recruitment/migrations/__pycache__/0005_zoommeeting.cpython-313.pyc deleted file mode 100644 index 4b39aa5..0000000 Binary files a/recruitment/migrations/__pycache__/0005_zoommeeting.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0006_jobposting_alter_candidate_options_alter_job_options_and_more.cpython-312.pyc b/recruitment/migrations/__pycache__/0006_jobposting_alter_candidate_options_alter_job_options_and_more.cpython-312.pyc deleted file mode 100644 index ab445a7..0000000 Binary files a/recruitment/migrations/__pycache__/0006_jobposting_alter_candidate_options_alter_job_options_and_more.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0006_jobposting_alter_candidate_options_alter_job_options_and_more.cpython-313.pyc b/recruitment/migrations/__pycache__/0006_jobposting_alter_candidate_options_alter_job_options_and_more.cpython-313.pyc deleted file mode 100644 index 8ba6794..0000000 Binary files a/recruitment/migrations/__pycache__/0006_jobposting_alter_candidate_options_alter_job_options_and_more.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0007_alter_jobposting_status.cpython-312.pyc b/recruitment/migrations/__pycache__/0007_alter_jobposting_status.cpython-312.pyc deleted file mode 100644 index 35501e4..0000000 Binary files a/recruitment/migrations/__pycache__/0007_alter_jobposting_status.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0007_alter_jobposting_status.cpython-313.pyc b/recruitment/migrations/__pycache__/0007_alter_jobposting_status.cpython-313.pyc deleted file mode 100644 index 2a384b0..0000000 Binary files a/recruitment/migrations/__pycache__/0007_alter_jobposting_status.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0008_jobposting_published_at_alter_jobposting_status.cpython-312.pyc b/recruitment/migrations/__pycache__/0008_jobposting_published_at_alter_jobposting_status.cpython-312.pyc deleted file mode 100644 index 3da5401..0000000 Binary files a/recruitment/migrations/__pycache__/0008_jobposting_published_at_alter_jobposting_status.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0008_jobposting_published_at_alter_jobposting_status.cpython-313.pyc b/recruitment/migrations/__pycache__/0008_jobposting_published_at_alter_jobposting_status.cpython-313.pyc deleted file mode 100644 index 8930ea6..0000000 Binary files a/recruitment/migrations/__pycache__/0008_jobposting_published_at_alter_jobposting_status.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0009_candidate_slug_job_slug_jobposting_slug_and_more.cpython-312.pyc b/recruitment/migrations/__pycache__/0009_candidate_slug_job_slug_jobposting_slug_and_more.cpython-312.pyc deleted file mode 100644 index 594a752..0000000 Binary files a/recruitment/migrations/__pycache__/0009_candidate_slug_job_slug_jobposting_slug_and_more.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0009_candidate_slug_job_slug_jobposting_slug_and_more.cpython-313.pyc b/recruitment/migrations/__pycache__/0009_candidate_slug_job_slug_jobposting_slug_and_more.cpython-313.pyc deleted file mode 100644 index 06259de..0000000 Binary files a/recruitment/migrations/__pycache__/0009_candidate_slug_job_slug_jobposting_slug_and_more.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0010_remove_candidate_name.cpython-312.pyc b/recruitment/migrations/__pycache__/0010_remove_candidate_name.cpython-312.pyc deleted file mode 100644 index fda42e8..0000000 Binary files a/recruitment/migrations/__pycache__/0010_remove_candidate_name.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0010_remove_candidate_name.cpython-313.pyc b/recruitment/migrations/__pycache__/0010_remove_candidate_name.cpython-313.pyc deleted file mode 100644 index fa50f31..0000000 Binary files a/recruitment/migrations/__pycache__/0010_remove_candidate_name.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0011_alter_candidate_stage.cpython-312.pyc b/recruitment/migrations/__pycache__/0011_alter_candidate_stage.cpython-312.pyc deleted file mode 100644 index b9294cd..0000000 Binary files a/recruitment/migrations/__pycache__/0011_alter_candidate_stage.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0011_alter_candidate_stage.cpython-313.pyc b/recruitment/migrations/__pycache__/0011_alter_candidate_stage.cpython-313.pyc deleted file mode 100644 index 9536144..0000000 Binary files a/recruitment/migrations/__pycache__/0011_alter_candidate_stage.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0012_form_formsubmission_uploadedfile.cpython-312.pyc b/recruitment/migrations/__pycache__/0012_form_formsubmission_uploadedfile.cpython-312.pyc deleted file mode 100644 index 372cae6..0000000 Binary files a/recruitment/migrations/__pycache__/0012_form_formsubmission_uploadedfile.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0012_form_formsubmission_uploadedfile.cpython-313.pyc b/recruitment/migrations/__pycache__/0012_form_formsubmission_uploadedfile.cpython-313.pyc deleted file mode 100644 index 25b7217..0000000 Binary files a/recruitment/migrations/__pycache__/0012_form_formsubmission_uploadedfile.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0013_candidate_criteria_checklist_candidate_match_score_and_more.cpython-312.pyc b/recruitment/migrations/__pycache__/0013_candidate_criteria_checklist_candidate_match_score_and_more.cpython-312.pyc deleted file mode 100644 index 41656b9..0000000 Binary files a/recruitment/migrations/__pycache__/0013_candidate_criteria_checklist_candidate_match_score_and_more.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0013_candidate_criteria_checklist_candidate_match_score_and_more.cpython-313.pyc b/recruitment/migrations/__pycache__/0013_candidate_criteria_checklist_candidate_match_score_and_more.cpython-313.pyc deleted file mode 100644 index 9e12a7f..0000000 Binary files a/recruitment/migrations/__pycache__/0013_candidate_criteria_checklist_candidate_match_score_and_more.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0013_formfield_formstage_remove_formsubmission_form_and_more.cpython-312.pyc b/recruitment/migrations/__pycache__/0013_formfield_formstage_remove_formsubmission_form_and_more.cpython-312.pyc deleted file mode 100644 index 4c63586..0000000 Binary files a/recruitment/migrations/__pycache__/0013_formfield_formstage_remove_formsubmission_form_and_more.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0013_formfield_formstage_remove_formsubmission_form_and_more.cpython-313.pyc b/recruitment/migrations/__pycache__/0013_formfield_formstage_remove_formsubmission_form_and_more.cpython-313.pyc deleted file mode 100644 index e488061..0000000 Binary files a/recruitment/migrations/__pycache__/0013_formfield_formstage_remove_formsubmission_form_and_more.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0014_source_jobposting_source.cpython-312.pyc b/recruitment/migrations/__pycache__/0014_source_jobposting_source.cpython-312.pyc deleted file mode 100644 index d3c978a..0000000 Binary files a/recruitment/migrations/__pycache__/0014_source_jobposting_source.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0014_source_jobposting_source.cpython-313.pyc b/recruitment/migrations/__pycache__/0014_source_jobposting_source.cpython-313.pyc deleted file mode 100644 index 350f334..0000000 Binary files a/recruitment/migrations/__pycache__/0014_source_jobposting_source.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0015_hiringagency_candidate_submitted_by_agency_and_more.cpython-312.pyc b/recruitment/migrations/__pycache__/0015_hiringagency_candidate_submitted_by_agency_and_more.cpython-312.pyc deleted file mode 100644 index 6ff14ba..0000000 Binary files a/recruitment/migrations/__pycache__/0015_hiringagency_candidate_submitted_by_agency_and_more.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0015_hiringagency_candidate_submitted_by_agency_and_more.cpython-313.pyc b/recruitment/migrations/__pycache__/0015_hiringagency_candidate_submitted_by_agency_and_more.cpython-313.pyc deleted file mode 100644 index b1ea43d..0000000 Binary files a/recruitment/migrations/__pycache__/0015_hiringagency_candidate_submitted_by_agency_and_more.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0016_alter_source_options_hiringagency_address_and_more.cpython-312.pyc b/recruitment/migrations/__pycache__/0016_alter_source_options_hiringagency_address_and_more.cpython-312.pyc deleted file mode 100644 index a5f4837..0000000 Binary files a/recruitment/migrations/__pycache__/0016_alter_source_options_hiringagency_address_and_more.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0016_alter_source_options_hiringagency_address_and_more.cpython-313.pyc b/recruitment/migrations/__pycache__/0016_alter_source_options_hiringagency_address_and_more.cpython-313.pyc deleted file mode 100644 index c7cd58e..0000000 Binary files a/recruitment/migrations/__pycache__/0016_alter_source_options_hiringagency_address_and_more.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0017_alter_jobposting_hiring_agency.cpython-312.pyc b/recruitment/migrations/__pycache__/0017_alter_jobposting_hiring_agency.cpython-312.pyc deleted file mode 100644 index 250d210..0000000 Binary files a/recruitment/migrations/__pycache__/0017_alter_jobposting_hiring_agency.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0017_alter_jobposting_hiring_agency.cpython-313.pyc b/recruitment/migrations/__pycache__/0017_alter_jobposting_hiring_agency.cpython-313.pyc deleted file mode 100644 index 4ec6bb9..0000000 Binary files a/recruitment/migrations/__pycache__/0017_alter_jobposting_hiring_agency.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0018_alter_jobposting_hiring_agency.cpython-312.pyc b/recruitment/migrations/__pycache__/0018_alter_jobposting_hiring_agency.cpython-312.pyc deleted file mode 100644 index a339fa6..0000000 Binary files a/recruitment/migrations/__pycache__/0018_alter_jobposting_hiring_agency.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0018_alter_jobposting_hiring_agency.cpython-313.pyc b/recruitment/migrations/__pycache__/0018_alter_jobposting_hiring_agency.cpython-313.pyc deleted file mode 100644 index be96779..0000000 Binary files a/recruitment/migrations/__pycache__/0018_alter_jobposting_hiring_agency.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0019_merge_20251006_1224.cpython-312.pyc b/recruitment/migrations/__pycache__/0019_merge_20251006_1224.cpython-312.pyc deleted file mode 100644 index 7591704..0000000 Binary files a/recruitment/migrations/__pycache__/0019_merge_20251006_1224.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0019_merge_20251006_1224.cpython-313.pyc b/recruitment/migrations/__pycache__/0019_merge_20251006_1224.cpython-313.pyc deleted file mode 100644 index 180c2e8..0000000 Binary files a/recruitment/migrations/__pycache__/0019_merge_20251006_1224.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0020_delete_job.cpython-313.pyc b/recruitment/migrations/__pycache__/0020_delete_job.cpython-313.pyc deleted file mode 100644 index b33ef20..0000000 Binary files a/recruitment/migrations/__pycache__/0020_delete_job.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0021_source_api_key_source_api_secret_source_description_and_more.cpython-313.pyc b/recruitment/migrations/__pycache__/0021_source_api_key_source_api_secret_source_description_and_more.cpython-313.pyc deleted file mode 100644 index 53f32bd..0000000 Binary files a/recruitment/migrations/__pycache__/0021_source_api_key_source_api_secret_source_description_and_more.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/0022_alter_source_trusted_ips.cpython-313.pyc b/recruitment/migrations/__pycache__/0022_alter_source_trusted_ips.cpython-313.pyc deleted file mode 100644 index 99727f1..0000000 Binary files a/recruitment/migrations/__pycache__/0022_alter_source_trusted_ips.cpython-313.pyc and /dev/null differ 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 deleted file mode 100644 index fbfeec0..0000000 Binary files a/recruitment/migrations/__pycache__/0023_alter_jobposting_application_url_and_more.cpython-313.pyc and /dev/null differ 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 deleted file mode 100644 index f596782..0000000 Binary files a/recruitment/migrations/__pycache__/0024_fieldresponse_created_at_fieldresponse_slug_and_more.cpython-313.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/__init__.cpython-311.pyc b/recruitment/migrations/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 6c581a1..0000000 Binary files a/recruitment/migrations/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/__init__.cpython-312.pyc b/recruitment/migrations/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index c4ac56c..0000000 Binary files a/recruitment/migrations/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/recruitment/migrations/__pycache__/__init__.cpython-313.pyc b/recruitment/migrations/__pycache__/__init__.cpython-313.pyc index f41c210..b67d53b 100644 Binary files a/recruitment/migrations/__pycache__/__init__.cpython-313.pyc and b/recruitment/migrations/__pycache__/__init__.cpython-313.pyc differ diff --git a/recruitment/models.py b/recruitment/models.py index 40ede85..4b3a97b 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -11,8 +11,10 @@ from django.urls import reverse class Profile(models.Model): - profile_image=models.ImageField(null=True,blank=True,upload_to='profile_pic/') - user=models.OneToOneField(User,on_delete=models.CASCADE,related_name='profile') + profile_image = models.ImageField(null=True, blank=True, upload_to="profile_pic/") + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") + + class Base(models.Model): created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated at")) @@ -105,8 +107,9 @@ class JobPosting(Base): # Status Fields STATUS_CHOICES = [ ("DRAFT", "Draft"), - ("PUBLISHED", "Published"), + ("ACTIVE", "Active"), ("CLOSED", "Closed"), + ("CANCELLED", "Cancelled"), ("ARCHIVED", "Archived"), ] status = models.CharField( @@ -165,6 +168,18 @@ class JobPosting(Base): "External agency responsible for sourcing candidates for this role" ), ) + cancel_reason = models.TextField( + blank=True, + help_text=_("Reason for canceling the job posting"), + verbose_name=_("Cancel Reason"), + ) + cancelled_by = models.CharField( + max_length=100, + blank=True, + help_text=_("Name of person who cancelled this job"), + verbose_name=_("Cancelled By"), + ) + cancelled_at = models.DateTimeField(null=True, blank=True) class Meta: ordering = ["-created_at"] @@ -197,7 +212,7 @@ class JobPosting(Base): else: next_num = 1 - self.internal_job_id = f"{prefix}-{year}-{next_num:04d}" + self.internal_job_id = f"{prefix}-{year}-{next_num:06d}" super().save(*args, **kwargs) @@ -260,8 +275,11 @@ class Candidate(Base): last_name = models.CharField(max_length=255, verbose_name=_("Last Name")) email = models.EmailField(verbose_name=_("Email")) phone = models.CharField(max_length=20, verbose_name=_("Phone")) - address = models.TextField(max_length=200,verbose_name=_("Address")) + address = models.TextField(max_length=200, verbose_name=_("Address")) resume = models.FileField(upload_to="resumes/", verbose_name=_("Resume")) + is_resume_parsed = models.BooleanField( + default=False, verbose_name=_("Resume Parsed") + ) parsed_summary = models.TextField(blank=True, verbose_name=_("Parsed Summary")) applied = models.BooleanField(default=False, verbose_name=_("Applied")) stage = models.CharField( @@ -331,6 +349,7 @@ class Candidate(Base): if self.resume: return self.resume.size return 0 + def clean(self): """Validate stage transitions""" # Only validate if this is an existing record (not being created) @@ -376,6 +395,14 @@ class Candidate(Base): old_stage = self.__class__.objects.get(pk=self.pk).stage return self.STAGE_SEQUENCE.get(old_stage, []) + @property + def submission(self): + return FormSubmission.objects.filter(template__job=self.job).first() + @property + def responses(self): + if self.submission: + return self.submission.responses.all() + return [] def __str__(self): return self.full_name @@ -449,7 +476,7 @@ class FormTemplate(Base): User, on_delete=models.CASCADE, related_name="form_templates" ) is_active = models.BooleanField( - default=True, help_text="Whether this template is active" + default=False, help_text="Whether this template is active" ) class Meta: @@ -595,6 +622,9 @@ class FormField(Base): if self.order < 0: raise ValidationError("Order must be a positive integer") + def __str__(self): + return f"{self.stage.template.name} - {self.stage.name} - {self.label}" + class FormSubmission(Base): """ @@ -658,16 +688,19 @@ class FieldResponse(Base): if self.uploaded_file: return True return False + @property def get_file(self): if self.is_file: return self.uploaded_file return None + @property def get_file_size(self): if self.is_file: return self.uploaded_file.size return 0 + @property def display_value(self): """Return a human-readable representation of the response value""" @@ -885,9 +918,7 @@ class InterviewSchedule(Base): job = models.ForeignKey( JobPosting, on_delete=models.CASCADE, related_name="interview_schedules" ) - candidates = models.ManyToManyField( - Candidate, related_name="interview_schedules" - ) + candidates = models.ManyToManyField(Candidate, related_name="interview_schedules") start_date = models.DateField(verbose_name=_("Start Date")) end_date = models.DateField(verbose_name=_("End Date")) working_days = models.JSONField( @@ -895,9 +926,7 @@ class InterviewSchedule(Base): ) # Store days of week as [0,1,2,3,4] for Mon-Fri start_time = models.TimeField(verbose_name=_("Start Time")) end_time = models.TimeField(verbose_name=_("End Time")) - breaks = models.ManyToManyField( - BreakTime, blank=True, related_name="schedules" - ) + breaks = models.ManyToManyField(BreakTime, blank=True, related_name="schedules") interview_duration = models.PositiveIntegerField( verbose_name=_("Interview Duration (minutes)") ) diff --git a/recruitment/signals.py b/recruitment/signals.py index 89ec37c..2c6edb1 100644 --- a/recruitment/signals.py +++ b/recruitment/signals.py @@ -24,6 +24,8 @@ import asyncio @receiver(post_save, sender=models.Candidate) def score_candidate_resume(sender, instance, created, **kwargs): + if instance.is_resume_parsed: + return try: # Get absolute file path file_path = instance.resume.path @@ -108,12 +110,12 @@ def score_candidate_resume(sender, instance, created, **kwargs): instance.weaknesses = result1.get('weaknesses', '') instance.criteria_checklist = result1.get('criteria_checklist', {}) - + instance.is_resume_parsed = True # Save only scoring-related fields to avoid recursion instance.save(update_fields=[ 'match_score', 'strengths', 'weaknesses', - 'criteria_checklist','parsed_summary' + 'criteria_checklist','parsed_summary', 'is_resume_parsed' ]) logger.info(f"Successfully scored resume for candidate {instance.id}") diff --git a/recruitment/utils.py b/recruitment/utils.py index fdfa198..d925ad8 100644 --- a/recruitment/utils.py +++ b/recruitment/utils.py @@ -535,4 +535,19 @@ def get_available_time_slots(schedule, breaks=None): current_date += timedelta(days=1) print(f"Total slots generated: {len(slots)}") - return slots \ No newline at end of file + return slots + + + +def json_to_markdown_table(data_list): + if not data_list: + return "" + + headers = data_list[0].keys() + markdown = "| " + " | ".join(headers) + " |\n" + markdown += "| " + " | ".join(["---"] * len(headers)) + " |\n" + + for row in data_list: + values = [str(row.get(header, "")) for header in headers] + markdown += "| " + " | ".join(values) + " |\n" + return markdown \ No newline at end of file diff --git a/recruitment/views.py b/recruitment/views.py index 9ab9d48..794ce67 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -11,17 +11,40 @@ 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,FormTemplateForm,InterviewScheduleForm,BreakTimeFormSet +from .forms import ( + ZoomMeetingForm, + JobPostingForm, + FormTemplateForm, + InterviewScheduleForm, + BreakTimeFormSet, +) from rest_framework import viewsets from django.contrib import messages from django.core.paginator import Paginator from .linkedin_service import LinkedInService from .serializers import JobPostingSerializer, CandidateSerializer 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,schedule_interviews,get_available_time_slots +from django.views.generic import CreateView, UpdateView, DetailView, ListView +from .utils import ( + create_zoom_meeting, + delete_zoom_meeting, + update_zoom_meeting, + schedule_interviews, + get_available_time_slots, +) from django.views.decorators.csrf import ensure_csrf_cookie -from .models import FormTemplate, FormStage, FormField,FieldResponse,FormSubmission,InterviewSchedule,BreakTime, ZoomMeeting, Candidate, JobPosting +from .models import ( + FormTemplate, + FormStage, + FormField, + FieldResponse, + FormSubmission, + InterviewSchedule, + BreakTime, + ZoomMeeting, + Candidate, + JobPosting, +) import logging from datastar_py.django import ( DatastarResponse, @@ -29,13 +52,14 @@ from datastar_py.django import ( read_signals, ) -logger=logging.getLogger(__name__) +logger = logging.getLogger(__name__) class JobPostingViewSet(viewsets.ModelViewSet): queryset = JobPosting.objects.all() serializer_class = JobPostingSerializer + class CandidateViewSet(viewsets.ModelViewSet): queryset = Candidate.objects.all() serializer_class = CandidateSerializer @@ -43,9 +67,9 @@ class CandidateViewSet(viewsets.ModelViewSet): class ZoomMeetingCreateView(CreateView): model = ZoomMeeting - template_name = 'meetings/create_meeting.html' + template_name = "meetings/create_meeting.html" form_class = ZoomMeetingForm - success_url = '/' + success_url = "/" def form_valid(self, form): instance = form.save(commit=False) @@ -53,82 +77,85 @@ class ZoomMeetingCreateView(CreateView): topic = instance.topic if instance.start_time < timezone.now(): messages.error(self.request, "Start time must be in the future.") - return redirect('/create-meeting/', status=400) + return redirect("/create-meeting/", status=400) start_time = instance.start_time.isoformat() + "Z" duration = instance.duration result = create_zoom_meeting(topic, start_time, duration) if result["status"] == "success": - instance.meeting_id = result['meeting_details']['meeting_id'] - instance.join_url = result['meeting_details']['join_url'] - instance.host_email = result['meeting_details']['host_email'] - instance.zoom_gateway_response = result['zoom_gateway_response'] + instance.meeting_id = result["meeting_details"]["meeting_id"] + instance.join_url = result["meeting_details"]["join_url"] + instance.host_email = result["meeting_details"]["host_email"] + instance.zoom_gateway_response = result["zoom_gateway_response"] instance.save() messages.success(self.request, result["message"]) - return redirect('/', status=201) + return redirect("/", status=201) else: messages.error(self.request, result["message"]) - return redirect('/', status=400) + return redirect("/", status=400) except Exception as e: - return redirect('/', status=500) + return redirect("/", status=500) + class ZoomMeetingListView(ListView): model = ZoomMeeting - template_name = 'meetings/list_meetings.html' - context_object_name = 'meetings' + template_name = "meetings/list_meetings.html" + context_object_name = "meetings" paginate_by = 10 def get_queryset(self): - queryset = super().get_queryset().order_by('-start_time') + queryset = super().get_queryset().order_by("-start_time") # Handle search - search_query = self.request.GET.get('search', '') + search_query = self.request.GET.get("search", "") if search_query: queryset = queryset.filter( - Q(topic__icontains=search_query) | - Q(meeting_id__icontains=search_query) + Q(topic__icontains=search_query) | Q(meeting_id__icontains=search_query) ) return queryset def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['search_query'] = self.request.GET.get('search', '') + context["search_query"] = self.request.GET.get("search", "") return context + class ZoomMeetingDetailsView(DetailView): model = ZoomMeeting - template_name = 'meetings/meeting_details.html' - context_object_name = 'meeting' + template_name = "meetings/meeting_details.html" + context_object_name = "meeting" + class ZoomMeetingUpdateView(UpdateView): model = ZoomMeeting form_class = ZoomMeetingForm - context_object_name = 'meeting' - template_name = 'meetings/update_meeting.html' - success_url = '/' + context_object_name = "meeting" + template_name = "meetings/update_meeting.html" + success_url = "/" def form_valid(self, form): instance = form.save(commit=False) updated_data = { - 'topic': instance.topic, - 'start_time': instance.start_time.isoformat() + "Z", - 'duration': instance.duration + "topic": instance.topic, + "start_time": instance.start_time.isoformat() + "Z", + "duration": instance.duration, } if instance.start_time < timezone.now(): messages.error(self.request, "Start time must be in the future.") - return redirect(f'/update-meeting/{instance.pk}/', status=400) + return redirect(f"/update-meeting/{instance.pk}/", status=400) result = update_zoom_meeting(instance.meeting_id, updated_data) if result["status"] == "success": instance.save() messages.success(self.request, result["message"]) - return redirect(reverse('meeting_details', kwargs={'pk': instance.pk})) + return redirect(reverse("meeting_details", kwargs={"pk": instance.pk})) else: messages.error(self.request, result["message"]) - return redirect(reverse('meeting_details', kwargs={'pk': instance.pk})) + return redirect(reverse("meeting_details", kwargs={"pk": instance.pk})) + def ZoomMeetingDeleteView(request, pk): meeting = get_object_or_404(ZoomMeeting, pk=pk) @@ -140,13 +167,13 @@ def ZoomMeetingDeleteView(request, pk): messages.success(request, result["message"]) else: messages.error(request, result["message"]) - return redirect('/') + return redirect("/") except Exception as e: messages.error(request, str(e)) - return redirect('/') + return redirect("/") -#Job Posting +# Job Posting # def job_list(request): # """Display the list of job postings order by creation date descending""" # jobs=JobPosting.objects.all().order_by('-created_at') @@ -154,7 +181,7 @@ def ZoomMeetingDeleteView(request, pk): # # Filter by status if provided # print(f"the request is: {request} ") # status=request.GET.get('status') -# print(f"DEBUG: Status filter received: {status}") +# print(f"DEBUG: Status filter received: {status}") # if status: # jobs=jobs.filter(status=status) @@ -170,147 +197,159 @@ def ZoomMeetingDeleteView(request, pk): def create_job(request): """Create a new job posting""" - - if request.method=='POST': - form=JobPostingForm(request.POST,is_anonymous_user=not request.user.is_authenticated) - #to check user is authenticated or not + if request.method == "POST": + form = JobPostingForm( + request.POST, is_anonymous_user=not request.user.is_authenticated + ) + # to check user is authenticated or not if form.is_valid(): try: - job=form.save(commit=False) + job = form.save(commit=False) if request.user.is_authenticated: - job.created_by=request.user.get_full_name() or request.user.username + job.created_by = ( + request.user.get_full_name() or request.user.username + ) else: - job.created_by=request.POST.get('created_by','').strip() + job.created_by = request.POST.get("created_by", "").strip() if not job.created_by: - job.created_by="University Administrator" + job.created_by = "University Administrator" job.save() - messages.success(request,f'Job "{job.title}" created successfully!') - return redirect('job_list') + messages.success(request, f'Job "{job.title}" created successfully!') + return redirect("job_list") except Exception as e: logger.error(f"Error creating job: {e}") - messages.error(request,f"Error creating job: {e}") + messages.error(request, f"Error creating job: {e}") else: - messages.error(request, f'Please correct the errors below.{form.errors}') + messages.error(request, f"Please correct the errors below.{form.errors}") else: - form=JobPostingForm(is_anonymous_user=not request.user.is_authenticated) - return render(request,'jobs/create_job.html',{'form':form}) + form = JobPostingForm(is_anonymous_user=not request.user.is_authenticated) + return render(request, "jobs/create_job.html", {"form": form}) - - -def edit_job(request,slug): +def edit_job(request, slug): """Edit an existing job posting""" - if request.method=='POST': - job=get_object_or_404(JobPosting,slug=slug) - form=JobPostingForm(request.POST,instance=job,is_anonymous_user=not request.user.is_authenticated) + if request.method == "POST": + job = get_object_or_404(JobPosting, slug=slug) + form = JobPostingForm( + request.POST, + instance=job, + is_anonymous_user=not request.user.is_authenticated, + ) if form.is_valid(): try: - job=form.save(commit=False) + job = form.save(commit=False) if request.user.is_authenticated: - job.created_by=request.user.get_full_name() or request.user.username + job.created_by = ( + request.user.get_full_name() or request.user.username + ) else: - job.created_by=request.POST.get('created_by','').strip() + job.created_by = request.POST.get("created_by", "").strip() if not job.created_by: - job.created_by="University Administrator" + job.created_by = "University Administrator" job.save() - messages.success(request,f'Job "{job.title}" updated successfully!') - return redirect('job_list') + messages.success(request, f'Job "{job.title}" updated successfully!') + return redirect("job_list") except Exception as e: logger.error(f"Error updating job: {e}") - messages.error(request,f"Error updating job: {e}") + messages.error(request, f"Error updating job: {e}") else: - messages.error(request, 'Please correct the errors below.') + messages.error(request, "Please correct the errors below.") else: - job=get_object_or_404(JobPosting,slug=slug) - form=JobPostingForm(instance=job,is_anonymous_user=not request.user.is_authenticated) - return render(request,'jobs/edit_job.html',{'form':form,'job':job}) + job = get_object_or_404(JobPosting, slug=slug) + form = JobPostingForm( + instance=job, is_anonymous_user=not request.user.is_authenticated + ) + return render(request, "jobs/edit_job.html", {"form": form, "job": job}) + 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 - candidates = job.candidates.all().order_by('-created_at') + candidates = job.candidates.all().order_by("-created_at") # Count candidates by stage for summary statistics total_candidates = candidates.count() - applied_count = candidates.filter(stage='Applied').count() - interview_count = candidates.filter(stage='Interview').count() - offer_count = candidates.filter(stage='Offer').count() + applied_count = candidates.filter(stage="Applied").count() + interview_count = candidates.filter(stage="Interview").count() + offer_count = candidates.filter(stage="Offer").count() context = { - 'job': job, - 'candidates': candidates, - 'total_candidates': total_candidates, - 'applied_count': applied_count, - 'interview_count': interview_count, - 'offer_count': offer_count, + "job": job, + "candidates": candidates, + "total_candidates": total_candidates, + "applied_count": applied_count, + "interview_count": interview_count, + "offer_count": offer_count, } - return render(request, 'jobs/job_detail.html', context) + return render(request, "jobs/job_detail.html", context) # job detail facing the candidate: -def job_detail_candidate(request,slug): - job=get_object_or_404(JobPosting,slug=slug) - return render(request,'jobs/job_detail_candidate.html',{'job':job}) +def job_detail_candidate(request, slug): + job = get_object_or_404(JobPosting, slug=slug) + return render(request, "jobs/job_detail_candidate.html", {"job": job}) -def post_to_linkedin(request,slug): + +def post_to_linkedin(request, slug): """Post a job to LinkedIn""" - job=get_object_or_404(JobPosting,slug=slug) - if job.status!='ACTIVE': - messages.info(request,'Only active jobs can be posted to LinkedIn.') - return redirect('job_list') + job = get_object_or_404(JobPosting, slug=slug) + if job.status != "ACTIVE": + messages.info(request, "Only active jobs can be posted to LinkedIn.") + return redirect("job_list") - if request.method=='POST': + if request.method == "POST": try: # Check if user is authenticated with LinkedIn - if 'linkedin_access_token' not in request.session: - messages.error(request,'Please authenticate with LinkedIn first.') - return redirect('linkedin_login') + if "linkedin_access_token" not in request.session: + messages.error(request, "Please authenticate with LinkedIn first.") + return redirect("linkedin_login") # Clear previous LinkedIn data for re-posting - job.posted_to_linkedin=False - job.linkedin_post_id='' - job.linkedin_post_url='' - job.linkedin_post_status='' - job.linkedin_posted_at=None + job.posted_to_linkedin = False + job.linkedin_post_id = "" + job.linkedin_post_url = "" + job.linkedin_post_status = "" + job.linkedin_posted_at = None job.save() # Initialize LinkedIn service - service=LinkedInService() - service.access_token=request.session['linkedin_access_token'] + service = LinkedInService() + service.access_token = request.session["linkedin_access_token"] # Post to LinkedIn - result=service.create_job_post(job) - if result['success']: + result = service.create_job_post(job) + if result["success"]: # Update job with LinkedIn info - job.posted_to_linkedin=True - job.linkedin_post_id=result['post_id'] - job.linkedin_post_url=result['post_url'] - job.linkedin_post_status='SUCCESS' - job.linkedin_posted_at=timezone.now() + job.posted_to_linkedin = True + job.linkedin_post_id = result["post_id"] + job.linkedin_post_url = result["post_url"] + job.linkedin_post_status = "SUCCESS" + job.linkedin_posted_at = timezone.now() job.save() - messages.success(request,'Job posted to LinkedIn successfully!') + messages.success(request, "Job posted to LinkedIn successfully!") else: - error_msg=result.get('error','Unknown error') - job.linkedin_post_status=f'ERROR: {error_msg}' + error_msg = result.get("error", "Unknown error") + job.linkedin_post_status = f"ERROR: {error_msg}" job.save() - messages.error(request,f'Error posting to LinkedIn: {error_msg}') + messages.error(request, f"Error posting to LinkedIn: {error_msg}") except Exception as e: logger.error(f"Error in post_to_linkedin: {e}") - job.linkedin_post_status = f'ERROR: {str(e)}' + job.linkedin_post_status = f"ERROR: {str(e)}" job.save() - messages.error(request, f'Error posting to LinkedIn: {e}') + messages.error(request, f"Error posting to LinkedIn: {e}") + + return redirect("job_detail", slug=job.slug) - return redirect('job_detail', slug=job.slug) def linkedin_login(request): """Redirect to LinkedIn OAuth""" - service=LinkedInService() - auth_url=service.get_auth_url() + service = LinkedInService() + auth_url = service.get_auth_url() """ It creates a special URL that: Sends the user to LinkedIn to log in @@ -323,31 +362,31 @@ def linkedin_login(request): def linkedin_callback(request): """Handle LinkedIn OAuth callback""" - code=request.GET.get('code') + code = request.GET.get("code") if not code: - messages.error(request,'No authorization code received from LinkedIn.') - return redirect('job_list') + messages.error(request, "No authorization code received from LinkedIn.") + return redirect("job_list") try: - service=LinkedInService() - #get_access_token(code)->It makes a POST request to LinkedIn’s token endpoint with parameters - access_token=service.get_access_token(code) - request.session['linkedin_access_token']=access_token - request.session['linkedin_authenticated']=True + service = LinkedInService() + # get_access_token(code)->It makes a POST request to LinkedIn’s token endpoint with parameters + 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!') + messages.success(request, "Successfully authenticated with LinkedIn!") except Exception as e: logger.error(f"LinkedIn authentication error: {e}") - messages.error(request,f'LinkedIn authentication failed: {e}') + messages.error(request, f"LinkedIn authentication failed: {e}") - return redirect('job_list') + return redirect("job_list") -#applicant views -def applicant_job_detail(request,slug): +# applicant views +def applicant_job_detail(request, slug): """View job details for applicants""" - job=get_object_or_404(JobPosting,slug=slug,status='ACTIVE') - return render(request,'jobs/applicant_job_detail.html',{'job':job}) + job = get_object_or_404(JobPosting, slug=slug, status="ACTIVE") + return render(request, "jobs/applicant_job_detail.html", {"job": job}) # Form Preview Views @@ -582,16 +621,17 @@ def applicant_job_detail(request,slug): # - @ensure_csrf_cookie def form_builder(request, template_id=None): """Render the form builder interface""" context = {} if template_id: - template = get_object_or_404(FormTemplate, id=template_id, created_by=request.user) - context['template_id'] = template.id - context['template_name'] = template.name - return render(request,'forms/form_builder.html',context) + template = get_object_or_404( + FormTemplate, id=template_id, created_by=request.user + ) + context["template_id"] = template.id + context["template_name"] = template.name + return render(request, "forms/form_builder.html", context) @csrf_exempt @@ -600,13 +640,15 @@ def save_form_template(request): """Save a new or existing form template""" try: data = json.loads(request.body) - template_name = data.get('name', 'Untitled Form') - stages_data = data.get('stages', []) - template_id = data.get('template_id') + template_name = data.get("name", "Untitled Form") + stages_data = data.get("stages", []) + template_id = data.get("template_id") if template_id: # Update existing template - template = get_object_or_404(FormTemplate, id=template_id, created_by=request.user) + template = get_object_or_404( + FormTemplate, id=template_id, created_by=request.user + ) template.name = template_name template.save() # Clear existing stages and fields @@ -614,50 +656,48 @@ def save_form_template(request): else: # Create new template template = FormTemplate.objects.create( - name=template_name, - created_by=request.user + name=template_name, created_by=request.user ) # Create stages and fields for stage_order, stage_data in enumerate(stages_data): stage = FormStage.objects.create( template=template, - name=stage_data['name'], + name=stage_data["name"], order=stage_order, - is_predefined=stage_data.get('predefined', False) + is_predefined=stage_data.get("predefined", False), ) - for field_order, field_data in enumerate(stage_data['fields']): - options = field_data.get('options', []) + for field_order, field_data in enumerate(stage_data["fields"]): + options = field_data.get("options", []) if not isinstance(options, list): options = [] - file_types = field_data.get('fileTypes', '') - max_file_size = field_data.get('maxFileSize', 5) + file_types = field_data.get("fileTypes", "") + max_file_size = field_data.get("maxFileSize", 5) FormField.objects.create( stage=stage, - label=field_data.get('label', ''), - field_type=field_data.get('type', 'text'), - placeholder=field_data.get('placeholder', ''), - required=field_data.get('required', False), + label=field_data.get("label", ""), + field_type=field_data.get("type", "text"), + placeholder=field_data.get("placeholder", ""), + required=field_data.get("required", False), order=field_order, - is_predefined=field_data.get('predefined', False), + is_predefined=field_data.get("predefined", False), options=options, file_types=file_types, - max_file_size=max_file_size + max_file_size=max_file_size, ) - return JsonResponse({ - 'success': True, - 'template_id': template.id, - 'message': 'Form template saved successfully!' - }) + return JsonResponse( + { + "success": True, + "template_id": template.id, + "message": "Form template saved successfully!", + } + ) except Exception as e: - return JsonResponse({ - 'success': False, - 'error': str(e) - }, status=400) + return JsonResponse({"success": False, "error": str(e)}, status=400) @require_http_methods(["GET"]) @@ -669,38 +709,46 @@ def load_form_template(request, template_id): for stage in template.stages.all(): fields = [] for field in stage.fields.all(): - fields.append({ - 'id': field.id, - 'type': field.field_type, - 'label': field.label, - 'placeholder': field.placeholder, - 'required': field.required, - 'options': field.options, - 'fileTypes': field.file_types, - 'maxFileSize': field.max_file_size, - 'predefined': field.is_predefined - }) - stages.append({ - 'id': stage.id, - 'name': stage.name, - 'predefined': stage.is_predefined, - 'fields': fields - }) + fields.append( + { + "id": field.id, + "type": field.field_type, + "label": field.label, + "placeholder": field.placeholder, + "required": field.required, + "options": field.options, + "fileTypes": field.file_types, + "maxFileSize": field.max_file_size, + "predefined": field.is_predefined, + } + ) + stages.append( + { + "id": stage.id, + "name": stage.name, + "predefined": stage.is_predefined, + "fields": fields, + } + ) - return JsonResponse({ - 'success': True, - 'template': { - '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 + return JsonResponse( + { + "success": True, + "template": { + "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', '') + query = request.GET.get("q", "") templates = FormTemplate.objects.filter(created_by=request.user) if query: @@ -708,154 +756,176 @@ def form_templates_list(request): Q(name__icontains=query) | Q(description__icontains=query) ) - templates = templates.order_by('-created_at') + templates = templates.order_by("-created_at") paginator = Paginator(templates, 10) # Show 10 templates per page - page_number = request.GET.get('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) + 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': + 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_templates_list') + messages.success( + request, f'Form template "{template.name}" created successfully!' + ) + return redirect("form_templates_list") else: form = FormTemplateForm() - return render(request, 'forms/create_form_template.html', {'form': form}) + 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""" templates = FormTemplate.objects.filter(created_by=request.user).values( - 'id', 'name', 'description', 'created_at', 'updated_at' + "id", "name", "description", "created_at", "updated_at" ) - return JsonResponse({ - 'success': True, - 'templates': list(templates) - }) + return JsonResponse({"success": True, "templates": list(templates)}) + @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.delete() - return JsonResponse({'success': True, 'message': 'Form template deleted successfully!'}) - + return JsonResponse( + {"success": True, "message": "Form template deleted successfully!"} + ) def form_wizard_view(request, template_id): """Display the form as a step-by-step wizard""" template = get_object_or_404(FormTemplate, id=template_id, is_active=True) - job_id=template.job.internal_job_id - return render(request, 'forms/form_wizard.html', {'template_id': template_id,'job_id':job_id}) -@require_http_methods(["POST"]) + job_id = template.job.internal_job_id + return render( + request, + "forms/form_wizard.html", + {"template_id": template_id, "job_id": job_id}, + ) + + +@require_http_methods(["GET", "POST"]) def submit_form(request, template_id): """Handle form submission""" - try: - template = get_object_or_404(FormTemplate, id=template_id) - - # # Create form submission - # print({key: value for key, value in request.POST.items()}) - # first_name = next((value for key, value in request.POST.items() if key == 'First Name'), None) - # last_name = next((value for key, value in request.POST.items() if key == 'Last Name'), None) - # email = next((value for key, value in request.POST.items() if key == 'Email Address'), None) - # phone = next((value for key, value in request.POST.items() if key == 'Phone Number'), None) - # address = next((value for key, value in request.POST.items() if key == 'Address'), None) - # resume = next((value for key, value in request.POST.items() if key == 'Resume Upload'), None) - # print(first_name, last_name, email, phone, address, resume) - # create candidate - - submission = FormSubmission.objects.create(template=template) - # Process field responses - for field_id, value in request.POST.items(): - if field_id.startswith('field_'): - actual_field_id = field_id.replace('field_', '') - try: - field = FormField.objects.get(id=actual_field_id, stage__template=template) - FieldResponse.objects.create( - submission=submission, - field=field, - value=value if value else None - ) - except FormField.DoesNotExist: - continue - - # Handle file uploads - for field_id, uploaded_file in request.FILES.items(): - if field_id.startswith('field_'): - actual_field_id = field_id.replace('field_', '') - try: - field = FormField.objects.get(id=actual_field_id, stage__template=template) - FieldResponse.objects.create( - submission=submission, - field=field, - uploaded_file=uploaded_file - ) - except FormField.DoesNotExist: - continue + print("request method", request.method) + if request.method == "POST": try: - first_name = submission.responses.get(field__label="First Name") - last_name = submission.responses.get(field__label="Last Name") - email = submission.responses.get(field__label="Email Address") - phone = submission.responses.get(field__label="Phone Number") - address = submission.responses.get(field__label="Address") - resume = submission.responses.get(field__label="Resume Upload") + template = get_object_or_404(FormTemplate, id=template_id) - submission.applicant_name = f"{first_name.display_value} {last_name.display_value}" - submission.applicant_email = email.display_value - submission.save() - Candidate.objects.create( - first_name=first_name.display_value, - last_name=last_name.display_value, - email=email.display_value, - phone=phone.display_value, - address=address.display_value, - resume=resume.get_file if resume.is_file else None, - job=submission.template.job + # # Create form submission + # print({key: value for key, value in request.POST.items()}) + # first_name = next((value for key, value in request.POST.items() if key == 'First Name'), None) + # last_name = next((value for key, value in request.POST.items() if key == 'Last Name'), None) + # email = next((value for key, value in request.POST.items() if key == 'Email Address'), None) + # phone = next((value for key, value in request.POST.items() if key == 'Phone Number'), None) + # address = next((value for key, value in request.POST.items() if key == 'Address'), None) + # resume = next((value for key, value in request.POST.items() if key == 'Resume Upload'), None) + # print(first_name, last_name, email, phone, address, resume) + # create candidate + + submission = FormSubmission.objects.create(template=template) + # Process field responses + for field_id, value in request.POST.items(): + if field_id.startswith("field_"): + actual_field_id = field_id.replace("field_", "") + try: + field = FormField.objects.get( + id=actual_field_id, stage__template=template + ) + FieldResponse.objects.create( + submission=submission, + field=field, + value=value if value else None, + ) + except FormField.DoesNotExist: + continue + + # Handle file uploads + for field_id, uploaded_file in request.FILES.items(): + if field_id.startswith("field_"): + actual_field_id = field_id.replace("field_", "") + try: + field = FormField.objects.get( + id=actual_field_id, stage__template=template + ) + FieldResponse.objects.create( + submission=submission, + field=field, + uploaded_file=uploaded_file, + ) + except FormField.DoesNotExist: + continue + try: + first_name = submission.responses.get(field__label="First Name") + last_name = submission.responses.get(field__label="Last Name") + email = submission.responses.get(field__label="Email Address") + phone = submission.responses.get(field__label="Phone Number") + address = submission.responses.get(field__label="Address") + resume = submission.responses.get(field__label="Resume Upload") + + submission.applicant_name = ( + f"{first_name.display_value} {last_name.display_value}" + ) + submission.applicant_email = email.display_value + submission.save() + Candidate.objects.create( + first_name=first_name.display_value, + last_name=last_name.display_value, + email=email.display_value, + phone=phone.display_value, + address=address.display_value, + resume=resume.get_file if resume.is_file else None, + job=submission.template.job, + ) + except Exception as e: + logger.error(f"Candidate creation failed,{e}") + pass + return JsonResponse( + { + "success": True, + "message": "Form submitted successfully!", + "submission_id": submission.id, + } ) except Exception as e: - logger.error(f"Candidate creation failed,{e}") - pass - return JsonResponse({ - 'success': True, - 'message': 'Form submitted successfully!', - 'submission_id': submission.id - }) - except Exception as e: - return JsonResponse({ - 'success': False, - 'error': str(e) - }, status=400) + return JsonResponse({"success": False, "error": str(e)}, status=400) + else: + # Handle GET request - this should not happen for form submission + return JsonResponse( + {"success": False, "error": "GET method not allowed for form submission"}, + status=405, + ) -def form_template_submissions_list(request, template_slug): + +def form_template_submissions_list(request, slug): """List all submissions for a specific form template""" - template = get_object_or_404(FormTemplate, slug=template_slug, created_by=request.user) + template = get_object_or_404(FormTemplate, slug=slug) - submissions = FormSubmission.objects.filter(template=template).order_by('-submitted_at') + submissions = FormSubmission.objects.filter(template=template).order_by( + "-submitted_at" + ) # Pagination paginator = Paginator(submissions, 10) # Show 10 submissions per page - page_number = request.GET.get('page') + page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) - return render(request, 'forms/form_template_submissions_list.html', { - 'template': template, - 'page_obj': page_obj - }) + return render( + request, + "forms/form_template_submissions_list.html", + {"template": template, "page_obj": page_obj}, + ) + def form_submission_details(request, template_id, submission_id): """Display detailed view of a specific form submission""" @@ -866,59 +936,66 @@ def form_submission_details(request, template_id, submission_id): submission = get_object_or_404(FormSubmission, id=submission_id, template=template) # Get all stages with their fields - stages = template.stages.prefetch_related('fields').order_by('order') + stages = template.stages.prefetch_related("fields").order_by("order") # Get all responses for this submission, ordered by field order - responses = submission.responses.select_related('field').order_by('field__order') + responses = submission.responses.select_related("field").order_by("field__order") # Group responses by stage stage_responses = {} for stage in stages: stage_responses[stage.id] = { - 'stage': stage, - 'responses': responses.filter(field__stage=stage) + "stage": stage, + "responses": responses.filter(field__stage=stage), } - return render(request, 'forms/form_submission_details.html', { - 'template': template, - 'submission': submission, - 'stages': stages, - 'responses': responses, - 'stage_responses': stage_responses - }) + return render( + request, + "forms/form_submission_details.html", + { + "template": template, + "submission": submission, + "stages": stages, + "responses": responses, + "stage_responses": stage_responses, + }, + ) + def schedule_interviews_view(request, job_id): - job = get_object_or_404(Job, id=job_id) + job = get_object_or_404(JobPosting, id=job_id) - if request.method == 'POST': + if request.method == "POST": form = InterviewScheduleForm(job_id, request.POST) break_formset = BreakTimeFormSet(request.POST) # Check if this is a confirmation request - if 'confirm_schedule' in request.POST: + if "confirm_schedule" in request.POST: # Get the schedule data from session - schedule_data = request.session.get('interview_schedule_data') + schedule_data = request.session.get("interview_schedule_data") if not schedule_data: messages.error(request, "Session expired. Please try again.") - return redirect('schedule_interviews', job_id=job_id) + return redirect("schedule_interviews", job_id=job_id) # Create the interview schedule schedule = InterviewSchedule.objects.create( - job=job, - created_by=request.user, - **schedule_data + job=job, created_by=request.user, **schedule_data ) # Add candidates to the schedule - candidates = Candidate.objects.filter(id__in=schedule_data['candidate_ids']) + candidates = Candidate.objects.filter(id__in=schedule_data["candidate_ids"]) schedule.candidates.set(candidates) # Add break times to the schedule - if 'breaks' in schedule_data and schedule_data['breaks']: - for break_data in schedule_data['breaks']: + if "breaks" in schedule_data and schedule_data["breaks"]: + for break_data in schedule_data["breaks"]: break_time = BreakTime.objects.create( - start_time=datetime.strptime(break_data['start_time'], '%H:%M:%S').time(), - end_time=datetime.strptime(break_data['end_time'], '%H:%M:%S').time() + start_time=datetime.strptime( + break_data["start_time"], "%H:%M:%S" + ).time(), + end_time=datetime.strptime( + break_data["end_time"], "%H:%M:%S" + ).time(), ) schedule.breaks.add(break_time) @@ -926,40 +1003,42 @@ def schedule_interviews_view(request, job_id): try: scheduled_count = schedule_interviews(schedule) messages.success( - request, - f"Successfully scheduled {scheduled_count} interviews." + request, f"Successfully scheduled {scheduled_count} interviews." ) # Clear the session data - if 'interview_schedule_data' in request.session: - del request.session['interview_schedule_data'] - return redirect('job_detail', pk=job_id) + if "interview_schedule_data" in request.session: + del request.session["interview_schedule_data"] + return redirect("job_detail", pk=job_id) except Exception as e: - messages.error( - request, - f"Error scheduling interviews: {str(e)}" - ) - return redirect('schedule_interviews', job_id=job_id) + messages.error(request, f"Error scheduling interviews: {str(e)}") + return redirect("schedule_interviews", job_id=job_id) # This is the initial form submission if form.is_valid() and break_formset.is_valid(): # Get the form data - candidates = form.cleaned_data['candidates'] - start_date = form.cleaned_data['start_date'] - end_date = form.cleaned_data['end_date'] - working_days = form.cleaned_data['working_days'] - start_time = form.cleaned_data['start_time'] - end_time = form.cleaned_data['end_time'] - interview_duration = form.cleaned_data['interview_duration'] - buffer_time = form.cleaned_data['buffer_time'] + candidates = form.cleaned_data["candidates"] + start_date = form.cleaned_data["start_date"] + end_date = form.cleaned_data["end_date"] + working_days = form.cleaned_data["working_days"] + start_time = form.cleaned_data["start_time"] + end_time = form.cleaned_data["end_time"] + interview_duration = form.cleaned_data["interview_duration"] + buffer_time = form.cleaned_data["buffer_time"] # Process break times breaks = [] for break_form in break_formset: - if break_form.cleaned_data and not break_form.cleaned_data.get('DELETE'): - breaks.append({ - 'start_time': break_form.cleaned_data['start_time'].isoformat(), - 'end_time': break_form.cleaned_data['end_time'].isoformat() - }) + if break_form.cleaned_data and not break_form.cleaned_data.get( + "DELETE" + ): + breaks.append( + { + "start_time": break_form.cleaned_data[ + "start_time" + ].isoformat(), + "end_time": break_form.cleaned_data["end_time"].isoformat(), + } + ) # Create a temporary schedule object (not saved to DB) temp_schedule = InterviewSchedule( @@ -970,16 +1049,22 @@ def schedule_interviews_view(request, job_id): start_time=start_time, end_time=end_time, interview_duration=interview_duration, - buffer_time=buffer_time + buffer_time=buffer_time, ) # Create temporary break time objects temp_breaks = [] for break_data in breaks: - temp_breaks.append(BreakTime( - start_time=datetime.strptime(break_data['start_time'], '%H:%M:%S').time(), - end_time=datetime.strptime(break_data['end_time'], '%H:%M:%S').time() - )) + temp_breaks.append( + BreakTime( + start_time=datetime.strptime( + break_data["start_time"], "%H:%M:%S" + ).time(), + end_time=datetime.strptime( + break_data["end_time"], "%H:%M:%S" + ).time(), + ) + ) # Get available slots available_slots = get_available_time_slots(temp_schedule, temp_breaks) @@ -987,57 +1072,59 @@ def schedule_interviews_view(request, job_id): if len(available_slots) < len(candidates): messages.error( request, - f"Not enough available slots. Required: {len(candidates)}, Available: {len(available_slots)}" + f"Not enough available slots. Required: {len(candidates)}, Available: {len(available_slots)}", + ) + return render( + request, + "interviews/schedule_interviews.html", + {"form": form, "break_formset": break_formset, "job": job}, ) - return render(request, 'interviews/schedule_interviews.html', { - 'form': form, - 'break_formset': break_formset, - 'job': job - }) # Create a preview schedule preview_schedule = [] for i, candidate in enumerate(candidates): slot = available_slots[i] - preview_schedule.append({ - 'candidate': candidate, - 'date': slot['date'], - 'time': slot['time'] - }) + preview_schedule.append( + {"candidate": candidate, "date": slot["date"], "time": slot["time"]} + ) # Save the form data to session for later use schedule_data = { - 'start_date': start_date.isoformat(), - 'end_date': end_date.isoformat(), - 'working_days': working_days, - 'start_time': start_time.isoformat(), - 'end_time': end_time.isoformat(), - 'interview_duration': interview_duration, - 'buffer_time': buffer_time, - 'candidate_ids': [c.id for c in candidates], - 'breaks': breaks + "start_date": start_date.isoformat(), + "end_date": end_date.isoformat(), + "working_days": working_days, + "start_time": start_time.isoformat(), + "end_time": end_time.isoformat(), + "interview_duration": interview_duration, + "buffer_time": buffer_time, + "candidate_ids": [c.id for c in candidates], + "breaks": breaks, } - request.session['interview_schedule_data'] = schedule_data + request.session["interview_schedule_data"] = schedule_data # Render the preview page - return render(request, 'interviews/preview_schedule.html', { - 'job': job, - 'schedule': preview_schedule, - 'start_date': start_date, - 'end_date': end_date, - 'working_days': working_days, - 'start_time': start_time, - 'end_time': end_time, - 'breaks': breaks, - 'interview_duration': interview_duration, - 'buffer_time': buffer_time - }) + return render( + request, + "interviews/preview_schedule.html", + { + "job": job, + "schedule": preview_schedule, + "start_date": start_date, + "end_date": end_date, + "working_days": working_days, + "start_time": start_time, + "end_time": end_time, + "breaks": breaks, + "interview_duration": interview_duration, + "buffer_time": buffer_time, + }, + ) else: form = InterviewScheduleForm(job_id=job_id) break_formset = BreakTimeFormSet() - return render(request, 'interviews/schedule_interviews.html', { - 'form': form, - 'break_formset': break_formset, - 'job': job - }) + return render( + request, + "interviews/schedule_interviews.html", + {"form": form, "break_formset": break_formset, "job": job}, + ) diff --git a/recruitment/views_frontend.py b/recruitment/views_frontend.py index f323882..427f744 100644 --- a/recruitment/views_frontend.py +++ b/recruitment/views_frontend.py @@ -1,5 +1,8 @@ +import json from django.shortcuts import render, get_object_or_404 from django.http import JsonResponse + +from recruitment.utils import json_to_markdown_table from . import models from django.utils.translation import get_language from . import forms @@ -19,6 +22,8 @@ from datastar_py.django import ( ServerSentEventGenerator as SSE, read_signals, ) +# from rich import print +from rich.markdown import CodeBlock class JobListView(LoginRequiredMixin, ListView): model = models.JobPosting @@ -41,7 +46,7 @@ class JobListView(LoginRequiredMixin, ListView): # Filter for non-staff users if not self.request.user.is_staff: queryset = queryset.filter(status='Published') - + status=self.request.GET.get('status') if status: queryset=queryset.filter(status=status) @@ -49,7 +54,7 @@ class JobListView(LoginRequiredMixin, ListView): return queryset def get_context_data(self, **kwargs): - + context = super().get_context_data(**kwargs) context['search_query'] = self.request.GET.get('search', '') context['lang'] = get_language() @@ -201,6 +206,7 @@ def training_list(request): def candidate_detail(request, slug): + from rich.json import JSON candidate = get_object_or_404(models.Candidate, slug=slug) try: parsed = ast.literal_eval(candidate.parsed_summary) @@ -212,6 +218,8 @@ def candidate_detail(request, slug): if request.user.is_staff: stage_form = forms.CandidateStageForm(candidate=candidate) + # parsed = JSON(json.dumps(parsed), indent=2, highlight=True, skip_keys=False, ensure_ascii=False, check_circular=True, allow_nan=True, default=None, sort_keys=False) + parsed = json_to_markdown_table([parsed]) return render(request, 'recruitment/candidate_detail.html', { 'candidate': candidate, 'parsed': parsed, @@ -219,7 +227,7 @@ def candidate_detail(request, slug): }) def candidate_update_stage(request, slug): - """Handle HTMX stage update requests""" + """Handle HTMX stage update requests""" try: if not request.user.is_staff: return render(request, 'recruitment/partials/error.html', {'error': 'Permission denied'}, status=403) @@ -293,7 +301,7 @@ class TrainingListView(LoginRequiredMixin, ListView): template_name = 'recruitment/training_list.html' context_object_name = 'materials' paginate_by = 10 - + def get_queryset(self): queryset = super().get_queryset() diff --git a/templates/forms/form_template_submissions_list.html b/templates/forms/form_template_submissions_list.html index 6cdec8a..6ab346a 100644 --- a/templates/forms/form_template_submissions_list.html +++ b/templates/forms/form_template_submissions_list.html @@ -1,100 +1,331 @@ -{% extends "base.html" %} +{% extends 'base.html' %} +{% load static i18n crispy_forms_tags %} +{% load partials %} -{% block title %}Submissions for {{ template.name }}{% endblock %} +{% block title %}Submissions for {{ template.name }} - ATS{% endblock %} + +{% block customCSS %} + +{% endblock %} {% block content %} -