diff --git a/NorahUniversity/__pycache__/settings.cpython-312.pyc b/NorahUniversity/__pycache__/settings.cpython-312.pyc index 8d89095..160b1cf 100644 Binary files a/NorahUniversity/__pycache__/settings.cpython-312.pyc and b/NorahUniversity/__pycache__/settings.cpython-312.pyc differ diff --git a/NorahUniversity/__pycache__/urls.cpython-312.pyc b/NorahUniversity/__pycache__/urls.cpython-312.pyc index e44eacc..df79ce7 100644 Binary files a/NorahUniversity/__pycache__/urls.cpython-312.pyc and b/NorahUniversity/__pycache__/urls.cpython-312.pyc differ diff --git a/NorahUniversity/settings.py b/NorahUniversity/settings.py index 605ed40..705d22e 100644 --- a/NorahUniversity/settings.py +++ b/NorahUniversity/settings.py @@ -135,13 +135,21 @@ WSGI_APPLICATION = 'NorahUniversity.wsgi.application' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'norahuniversity', - 'USER': 'norahuniversity', - 'PASSWORD': 'norahuniversity', + 'NAME': 'haikal_db', + 'USER': 'faheed', + 'PASSWORD': 'Faheed@215', 'HOST': '127.0.0.1', 'PORT': '5432', } } + +# DATABASES = { +# 'default': { +# 'ENGINE': 'django.db.backends.sqlite3', +# 'NAME': BASE_DIR / 'db.sqlite3', +# } +# } + # Password validation # https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators @@ -163,7 +171,6 @@ AUTH_PASSWORD_VALIDATORS = [ ] - ACCOUNT_LOGIN_METHODS = ['email'] ACCOUNT_SIGNUP_FIELDS = ['email*', 'password1*', 'password2*'] @@ -171,6 +178,7 @@ ACCOUNT_UNIQUE_EMAIL = True ACCOUNT_EMAIL_VERIFICATION = 'none' ACCOUNT_USER_MODEL_USERNAME_FIELD = None + ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True @@ -189,6 +197,10 @@ CRISPY_BS5 = { 'use_css_helpers': True, } +ACCOUNT_RATE_LIMITS = { + 'send_email_confirmation': None, # Disables the limit +} + # Internationalization # https://docs.djangoproject.com/en/5.2/topics/i18n/ diff --git a/recruitment/__pycache__/admin.cpython-312.pyc b/recruitment/__pycache__/admin.cpython-312.pyc index 07aeb8c..0b0a860 100644 Binary files a/recruitment/__pycache__/admin.cpython-312.pyc and b/recruitment/__pycache__/admin.cpython-312.pyc differ diff --git a/recruitment/__pycache__/forms.cpython-312.pyc b/recruitment/__pycache__/forms.cpython-312.pyc index bba0bac..9725eb3 100644 Binary files a/recruitment/__pycache__/forms.cpython-312.pyc and b/recruitment/__pycache__/forms.cpython-312.pyc differ diff --git a/recruitment/__pycache__/linkedin_service.cpython-312.pyc b/recruitment/__pycache__/linkedin_service.cpython-312.pyc index 63027e7..c18ea68 100644 Binary files a/recruitment/__pycache__/linkedin_service.cpython-312.pyc and b/recruitment/__pycache__/linkedin_service.cpython-312.pyc differ diff --git a/recruitment/__pycache__/models.cpython-312.pyc b/recruitment/__pycache__/models.cpython-312.pyc index fa57734..53b4e48 100644 Binary files a/recruitment/__pycache__/models.cpython-312.pyc and b/recruitment/__pycache__/models.cpython-312.pyc differ diff --git a/recruitment/__pycache__/urls.cpython-312.pyc b/recruitment/__pycache__/urls.cpython-312.pyc index b4efb3c..0baf2e8 100644 Binary files a/recruitment/__pycache__/urls.cpython-312.pyc and b/recruitment/__pycache__/urls.cpython-312.pyc differ diff --git a/recruitment/__pycache__/utils.cpython-312.pyc b/recruitment/__pycache__/utils.cpython-312.pyc index b9c315a..83c8083 100644 Binary files a/recruitment/__pycache__/utils.cpython-312.pyc and b/recruitment/__pycache__/utils.cpython-312.pyc differ diff --git a/recruitment/__pycache__/validators.cpython-312.pyc b/recruitment/__pycache__/validators.cpython-312.pyc index 56c5562..0df3c0e 100644 Binary files a/recruitment/__pycache__/validators.cpython-312.pyc and b/recruitment/__pycache__/validators.cpython-312.pyc differ diff --git a/recruitment/__pycache__/views.cpython-312.pyc b/recruitment/__pycache__/views.cpython-312.pyc index cf28cc0..4f4ce2a 100644 Binary files a/recruitment/__pycache__/views.cpython-312.pyc and b/recruitment/__pycache__/views.cpython-312.pyc differ diff --git a/recruitment/__pycache__/views_frontend.cpython-312.pyc b/recruitment/__pycache__/views_frontend.cpython-312.pyc index a6f5a1f..188826c 100644 Binary files a/recruitment/__pycache__/views_frontend.cpython-312.pyc and b/recruitment/__pycache__/views_frontend.cpython-312.pyc differ diff --git a/recruitment/forms.py b/recruitment/forms.py index ec2186b..1794f3f 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -693,3 +693,7 @@ class StaffUserCreationForm(UserCreationForm): +class ToggleAccountForm(forms.Form): + pass + + diff --git a/recruitment/linkedin_service.py b/recruitment/linkedin_service.py index d4095ae..63da644 100644 --- a/recruitment/linkedin_service.py +++ b/recruitment/linkedin_service.py @@ -252,6 +252,9 @@ class LinkedInService: hashtags.insert(0, dept_hashtag) message_parts.append("\n" + " ".join(hashtags)) + + if len(message_parts)>=3000: + message_parts=message_parts[0:2980]+"........" return "\n".join(message_parts) diff --git a/recruitment/migrations/0001_initial.py b/recruitment/migrations/0001_initial.py index 1895908..3cfbbcb 100644 --- a/recruitment/migrations/0001_initial.py +++ b/recruitment/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.7 on 2025-10-17 19:41 +# Generated by Django 5.2.7 on 2025-10-19 15:50 import django.core.validators import django.db.models.deletion @@ -99,8 +99,8 @@ class Migration(migrations.Migration): ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), ('topic', models.CharField(max_length=255, verbose_name='Topic')), - ('meeting_id', models.CharField(max_length=20, unique=True, verbose_name='Meeting ID')), - ('start_time', models.DateTimeField(verbose_name='Start Time')), + ('meeting_id', models.CharField(db_index=True, max_length=20, unique=True, verbose_name='Meeting ID')), + ('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')), ('duration', models.PositiveIntegerField(verbose_name='Duration')), ('timezone', models.CharField(max_length=50, verbose_name='Timezone')), ('join_url', models.URLField(verbose_name='Join URL')), @@ -110,7 +110,7 @@ class Migration(migrations.Migration): ('mute_upon_entry', models.BooleanField(default=False, verbose_name='Mute Upon Entry')), ('waiting_room', models.BooleanField(default=False, verbose_name='Waiting Room')), ('zoom_gateway_response', models.JSONField(blank=True, null=True, verbose_name='Zoom Gateway Response')), - ('status', models.CharField(blank=True, max_length=20, null=True, verbose_name='Status')), + ('status', models.CharField(blank=True, db_index=True, default='waiting', max_length=20, null=True, verbose_name='Status')), ], options={ 'abstract': False, @@ -142,41 +142,6 @@ class Migration(migrations.Migration): 'ordering': ['order'], }, ), - migrations.CreateModel( - name='FormSubmission', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), - ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), - ('submitted_at', models.DateTimeField(auto_now_add=True)), - ('applicant_name', models.CharField(blank=True, help_text='Name of the applicant', max_length=200)), - ('applicant_email', models.EmailField(blank=True, help_text='Email of the applicant', max_length=254)), - ('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='form_submissions', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'Form Submission', - 'verbose_name_plural': 'Form Submissions', - 'ordering': ['-submitted_at'], - }, - ), - migrations.CreateModel( - name='FieldResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), - ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), - ('value', models.JSONField(blank=True, help_text='Response value (stored as JSON)', null=True)), - ('uploaded_file', models.FileField(blank=True, null=True, upload_to='form_uploads/')), - ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formfield')), - ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formsubmission')), - ], - options={ - 'verbose_name': 'Field Response', - 'verbose_name_plural': 'Field Responses', - }, - ), migrations.CreateModel( name='FormTemplate', fields=[ @@ -195,10 +160,24 @@ class Migration(migrations.Migration): 'ordering': ['-created_at'], }, ), - migrations.AddField( - model_name='formsubmission', - name='template', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='recruitment.formtemplate'), + migrations.CreateModel( + name='FormSubmission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('submitted_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('applicant_name', models.CharField(blank=True, help_text='Name of the applicant', max_length=200)), + ('applicant_email', models.EmailField(blank=True, db_index=True, help_text='Email of the applicant', max_length=254)), + ('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='form_submissions', to=settings.AUTH_USER_MODEL)), + ('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='recruitment.formtemplate')), + ], + options={ + 'verbose_name': 'Form Submission', + 'verbose_name_plural': 'Form Submissions', + 'ordering': ['-submitted_at'], + }, ), migrations.AddField( model_name='formstage', @@ -214,7 +193,7 @@ class Migration(migrations.Migration): ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), ('first_name', models.CharField(max_length=255, verbose_name='First Name')), ('last_name', models.CharField(max_length=255, verbose_name='Last Name')), - ('email', models.EmailField(max_length=254, verbose_name='Email')), + ('email', models.EmailField(db_index=True, 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')), @@ -222,19 +201,16 @@ class Migration(migrations.Migration): ('is_potential_candidate', models.BooleanField(default=False, verbose_name='Potential Candidate')), ('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')), ('applied', models.BooleanField(default=False, verbose_name='Applied')), - ('stage', models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer')], default='Applied', max_length=100, verbose_name='Stage')), + ('stage', models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer')], db_index=True, default='Applied', max_length=100, verbose_name='Stage')), ('applicant_status', models.CharField(blank=True, choices=[('Applicant', 'Applicant'), ('Candidate', 'Candidate')], default='Applicant', max_length=100, null=True, verbose_name='Applicant Status')), ('exam_date', models.DateTimeField(blank=True, null=True, verbose_name='Exam Date')), ('exam_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=100, null=True, verbose_name='Exam Status')), ('interview_date', models.DateTimeField(blank=True, null=True, verbose_name='Interview Date')), - ('interview_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected')], max_length=100, null=True, verbose_name='Interview Status')), + ('interview_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=100, null=True, verbose_name='Interview Status')), ('offer_date', models.DateField(blank=True, null=True, verbose_name='Offer Date')), ('offer_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected')], max_length=100, null=True, verbose_name='Offer Status')), ('join_date', models.DateField(blank=True, null=True, verbose_name='Join Date')), - ('match_score', models.IntegerField(blank=True, null=True)), - ('strengths', models.TextField(blank=True)), - ('weaknesses', models.TextField(blank=True)), - ('criteria_checklist', models.JSONField(blank=True, default=dict)), + ('ai_analysis_data', models.JSONField(default=dict, help_text='Full JSON output from the resume scoring model.', verbose_name='AI Analysis Data')), ('submitted_by_agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='submitted_candidates', to='recruitment.hiringagency', verbose_name='Submitted by Agency')), ], options={ @@ -261,22 +237,23 @@ class Migration(migrations.Migration): ('benefits', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)), ('application_url', models.URLField(blank=True, help_text='URL where candidates apply', null=True, validators=[django.core.validators.URLValidator()])), ('application_start_date', models.DateField(blank=True, null=True)), - ('application_deadline', models.DateField(blank=True, null=True)), + ('application_deadline', models.DateField(blank=True, db_index=True, null=True)), ('application_instructions', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)), ('internal_job_id', models.CharField(editable=False, max_length=50, primary_key=True, serialize=False)), ('created_by', models.CharField(blank=True, help_text='Name of person who created this job', max_length=100)), - ('status', models.CharField(choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], default='DRAFT', max_length=20)), + ('status', models.CharField(choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], db_index=True, default='DRAFT', max_length=20)), ('hash_tags', models.CharField(blank=True, help_text='Comma-separated hashtags for linkedin post like #hiring,#jobopening', max_length=200, validators=[recruitment.validators.validate_hash_tags])), ('linkedin_post_id', models.CharField(blank=True, help_text='LinkedIn post ID after posting', max_length=200)), ('linkedin_post_url', models.URLField(blank=True, help_text='Direct URL to LinkedIn post')), ('posted_to_linkedin', models.BooleanField(default=False)), ('linkedin_post_status', models.CharField(blank=True, help_text='Status of LinkedIn posting', max_length=50)), ('linkedin_posted_at', models.DateTimeField(blank=True, null=True)), - ('published_at', models.DateTimeField(blank=True, null=True)), + ('published_at', models.DateTimeField(blank=True, db_index=True, null=True)), ('position_number', models.CharField(blank=True, help_text='University position number', max_length=50)), ('reporting_to', models.CharField(blank=True, help_text='Who this position reports to', max_length=100)), ('joining_date', models.DateField(blank=True, help_text='Desired start date', null=True)), ('open_positions', models.PositiveIntegerField(default=1, help_text='Number of open positions for this job')), + ('max_applications', models.PositiveIntegerField(blank=True, default=1000, help_text='Maximum number of applications allowed', null=True)), ('cancel_reason', models.TextField(blank=True, help_text='Reason for canceling the job posting', verbose_name='Cancel Reason')), ('cancelled_by', models.CharField(blank=True, help_text='Name of person who cancelled this job', max_length=100, verbose_name='Cancelled By')), ('cancelled_at', models.DateTimeField(blank=True, null=True)), @@ -293,24 +270,22 @@ class Migration(migrations.Migration): name='InterviewSchedule', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), - ('start_date', models.DateField(verbose_name='Start Date')), - ('end_date', models.DateField(verbose_name='End Date')), + ('start_date', models.DateField(db_index=True, verbose_name='Start Date')), + ('end_date', models.DateField(db_index=True, verbose_name='End Date')), ('working_days', models.JSONField(verbose_name='Working Days')), ('start_time', models.TimeField(verbose_name='Start Time')), ('end_time', models.TimeField(verbose_name='End Time')), - ('breaks', models.JSONField(blank=True, default=list, verbose_name='Break Times')), + ('break_start_time', models.TimeField(blank=True, null=True, verbose_name='Break Start Time')), + ('break_end_time', models.TimeField(blank=True, null=True, verbose_name='Break End Time')), ('interview_duration', models.PositiveIntegerField(verbose_name='Interview Duration (minutes)')), ('buffer_time', models.PositiveIntegerField(default=0, verbose_name='Buffer Time (minutes)')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('candidates', models.ManyToManyField(related_name='interview_schedules', to='recruitment.candidate')), + ('candidates', models.ManyToManyField(blank=True, null=True, related_name='interview_schedules', to='recruitment.candidate')), ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_schedules', to='recruitment.jobposting')), ], - options={ - 'abstract': False, - }, ), migrations.AddField( model_name='formtemplate', @@ -402,9 +377,9 @@ class Migration(migrations.Migration): 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_date', models.DateField(db_index=True, 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)), + ('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], db_index=True, default='scheduled', max_length=20)), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), ('candidate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.candidate')), @@ -412,8 +387,92 @@ class Migration(migrations.Migration): ('schedule', models.ForeignKey(blank=True, null=True, 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')), ], + ), + migrations.CreateModel( + name='MeetingComment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Content')), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meeting_comments', to=settings.AUTH_USER_MODEL, verbose_name='Author')), + ('meeting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='recruitment.zoommeeting', verbose_name='Meeting')), + ], options={ - 'abstract': False, + 'verbose_name': 'Meeting Comment', + 'verbose_name_plural': 'Meeting Comments', + 'ordering': ['-created_at'], }, ), + migrations.CreateModel( + name='FieldResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('value', models.JSONField(blank=True, help_text='Response value (stored as JSON)', null=True)), + ('uploaded_file', models.FileField(blank=True, null=True, upload_to='form_uploads/')), + ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formfield')), + ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formsubmission')), + ], + options={ + 'verbose_name': 'Field Response', + 'verbose_name_plural': 'Field Responses', + 'indexes': [models.Index(fields=['submission'], name='recruitment_submiss_474130_idx'), models.Index(fields=['field'], name='recruitment_field_i_097e5b_idx')], + }, + ), + migrations.AddIndex( + model_name='formsubmission', + index=models.Index(fields=['submitted_at'], name='recruitment_submitt_7946c8_idx'), + ), + migrations.AddIndex( + model_name='interviewschedule', + index=models.Index(fields=['start_date'], name='recruitment_start_d_15d55e_idx'), + ), + migrations.AddIndex( + model_name='interviewschedule', + index=models.Index(fields=['end_date'], name='recruitment_end_dat_aeb00e_idx'), + ), + migrations.AddIndex( + model_name='interviewschedule', + index=models.Index(fields=['created_by'], name='recruitment_created_d0bdcc_idx'), + ), + migrations.AddIndex( + model_name='formtemplate', + index=models.Index(fields=['created_at'], name='recruitment_created_c21775_idx'), + ), + migrations.AddIndex( + model_name='formtemplate', + index=models.Index(fields=['is_active'], name='recruitment_is_acti_ae5efb_idx'), + ), + migrations.AddIndex( + model_name='candidate', + index=models.Index(fields=['stage'], name='recruitment_stage_f1c6eb_idx'), + ), + migrations.AddIndex( + model_name='candidate', + index=models.Index(fields=['created_at'], name='recruitment_created_73590f_idx'), + ), + migrations.AddIndex( + model_name='jobposting', + index=models.Index(fields=['status', 'created_at', 'title'], name='recruitment_status_8b77aa_idx'), + ), + migrations.AddIndex( + model_name='jobposting', + index=models.Index(fields=['slug'], name='recruitment_slug_004045_idx'), + ), + migrations.AddIndex( + model_name='scheduledinterview', + index=models.Index(fields=['job', 'status'], name='recruitment_job_id_f09e22_idx'), + ), + migrations.AddIndex( + model_name='scheduledinterview', + index=models.Index(fields=['interview_date', 'interview_time'], name='recruitment_intervi_7f5877_idx'), + ), + migrations.AddIndex( + model_name='scheduledinterview', + index=models.Index(fields=['candidate', 'job'], name='recruitment_candida_43d5b0_idx'), + ), ] diff --git a/recruitment/migrations/0014_formtemplate_close_at_formtemplate_max_applications_and_more.py b/recruitment/migrations/0014_formtemplate_close_at_formtemplate_max_applications_and_more.py deleted file mode 100644 index ac56849..0000000 --- a/recruitment/migrations/0014_formtemplate_close_at_formtemplate_max_applications_and_more.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-15 10:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0013_alter_formtemplate_created_by'), - ] - - operations = [ - migrations.AddField( - model_name='formtemplate', - name='close_at', - field=models.DateTimeField(blank=True, help_text='Date and time at which applications close', null=True), - ), - migrations.AddField( - model_name='formtemplate', - name='max_applications', - field=models.PositiveIntegerField(default=1000, help_text='Maximum number of applications allowed'), - ), - migrations.AlterField( - model_name='formtemplate', - name='created_at', - field=models.DateTimeField(auto_now_add=True), - ), - migrations.AlterField( - model_name='formtemplate', - name='updated_at', - field=models.DateTimeField(auto_now=True), - ), - ] diff --git a/recruitment/migrations/0015_remove_formtemplate_close_at_and_more.py b/recruitment/migrations/0015_remove_formtemplate_close_at_and_more.py deleted file mode 100644 index b519f91..0000000 --- a/recruitment/migrations/0015_remove_formtemplate_close_at_and_more.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-15 10:47 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0014_formtemplate_close_at_formtemplate_max_applications_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='formtemplate', - name='close_at', - ), - migrations.AlterField( - model_name='formtemplate', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), - ), - migrations.AlterField( - model_name='formtemplate', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), - ), - ] diff --git a/recruitment/migrations/0016_remove_formtemplate_max_applications_and_more.py b/recruitment/migrations/0016_remove_formtemplate_max_applications_and_more.py deleted file mode 100644 index e660370..0000000 --- a/recruitment/migrations/0016_remove_formtemplate_max_applications_and_more.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-15 10:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0015_remove_formtemplate_close_at_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='formtemplate', - name='max_applications', - ), - migrations.AddField( - model_name='jobposting', - name='max_applications', - field=models.PositiveIntegerField(default=1000, help_text='Maximum number of applications allowed'), - ), - ] diff --git a/recruitment/migrations/0017_remove_interviewschedule_breaks_and_more.py b/recruitment/migrations/0017_remove_interviewschedule_breaks_and_more.py deleted file mode 100644 index 4961d4d..0000000 --- a/recruitment/migrations/0017_remove_interviewschedule_breaks_and_more.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-15 15:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0016_remove_formtemplate_max_applications_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='interviewschedule', - name='breaks', - ), - migrations.AddField( - model_name='interviewschedule', - name='break_end', - field=models.TimeField(blank=True, null=True, verbose_name='Break End Time'), - ), - migrations.AddField( - model_name='interviewschedule', - name='break_start', - field=models.TimeField(blank=True, null=True, verbose_name='Break Start Time'), - ), - ] diff --git a/recruitment/migrations/0018_rename_break_end_interviewschedule_break_end_time_and_more.py b/recruitment/migrations/0018_rename_break_end_interviewschedule_break_end_time_and_more.py deleted file mode 100644 index 16b2ed2..0000000 --- a/recruitment/migrations/0018_rename_break_end_interviewschedule_break_end_time_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-15 15:55 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0017_remove_interviewschedule_breaks_and_more'), - ] - - operations = [ - migrations.RenameField( - model_name='interviewschedule', - old_name='break_end', - new_name='break_end_time', - ), - migrations.RenameField( - model_name='interviewschedule', - old_name='break_start', - new_name='break_start_time', - ), - ] diff --git a/recruitment/migrations/0019_alter_interviewschedule_candidates.py b/recruitment/migrations/0019_alter_interviewschedule_candidates.py deleted file mode 100644 index d10d068..0000000 --- a/recruitment/migrations/0019_alter_interviewschedule_candidates.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-15 16:07 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0018_rename_break_end_interviewschedule_break_end_time_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='interviewschedule', - name='candidates', - field=models.ManyToManyField(blank=True, null=True, related_name='interview_schedules', to='recruitment.candidate'), - ), - ] diff --git a/recruitment/migrations/0020_alter_interviewschedule_created_at.py b/recruitment/migrations/0020_alter_interviewschedule_created_at.py deleted file mode 100644 index 3824c35..0000000 --- a/recruitment/migrations/0020_alter_interviewschedule_created_at.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-15 16:09 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0019_alter_interviewschedule_candidates'), - ] - - operations = [ - migrations.AlterField( - model_name='interviewschedule', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), - ), - ] diff --git a/recruitment/migrations/0021_meetingcomment.py b/recruitment/migrations/0021_meetingcomment.py deleted file mode 100644 index 734d421..0000000 --- a/recruitment/migrations/0021_meetingcomment.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-16 13:52 - -import django.db.models.deletion -import django_ckeditor_5.fields -import django_extensions.db.fields -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0020_alter_interviewschedule_created_at'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='MeetingComment', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), - ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), - ('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Content')), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meeting_comments', to=settings.AUTH_USER_MODEL, verbose_name='Author')), - ('meeting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='recruitment.zoommeeting', verbose_name='Meeting')), - ], - options={ - 'verbose_name': 'Meeting Comment', - 'verbose_name_plural': 'Meeting Comments', - 'ordering': ['-created_at'], - }, - ), - ] diff --git a/recruitment/migrations/0022_candidate_resume_parsed_category.py b/recruitment/migrations/0022_candidate_resume_parsed_category.py deleted file mode 100644 index f767301..0000000 --- a/recruitment/migrations/0022_candidate_resume_parsed_category.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-16 19:39 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0021_meetingcomment'), - ] - - operations = [ - migrations.AddField( - model_name='candidate', - name='resume_parsed_category', - field=models.TextField(blank=True, verbose_name='Resume Parsed Category'), - ), - ] diff --git a/recruitment/migrations/0023_alter_jobposting_max_applications.py b/recruitment/migrations/0023_alter_jobposting_max_applications.py deleted file mode 100644 index 94104e1..0000000 --- a/recruitment/migrations/0023_alter_jobposting_max_applications.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-16 19:47 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0022_candidate_resume_parsed_category'), - ] - - operations = [ - migrations.AlterField( - model_name='jobposting', - name='max_applications', - field=models.PositiveIntegerField(blank=True, default=1000, help_text='Maximum number of applications allowed', null=True), - ), - ] diff --git a/recruitment/migrations/0024_alter_zoommeeting_status.py b/recruitment/migrations/0024_alter_zoommeeting_status.py deleted file mode 100644 index 9a69166..0000000 --- a/recruitment/migrations/0024_alter_zoommeeting_status.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.4 on 2025-10-17 20:58 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0023_alter_jobposting_max_applications'), - ] - - operations = [ - migrations.AlterField( - model_name='zoommeeting', - name='status', - field=models.CharField(blank=True, default='waiting', max_length=20, null=True, verbose_name='Status'), - ), - ] diff --git a/recruitment/migrations/0025_candidate_recommendation.py b/recruitment/migrations/0025_candidate_recommendation.py deleted file mode 100644 index 061dbf7..0000000 --- a/recruitment/migrations/0025_candidate_recommendation.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.4 on 2025-10-17 21:35 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0024_alter_zoommeeting_status'), - ] - - operations = [ - migrations.AddField( - model_name='candidate', - name='recommendation', - field=models.TextField(blank=True, verbose_name='Recommendation'), - ), - ] diff --git a/recruitment/migrations/0026_remove_candidate_resume_parsed_category_and_more.py b/recruitment/migrations/0026_remove_candidate_resume_parsed_category_and_more.py deleted file mode 100644 index 1a06a70..0000000 --- a/recruitment/migrations/0026_remove_candidate_resume_parsed_category_and_more.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.2.4 on 2025-10-17 21:50 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0025_candidate_recommendation'), - ] - - operations = [ - migrations.RemoveField( - model_name='candidate', - name='resume_parsed_category', - ), - migrations.AddField( - model_name='candidate', - name='major_category_name', - field=models.TextField(blank=True, verbose_name='Major Category Name'), - ), - ] diff --git a/recruitment/migrations/0027_alter_candidate_email_and_more.py b/recruitment/migrations/0027_alter_candidate_email_and_more.py deleted file mode 100644 index af3b674..0000000 --- a/recruitment/migrations/0027_alter_candidate_email_and_more.py +++ /dev/null @@ -1,159 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-18 17:51 - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0026_remove_candidate_resume_parsed_category_and_more'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AlterField( - model_name='candidate', - name='email', - field=models.EmailField(db_index=True, max_length=254, verbose_name='Email'), - ), - migrations.AlterField( - model_name='candidate', - name='major_category_name', - field=models.TextField(blank=True, db_index=True, verbose_name='Major Category Name'), - ), - migrations.AlterField( - model_name='candidate', - name='match_score', - field=models.IntegerField(blank=True, db_index=True, null=True), - ), - migrations.AlterField( - model_name='candidate', - name='stage', - field=models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer')], db_index=True, default='Applied', max_length=100, verbose_name='Stage'), - ), - migrations.AlterField( - model_name='formsubmission', - name='applicant_email', - field=models.EmailField(blank=True, db_index=True, help_text='Email of the applicant', max_length=254), - ), - migrations.AlterField( - model_name='formsubmission', - name='submitted_at', - field=models.DateTimeField(auto_now_add=True, db_index=True), - ), - migrations.AlterField( - model_name='interviewschedule', - name='end_date', - field=models.DateField(db_index=True, verbose_name='End Date'), - ), - migrations.AlterField( - model_name='interviewschedule', - name='start_date', - field=models.DateField(db_index=True, verbose_name='Start Date'), - ), - migrations.AlterField( - model_name='jobposting', - name='application_deadline', - field=models.DateField(blank=True, db_index=True, null=True), - ), - migrations.AlterField( - model_name='jobposting', - name='published_at', - field=models.DateTimeField(blank=True, db_index=True, null=True), - ), - migrations.AlterField( - model_name='jobposting', - name='status', - field=models.CharField(choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], db_index=True, default='DRAFT', max_length=20), - ), - migrations.AlterField( - model_name='scheduledinterview', - name='interview_date', - field=models.DateField(db_index=True, verbose_name='Interview Date'), - ), - migrations.AlterField( - model_name='scheduledinterview', - name='status', - field=models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], db_index=True, default='scheduled', max_length=20), - ), - migrations.AlterField( - model_name='zoommeeting', - name='meeting_id', - field=models.CharField(db_index=True, max_length=20, unique=True, verbose_name='Meeting ID'), - ), - migrations.AlterField( - model_name='zoommeeting', - name='start_time', - field=models.DateTimeField(db_index=True, verbose_name='Start Time'), - ), - migrations.AlterField( - model_name='zoommeeting', - name='status', - field=models.CharField(blank=True, db_index=True, default='waiting', max_length=20, null=True, verbose_name='Status'), - ), - migrations.AddIndex( - model_name='candidate', - index=models.Index(fields=['job', 'stage'], name='recruitment_job_id_766dbe_idx'), - ), - migrations.AddIndex( - model_name='candidate', - index=models.Index(fields=['job', 'stage', 'match_score'], name='recruitment_job_id_bd6512_idx'), - ), - migrations.AddIndex( - model_name='candidate', - index=models.Index(fields=['created_at'], name='recruitment_created_73590f_idx'), - ), - migrations.AddIndex( - model_name='fieldresponse', - index=models.Index(fields=['submission'], name='recruitment_submiss_474130_idx'), - ), - migrations.AddIndex( - model_name='fieldresponse', - index=models.Index(fields=['field'], name='recruitment_field_i_097e5b_idx'), - ), - migrations.AddIndex( - model_name='formsubmission', - index=models.Index(fields=['submitted_at'], name='recruitment_submitt_7946c8_idx'), - ), - migrations.AddIndex( - model_name='formtemplate', - index=models.Index(fields=['created_at'], name='recruitment_created_c21775_idx'), - ), - migrations.AddIndex( - model_name='formtemplate', - index=models.Index(fields=['is_active'], name='recruitment_is_acti_ae5efb_idx'), - ), - migrations.AddIndex( - model_name='interviewschedule', - index=models.Index(fields=['start_date'], name='recruitment_start_d_15d55e_idx'), - ), - migrations.AddIndex( - model_name='interviewschedule', - index=models.Index(fields=['end_date'], name='recruitment_end_dat_aeb00e_idx'), - ), - migrations.AddIndex( - model_name='interviewschedule', - index=models.Index(fields=['created_by'], name='recruitment_created_d0bdcc_idx'), - ), - migrations.AddIndex( - model_name='jobposting', - index=models.Index(fields=['status', 'created_at'], name='recruitment_status_42c036_idx'), - ), - migrations.AddIndex( - model_name='jobposting', - index=models.Index(fields=['slug'], name='recruitment_slug_004045_idx'), - ), - migrations.AddIndex( - model_name='scheduledinterview', - index=models.Index(fields=['job', 'status'], name='recruitment_job_id_f09e22_idx'), - ), - migrations.AddIndex( - model_name='scheduledinterview', - index=models.Index(fields=['interview_date', 'interview_time'], name='recruitment_intervi_7f5877_idx'), - ), - migrations.AddIndex( - model_name='scheduledinterview', - index=models.Index(fields=['candidate', 'job'], name='recruitment_candida_43d5b0_idx'), - ), - ] diff --git a/recruitment/migrations/0028_alter_candidate_interview_status.py b/recruitment/migrations/0028_alter_candidate_interview_status.py deleted file mode 100644 index f9b6c05..0000000 --- a/recruitment/migrations/0028_alter_candidate_interview_status.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.4 on 2025-10-18 21:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0027_alter_candidate_email_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='candidate', - name='interview_status', - field=models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=100, null=True, verbose_name='Interview Status'), - ), - ] diff --git a/recruitment/migrations/0029_remove_candidate_recruitment_job_id_766dbe_idx_and_more.py b/recruitment/migrations/0029_remove_candidate_recruitment_job_id_766dbe_idx_and_more.py deleted file mode 100644 index 1ab4af2..0000000 --- a/recruitment/migrations/0029_remove_candidate_recruitment_job_id_766dbe_idx_and_more.py +++ /dev/null @@ -1,62 +0,0 @@ -# Generated by Django 5.2.4 on 2025-10-19 10:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0028_alter_candidate_interview_status'), - ] - - operations = [ - migrations.RemoveIndex( - model_name='candidate', - name='recruitment_job_id_766dbe_idx', - ), - migrations.RemoveIndex( - model_name='candidate', - name='recruitment_job_id_bd6512_idx', - ), - migrations.RemoveIndex( - model_name='jobposting', - name='recruitment_status_42c036_idx', - ), - migrations.RemoveField( - model_name='candidate', - name='criteria_checklist', - ), - migrations.RemoveField( - model_name='candidate', - name='major_category_name', - ), - migrations.RemoveField( - model_name='candidate', - name='match_score', - ), - migrations.RemoveField( - model_name='candidate', - name='recommendation', - ), - migrations.RemoveField( - model_name='candidate', - name='strengths', - ), - migrations.RemoveField( - model_name='candidate', - name='weaknesses', - ), - migrations.AddField( - model_name='candidate', - name='ai_analysis_data', - field=models.JSONField(default=dict, help_text='Full JSON output from the resume scoring model.', verbose_name='AI Analysis Data'), - ), - migrations.AddIndex( - model_name='candidate', - index=models.Index(fields=['stage'], name='recruitment_stage_f1c6eb_idx'), - ), - migrations.AddIndex( - model_name='jobposting', - index=models.Index(fields=['status', 'created_at', 'title'], name='recruitment_status_8b77aa_idx'), - ), - ] diff --git a/recruitment/migrations/0030_alter_candidate_options.py b/recruitment/migrations/0030_alter_candidate_options.py deleted file mode 100644 index eff8232..0000000 --- a/recruitment/migrations/0030_alter_candidate_options.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-19 13:39 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0029_remove_candidate_recruitment_job_id_766dbe_idx_and_more'), - ] - - operations = [ - migrations.AlterModelOptions( - name='candidate', - options={'ordering': ['-ai_analysis_data__match_score', '-created_at'], 'verbose_name': 'Candidate', 'verbose_name_plural': 'Candidates'}, - ), - ] diff --git a/recruitment/migrations/0031_alter_candidate_options.py b/recruitment/migrations/0031_alter_candidate_options.py deleted file mode 100644 index ffdc406..0000000 --- a/recruitment/migrations/0031_alter_candidate_options.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-19 13:43 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0030_alter_candidate_options'), - ] - - operations = [ - migrations.AlterModelOptions( - name='candidate', - options={'verbose_name': 'Candidate', 'verbose_name_plural': 'Candidates'}, - ), - ] diff --git a/recruitment/models.py b/recruitment/models.py index 4ebd9d8..6bfc5e2 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -26,9 +26,12 @@ class Base(models.Model): abstract = True class Profile(models.Model): - profile_image = models.ImageField(null=True, blank=True, upload_to="profile_pic/") + profile_image = models.ImageField(null=True, blank=True, upload_to="profile_pic/",validators=[validate_image_size]) user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") + def __str__(self): + return f"image for user {self.user}" + class JobPosting(Base): # Basic Job Information JOB_TYPES = [ @@ -301,6 +304,26 @@ class JobPosting(Base): @property def offer_candidates(self): return self.all_candidates.filter(stage="Offer") + + + #counts + @property + def all_candidates_count(self): + return self.candidates.annotate(sortable_score=Cast('ai_analysis_data__match_score',output_field=CharField())).order_by('-sortable_score').count() + @property + def screening_candidates_count(self): + return self.all_candidates.filter(stage="Applied").count() + + @property + def exam_candidates_count(self): + return self.all_candidates.filter(stage="Exam").count() + @property + def interview_candidates_count(self): + return self.all_candidates.filter(stage="Interview").count() + + @property + def offer_candidates_count(self): + return self.all_candidates.filter(stage="Offer").count() class JobPostingImage(models.Model): job=models.OneToOneField('JobPosting',on_delete=models.CASCADE,related_name='post_images') diff --git a/recruitment/templatetags/__pycache__/form_filters.cpython-312.pyc b/recruitment/templatetags/__pycache__/form_filters.cpython-312.pyc index d53ed79..479f42f 100644 Binary files a/recruitment/templatetags/__pycache__/form_filters.cpython-312.pyc and b/recruitment/templatetags/__pycache__/form_filters.cpython-312.pyc differ diff --git a/recruitment/urls.py b/recruitment/urls.py index c755962..8d3a50f 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -108,6 +108,7 @@ urlpatterns = [ path('jobs//candidates//schedule-meeting-page/', views.schedule_meeting_for_candidate, name='schedule_meeting_for_candidate'), path('jobs//candidates//delete_meeting_for_candidate//', views.delete_meeting_for_candidate, name='delete_meeting_for_candidate'), + # users urls path('user/',views.user_detail,name='user_detail'), path('user/user_profile_image_update/',views.user_profile_image_update,name='user_profile_image_update'), @@ -115,6 +116,10 @@ urlpatterns = [ path('settings/',views.admin_settings,name='admin_settings'), path('staff/create',views.create_staff_user,name='create_staff_user'), path('set_staff_password//',views.set_staff_password,name='set_staff_password'), + path('account_toggle_status/',views.account_toggle_status,name='account_toggle_status'), + + + # Meeting Comments URLs path('meetings//comments/add/', views.add_meeting_comment, name='add_meeting_comment'), diff --git a/recruitment/validators.py b/recruitment/validators.py index 6277b64..8648da6 100644 --- a/recruitment/validators.py +++ b/recruitment/validators.py @@ -1,7 +1,7 @@ from django.core.exceptions import ValidationError def validate_image_size(image): - max_size_mb = 1 + max_size_mb = 2 if image.size > max_size_mb * 1024 * 1024: raise ValidationError(f"Image size should not exceed {max_size_mb}MB.") diff --git a/recruitment/views.py b/recruitment/views.py index cf030aa..c4dbc36 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -26,8 +26,10 @@ from .forms import ( BreakTimeFormSet, JobPostingImageForm, ProfileImageUploadForm, - StaffUserCreationForm - ,MeetingCommentForm + StaffUserCreationForm, + MeetingCommentForm, + ToggleAccountForm, + ) from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent from rest_framework import viewsets @@ -752,6 +754,23 @@ def form_wizard_view(request, template_id): """Display the form as a step-by-step wizard""" template = get_object_or_404(FormTemplate, pk=template_id, is_active=True) job_id = template.job.internal_job_id + job=template.job + is_limit_exceeded=job.is_application_limit_reached + if is_limit_exceeded: + messages.error( + request, + 'Application limit reached: This job is no longer accepting new applications. Please explore other available positions.' + ) + return redirect('job_detail_candidate',slug=job.slug) + + if job.is_expired: + messages.error( + request, + 'Application deadline passed: This job is no longer accepting new applications. Please explore other available positions.' + ) + return redirect('job_detail_candidate',slug=job.slug) + + return render( request, "forms/form_wizard.html", @@ -763,6 +782,8 @@ def form_wizard_view(request, template_id): def submit_form(request, template_id): """Handle form submission""" template = get_object_or_404(FormTemplate, id=template_id) + + if request.method == "POST": try: with transaction.atomic(): @@ -829,7 +850,7 @@ def submit_form(request, template_id): job=submission.template.job, ) - return redirect('application_success') + return redirect('application_success',slug=job.slug) except Exception as e: logger.error(f"Candidate creation failed,{e}") @@ -1929,37 +1950,51 @@ def schedule_meeting_for_candidate(request, slug, candidate_pk): from django.core.exceptions import ObjectDoesNotExist def user_profile_image_update(request, pk): + user = get_object_or_404(User, pk=pk) + + # 2. Ensure Profile exists and get the instance try: - instance =user.profile - - except ObjectDoesNotExist as e: - Profile.objects.create(user=user) - + + profile_instance = user.profile + except ObjectDoesNotExist: + + profile_instance = Profile.objects.create(user=user) + if request.method == 'POST': - profile_form = ProfileImageUploadForm(request.POST, request.FILES, instance=user.profile) + + profile_form = ProfileImageUploadForm( + request.POST, + request.FILES, + instance=profile_instance # <--- USE profile_instance HERE + ) + if profile_form.is_valid(): profile_form.save() - messages.success(request, 'Image uploaded successfully') - return redirect('user_detail', pk=user.pk) + messages.success(request, 'Image uploaded successfully.') + return redirect('user_detail', pk=user.pk) else: - messages.error(request, 'An error occurred while uploading the image') + messages.error(request, 'An error occurred while uploading the image. Please check the errors below.') else: - profile_form = ProfileImageUploadForm(instance=user.profile) - + # + profile_form = ProfileImageUploadForm(instance=profile_instance) context = { 'profile_form': profile_form, 'user': user, } return render(request, 'user/profile.html', context) - - def user_detail(request, pk): user = get_object_or_404(User, pk=pk) + + + try: + profile_instance = user.profile + profile_form = ProfileImageUploadForm(instance=profile_instance) + except: + profile_form = ProfileImageUploadForm() - profile_form = ProfileImageUploadForm() if request.method == 'POST': first_name=request.POST.get('first_name') last_name=request.POST.get('last_name') @@ -2057,15 +2092,17 @@ def create_staff_user(request): @user_passes_test(is_superuser_check) def admin_settings(request): staffs=User.objects.filter(is_superuser=False) + form = ToggleAccountForm() context={ - 'staffs':staffs + 'staffs':staffs, + 'form':form } return render(request,'user/admin_settings.html',context) from django.contrib.auth.forms import SetPasswordForm - +@user_passes_test(is_superuser_check) def set_staff_password(request,pk): user=get_object_or_404(User,pk=pk) print(request.POST) @@ -2074,10 +2111,11 @@ def set_staff_password(request,pk): if form.is_valid(): form.save() messages.success(request,f'Password successfully changed') + return redirect('admin_settings') else: form=SetPasswordForm(user=user) messages.error(request,f'Password does not match please try again.') - return redirect('set_staff_password',user=user) + return redirect('admin_settings') else: form=SetPasswordForm(user=user) @@ -2085,11 +2123,33 @@ def set_staff_password(request,pk): +@user_passes_test(is_superuser_check) +def account_toggle_status(request,pk): + user=get_object_or_404(User,pk=pk) + if request.method=='POST': + print(user.is_active) + form=ToggleAccountForm(request.POST) + if form.is_valid(): + if user.is_active: + user.is_active=False + user.save() + messages.success(request,f'Staff with email: {user.email} deactivated successfully') + return redirect('admin_settings') + else: + user.is_active=True + user.save() + messages.success(request,f'Staff with email: {user.email} activated successfully') + return redirect('admin_settings') + else: + messages.error(f'Please correct the error below') + -@login_required -def user_detail(requests,pk): - user=get_object_or_404(User,pk=pk) - return render(requests,'user/profile.html') + + +# @login_required +# def user_detail(requests,pk): +# user=get_object_or_404(User,pk=pk) +# return render(requests,'user/profile.html') @csrf_exempt @@ -2173,3 +2233,11 @@ def edit_meeting_comment(request, slug, comment_id): return redirect('meeting_details', slug=slug) else: form = MeetingCommentForm(instance=comment) + + +def delete_meeting_comment(request): + pass + + +def set_meeting_candidate(request): + pass \ No newline at end of file diff --git a/templates/account/account_inactive.html b/templates/account/account_inactive.html new file mode 100644 index 0000000..c64e2ad --- /dev/null +++ b/templates/account/account_inactive.html @@ -0,0 +1,155 @@ +{% load static i18n %} + + + + + + {% translate "Account Inactive" %} - KAAUH ATS + + + {# Include Font Awesome for icons #} + + + + + + +
+ +
+
+

+ +
+
{% translate "جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية" %}
+
{% translate "ومستشفى الملك عبدالله بن عبدالعزيز التخصصي" %}
+
{% translate "Princess Nourah bint Abdulrahman University" %}
+
{% translate "King Abdullah bin Abdulaziz University Hospital" %}
+
+
+

+ Powered By TENHAL | تنحل +
+
+ +
+ +
+ + + +

{% translate "Account Inactive" %}

+ +
+

+ {% translate "Access denied. This account has been marked as inactive by an administrator." %} +

+

+ {% translate "If you believe this is an error, please contact the system administrator for assistance." %} +

+ + + +
+
+
+
+ + + + + \ No newline at end of file diff --git a/templates/account/email.html b/templates/account/email.html new file mode 100644 index 0000000..5e62859 --- /dev/null +++ b/templates/account/email.html @@ -0,0 +1,125 @@ +{% extends "base.html" %} +{% load i18n %} +{% load account %} +{% load crispy_forms_tags %} + +{% block title %}{% translate "Email Addresses" %}{% endblock %} + +{% block content %} +
+ +
+
+

{% translate "Account Settings" %}

+

{% translate "Manage your personal details and security." %}

+
+
+ +
+ + {# ------------------- LEFT COLUMN: ACCOUNT MENU (New Card Style) ------------------- #} +
+
+
+
+ {# Assuming a main 'Profile' or 'Personal Information' page exists #} + + {% translate "Personal Information" %} + + + {# Highlight the current page (Email) as active #} + + {% translate "Email Addresses" %} + + + {% translate "Change Password" %} + + + + {% translate "Sign Out" %} + +
+
+
+
+ + {# ------------------- RIGHT COLUMN: EMAIL MANAGEMENT ------------------- #} +
+
+
+ +
{% translate "Email Addresses" %}
+

{% translate "These email addresses are linked to your account. You can set the primary address, resend verification, or remove an address." %}

+ + {% if emailaddresses %} + {% for emailaddress in emailaddresses %} +
+ +

+ {{ emailaddress.email }} + + {# Status Badges: Using rounded-pill and appropriate colors #} + {% if emailaddress.primary %} + {% translate "Primary" %} + {% endif %} + {% if emailaddress.verified %} + {% translate "Verified" %} + {% else %} + {% translate "Unverified" %} + {% endif %} +

+ +
+ + {# 1. MAKE PRIMARY ACTION #} + {% if not emailaddress.primary %} +
+ {% csrf_token %} + + + +
+ {% endif %} + + {# 2. RESEND VERIFICATION ACTION #} + {% if not emailaddress.verified %} +
+ {% csrf_token %} + + + +
+ {% endif %} + + {# 3. REMOVE ACTION #} + {% if not emailaddress.primary %} +
+ {% csrf_token %} + + + +
+ {% endif %} +
+
+ {% endfor %} + {% else %} +

{% translate "No email addresses found." %}

+ {% endif %} + +
+ + {# ------------------- ADD EMAIL FORM ------------------- #} +
{% translate "Add Email Address" %}
+
+ {% csrf_token %} + {{ form|crispy }} + {# Teal/Dark Green button consistent with "Save Changes" #} + +
+
+
+
+
+
+{% endblock content %} \ No newline at end of file diff --git a/templates/account/email/password_reset_key_message.html b/templates/account/email/password_reset_key_message.html new file mode 100644 index 0000000..74b722d --- /dev/null +++ b/templates/account/email/password_reset_key_message.html @@ -0,0 +1,39 @@ +{% load i18n %} +{% load static %} +{% autoescape off %} + +
+ +
+

{% trans "Password Reset Request" %}

+
+ +
+

{% trans "Hello," %}

+ +

{% trans "You are receiving this email because you or someone else has requested a password reset for your account at" %} {{ current_site.name }}.

+ +

+ + {% trans "Click Here to Reset Your Password" %} + +

+ +

{% trans "This link is only valid for a limited time." %}

+ +

{% trans "If you did not request a password reset, please ignore this email. Your password will remain unchanged." %}

+ +

+ {% trans "Thank you," %}
+ {% trans "KAAUH ATS Team" %} +

+
+ +
+ {% trans "If the button above does not work, copy and paste the following link into your browser:" %}
+ {{ password_reset_url }} +
+
+ +{% endautoescape %} \ No newline at end of file diff --git a/templates/account/email_confirm.html b/templates/account/email_confirm.html new file mode 100644 index 0000000..3b802d6 --- /dev/null +++ b/templates/account/email_confirm.html @@ -0,0 +1,71 @@ +{% extends "base.html" %} +{% load i18n %} +{% load account %} + +{% block title %}{% translate "Confirm Email Address" %}{% endblock %} + +{% block content %} +
+ +
+
+

{% translate "Account Verification" %}

+

{% translate "Verify your email to secure your account and unlock full features." %}

+
+
+ +
+
+
+
+ + {% with emailaddress.email as email %} + + {% if confirmation %} + + {# ------------------- CONFIRMATION REQUEST (GET) ------------------- #} + {% user_display confirmation.email_address.user as user_display %} + + +

{% translate "Confirm Your Email Address" %}

+ +

+ {% blocktrans with email as email %}Please confirm that **{{ email }}** is an email address for user **{{ user_display }}**.{% endblocktrans %} +

+ + {# Confirmation Form #} +
+ {% csrf_token %} + + {# Teal/Dark Green button consistent with the UI theme #} + +
+ + {% else %} + + {# ------------------- CONFIRMATION FAILED (Error) ------------------- #} + +

{% translate "Invalid Link" %}

+ +

+ {% translate "The email confirmation link has expired or is invalid." %} +

+

+ {% translate "Please request a new verification email from your account settings page." %} +

+ + + {% translate "Go to Settings" %} + + + {% endif %} + + {% endwith %} +
+
+
+
+
+{% endblock content %} \ No newline at end of file diff --git a/templates/account/logout.html b/templates/account/logout.html new file mode 100644 index 0000000..4af2a35 --- /dev/null +++ b/templates/account/logout.html @@ -0,0 +1,80 @@ +{% extends "base.html" %} +{% load i18n %} +{% load account %} + +{% block title %}{% translate "Sign Out" %}{% endblock %} + +{% block content %} +
+ +
+
+

{% translate "Account Settings" %}

+

{% translate "Manage your personal details and security." %}

+
+
+ +
+ + {# ------------------- LEFT COLUMN: ACCOUNT MENU (New Card Style) ------------------- #} +
+
+
+
+ {# Assuming a main 'Profile' or 'Personal Information' page exists #} + + {% translate "Personal Information" %} + + + {% translate "Email Addresses" %} + + + {% translate "Change Password" %} + + + {# Highlight the current page (Sign Out) as active #} + + {% translate "Sign Out" %} + +
+
+
+
+ + {# ------------------- RIGHT COLUMN: LOGOUT CONFIRMATION ------------------- #} +
+
+
+ + +

{% translate "Confirm Sign Out" %}

+ +

{% translate "Are you sure you want to sign out of your account?" %}

+ +
+ {% csrf_token %} + + {% if redirect_field_value %} + + {% endif %} + +
+ {# Sign Out button in danger color #} + + + {# Cancel/Go Back button with outline #} + + {% translate "Cancel" %} + +
+
+
+
+
+
+
+{% endblock content %} \ No newline at end of file diff --git a/templates/account/password_change.html b/templates/account/password_change.html index b05d6df..2e5a12f 100644 --- a/templates/account/password_change.html +++ b/templates/account/password_change.html @@ -7,7 +7,7 @@ {% endblock %} {% block content %} -
+
diff --git a/templates/account/password_reset_done.html b/templates/account/password_reset_done.html new file mode 100644 index 0000000..ca26bef --- /dev/null +++ b/templates/account/password_reset_done.html @@ -0,0 +1,187 @@ +{% load static i18n %} + + + + + + {% trans "Password Reset Sent" %} - KAAUH ATS + + + + {% get_current_language as LANGUAGE_CODE %} + + + + + +
+ +
+
+

+ +
+
{% trans "جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية" %}
+
{% trans "ومستشفى الملك عبدالله بن عبدالعزيز التخصصي" %}
+
{% trans "Princess Nourah bint Abdulrahman University" %}
+
{% trans "King Abdullah bin Abdulaziz University Hospital" %}
+
+
+

+ Powered By TENHAL | تنحل +
+
+ +
+ +
+ +

{% trans "Password Reset Sent" %}

+ +
+ +
+ +
+ +

+ {% blocktrans %} + We've **sent an email** to the address you provided with instructions on how to reset your password. + {% endblocktrans %} +

+ + + + {# Button to return to the login page #} + +
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/templates/account/password_reset_from_key.html b/templates/account/password_reset_from_key.html new file mode 100644 index 0000000..a873279 --- /dev/null +++ b/templates/account/password_reset_from_key.html @@ -0,0 +1,165 @@ +{% load static i18n %} + + + + + + {% trans "Set New Password" %} - KAAUH ATS + + + + + {% get_current_language as LANGUAGE_CODE %} + + + + + +
+ +
+
+

+ +
+
{% trans "جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية" %}
+
{% trans "ومستشفى الملك عبدالله بن عبدالعزيز التخصصي" %}
+
{% trans "Princess Nourah bint Abdulrahman University" %}
+
{% trans "King Abdullah bin Abdulaziz University Hospital" %}
+
+
+

+ Powered By TENHAL | تنحل +
+
+ +
+ +
+ +

{% trans "Set New Password" %}

+ +
+ + {% if form %} +

+ {% trans 'Please enter your new password below.' %} +

+

+ {% trans 'You can then log in.' %} +

+ +
+ {% csrf_token %} + + {# Non-Field Errors (General errors like tokens or passwords not matching) #} + {% if form.non_field_errors %} + + {% endif %} + + {# Password 1 Field #} +
+ + + {# **CRITICAL FIX:** Iterate over the errors to display them correctly #} + {% if form.password.errors %} +
+ {% for error in form.password.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} + + +
+ + {# Password 2 Field #} +
+ + + {# **CRITICAL FIX:** Iterate over the errors to display them correctly #} + {% if form.password2.errors %} +
+ {% for error in form.password2.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} + + +
+ + {# Hidden fields MUST be present for the POST request to be valid #} + {{ form.uid }} + {{ form.token }} + + {# Submit Button #} + +
+ + {% else %} + {# Message when the reset key is invalid or expired #} +

{% trans "Password Reset Failed" %}

+

+ {% trans "The password reset link is invalid or has expired." %} +

+ + {% endif %} + + +
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/templates/account/password_reset_from_key_done.html b/templates/account/password_reset_from_key_done.html new file mode 100644 index 0000000..67dacb8 --- /dev/null +++ b/templates/account/password_reset_from_key_done.html @@ -0,0 +1,87 @@ +{% load static i18n %} + + + + + + {% trans "Password Changed" %} - KAAUH ATS + + + + + {% get_current_language as LANGUAGE_CODE %} + + + + + +
+ +
+
+

+ +
+
{% trans "جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية" %}
+
{% trans "ومستشفى الملك عبدالله بن عبدالعزيز التخصصي" %}
+
{% trans "Princess Nourah bint Abdulrahman University" %}
+
{% trans "King Abdullah bin Abdulaziz University Hospital" %}
+
+
+

+ Powered By TENHAL | تنحل +
+
+ +
+ +
+ + + +

{% trans "Password Changed Successfully" %}

+ +

+ {% trans "Your password has been set. You can now use your new password to sign in." %} +

+ + +
+
+
+ + + + + \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index a3a4293..b4a6c35 100644 --- a/templates/base.html +++ b/templates/base.html @@ -16,7 +16,9 @@ {% endif %} - + + + @@ -56,7 +58,7 @@ {% trans 'kaauh logo green bg' %} - + {% trans 'kaauh logo green bg' %} @@ -118,7 +120,7 @@ {% endif %} - + {% endcomment %}
diff --git a/templates/forms/job_detail_candidate.html b/templates/forms/job_detail_candidate.html index 7bb8472..0ecccf1 100644 --- a/templates/forms/job_detail_candidate.html +++ b/templates/forms/job_detail_candidate.html @@ -8,6 +8,28 @@
+ + {# ================================================= #} + {# DJANGO MESSAGE BLOCK - Placed directly below the main navbar #} + {# ================================================= #} + {% if messages %} +
+
+ {# Using responsive columns to center the message content, similar to your form structure #} +
+ {% for message in messages %} + {# Use 'alert-{{ message.tags }}' to apply Bootstrap styling based on Django's tag (success, error/danger, info, warning) #} + + {% endfor %} +
+
+
+ {% endif %} + {# ================================================= #} +
@@ -85,4 +107,6 @@ {% endif %}
+ + {% endblock content%} \ No newline at end of file diff --git a/templates/forms/partials/candidate_facing_base.html b/templates/forms/partials/candidate_facing_base.html index c171571..fdd76ef 100644 --- a/templates/forms/partials/candidate_facing_base.html +++ b/templates/forms/partials/candidate_facing_base.html @@ -9,7 +9,7 @@ {% translate "Application Form" %} - + {% comment %} Load the correct Bootstrap CSS file for RTL/LTR {% endcomment %} {% if LANGUAGE_CODE == 'ar' %} @@ -27,13 +27,13 @@ --kaauh-teal-dark: #004a53; --success: #198754; --danger: #dc3545; - --light-bg: #f8f9fa; + --light-bg: #f8f9fa; --gray-text: #6c757d; --kaauh-border: #eaeff3; /* Added for dropdown styling */ - + /* CALCULATED STICKY HEIGHTS */ - --navbar-height: 56px; - --navbar-gap: 16px; + --navbar-height: 56px; + --navbar-gap: 16px; --sticky-navbar-total-height: 128px; } @@ -42,7 +42,7 @@ background-color: #f0f0f5; /* Light gray background for contrast */ padding-top: 0; } - + .btn-main-action { background-color: var(--kaauh-teal); color: white; @@ -60,7 +60,38 @@ .bg-kaauh-teal-dark { background-color: var(--kaauh-teal-dark) !important; } - + + /* ---------------------------------------------------------------------- */ + /* NEW: MESSAGES STYLING */ + /* ---------------------------------------------------------------------- */ + .message-container { + /* Position right below the sticky navbar (56px) with a small top margin */ + margin-top: calc(var(--navbar-height) + 10px); + } + .alert { + padding: 0.75rem 1.25rem; + border-radius: 0.5rem; + font-weight: 500; + box-shadow: 0 4px 8px rgba(0,0,0,0.08); + margin-bottom: 0; /* Handled by container margin */ + border-left: 5px solid; /* Feature highlight */ + } + .alert-success { + color: var(--success); + background-color: #d1e7dd; + border-color: var(--success); + } + .alert-error, .alert-danger { + color: var(--danger); + background-color: #f8d7da; + border-color: var(--danger); + } + .alert-info { + color: var(--kaauh-teal-dark); + background-color: #cff4fc; + border-color: var(--kaauh-teal); + } + /* ---------------------------------------------------------------------- */ /* LANGUAGE TOGGLE STYLES (COPIED FROM MAIN LAYOUT) */ /* ---------------------------------------------------------------------- */ @@ -111,18 +142,18 @@ #topNavbar { z-index: 1040; /* Higher than the bottom bar */ } - + /* 1. Position the dark navbar below the white navbar + gap */ #bottomNavbar { /* 56px (white nav) + 16px (mb-3) = 72px */ - top: calc(var(--navbar-height) + var(--navbar-gap)); + top: calc(var(--navbar-height) + var(--navbar-gap)); z-index: 1030; } /* 2. Pushes the main content down so it's not hidden under the navbars */ .main-content-area { /* Total Sticky Height (128px) + Extra Margin (12px) = 140px */ - margin-top: calc(var(--sticky-navbar-total-height) + 12px); + margin-top: calc(var(--sticky-navbar-total-height) + 12px); } /* 3. Positions the sticky sidebar correctly */ @@ -137,23 +168,29 @@ html[dir="rtl"] { text-align: right; } - + /* Flip Margin Utilities (m-end and m-start) */ html[dir="rtl"] .ms-auto { margin-right: auto !important; margin-left: 0 !important; } html[dir="rtl"] .me-auto { margin-left: auto !important; margin-right: 0 !important; } html[dir="rtl"] .ms-2 { margin-right: 0.5rem !important; margin-left: 0 !important; } html[dir="rtl"] .me-2 { margin-left: 0.5rem !important; margin-right: 0 !important; } html[dir="rtl"] .me-1 { margin-left: 0.25rem !important; margin-right: 0 !important; } /* For the globe icon */ - + /* Flip alignment for text-end/text-start */ html[dir="rtl"] .text-end { text-align: left !important; } html[dir="rtl"] .text-start { text-align: right !important; } + /* Flip border-left for RTL alerts */ + html[dir="rtl"] .alert { + border-right: 5px solid; + border-left: none; + } + /* ---------------------------------------------------------------------- */ /* MOBILE RESPONSIVE STYLES (Below 992px) */ /* ---------------------------------------------------------------------- */ @media (max-width: 991.98px) { - + /* Ensures dropdown items in mobile menu align correctly */ html[dir="rtl"] .navbar-collapse .dropdown-menu { text-align: right; @@ -165,12 +202,12 @@ #bottomNavbar { top: calc(var(--navbar-height) + var(--navbar-gap)); } - + .main-content-area { /* Reduced margin-top for smaller screens */ - margin-top: calc(var(--sticky-navbar-total-height) / 2); + margin-top: calc(var(--sticky-navbar-total-height) / 2); } - + /* Mobile Fixed Footer Bar for Application */ .mobile-fixed-apply-bar { position: fixed; @@ -199,7 +236,7 @@ - + + + {% block content %} {% endblock content %} diff --git a/templates/includes/easy_logs.html b/templates/includes/easy_logs.html index 284b600..094b933 100644 --- a/templates/includes/easy_logs.html +++ b/templates/includes/easy_logs.html @@ -225,7 +225,7 @@ {% if active_tab == 'crud' %} {{ log.datetime|date:"Y-m-d H:i:s" }} - {{ log.user.get_full_name|default:log.user.username|default:"N/A" }} + {{ log.user.email|default:"N/A" }} - {{ log.user.get_full_name|default:log.user.username|default:"Anonymous" }} + {{ log.user.get_full_name|default:log.user.email|default:"Anonymous" }} {{ log.method }} diff --git a/templates/jobs/create_job.html b/templates/jobs/create_job.html index be9dc36..5e3fa74 100644 --- a/templates/jobs/create_job.html +++ b/templates/jobs/create_job.html @@ -4,8 +4,6 @@ {% block title %}Create New Job Post - {{ block.super }}{% endblock %} {% block customCSS %} -{# 💡 1. Add Summernote CSS Media in the head #} -{{ form.media.css }} +{{form.media.css}} {% endblock %} {% block content %} @@ -150,7 +151,7 @@
{# ================================================= #} - {# SECTION 2: JOB CONTENT (All Summernote Fields) #} + {# SECTION 2: JOB CONTENT (CKEDITOR 5 Fields) #} {# ================================================= #}
@@ -193,14 +194,7 @@ {% if form.salary_range.errors %}
{{ form.salary_range.errors }}
{% endif %}
- {% comment %}
-
- - {{ form.application_url }} - {% if form.application_url.errors %}
{{ form.application_url.errors }}
{% endif %} -
{% trans "Full URL where candidates will apply" %}
-
-
{% endcomment %} + {% comment %} (application_url comment removed for brevity) {% endcomment %}
@@ -261,16 +255,16 @@
- - {{ form.start_date }} - {% if form.start_date.errors %}
{{ form.start_date.errors }}
{% endif %} + + {{ form.application_start_date }} + {% if form.application_start_date.errors %}
{{ form.application_start_date.errors }}
{% endif %}
- - {{ form.status }} - {% if form.status.errors %}
{{ form.status.errors }}
{% endif %} + + {{ form.joining_date }} + {% if form.joining_date.errors %}
{{ form.joining_date.errors }}
{% endif %}
@@ -300,14 +294,21 @@ {% if form.reporting_to.errors %}
{{ form.reporting_to.errors }}
{% endif %} -
+
{{ form.open_positions }} {% if form.open_positions.errors %}
{{ form.open_positions.errors }}
{% endif %}
-
+
+
+ + {{ form.max_applications }} + {% if form.max_applications.errors %}
{{ form.max_applications.errors }}
{% endif %} +
+
+
{{ form.created_by }} @@ -340,7 +341,11 @@
-{# 💡 2. Add Summernote JS Media at the end of the body #} -{{ form.media.js }} -{% endblock %} \ No newline at end of file + +{% endblock %} + +{% block customJS %} + +{{ form.media.js }} +{% endblock%} \ No newline at end of file diff --git a/templates/jobs/edit_job.html b/templates/jobs/edit_job.html index cfade4d..5e3fa74 100644 --- a/templates/jobs/edit_job.html +++ b/templates/jobs/edit_job.html @@ -1,11 +1,9 @@ {% extends "base.html" %} {% load static i18n %} -{% block title %}Edit {{ job.title }} - University ATS{% endblock %} +{% block title %}Create New Job Post - {{ block.super }}{% endblock %} {% block customCSS %} -{# 💡 1. Add Summernote CSS Media in the head #} -{{ form.media.css }} +{{form.media.css}} {% endblock %} {% block content %} -
+

- {# UPDATED TITLE FOR EDIT CONTEXT #} - {% trans "Edit Job Posting" %} - {% if job.title %} - {{ job.title }} {% endif %} + {% if form.instance.pk %} {% trans "Edit Job Posting" %} {% else %} {% trans "Create New Job Posting" %} {% endif %}

@@ -151,26 +146,12 @@ {% if form.workplace_type.errors %}
{{ form.workplace_type.errors }}
{% endif %}
-
-
- - {{ form.application_deadline }} - {% if form.application_deadline.errors %}
{{ form.application_deadline.errors }}
{% endif %} -
-
-
-
- - {{ form.max_applications }} - {% if form.max_applications.errors %}
{{ form.max_applications.errors }}
{% endif %} -
-
{# ================================================= #} - {# SECTION 2: JOB CONTENT (All Summernote Fields) #} + {# SECTION 2: JOB CONTENT (CKEDITOR 5 Fields) #} {# ================================================= #}
@@ -181,7 +162,7 @@
- {{ form.description }} + {{ form.description}} {% if form.description.errors %}
{{ form.description.errors }}
{% endif %}
@@ -189,7 +170,7 @@
- {{ form.qualifications }} + {{ form.qualifications}} {% if form.qualifications.errors %}
{{ form.qualifications.errors }}
{% endif %}
@@ -213,14 +194,7 @@ {% if form.salary_range.errors %}
{{ form.salary_range.errors }}
{% endif %}
- {% comment %}
-
- - {{ form.application_url }} - {% if form.application_url.errors %}
{{ form.application_url.errors }}
{% endif %} -
{% trans "Full URL where candidates will apply" %}
-
-
{% endcomment %} + {% comment %} (application_url comment removed for brevity) {% endcomment %}
@@ -271,11 +245,26 @@ {% if form.location_country.errors %}
{{ form.location_country.errors }}
{% endif %}
+
- - {{ form.start_date }} - {% if form.start_date.errors %}
{{ form.start_date.errors }}
{% endif %} + + {{ form.application_deadline }} + {% if form.application_deadline.errors %}
{{ form.application_deadline.errors }}
{% endif %} +
+
+
+
+ + {{ form.application_start_date }} + {% if form.application_start_date.errors %}
{{ form.application_start_date.errors }}
{% endif %} +
+
+
+
+ + {{ form.joining_date }} + {% if form.joining_date.errors %}
{{ form.joining_date.errors }}
{% endif %}
@@ -305,14 +294,21 @@ {% if form.reporting_to.errors %}
{{ form.reporting_to.errors }}
{% endif %} -
+
{{ form.open_positions }} {% if form.open_positions.errors %}
{{ form.open_positions.errors }}
{% endif %}
-
+
+
+ + {{ form.max_applications }} + {% if form.max_applications.errors %}
{{ form.max_applications.errors }}
{% endif %} +
+
+
{{ form.created_by }} @@ -335,19 +331,21 @@ {# ACTION BUTTONS #} {# ================================================= #}
- {# UPDATED CANCEL URL for Job Detail #} - + {% trans "Cancel" %}
-{# 💡 2. Add Summernote JS Media at the end of the body #} -{{ form.media.js }} -{% endblock %} \ No newline at end of file + +{% endblock %} + +{% block customJS %} + +{{ form.media.js }} +{% endblock%} \ No newline at end of file diff --git a/templates/jobs/job_list.html b/templates/jobs/job_list.html index 115565c..f1f8a64 100644 --- a/templates/jobs/job_list.html +++ b/templates/jobs/job_list.html @@ -81,7 +81,7 @@ /* --- TABLE ALIGNMENT AND SIZING FIXES --- */ .table { - table-layout: fixed; + table-layout: fixed; /* Ensures width calculations are respected */ width: 100%; border-collapse: collapse; } @@ -97,14 +97,28 @@ background-color: #f3f7f9; } - /* Optimized Main Table Column Widths (Total must be 100%) */ - .table th:nth-child(1) { width: 22%; } - .table th:nth-child(2) { width: 12%; } - .table th:nth-child(3) { width: 8%; } - .table th:nth-child(4) { width: 8%; } - .table th:nth-child(5) { width: 50%; } + /* + * OPTIMIZED MAIN TABLE COLUMN WIDTHS (Total must be 100%) + * -------------------------------------------------------- + * 1. Job Title/ID: 25% (Needs the most space) + * 2. Source: 10% + * 3. Max Apps: 7% + * 4. Deadline: 10% + * 5. Actions: 8% + * 6. Manage Forms: 10% + * 7. Applicants Metrics: 30% (Colspan 5) + * TOTAL: 25 + 10 + 7 + 10 + 8 + 10 + 30 = 100% + */ + .table th:nth-child(1) { width: 20%; } /* Job Title */ + .table th:nth-child(2) { width: 10%; } /* Source */ + .table th:nth-child(3) { width: 7%; } /* Max Apps */ + .table th:nth-child(4) { width: 10%; } /* Deadline */ + .table th:nth-child(5) { width: 8%; } /* Actions */ + .table th:nth-child(6) { width: 10%; } /* Manage Forms */ + /* The 7th column (Metrics) is 30% and is handled by its colspan */ - /* Candidate Management Header Row (The one with P/F) */ + + /* Candidate Management Header Row (The one with the stage names) */ .nested-metrics-row th { font-weight: 500; color: #6c757d; @@ -114,23 +128,13 @@ text-align: center; border-left: 1px solid var(--kaauh-border); } - + + /* Metrics Sub-Column Widths (7 total sub-columns, total 30%) */ + /* We have 5 main metrics: Applied, Screened, Exam, Interview, Offer. + * Let's allocate the 30% evenly: 30% / 5 = 6% per metric column. + */ .nested-metrics-row th { - width: calc(50% / 7); - } - .nested-metrics-row th[colspan="2"] { - width: calc(50% / 7 * 2); - position: relative; - } - - /* Inner P/F Headers */ - .nested-stage-metrics { - display: flex; - justify-content: space-around; - padding-top: 5px; - font-weight: 600; - color: var(--kaauh-teal-dark); - font-size: 0.7rem; + width: 6%; /* 30% / 5 metrics = 6% per metric column */ } /* Main TH for Candidate Management Header Title */ @@ -143,7 +147,7 @@ color: var(--kaauh-teal-dark); } - /* Candidate Management Data Cells (7 columns total) */ + /* Candidate Management Data Cells (5 columns total for metrics) */ .candidate-data-cell { text-align: center; vertical-align: middle; @@ -154,7 +158,8 @@ .table tbody td.candidate-data-cell:not(:first-child) { border-left: 1px solid var(--kaauh-border); } - .table tbody tr td:nth-child(5) { + /* Adds a distinctive vertical line before the metrics group (7th column) */ + .table tbody tr td:nth-child(7) { border-left: 2px solid var(--kaauh-teal); } @@ -170,7 +175,7 @@ font-size: 0.75rem; } - /* Additional CSS for Card View layout */ + /* Additional CSS for Card View layout (rest of your styles...) */ .card-view .card { height: 100%; } @@ -193,6 +198,7 @@ {% block content %}
+ {# ... (Rest of the header and filter content) ... #}

{% trans "Job Postings" %} @@ -202,6 +208,7 @@

+ {# ... (Filter card) ... #}
@@ -248,7 +255,6 @@ {# --- START OF JOB LIST CONTAINER --- #}
- {# View Switcher (Contains the Card/Table buttons and JS/CSS logic) #} {% include "includes/_list_view_switcher.html" with list_id="job-list" %} {# 1. TABLE VIEW (Default Active) #} @@ -260,12 +266,12 @@ {# --- Corrected Multi-Row Header Structure --- #} - {% trans "Job Title / ID" %} - {% trans "Source" %} - {% trans "Number Of Applicants" %} - {% trans "Application Deadline" %} - {% trans "Actions" %} - {% trans "Manage Forms" %} + {% trans "Job Title / ID" %} + {% trans "Source" %} + {% trans "Max Apps" %} + {% trans "Deadline" %} + {% trans "Actions" %} + {% trans "Manage Forms" %} {% trans "Applicants Metrics" %} @@ -273,11 +279,11 @@ - {% trans "Applied" %} - {% trans "Screened" %} - {% trans "Exam" %} - {% trans "Interview" %} - {% trans "Offer" %} + {% trans "Applied" %} + {% trans "Screened" %} + {% trans "Exam" %} + {% trans "Interview" %} + {% trans "Offer" %} @@ -320,11 +326,11 @@ {# CANDIDATE MANAGEMENT DATA - URLS NEUTRALIZED #} - {% if job.applying_count %}{{ job.applying_count }}{% else %}-{% endif %} - {% if job.screening_count %}{{ job.screening_count }}{% else %}-{% endif %} - {% if job.exam_count %}{{ job.exam_count }}{% else %}-{% endif %} - {% if job.interview_count %}{{ job.interview_count }}{% else %}-{% endif %} - {% if job.offer_count %}{{ job.offer_count }}{% else %}-{% endif %} + {% if job.all_candidates_count %}{{job.all_candidates_count}}{% else %}-{% endif %} + {% if job.screening_candidates_count %}{{ job.screening_candidates_count }}{% else %}-{% endif %} + {% if job.exam_candidates_count %}{{ job.exam_candidates_count}}{% else %}-{% endif %} + {% if job.interview_candidates_count %}{{ job.interview_candidates_count}}{% else %}-{% endif %} + {% if job.offer_candidates_count%}{{ job.offer_candidates_count }}{% else %}-{% endif %} {% endfor %} @@ -333,7 +339,7 @@
- {# 2. CARD VIEW (Previously Missing) - Added Bootstrap row/col structure for layout #} + {# ... (Card View and Paginator content) ... #}
{% for job in jobs %}
diff --git a/templates/meetings/update_meeting.html b/templates/meetings/update_meeting.html index 2515b09..2e614c4 100644 --- a/templates/meetings/update_meeting.html +++ b/templates/meetings/update_meeting.html @@ -1,62 +1,242 @@ {% extends "base.html" %} {% load static i18n %} + {% block title %}{% trans "Update Zoom Meeting" %} - {{ block.super }}{% endblock %} {% block customCSS %} - + {% endblock %} {% block content %} -
-
-

{% trans "Update Zoom Meeting" %}

-

{% trans "Modify the details of your scheduled meeting" %}

-
- - {# Apply KAAT-S theme styles to Django messages #} - {% if messages %} -
- {% for message in messages %} - {# Use message tags to map to alert classes: success, danger, info #} -
+
+ {# Display Django messages outside the card. Using safe if/else for tag mapping. #} + {% if messages %} +
+ {% for message in messages %} + {% if 'error' in message.tags %} +
+ {% elif 'success' in message.tags %} +
+ {% elif 'warning' in message.tags %} +
+ {% else %} +
+ {% endif %} {{ message }}
- {% endfor %} -
- {% endif %} - -
-

{% trans "Meeting Information" %}

- -
- {% csrf_token %} - -
- - {{ form.topic }} -
-
- - {{ form.start_time }} -
-
- - {{ form.duration }} -
- -
- - {# Using custom secondary button for 'Cancel' link #} - - {% trans "Cancel" %} - -
-
+ {% endfor %}
+ {% endif %} +
+ +
+
+
+

+ + {# Using a generic edit/pencil icon for update #} + + + {% trans "Update Zoom Meeting" %} +

+

{% trans "Modify the details of your scheduled meeting" %}

+
+ + {# BUTTON 1: Back to Details (matching the visual style of the create page's "Back to Meetings") #} + + + {# Arrow left icon #} + + + {% trans "Back to Details" %} +
+ +
+ {% csrf_token %} + +
+ + {{ form.topic }} +
+
+ + {{ form.start_time }} +
+
+ + {{ form.duration }} +
+ + {# BUTTONS 2 & 3: Action Buttons at the bottom #} +
+ + + + {% trans "Cancel" %} + +
+
+
{% endblock %} \ No newline at end of file diff --git a/templates/recruitment/candidate_list.html b/templates/recruitment/candidate_list.html index 7790d9d..b1ebf7c 100644 --- a/templates/recruitment/candidate_list.html +++ b/templates/recruitment/candidate_list.html @@ -149,59 +149,63 @@
-
-
-
- -
-
- {% include 'includes/search_form.html' %} -
-
-
-
- {% url 'candidate_list' as candidate_list_url %} - -
- {% if search_query %}{% endif %} - -
-
- -
- - -
-
- -
-
- - {% if job_filter or search_query %} - - {% trans "Clear" %} - - {% endif %} -
-
+
+
+
+ +
+ + {% include 'includes/search_form.html' %}
+ +
+ {% url 'candidate_list' as candidate_list_url %} + +
+ {% if search_query %}{% endif %} + + {# Filter Group #} +
+ + +
+ +
+ + +
+ + {# Buttons Group (pushed to the right/bottom) #} +
+
+ + {% if job_filter or stage_filter or search_query %} + + {% trans "Clear" %} + + {% endif %} +
+
+
+
+
{% if candidates %}
{# View Switcher - list_id must match the container ID #} diff --git a/templates/user/admin_settings.html b/templates/user/admin_settings.html index 30b6911..eb9b54f 100644 --- a/templates/user/admin_settings.html +++ b/templates/user/admin_settings.html @@ -1,25 +1,25 @@ {% extends "base.html" %} {% load static %} {% load i18n %} +{% load humanize %} {% block title %}{% trans "Admin Settings" %} - KAAUH ATS{% endblock %} + {% block customCSS %} -{% block styles %} {% endblock %} {% block content %} -
+

@@ -88,177 +129,116 @@

- - {# --- User Management Section (Cards) --- #} - {# --- Paged User Table Section --- #}
-
-

{% trans "Staff User List" %}

+ +
+ +

{% trans "Staff User List" %}

+ + {# 1. Create User Button - Using the enhanced btn-main-action class #} + + {% trans "Create New User" %} + +
- {# Assumes 'page_obj' contains the paginated queryset from the view #} - - - - - - + + + + + + + {% for user in staffs %} - - - + + {# Column: Last Login Date #} + + + {% empty %} - + {% endfor %}
{% trans "ID" %}{% trans "Username" %}{% trans "Full Name" %}{% trans "Email" %}{% trans "Status" %}{% trans "Actions" %}{% trans "ID" %}{% trans "Full Name" %}{% trans "Email" %}{% trans "Status" %}{% trans "First Join" %}{% trans "Last Login" %}{% trans "Actions" %}
{{ user.pk }}{{ user.username }} {{ user.get_full_name|default:user.first_name }} {{ user.email }} + {% if user.is_active %} - {% trans "Active" %} + {% trans "Active" %} {% else %} - {% trans "Inactive" %} + {% trans "Inactive" %} {% endif %} - - {# 1. Edit Button (Pencil Icon) #} - - - + + {# Column: First Join Date #} + + {{ user.date_joined|date:"d M Y" }} + + {% if user.last_login %} + {{ user.last_login|naturaltime }} + {% else %} + {% trans "Never" %} + {% endif %} + +
+ - {# 2. Change Password Button (Key Icon) #} - {# NOTE: You must define a URL named 'user_password_change' that accepts the user ID #} - - - + {# 2. Change Password Button (Key Icon) #} + + + {% trans 'Change Password' %} + - {# 3. Delete Button (Trash Icon) #} - {# NOTE: You must define a URL named 'user_delete' that accepts the user ID #} - - - + {# 3. Delete Button (Trash Icon) #} + + {% if user.is_active %} + + +
+ {% csrf_token %} + + {# The button for DEACTIVATION #} + +
+ + {% else %} + +
+ {% csrf_token %} + + {# The button for REACTIVATION #} + + + + + +
+ + {% endif %} +
{% trans "No staff users found." %}{% trans "No staff users found." %}
- {# --- Pagination Controls --- #} - {% if page_obj.has_other_pages %} - - {% endif %} -
-
- - {# --- Permissions & Group Management Section --- #} -
diff --git a/templates/user/create_staff.html b/templates/user/create_staff.html index b1ba2c9..9078193 100644 --- a/templates/user/create_staff.html +++ b/templates/user/create_staff.html @@ -29,7 +29,7 @@ {% endblock %} {% block content %} -
+
@@ -70,7 +70,7 @@ -
+

{% trans "Back to Settings" %} diff --git a/templates/user/profile.html b/templates/user/profile.html index eb057e3..75af172 100644 --- a/templates/user/profile.html +++ b/templates/user/profile.html @@ -122,7 +122,7 @@

{% trans "Personal Information" %}
-
+ {% csrf_token %}
@@ -191,9 +191,11 @@
+
- + +