frontend #18

Merged
ismail merged 7 commits from frontend into main 2025-10-21 14:14:53 +03:00
60 changed files with 1915 additions and 1126 deletions

View File

@ -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/

View File

@ -693,3 +693,7 @@ class StaffUserCreationForm(UserCreationForm):
class ToggleAccountForm(forms.Form):
pass

View File

@ -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)

View File

@ -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'),
),
]

View File

@ -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),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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',
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'],
},
),
]

View File

@ -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'),
),
]

View File

@ -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),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'},
),
]

View File

@ -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'},
),
]

View File

@ -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')

View File

@ -108,6 +108,7 @@ urlpatterns = [
path('jobs/<slug:slug>/candidates/<int:candidate_pk>/schedule-meeting-page/', views.schedule_meeting_for_candidate, name='schedule_meeting_for_candidate'),
path('jobs/<slug:slug>/candidates/<int:candidate_pk>/delete_meeting_for_candidate/<int:meeting_id>/', views.delete_meeting_for_candidate, name='delete_meeting_for_candidate'),
# users urls
path('user/<int:pk>',views.user_detail,name='user_detail'),
path('user/user_profile_image_update/<int:pk>',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/<int:pk>/',views.set_staff_password,name='set_staff_password'),
path('account_toggle_status/<int:pk>',views.account_toggle_status,name='account_toggle_status'),
# Meeting Comments URLs
path('meetings/<slug:slug>/comments/add/', views.add_meeting_comment, name='add_meeting_comment'),

View File

@ -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.")

View File

@ -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

View File

@ -0,0 +1,155 @@
{% load static i18n %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% translate "Account Inactive" %} - KAAUH ATS</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
{# Include Font Awesome for icons #}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" crossorigin="anonymous">
<style>
/* -------------------------------------------------------------------------- */
/* CUSTOM TEAL THEME OVERRIDES FOR BOOTSTRAP (Copied from your Sign In page) */
/* -------------------------------------------------------------------------- */
:root {
--bs-primary: #00636e; /* Dark Teal */
--bs-primary-rgb: 0, 99, 110;
--bs-primary-light: #007a88;
--bs-body-bg: #f8f9fa;
--bs-body-color: #212529;
--bs-border-color: #dee2e6;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bs-body-bg);
}
/* Custom Left Panel */
.left-panel {
flex: 1;
background: url("{% static 'image/kaauh_banner.png' %}") no-repeat center center;
background-size: cover;
position: relative;
display: flex;
align-items: flex-end;
padding: 3rem;
color: white;
z-index: 1;
}
.left-panel::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0) 50%);
z-index: 0;
}
.left-panel-content {
position: relative;
z-index: 2;
}
/* Right Panel Styling */
.right-panel {
background-color: white;
padding: 3rem;
}
.form-fields {
max-height: 100%;
overflow-y: auto;
}
/* Component Overrides to use Teal Theme */
.btn-primary {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
font-weight: 600;
border-radius: 0.5rem;
box-shadow: 0 4px 8px rgba(0, 99, 110, 0.2);
transition: all 0.2s ease;
}
.btn-primary:hover {
background-color: var(--bs-primary-light);
border-color: var(--bs-primary-light);
box-shadow: 0 6px 10px rgba(0, 99, 110, 0.3);
}
.text-accent {
color: var(--bs-primary) !important;
}
/* ADJUSTED: Custom size adjustment for right panel on desktop */
@media (min-width: 992px) {
.right-panel-col {
flex: 0 0 450px;
}
.right-panel-col > .d-flex {
height: 100%;
justify-content: center;
align-items: center;
padding-top: 0;
padding-bottom: 0;
}
.right-panel-content-wrapper {
max-width: 350px;
width: 100%;
}
}
</style>
</head>
<body>
<div class="d-flex vh-100 w-100">
<div class="left-panel d-none d-lg-flex flex-grow-1">
<div class="left-panel-content">
<h1 class="text-4xl font-weight-bold mb-4" style="font-size: 1.5rem;">
<span class="text-white">
<div class="hospital-text text-center text-md-start me-3">
<div class="ar small">{% translate "جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية" %}</div>
<div class="ar small">{% translate "ومستشفى الملك عبدالله بن عبدالعزيز التخصصي" %}</div>
<div class="en small">{% translate "Princess Nourah bint Abdulrahman University" %}</div>
<div class="en small">{% translate "King Abdullah bin Abdulaziz University Hospital" %}</div>
</div>
</span>
</h1>
<small>Powered By TENHAL | تنحل</small>
</div>
</div>
<div class="d-flex flex-column right-panel right-panel-col flex-grow-1 align-items-center justify-content-center">
<div class="right-panel-content-wrapper text-center">
<i class="fas fa-user-slash text-danger mb-4" style="font-size: 3rem;"></i>
<h2 id="form-title" class="h3 fw-bold mb-3 text-danger">{% translate "Account Inactive" %}</h2>
<div class="form-fields">
<p class="lead text-muted mb-4">
{% translate "Access denied. This account has been marked as inactive by an administrator." %}
</p>
<p class="text-muted small">
{% translate "If you believe this is an error, please contact the system administrator for assistance." %}
</p>
<div class="d-grid mt-4">
<a href="{% url 'account_login' %}" class="btn btn-outline-secondary w-100">
<i class="fas fa-sign-in-alt me-2"></i> {% translate "Return to Sign In" %}
</a>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js" crossorigin="anonymous"></script>
</body>
</html>

View File

@ -0,0 +1,125 @@
{% extends "base.html" %}
{% load i18n %}
{% load account %}
{% load crispy_forms_tags %}
{% block title %}{% translate "Email Addresses" %}{% endblock %}
{% block content %}
<div class="container my-5">
<div class="row mb-4">
<div class="col">
<h3 class="fw-bold">{% translate "Account Settings" %}</h3>
<p class="text-muted">{% translate "Manage your personal details and security." %}</p>
</div>
</div>
<div class="row">
{# ------------------- LEFT COLUMN: ACCOUNT MENU (New Card Style) ------------------- #}
<div class="col-md-4 col-lg-3 mb-4">
<div class="card shadow-sm border-0 rounded-4">
<div class="card-body p-0">
<div class="list-group list-group-flush rounded-4">
{# Assuming a main 'Profile' or 'Personal Information' page exists #}
<a href="{% url 'user_detail' user.pk %}" class="list-group-item list-group-item-action border-0 rounded-top-4 py-3">
<i class="fas fa-user-circle me-2"></i> {% translate "Personal Information" %}
</a>
{# Highlight the current page (Email) as active #}
<a href="{% url 'account_email' %}" class="list-group-item list-group-item-action active border-0 py-3" style="background-color: #e6f3f3; color: #008080; font-weight: 500;" aria-current="true">
<i class="fas fa-envelope me-2"></i> {% translate "Email Addresses" %}
</a>
<a href="{% url 'account_change_password' %}" class="list-group-item list-group-item-action border-0 py-3">
<i class="fas fa-lock me-2"></i> {% translate "Change Password" %}
</a>
<a href="{% url 'account_logout' %}" class="list-group-item list-group-item-action border-0 rounded-bottom-4 text-danger py-3">
<i class="fas fa-sign-out-alt me-2"></i> {% translate "Sign Out" %}
</a>
</div>
</div>
</div>
</div>
{# ------------------- RIGHT COLUMN: EMAIL MANAGEMENT ------------------- #}
<div class="col-md-8 col-lg-9">
<div class="card shadow-sm border-0 rounded-4">
<div class="card-body p-4">
<h5 class="fw-bold mb-3">{% translate "Email Addresses" %}</h5>
<p class="text-muted border-bottom pb-3">{% translate "These email addresses are linked to your account. You can set the primary address, resend verification, or remove an address." %}</p>
{% if emailaddresses %}
{% for emailaddress in emailaddresses %}
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-sm-center py-3 {% if not forloop.last %}border-bottom{% endif %}">
<p class="mb-2 mb-sm-0 me-3">
<span class="fw-bold text-dark">{{ emailaddress.email }}</span>
{# Status Badges: Using rounded-pill and appropriate colors #}
{% if emailaddress.primary %}
<span class="badge rounded-pill bg-info text-dark ms-2">{% translate "Primary" %}</span>
{% endif %}
{% if emailaddress.verified %}
<span class="badge rounded-pill bg-success ms-2">{% translate "Verified" %}</span>
{% else %}
<span class="badge rounded-pill bg-warning text-dark ms-2">{% translate "Unverified" %}</span>
{% endif %}
</p>
<div class="d-flex flex-wrap gap-2">
{# 1. MAKE PRIMARY ACTION #}
{% if not emailaddress.primary %}
<form method="post" action="{% url 'account_email' %}" class="d-inline">
{% csrf_token %}
<input type="hidden" name="email" value="{{ emailaddress.email }}" />
<input type="hidden" name="action" value="set_primary" />
<button type="submit" class="btn btn-sm btn-outline-primary">{% translate "Make Primary" %}</button>
</form>
{% endif %}
{# 2. RESEND VERIFICATION ACTION #}
{% if not emailaddress.verified %}
<form method="post" action="{% url 'account_email' %}" class="d-inline">
{% csrf_token %}
<input type="hidden" name="email" value="{{ emailaddress.email }}" />
<input type="hidden" name="action" value="send" />
<button type="submit" class="btn btn-sm btn-outline-warning">{% translate "Re-send Verification" %}</button>
</form>
{% endif %}
{# 3. REMOVE ACTION #}
{% if not emailaddress.primary %}
<form method="post" action="{% url 'account_email' %}" class="d-inline">
{% csrf_token %}
<input type="hidden" name="email" value="{{ emailaddress.email }}" />
<input type="hidden" name="action" value="remove" />
<button type="submit" class="btn btn-sm btn-outline-danger">{% translate "Remove" %}</button>
</form>
{% endif %}
</div>
</div>
{% endfor %}
{% else %}
<p class="alert alert-info mt-3">{% translate "No email addresses found." %}</p>
{% endif %}
<hr class="my-4">
{# ------------------- ADD EMAIL FORM ------------------- #}
<h5 class="fw-bold mb-3">{% translate "Add Email Address" %}</h5>
<form method="post" action="{% url 'account_email' %}">
{% csrf_token %}
{{ form|crispy }}
{# Teal/Dark Green button consistent with "Save Changes" #}
<button class="btn btn-success mt-3" type="submit" style="background-color: #008080; border-color: #008080;">{% translate "Add Email" %}</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,39 @@
{% load i18n %}
{% load static %}
{% autoescape off %}
<div style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: auto; border: 1px solid #ddd; border-radius: 8px; overflow: hidden;">
<div style="background-color: #00636e; padding: 20px; color: white; text-align: center;">
<h1 style="margin: 0; font-size: 24px;">{% trans "Password Reset Request" %}</h1>
</div>
<div style="padding: 20px;">
<p>{% trans "Hello," %}</p>
<p>{% trans "You are receiving this email because you or someone else has requested a password reset for your account at" %} <strong>{{ current_site.name }}</strong>.</p>
<p style="text-align: center; margin: 30px 0;">
<a href="{{ password_reset_url }}"
style="display: inline-block; padding: 10px 20px; color: white; background-color: #00636e; border-radius: 5px; text-decoration: none; font-weight: bold;">
{% trans "Click Here to Reset Your Password" %}
</a>
</p>
<p>{% trans "This link is only valid for a limited time." %}</p>
<p>{% trans "If you did not request a password reset, please ignore this email. Your password will remain unchanged." %}</p>
<p>
{% trans "Thank you," %}<br>
{% trans "KAAUH ATS Team" %}
</p>
</div>
<div style="background-color: #f4f4f4; padding: 15px; font-size: 12px; color: #777; text-align: center;">
{% trans "If the button above does not work, copy and paste the following link into your browser:" %}<br>
<a href="{{ password_reset_url }}" style="color: #00636e; word-break: break-all;">{{ password_reset_url }}</a>
</div>
</div>
{% endautoescape %}

View File

@ -0,0 +1,71 @@
{% extends "base.html" %}
{% load i18n %}
{% load account %}
{% block title %}{% translate "Confirm Email Address" %}{% endblock %}
{% block content %}
<div class="container my-5">
<div class="row mb-4">
<div class="col">
<h3 class="fw-bold">{% translate "Account Verification" %}</h3>
<p class="text-muted">{% translate "Verify your email to secure your account and unlock full features." %}</p>
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<div class="card shadow-sm border-0 rounded-4">
<div class="card-body p-5 text-center">
{% with emailaddress.email as email %}
{% if confirmation %}
{# ------------------- CONFIRMATION REQUEST (GET) ------------------- #}
{% user_display confirmation.email_address.user as user_display %}
<i class="fas fa-envelope-open-text mb-4" style="font-size: 3rem; color: #008080;"></i>
<h4 class="fw-bold mb-3">{% translate "Confirm Your Email Address" %}</h4>
<p class="lead text-muted">
{% blocktrans with email as email %}Please confirm that **{{ email }}** is an email address for user **{{ user_display }}**.{% endblocktrans %}
</p>
{# Confirmation Form #}
<form method="post" action="{% url 'account_confirm_email' confirmation.key %}">
{% csrf_token %}
{# Teal/Dark Green button consistent with the UI theme #}
<button class="btn btn-lg mt-4 px-5" type="submit" style="background-color: #008080; border-color: #008080; color: white;">
{% translate "Confirm" %}
</button>
</form>
{% else %}
{# ------------------- CONFIRMATION FAILED (Error) ------------------- #}
<i class="fas fa-exclamation-triangle text-danger mb-4" style="font-size: 3rem;"></i>
<h4 class="fw-bold mb-3 text-danger">{% translate "Invalid Link" %}</h4>
<p class="lead">
{% translate "The email confirmation link has expired or is invalid." %}
</p>
<p class="text-muted">
{% translate "Please request a new verification email from your account settings page." %}
</p>
<a href="{% url 'account_email' %}" class="btn btn-outline-secondary mt-3 px-5">
{% translate "Go to Settings" %}
</a>
{% endif %}
{% endwith %}
</div>
</div>
</div>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,80 @@
{% extends "base.html" %}
{% load i18n %}
{% load account %}
{% block title %}{% translate "Sign Out" %}{% endblock %}
{% block content %}
<div class="container my-5">
<div class="row mb-4">
<div class="col">
<h3 class="fw-bold">{% translate "Account Settings" %}</h3>
<p class="text-muted">{% translate "Manage your personal details and security." %}</p>
</div>
</div>
<div class="row">
{# ------------------- LEFT COLUMN: ACCOUNT MENU (New Card Style) ------------------- #}
<div class="col-md-4 col-lg-3 mb-4">
<div class="card shadow-sm border-0 rounded-4">
<div class="card-body p-0">
<div class="list-group list-group-flush rounded-4">
{# Assuming a main 'Profile' or 'Personal Information' page exists #}
<a href="{% url 'user_detail' user.pk %}" class="list-group-item list-group-item-action border-0 rounded-top-4 py-3">
<i class="fas fa-user-circle me-2"></i> {% translate "Personal Information" %}
</a>
<a href="{% url 'account_email' %}" class="list-group-item list-group-item-action border-0 py-3">
<i class="fas fa-envelope me-2"></i> {% translate "Email Addresses" %}
</a>
<a href="{% url 'account_change_password' %}" class="list-group-item list-group-item-action border-0 py-3">
<i class="fas fa-lock me-2"></i> {% translate "Change Password" %}
</a>
{# Highlight the current page (Sign Out) as active #}
<a href="{% url 'account_logout' %}" class="list-group-item list-group-item-action active border-0 rounded-bottom-4 py-3" style="background-color: #fce8e8; color: #dc3545; font-weight: 500;" aria-current="true">
<i class="fas fa-sign-out-alt me-2"></i> {% translate "Sign Out" %}
</a>
</div>
</div>
</div>
</div>
{# ------------------- RIGHT COLUMN: LOGOUT CONFIRMATION ------------------- #}
<div class="col-md-8 col-lg-9">
<div class="card shadow-sm border-0 rounded-4">
<div class="card-body p-4 text-center">
<i class="fas fa-sign-out-alt text-danger mb-4" style="font-size: 3rem;"></i>
<h4 class="fw-bold mb-3">{% translate "Confirm Sign Out" %}</h4>
<p class="lead mb-4">{% translate "Are you sure you want to sign out of your account?" %}</p>
<form method="post" action="{% url 'account_logout' %}">
{% csrf_token %}
{% if redirect_field_value %}
<input type="hidden"
name="{{ redirect_field_name }}"
value="{{ redirect_field_value }}" />
{% endif %}
<div class="d-flex justify-content-center gap-3 mt-4">
{# Sign Out button in danger color #}
<button class="btn btn-danger btn-lg px-5" type="submit">
{% translate "Sign Out" %}
</button>
{# Cancel/Go Back button with outline #}
<a class="btn btn-outline-secondary btn-lg px-5" href="{% url 'account_email' %}">
{% translate "Cancel" %}
</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}

View File

@ -7,7 +7,7 @@
{% endblock %}
{% block content %}
<div class="d-flex vh-80 w-100 justify-content-center align-items-center">
<div class="d-flex vh-80 w-100 justify-content-center align-items-center mt-5">
<div class="form-card">

View File

@ -0,0 +1,187 @@
{% load static i18n %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans "Password Reset Sent" %} - KAAUH ATS</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
{% get_current_language as LANGUAGE_CODE %}
<style>
/* -------------------------------------------------------------------------- */
/* CUSTOM TEAL THEME OVERRIDES FOR BOOTSTRAP (Copied from provided login page) */
/* -------------------------------------------------------------------------- */
:root {
/* Define TEAL as the primary color for Bootstrap overrides */
--bs-primary: #00636e; /* Dark Teal */
--bs-primary-rgb: 0, 99, 110;
--bs-primary-light: #007a88; /* Lighter Teal for hover */
/* Background and Text Colors */
--bs-body-bg: #f8f9fa; /* Light gray background */
--bs-body-color: #212529; /* Dark text */
/* Utility colors */
--bs-border-color: #dee2e6; /* Bootstrap default border */
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bs-body-bg);
}
/* Custom Left Panel (Replicating the original look) */
.left-panel {
flex: 1;
/* NOTE: Static image URL is included here. Ensure 'image/kaauh_banner.png' exists in your static files. */
background: url("{% static 'image/kaauh_banner.png' %}") no-repeat center center;
background-size: cover;
position: relative;
display: flex;
align-items: flex-end;
padding: 3rem;
color: white;
z-index: 1;
}
.left-panel::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0) 50%);
z-index: 0;
}
.left-panel-content {
position: relative;
z-index: 2;
}
/* Right Panel Styling */
.right-panel {
background-color: white;
padding: 3rem;
}
.form-fields {
max-height: 100%;
overflow-y: auto;
}
/* Component Overrides to use Teal Theme */
.btn-primary {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
font-weight: 600;
border-radius: 0.5rem;
box-shadow: 0 4px 8px rgba(0, 99, 110, 0.2);
transition: all 0.2s ease;
}
.btn-primary:hover {
background-color: var(--bs-primary-light);
border-color: var(--bs-primary-light);
box-shadow: 0 6px 10px rgba(0, 99, 110, 0.3);
}
.form-control:focus {
border-color: var(--bs-primary);
box-shadow: 0 0 0 0.25rem rgba(0, 99, 110, 0.25);
}
.text-accent {
color: var(--bs-primary) !important;
}
.text-accent:hover {
color: var(--bs-primary-light) !important;
text-decoration: underline;
}
.alert-info-custom {
background-color: #f0f8ff; /* Very light blue */
border-left: 5px solid var(--bs-primary);
color: var(--bs-primary);
}
/* ADJUSTED: Custom size adjustment for right panel on desktop */
@media (min-width: 992px) {
/* 1. Set a NARROWER fixed width for the right panel container */
.right-panel-col {
flex: 0 0 450px; /* Width of the panel */
}
/* 2. Vertically and horizontally center the content within the narrow panel */
.right-panel-col > .d-flex {
height: 100%;
justify-content: center;
align-items: center;
padding-top: 0;
padding-bottom: 0;
}
/* 3. Ensure the form container doesn't exceed a smaller size and is centered */
.right-panel-content-wrapper {
max-width: 350px; /* Max width of the form elements inside the panel */
width: 100%;
}
}
</style>
</head>
<body>
<div class="d-flex vh-100 w-100">
<div class="left-panel d-none d-lg-flex flex-grow-1">
<div class="left-panel-content">
<h1 class="text-4xl font-weight-bold mb-4" style="font-size: 1.5rem;">
<span class="text-white">
<div class="hospital-text text-center text-md-start me-3">
<div class="ar small">{% trans "جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية" %}</div>
<div class="ar small">{% trans "ومستشفى الملك عبدالله بن عبدالعزيز التخصصي" %}</div>
<div class="en small">{% trans "Princess Nourah bint Abdulrahman University" %}</div>
<div class="en small">{% trans "King Abdullah bin Abdulaziz University Hospital" %}</div>
</div>
</span>
</h1>
<small>Powered By TENHAL | تنحل</small>
</div>
</div>
<div class="d-flex flex-column right-panel right-panel-col flex-grow-1 align-items-center justify-content-center">
<div class="right-panel-content-wrapper">
<h2 id="form-title" class="h3 fw-bold mb-4 text-center text-accent">{% trans "Password Reset Sent" %}</h2>
<div class="form-fields">
<div class="text-center mb-4">
<i class="fas fa-envelope-open-text fa-4x mb-3 text-accent" aria-hidden="true"></i>
</div>
<p class="text-muted text-center mb-4">
{% blocktrans %}
We've **sent an email** to the address you provided with instructions on how to reset your password.
{% endblocktrans %}
</p>
<div class="alert alert-info-custom p-3 d-flex align-items-start" role="alert">
<i class="fas fa-info-circle me-3 mt-1" aria-hidden="true"></i>
<small>
{% trans "Please check your inbox (and spam folder). The link in the email is temporary and will expire soon for security reasons." %}
</small>
</div>
{# Button to return to the login page #}
<div class="d-grid mt-4">
<a href="{% url 'account_login' %}" class="btn btn-primary btn-lg">
<i class="fas fa-sign-in-alt me-2"></i> {% trans "Return to Login" %}
</a>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js" crossorigin="anonymous"></script>
</body>
</html>

View File

@ -0,0 +1,165 @@
{% load static i18n %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans "Set New Password" %} - KAAUH ATS</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" crossorigin="anonymous">
{% get_current_language as LANGUAGE_CODE %}
<style>
/* -------------------------------------------------------------------------- */
/* CUSTOM TEAL THEME OVERRIDES FOR BOOTSTRAP (Consistent with your UI) */
/* -------------------------------------------------------------------------- */
:root {
--bs-primary: #00636e; /* Dark Teal */
--bs-primary-rgb: 0, 99, 110;
--bs-primary-light: #007a88; /* Lighter Teal for hover */
--bs-body-bg: #f8f9fa;
--bs-body-color: #212529;
--bs-border-color: #dee2e6;
}
body { font-family: 'Inter', sans-serif; background-color: var(--bs-body-bg); }
.left-panel { flex: 1; background: url("{% static 'image/kaauh_banner.png' %}") no-repeat center center; background-size: cover; position: relative; display: flex; align-items: flex-end; padding: 3rem; color: white; z-index: 1; }
.left-panel::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0) 50%); z-index: 0; }
.left-panel-content { position: relative; z-index: 2; }
.right-panel { background-color: white; padding: 3rem; }
.form-fields { max-height: 100%; overflow-y: auto; }
.btn-primary { background-color: var(--bs-primary); border-color: var(--bs-primary); font-weight: 600; border-radius: 0.5rem; box-shadow: 0 4px 8px rgba(0, 99, 110, 0.2); transition: all 0.2s ease; }
.btn-primary:hover { background-color: var(--bs-primary-light); border-color: var(--bs-primary-light); box-shadow: 0 6px 10px rgba(0, 99, 110, 0.3); }
.form-control:focus { border-color: var(--bs-primary); box-shadow: 0 0 0 0.25rem rgba(0, 99, 110, 0.25); }
.text-accent { color: var(--bs-primary) !important; }
.text-accent:hover { color: var(--bs-primary-light) !important; text-decoration: underline; }
.alert-info-custom { background-color: #f0f8ff; border-left: 5px solid var(--bs-primary); color: var(--bs-primary); }
@media (min-width: 992px) {
.right-panel-col { flex: 0 0 450px; }
.right-panel-col > .d-flex { height: 100%; justify-content: center; align-items: center; padding-top: 0; padding-bottom: 0; }
.right-panel-content-wrapper { max-width: 350px; width: 100%; }
}
</style>
</head>
<body>
<div class="d-flex vh-100 w-100">
<div class="left-panel d-none d-lg-flex flex-grow-1">
<div class="left-panel-content">
<h1 class="text-4xl font-weight-bold mb-4" style="font-size: 1.5rem;">
<span class="text-white">
<div class="hospital-text text-center text-md-start me-3">
<div class="ar small">{% trans "جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية" %}</div>
<div class="ar small">{% trans "ومستشفى الملك عبدالله بن عبدالعزيز التخصصي" %}</div>
<div class="en small">{% trans "Princess Nourah bint Abdulrahman University" %}</div>
<div class="en small">{% trans "King Abdullah bin Abdulaziz University Hospital" %}</div>
</div>
</span>
</h1>
<small>Powered By TENHAL | تنحل</small>
</div>
</div>
<div class="d-flex flex-column right-panel right-panel-col flex-grow-1 align-items-center justify-content-center">
<div class="right-panel-content-wrapper">
<h2 id="form-title" class="h3 fw-bold mb-4 text-center text-accent">{% trans "Set New Password" %}</h2>
<div class="form-fields">
{% if form %}
<p class="text-muted small mb-1 text-center">
{% trans 'Please enter your new password below.' %}
</p>
<p class="text-muted small mb-4 text-center">
{% trans 'You can then log in.' %}
</p>
<form method="post" action=".">
{% csrf_token %}
{# Non-Field Errors (General errors like tokens or passwords not matching) #}
{% if form.non_field_errors %}
<div class="alert alert-danger" role="alert">
{% for error in form.non_field_errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
{# Password 1 Field #}
<div class="mb-3">
<label for="{{ form.password.id_for_label }}" class="form-label fw-semibold">{% trans "New Password" %} *</label>
{# **CRITICAL FIX:** Iterate over the errors to display them correctly #}
{% if form.password.errors %}
<div class="alert alert-danger p-2 small">
{% for error in form.password.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<input type="password" name="{{ form.password.name }}" id="{{ form.password.id_for_label }}"
class="form-control" placeholder="{% trans 'Enter new password' %}" required autofocus>
</div>
{# Password 2 Field #}
<div class="mb-3">
<label for="{{ form.password2.id_for_label }}" class="form-label fw-semibold">{% trans "Confirm New Password" %} *</label>
{# **CRITICAL FIX:** Iterate over the errors to display them correctly #}
{% if form.password2.errors %}
<div class="alert alert-danger p-2 small">
{% for error in form.password2.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<input type="password" name="{{ form.password2.name }}" id="{{ form.password2.id_for_label }}"
class="form-control" placeholder="{% trans 'Confirm new password' %}" required>
</div>
{# Hidden fields MUST be present for the POST request to be valid #}
{{ form.uid }}
{{ form.token }}
{# Submit Button #}
<button type="submit" class="btn btn-primary w-100 mt-4">
{% trans "Change Password" %}
</button>
</form>
{% else %}
{# Message when the reset key is invalid or expired #}
<h4 class="text-danger text-center mb-3">{% trans "Password Reset Failed" %}</h4>
<p class="text-muted text-center mb-4">
{% trans "The password reset link is invalid or has expired." %}
</p>
<div class="d-grid mt-4">
<a href="{% url 'account_reset_password' %}" class="btn btn-primary">
<i class="fas fa-redo me-2"></i> {% trans "Request New Reset Link" %}
</a>
</div>
{% endif %}
<div class="text-center mt-3">
<a href="{% url 'account_login' %}" class="small text-accent fw-medium">
<i class="fas fa-sign-in-alt me-1"></i> {% trans "Return to Sign In" %}
</a>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js" crossorigin="anonymous"></script>
</body>
</html>

View File

@ -0,0 +1,87 @@
{% load static i18n %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans "Password Changed" %} - KAAUH ATS</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" crossorigin="anonymous">
{% get_current_language as LANGUAGE_CODE %}
<style>
/* -------------------------------------------------------------------------- */
/* CUSTOM TEAL THEME OVERRIDES (Consistent with your UI) */
/* -------------------------------------------------------------------------- */
:root {
--bs-primary: #00636e; /* Dark Teal */
--bs-primary-rgb: 0, 99, 110;
--bs-primary-light: #007a88; /* Lighter Teal for hover */
--bs-body-bg: #f8f9fa;
--bs-body-color: #212529;
}
body { font-family: 'Inter', sans-serif; background-color: var(--bs-body-bg); }
.left-panel { flex: 1; background: url("{% static 'image/kaauh_banner.png' %}") no-repeat center center; background-size: cover; position: relative; display: flex; align-items: flex-end; padding: 3rem; color: white; z-index: 1; }
.left-panel::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0) 50%); z-index: 0; }
.left-panel-content { position: relative; z-index: 2; }
.right-panel { background-color: white; padding: 3rem; }
.btn-primary { background-color: var(--bs-primary); border-color: var(--bs-primary); font-weight: 600; border-radius: 0.5rem; box-shadow: 0 4px 8px rgba(0, 99, 110, 0.2); transition: all 0.2s ease; }
.btn-primary:hover { background-color: var(--bs-primary-light); border-color: var(--bs-primary-light); box-shadow: 0 6px 10px rgba(0, 99, 110, 0.3); }
.text-accent { color: var(--bs-primary) !important; }
.text-success-accent { color: #28a745 !important; } /* Standard green for success */
@media (min-width: 992px) {
.right-panel-col { flex: 0 0 450px; }
.right-panel-col > .d-flex { height: 100%; justify-content: center; align-items: center; padding-top: 0; padding-bottom: 0; }
.right-panel-content-wrapper { max-width: 350px; width: 100%; }
}
</style>
</head>
<body>
<div class="d-flex vh-100 w-100">
<div class="left-panel d-none d-lg-flex flex-grow-1">
<div class="left-panel-content">
<h1 class="text-4xl font-weight-bold mb-4" style="font-size: 1.5rem;">
<span class="text-white">
<div class="hospital-text text-center text-md-start me-3">
<div class="ar small">{% trans "جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية" %}</div>
<div class="ar small">{% trans "ومستشفى الملك عبدالله بن عبدالعزيز التخصصي" %}</div>
<div class="en small">{% trans "Princess Nourah bint Abdulrahman University" %}</div>
<div class="en small">{% trans "King Abdullah bin Abdulaziz University Hospital" %}</div>
</div>
</span>
</h1>
<small>Powered By TENHAL | تنحل</small>
</div>
</div>
<div class="d-flex flex-column right-panel right-panel-col flex-grow-1 align-items-center justify-content-center">
<div class="right-panel-content-wrapper text-center">
<i class="fas fa-check-circle text-success-accent mb-4" style="font-size: 3rem;"></i>
<h2 class="h3 fw-bold mb-3 text-accent">{% trans "Password Changed Successfully" %}</h2>
<p class="text-muted mb-4">
{% trans "Your password has been set. You can now use your new password to sign in." %}
</p>
<div class="d-grid mt-4">
<a href="{% url 'account_login' %}" class="btn btn-primary btn-lg">
<i class="fas fa-sign-in-alt me-2"></i> {% trans "Go to Sign In" %}
</a>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js" crossorigin="anonymous"></script>
</body>
</html>

View File

@ -16,7 +16,9 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
{% endif %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" integrity="sha512-Kc323vGBEqzTmouAECnVceyQqyqdsSiqLQISBL29aUW4U/M7pSPA/gEUZQqv1cwx4OnYxTxve5UMg5GT6L4JJg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="{% static 'css/main.css' %}">
@ -56,7 +58,7 @@
<img src="{% static 'image/kaauh_green1.png' %}" alt="{% trans 'kaauh logo green bg' %}" class="navbar-brand-mobile">
</a>
<a class="navbar-brand text-white d-none d-lg-block" href="{% url 'dashboard' %}" aria-label="Home">
<a class="navbar-brand text-white d-none d-lg-block me-4 pe-4" href="{% url 'dashboard' %}" aria-label="Home">
<img src="{% static 'image/kaauh_green1.png' %}" alt="{% trans 'kaauh logo green bg' %}" style="width: 60px; height: 60px;">
</a>
@ -118,7 +120,7 @@
{% endif %}
</button>
<ul
{# FINAL FIX: Always use dropdown-menu-end. In LTR (English), this aligns right. In RTL (Arabic), this aligns left. #}
class="dropdown-menu dropdown-menu-end py-0 shadow border-0 rounded-3"
style="min-width: 240px;"
>
@ -144,20 +146,20 @@
</li>
<li><hr class="dropdown-divider my-1"></li>
{% if request.user.is_authenticated %}
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="{% url 'user_detail' request.user.pk %}"><i class="fas fa-user-circle me-3 text-primary fs-5"></i> <span>{% trans "My Profile" %}</span></a></li>
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'user_detail' request.user.pk %}"><i class="fas fa-user-circle me-3 fs-5"></i> <span>{% trans "My Profile" %}</span></a></li>
{% if request.user.is_superuser %}
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="{% url 'admin_settings' %}"><i class="fas fa-cog me-3 text-primary fs-5"></i> <span>{% trans "Settings" %}</span></a></li>
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="{% url 'easy_logs' %}"><i class="fas fa-history me-3 text-primary fs-5"></i> <span>{% trans "Activity Log" %}</span></a></li>
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="#"><i class="fas fa-question-circle me-3 text-primary fs-5"></i> <span>{% trans "Help & Support" %}</span></a></li>
{% endif %}
{% if request.user.is_superuser %}
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'admin_settings' %}"><i class="fas fa-cog me-3 fs-5"></i> <span>{% trans "Settings" %}</span></a></li>
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'easy_logs' %}"><i class="fas fa-history me-3 fs-5"></i> <span>{% trans "Activity Log" %}</span></a></li>
{% comment %} <li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="#"><i class="fas fa-question-circle me-3 text-primary fs-5"></i> <span>{% trans "Help & Support" %}</span></a></li> {% endcomment %}
{% endif %}
{% endif %}
{% comment %} CORRECTED LINKEDIN BLOCK {% endcomment %}
{% if not request.session.linkedin_authenticated %}
<li>
<a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="{% url 'linkedin_login' %}">
<a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'linkedin_login' %}">
<i class="fab fa-linkedin me-3 text-primary fs-5"></i>
<span>{% trans "Connect LinkedIn" %}</span>
</a>
@ -176,11 +178,11 @@
{% csrf_token %}
<button
type="submit"
class="dropdown-item py-2 px-4 text-danger d-flex align-items-center border-0 bg-transparent text-start"
class="dropdown-item py-2 px-4 d-flex align-items-center border-0 bg-transparent text-start"
aria-label="{% trans 'Sign out' %}"
>
<i class="fas fa-sign-out-alt me-3 fs-5"></i>
<span>{% trans "Sign Out" %}</span>
<i class="fas fa-sign-out-alt me-3 fs-5 " style="color:red;"></i>
<span style="color:red;">{% trans "Sign Out" %}</span>
</button>
</form>
</li>
@ -221,7 +223,7 @@
</span>
</a>
</li>
<li class="nav-item me-lg-4">
{% comment %} <li class="nav-item me-lg-4">
<a class="nav-link {% if request.resolver_match.url_name == 'training_list' %}active{% endif %}" href="{% url 'training_list' %}">
<span class="d-flex align-items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
@ -230,8 +232,8 @@
{% trans "Training" %}
</span>
</a>
</li>
<li class="nav-item dropdown ms-lg-2">
</li> {% endcomment %}
{% comment %} <li class="nav-item dropdown ms-lg-2">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
data-bs-offset="0, 8" data-bs-auto-close="outside">
{% trans "More" %}
@ -246,7 +248,7 @@
<li><a class="dropdown-item" href="#"><i class="fas fa-users me-2"></i> {% trans "All Candidates" %}</a></li>
<li><a class="dropdown-item" href="#"><i class="fas fa-user-plus me-2"></i> {% trans "New Candidates" %}</a></li>
</ul>
</li>
</li> {% endcomment %}
</ul>
</div>
</div>

View File

@ -8,6 +8,28 @@
</div>
</nav>
{# ================================================= #}
{# DJANGO MESSAGE BLOCK - Placed directly below the main navbar #}
{# ================================================= #}
{% if messages %}
<div class="container-fluid message-container">
<div class="row">
{# Using responsive columns to center the message content, similar to your form structure #}
<div class="col-lg-8 offset-lg-2 col-md-10 offset-md-1 col-12">
{% for message in messages %}
{# Use 'alert-{{ message.tags }}' to apply Bootstrap styling based on Django's tag (success, error/danger, info, warning) #}
<div class="alert alert-{{ message.tags|default:'info' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{# ================================================= #}
<div class="container">
<div class="row mb-5 mt-3 main-content-area">
@ -85,4 +107,6 @@
</a>
{% endif %}
</div>
{% endblock content%}

View File

@ -9,7 +9,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% translate "Application Form" %}</title>
{% comment %} Load the correct Bootstrap CSS file for RTL/LTR {% endcomment %}
{% if LANGUAGE_CODE == 'ar' %}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.rtl.min.css" rel="stylesheet">
@ -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 @@
</style>
</head>
<body>
<nav id="topNavbar" class="navbar navbar-expand-lg sticky-top" style="background-color: white; z-index: 1040;">
<div class="container-fluid">
<a class="navbar-brand text-white fw-bold" href="{% url 'kaauh_career' %}">
@ -211,27 +248,27 @@
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
{% comment %} <li class="nav-item">
<a class="nav-link text-secondary" href="/applications/">{% translate "Applications" %}</a>
</li>
<li class="nav-item">
<a class="nav-link text-secondary" href="/profile/">{% translate "Profile" %}</a>
</li>
</li> {% endcomment %}
<li class="nav-item">
<a class="nav-link text-secondary" href="{% url 'kaauh_career' %}">{% translate "Careers" %}</a>
</li>
<li class="nav-item dropdown">
<button class="language-toggle-btn dropdown-toggle" type="button"
data-bs-toggle="dropdown" data-bs-offset="0, 8" aria-expanded="false"
<button class="language-toggle-btn dropdown-toggle" type="button"
data-bs-toggle="dropdown" data-bs-offset="0, 8" aria-expanded="false"
aria-label="{% trans 'Toggle language menu' %}">
<i class="fas fa-globe"></i>
<span class="d-inline">{{ LANGUAGE_CODE|upper }}</span>
</button>
<ul class="dropdown-menu {% if LANGUAGE_CODE == 'ar' %}dropdown-menu-start{% else %}dropdown-menu-end{% endif %}" aria-labelledby="navbarLanguageDropdown">
{% comment %} English Button {% endcomment %}
<li>
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
@ -257,6 +294,8 @@
</div>
</div>
</nav>
{% block content %}
{% endblock content %}

View File

@ -225,7 +225,7 @@
{% if active_tab == 'crud' %}
<tr>
<td>{{ log.datetime|date:"Y-m-d H:i:s" }}</td>
<td>{{ log.user.get_full_name|default:log.user.username|default:"N/A" }}</td>
<td>{{ log.user.email|default:"N/A" }}</td>
<td>
<span class="badge rounded-pill
{% if log.event_type == 1 %}badge-crud-create
@ -275,7 +275,7 @@
{% elif active_tab == 'request' %}
<tr>
<td>{{ log.datetime|date:"Y-m-d H:i:s" }}</td>
<td>{{ log.user.get_full_name|default:log.user.username|default:"Anonymous" }}</td>
<td>{{ log.user.get_full_name|default:log.user.email|default:"Anonymous" }}</td>
<td>
<span class="badge rounded-pill badge-request-method">{{ log.method }}</span>
</td>

View File

@ -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 }}
<style>
/* ================================================= */
@ -77,25 +75,28 @@
}
/* ================================================= */
/* ✅ CORRECTED SUMMERNOTE FULL-WIDTH STYLING */
/* REMOVED SUMMERNOTE CSS AND ADDED CKEDITOR 5 BASE STYLES */
/* ================================================= */
/* Make every Summernote editor fill its container */
.note-editor {
width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
max-width: none !important;
border-radius: 0.5rem;
/* CKEditor 5 base styling for the container */
.ck-editor__editable_inline {
/* This sets the content area height/min-height */
min-height: 200px; /* A default height */
border-radius: 0 0 0.5rem 0.5rem;
border: 1px solid #ced4da;
}
/* Set minimum heights for specific fields using sibling selector */
#id_description + .note-editor { min-height: 300px; }
#id_qualifications + .note-editor { min-height: 200px; }
#id_benefits + .note-editor,
#id_application_instructions + .note-editor { min-height: 150px; }
/* Override min-height for specific fields using ID */
#id_description .ck-editor__editable_inline { min-height: 300px; }
#id_qualifications .ck-editor__editable_inline { min-height: 200px; }
#id_benefits .ck-editor__editable_inline,
#id_application_instructions .ck-editor__editable_inline { min-height: 150px; }
.ck-toolbar {
border-radius: 0.5rem 0.5rem 0 0 !important;
}
</style>
{{form.media.css}}
{% endblock %}
{% block content %}
@ -150,7 +151,7 @@
</div>
{# ================================================= #}
{# SECTION 2: JOB CONTENT (All Summernote Fields) #}
{# SECTION 2: JOB CONTENT (CKEDITOR 5 Fields) #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
@ -193,14 +194,7 @@
{% if form.salary_range.errors %}<div class="text-danger small mt-1">{{ form.salary_range.errors }}</div>{% endif %}
</div>
</div>
{% comment %} <div class="col-md-6">
<div>
<label for="{{ form.application_url.id_for_label }}" class="form-label">{% trans "Application URL" %} <span class="text-danger">*</span></label>
{{ form.application_url }}
{% if form.application_url.errors %}<div class="text-danger small mt-1">{{ form.application_url.errors }}</div>{% endif %}
<div class="form-text">{% trans "Full URL where candidates will apply" %}</div>
</div>
</div> {% endcomment %}
{% comment %} (application_url comment removed for brevity) {% endcomment %}
<div class="col-12">
<div>
@ -261,16 +255,16 @@
</div>
<div class="col-md-4">
<div>
<label for="{{ form.start_date.id_for_label }}" class="form-label">{% trans "Desired Start Date" %}</label>
{{ form.start_date }}
{% if form.start_date.errors %}<div class="text-danger small mt-1">{{ form.start_date.errors }}</div>{% endif %}
<label for="{{ form.application_start_date.id_for_label }}" class="form-label">{% trans "Application Start Date" %}</label>
{{ form.application_start_date }}
{% if form.application_start_date.errors %}<div class="text-danger small mt-1">{{ form.application_start_date.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.status.id_for_label }}" class="form-label">{% trans "Status" %}</label>
{{ form.status }}
{% if form.status.errors %}<div class="text-danger small mt-1">{{ form.status.errors }}</div>{% endif %}
<label for="{{ form.joining_date.id_for_label }}" class="form-label">{% trans "Desired Joining Date" %}</label>
{{ form.joining_date }}
{% if form.joining_date.errors %}<div class="text-danger small mt-1">{{ form.joining_date.errors }}</div>{% endif %}
</div>
</div>
</div>
@ -300,14 +294,21 @@
{% if form.reporting_to.errors %}<div class="text-danger small mt-1">{{ form.reporting_to.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="col-md-4">
<div>
<label for="{{ form.open_positions.id_for_label }}" class="form-label">{% trans "Open Positions" %}</label>
{{ form.open_positions }}
{% if form.open_positions.errors %}<div class="text-danger small mt-1">{{ form.open_positions.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="col-md-4">
<div>
<label for="{{ form.max_applications.id_for_label }}" class="form-label">{% trans "Max Applications" %}</label>
{{ form.max_applications }}
{% if form.max_applications.errors %}<div class="text-danger small mt-1">{{ form.max_applications.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.created_by.id_for_label }}" class="form-label">{% trans "Created By" %}</label>
{{ form.created_by }}
@ -340,7 +341,11 @@
</form>
</div>
{# 💡 2. Add Summernote JS Media at the end of the body #}
{{ form.media.js }}
{% endblock %}
{% endblock %}
{% block customJS %}
{{ form.media.js }}
{% endblock%}

View File

@ -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 }}
<style>
/* ================================================= */
@ -44,7 +42,7 @@
}
.btn-secondary:hover {
background-color: #e9ecef;
color: var(--kaauh-teal-dark); /* Reverting color to dark theme text */
color: white;
border-color: var(--kaauh-teal-dark);
}
.card {
@ -73,41 +71,38 @@
border: 1px solid #ced4da;
width: 100%;
padding: 0.375rem 0.75rem;
box-sizing: border-box;
box-sizing: border-box;
}
/* ================================================= */
/* ✅ CORRECTED SUMMERNOTE FULL-WIDTH STYLING (Active Fix) */
/* REMOVED SUMMERNOTE CSS AND ADDED CKEDITOR 5 BASE STYLES */
/* ================================================= */
/* The most aggressive, universal fix for Summernote within Bootstrap columns */
.col-12 .note-editor {
/* This compensates for the 0.75rem padding on each side of the col-12 */
width: calc(100% + 1.5rem) !important;
margin-left: -0.75rem !important;
margin-right: -0.75rem !important;
/* General cleanup to maintain look */
box-sizing: border-box;
margin-bottom: 0 !important;
border-radius: 0.5rem;
/* CKEditor 5 base styling for the container */
.ck-editor__editable_inline {
/* This sets the content area height/min-height */
min-height: 200px; /* A default height */
border-radius: 0 0 0.5rem 0.5rem;
border: 1px solid #ced4da;
}
/* Set minimum heights for specific fields using sibling selector */
#id_description + .note-editor { min-height: 300px; }
#id_qualifications + .note-editor { min-height: 200px; }
#id_benefits + .note-editor,
#id_application_instructions + .note-editor { min-height: 150px; }
/* Override min-height for specific fields using ID */
#id_description .ck-editor__editable_inline { min-height: 300px; }
#id_qualifications .ck-editor__editable_inline { min-height: 200px; }
#id_benefits .ck-editor__editable_inline,
#id_application_instructions .ck-editor__editable_inline { min-height: 150px; }
.ck-toolbar {
border-radius: 0.5rem 0.5rem 0 0 !important;
}
</style>
{{form.media.css}}
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="container-fluid py-4">
<h1 class="h3 mb-4 text-primary fw-bold">
{# UPDATED TITLE FOR EDIT CONTEXT #}
<i class="fas fa-edit me-2"></i> {% trans "Edit Job Posting" %}
{% if job.title %} - {{ job.title }} {% endif %}
<i class="fas fa-bullhorn me-2"></i> {% if form.instance.pk %} {% trans "Edit Job Posting" %} {% else %} {% trans "Create New Job Posting" %} {% endif %}
</h1>
<form method="post" id="jobForm" class="mb-5" enctype="multipart/form-data">
@ -151,26 +146,12 @@
{% if form.workplace_type.errors %}<div class="text-danger small mt-1">{{ form.workplace_type.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div>
<label for="{{ form.application_deadline.id_for_label }}" class="form-label">{% trans "Application Deadline" %}</label>
{{ form.application_deadline }}
{% if form.application_deadline.errors %}<div class="text-danger small mt-1">{{ form.application_deadline.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div>
<label for="{{ form.max_applications.id_for_label }}" class="form-label">{% trans "Max Number Of Applicants" %}</label>
{{ form.max_applications }}
{% if form.max_applications.errors %}<div class="text-danger small mt-1">{{ form.max_applications.errors }}</div>{% endif %}
</div>
</div>
</div>
</div>
</div>
{# ================================================= #}
{# SECTION 2: JOB CONTENT (All Summernote Fields) #}
{# SECTION 2: JOB CONTENT (CKEDITOR 5 Fields) #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
@ -181,7 +162,7 @@
<div class="col-12">
<div>
<label for="{{ form.description.id_for_label }}" class="form-label">{% trans "Job Description" %} <span class="text-danger">*</span></label>
{{ form.description }}
{{ form.description}}
{% if form.description.errors %}<div class="text-danger small mt-1">{{ form.description.errors }}</div>{% endif %}
</div>
</div>
@ -189,7 +170,7 @@
<div class="col-12">
<div>
<label for="{{ form.qualifications.id_for_label }}" class="form-label">{% trans "Qualifications and Requirements" %}</label>
{{ form.qualifications }}
{{ form.qualifications}}
{% if form.qualifications.errors %}<div class="text-danger small mt-1">{{ form.qualifications.errors }}</div>{% endif %}
</div>
</div>
@ -213,14 +194,7 @@
{% if form.salary_range.errors %}<div class="text-danger small mt-1">{{ form.salary_range.errors }}</div>{% endif %}
</div>
</div>
{% comment %} <div class="col-md-6">
<div>
<label for="{{ form.application_url.id_for_label }}" class="form-label">{% trans "Application URL" %} <span class="text-danger">*</span></label>
{{ form.application_url }}
{% if form.application_url.errors %}<div class="text-danger small mt-1">{{ form.application_url.errors }}</div>{% endif %}
<div class="form-text">{% trans "Full URL where candidates will apply" %}</div>
</div>
</div> {% endcomment %}
{% comment %} (application_url comment removed for brevity) {% endcomment %}
<div class="col-12">
<div>
@ -271,11 +245,26 @@
{% if form.location_country.errors %}<div class="text-danger small mt-1">{{ form.location_country.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.start_date.id_for_label }}" class="form-label">{% trans "Desired Start Date" %}</label>
{{ form.start_date }}
{% if form.start_date.errors %}<div class="text-danger small mt-1">{{ form.start_date.errors }}</div>{% endif %}
<label for="{{ form.application_deadline.id_for_label }}" class="form-label">{% trans "Application Deadline" %}</label>
{{ form.application_deadline }}
{% if form.application_deadline.errors %}<div class="text-danger small mt-1">{{ form.application_deadline.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.application_start_date.id_for_label }}" class="form-label">{% trans "Application Start Date" %}</label>
{{ form.application_start_date }}
{% if form.application_start_date.errors %}<div class="text-danger small mt-1">{{ form.application_start_date.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.joining_date.id_for_label }}" class="form-label">{% trans "Desired Joining Date" %}</label>
{{ form.joining_date }}
{% if form.joining_date.errors %}<div class="text-danger small mt-1">{{ form.joining_date.errors }}</div>{% endif %}
</div>
</div>
</div>
@ -305,14 +294,21 @@
{% if form.reporting_to.errors %}<div class="text-danger small mt-1">{{ form.reporting_to.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="col-md-4">
<div>
<label for="{{ form.open_positions.id_for_label }}" class="form-label">{% trans "Open Positions" %}</label>
{{ form.open_positions }}
{% if form.open_positions.errors %}<div class="text-danger small mt-1">{{ form.open_positions.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="col-md-4">
<div>
<label for="{{ form.max_applications.id_for_label }}" class="form-label">{% trans "Max Applications" %}</label>
{{ form.max_applications }}
{% if form.max_applications.errors %}<div class="text-danger small mt-1">{{ form.max_applications.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.created_by.id_for_label }}" class="form-label">{% trans "Created By" %}</label>
{{ form.created_by }}
@ -335,19 +331,21 @@
{# ACTION BUTTONS #}
{# ================================================= #}
<div class="d-flex justify-content-between pt-2">
{# UPDATED CANCEL URL for Job Detail #}
<a href="{% url 'job_detail' job.slug %}" class="btn btn-secondary">
<a href="{% url 'job_list' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i> {% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-main-action">
{# UPDATED BUTTON TEXT for Edit action #}
<i class="fas fa-save me-1"></i> {% trans "Update Job" %}
<i class="fas fa-save me-1"></i> {% trans "Save Job" %}
</button>
</div>
</form>
</div>
{# 💡 2. Add Summernote JS Media at the end of the body #}
{{ form.media.js }}
{% endblock %}
{% endblock %}
{% block customJS %}
{{ form.media.js }}
{% endblock%}

View File

@ -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 %}
<div class="container-fluid py-4">
{# ... (Rest of the header and filter content) ... #}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-briefcase me-2"></i> {% trans "Job Postings" %}
@ -202,6 +208,7 @@
</a>
</div>
{# ... (Filter card) ... #}
<div class="card mb-4 shadow-sm no-hover">
<div class="card-body">
<div class="row">
@ -248,7 +255,6 @@
{# --- START OF JOB LIST CONTAINER --- #}
<div id="job-list">
{# 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 --- #}
<thead>
<tr>
<th scope="col" rowspan="2" style="width: 22%;">{% trans "Job Title / ID" %}</th>
<th scope="col" rowspan="2" style="width: 12%;">{% trans "Source" %}</th>
<th scope="col" rowspan="2" style="width: 10%;">{% trans "Number Of Applicants" %}</th>
<th scope="col" rowspan="2" style="width: 8%;">{% trans "Application Deadline" %}</th>
<th scope="col" rowspan="2" style="width: 8%;">{% trans "Actions" %}</th>
<th scope="col" rowspan="2" class="text-center" style="width: 8%;">{% trans "Manage Forms" %}</th>
<th scope="col" rowspan="2">{% trans "Job Title / ID" %}</th>
<th scope="col" rowspan="2">{% trans "Source" %}</th>
<th scope="col" rowspan="2">{% trans "Max Apps" %}</th>
<th scope="col" rowspan="2">{% trans "Deadline" %}</th>
<th scope="col" rowspan="2">{% trans "Actions" %}</th>
<th scope="col" rowspan="2" class="text-center">{% trans "Manage Forms" %}</th>
<th scope="col" colspan="5" class="candidate-management-header-title">
{% trans "Applicants Metrics" %}
@ -273,11 +279,11 @@
</tr>
<tr class="nested-metrics-row">
<th style="width: calc(50% / 7);">{% trans "Applied" %}</th>
<th style="width: calc(50% / 7);">{% trans "Screened" %}</th>
<th style="width: calc(50% / 7 * 2);">{% trans "Exam" %}</th>
<th style="width: calc(50% / 7 * 2);">{% trans "Interview" %}</th>
<th style="width: calc(50% / 7);">{% trans "Offer" %}</th>
<th scope="col">{% trans "Applied" %}</th>
<th scope="col">{% trans "Screened" %}</th>
<th scope="col">{% trans "Exam" %}</th>
<th scope="col">{% trans "Interview" %}</th>
<th scope="col">{% trans "Offer" %}</th>
</tr>
</thead>
@ -320,11 +326,11 @@
</td>
{# CANDIDATE MANAGEMENT DATA - URLS NEUTRALIZED #}
<td class="candidate-data-cell text-primary-theme"><a href="#" class="text-primary-theme">{% if job.applying_count %}{{ job.applying_count }}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-info"><a href="{% url 'candidate_screening_view' job.slug %}" class="text-info">{% if job.screening_count %}{{ job.screening_count }}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_screening_view' job.slug %}" class="text-success">{% if job.exam_count %}{{ job.exam_count }}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_exam_view' job.slug %}" class="text-success">{% if job.interview_count %}{{ job.interview_count }}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_interview_view' job.slug %}" class="text-success">{% if job.offer_count %}{{ job.offer_count }}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-primary-theme"><a href="#" class="text-primary-theme">{% if job.all_candidates_count %}{{job.all_candidates_count}}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-info"><a href="{% url 'candidate_screening_view' job.slug %}" class="text-info">{% if job.screening_candidates_count %}{{ job.screening_candidates_count }}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_screening_view' job.slug %}" class="text-success">{% if job.exam_candidates_count %}{{ job.exam_candidates_count}}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_exam_view' job.slug %}" class="text-success">{% if job.interview_candidates_count %}{{ job.interview_candidates_count}}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_interview_view' job.slug %}" class="text-success">{% if job.offer_candidates_count%}{{ job.offer_candidates_count }}{% else %}-{% endif %}</a></td>
</tr>
{% endfor %}
</tbody>
@ -333,7 +339,7 @@
</div>
</div>
{# 2. CARD VIEW (Previously Missing) - Added Bootstrap row/col structure for layout #}
{# ... (Card View and Paginator content) ... #}
<div class="card-view row g-4">
{% for job in jobs %}
<div class="col-xl-4 col-lg-6 col-md-6">

View File

@ -1,62 +1,242 @@
{% extends "base.html" %}
{% load static i18n %}
{% block title %}{% trans "Update Zoom Meeting" %} - {{ block.super }}{% endblock %}
{% block customCSS %}
<link rel="stylesheet" href="{% static 'css/update_meeting.css' %}">
<style>
/* UI Variables for the KAAT-S Theme */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
--kaauh-gray: #6c757d;
/* Status Colors for Django messages/alerts */
--kaauh-success: #28a745;
--kaauh-warning: #ffc107;
--kaauh-danger: #dc3545;
--kaauh-info: #17a2b8;
--kaauh-secondary: #6c757d; /* Added for button consistency */
}
/* CARD STYLING */
.card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
max-width: 600px; /* Constrain width for a better form layout */
margin: 2rem auto; /* Center the form card */
}
/* HEADER STYLING */
.card-header {
background-color: white; /* Ensure header background is white */
border-bottom: 1px solid var(--kaauh-border);
padding: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
border-top-left-radius: 0.75rem;
border-top-right-radius: 0.75rem;
}
/* Grouping the main title and subtitle */
.card-header-title-group {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.card-header h1 {
font-size: 1.5rem;
color: var(--kaauh-teal-dark);
font-weight: 700;
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.card-header h1 svg {
width: 1.25rem;
height: 1.25rem;
}
.card-header p {
color: var(--kaauh-gray);
font-size: 0.9rem;
margin: 0; /* Reset margin */
padding-left: 1.75rem; /* Indent to align with text after SVG */
}
/* FORM STYLING */
form {
padding: 1.5rem;
}
.form-row {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
font-weight: 600;
color: var(--kaauh-gray);
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.form-row input,
.form-row select {
display: block;
width: 100%;
padding: 0.75rem 1rem;
font-size: 1rem;
line-height: 1.5;
color: var(--kaauh-primary-text);
background-color: #fff;
border: 1px solid var(--kaauh-border);
border-radius: 0.5rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-row input:focus,
.form-row select:focus {
border-color: var(--kaauh-teal);
outline: 0;
box-shadow: 0 0 0 0.1rem rgba(0, 99, 110, 0.25);
}
input[type="datetime-local"] {
font-family: inherit;
}
/* BUTTON STYLING */
.btn-base {
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-align: center;
vertical-align: middle;
cursor: pointer;
user-select: none;
padding: 0.5rem 1rem;
font-size: 1rem;
line-height: 1.5;
border-radius: 0.5rem;
font-weight: 600;
border: 1px solid transparent;
transition: all 0.2s ease;
text-decoration: none;
}
.btn-main-action {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal);
color: white;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
color: white;
}
.btn-kaats-outline-secondary {
color: var(--kaauh-secondary);
border-color: var(--kaauh-secondary);
background-color: transparent;
}
.btn-kaats-outline-secondary:hover {
background-color: var(--kaauh-secondary);
color: white;
border-color: var(--kaauh-secondary);
}
/* MESSAGE STYLING (For Django messages) */
.messages-container {
max-width: 600px;
margin: 1rem auto;
}
.alert {
padding: 0.75rem 1.25rem;
margin-bottom: 1rem;
border: 1px solid transparent;
border-radius: 0.5rem;
font-size: 0.95rem;
}
.alert-success { background-color: #d4edda; color: var(--kaauh-success); border-color: #c3e6cb; }
.alert-danger { background-color: #f8d7da; color: var(--kaauh-danger); border-color: #f5c6cb; }
.alert-info { background-color: #d1ecf1; color: var(--kaauh-info); border-color: #bee5eb; }
.alert-warning { background-color: #fff3cd; color: var(--kaauh-warning); border-color: #ffeeba; }
</style>
{% endblock %}
{% block content %}
<div class="container">
<div class="header">
<h1>{% trans "Update Zoom Meeting" %}</h1>
<p>{% trans "Modify the details of your scheduled meeting" %}</p>
</div>
{# Apply KAAT-S theme styles to Django messages #}
{% if messages %}
<div class="messages">
{% for message in messages %}
{# Use message tags to map to alert classes: success, danger, info #}
<div class="alert alert-{{ message.tags|default:'info' }}">
<div class="messages-container">
{# Display Django messages outside the card. Using safe if/else for tag mapping. #}
{% if messages %}
<div class="messages">
{% for message in messages %}
{% if 'error' in message.tags %}
<div class="alert alert-danger">
{% elif 'success' in message.tags %}
<div class="alert alert-success">
{% elif 'warning' in message.tags %}
<div class="alert alert-warning">
{% else %}
<div class="alert alert-info">
{% endif %}
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
<div class="card">
<h2 class="card-title">{% trans "Meeting Information" %}</h2>
<form method="post" id="updateMeeting" action="{% url 'update_meeting' meeting.slug %}">
{% csrf_token %}
<div class="form-row">
<label class="form-label" for="{{ form.topic.id_for_label }}">{% trans "Topic" %}</label>
{{ form.topic }}
</div>
<div class="form-row">
<label class="form-label" for="{{ form.start_time.id_for_label }}">{% trans "Start Time" %}</label>
{{ form.start_time }}
</div>
<div class="form-row">
<label class="form-label" for="{{ form.duration.id_for_label }}">{% trans "Duration (minutes)" %}</label>
{{ form.duration }}
</div>
<div class="actions">
<button type="submit" class="btn-base btn-main-action">
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width: 1.25rem;">
<path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
{% trans "Update Meeting" %}
</button>
{# Using custom secondary button for 'Cancel' link #}
<a href="{% url 'meeting_details' meeting.pk %}" class="btn-base btn-kaats-outline-secondary">
{% trans "Cancel" %}
</a>
</div>
</form>
{% endfor %}
</div>
{% endif %}
</div>
<div class="card">
<div class="card-header">
<div class="card-header-title-group">
<h1>
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
{# Using a generic edit/pencil icon for update #}
<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7m-8 6l-5 1 1-5 9.5-9.5a1.5 1.5 0 012.12 0l2.88 2.88a1.5 1.5 0 010 2.12L13 19z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
{% trans "Update Zoom Meeting" %}
</h1>
<p>{% trans "Modify the details of your scheduled meeting" %}</p>
</div>
{# BUTTON 1: Back to Details (matching the visual style of the create page's "Back to Meetings") #}
<a href="{% url 'meeting_details' meeting.slug %}" class="btn-base btn-kaats-outline-secondary">
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width: 1.25rem;">
{# Arrow left icon #}
<path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
{% trans "Back to Details" %}
</a>
</div>
<form method="post" action="{% url 'update_meeting' meeting.slug %}">
{% csrf_token %}
<div class="form-row">
<label class="form-label" for="{{ form.topic.id_for_label }}">{% trans "Topic" %}</label>
{{ form.topic }}
</div>
<div class="form-row">
<label class="form-label" for="{{ form.start_time.id_for_label }}">{% trans "Start Time" %}</label>
{{ form.start_time }}
</div>
<div class="form-row">
<label class="form-label" for="{{ form.duration.id_for_label }}">{% trans "Duration (minutes)" %}</label>
{{ form.duration }}
</div>
{# BUTTONS 2 & 3: Action Buttons at the bottom #}
<div class="form-row d-flex gap-3">
<button type="submit" class="btn-base btn-main-action">
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width: 1.25rem;">
<path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
{% trans "Update Meeting" %}
</button>
<a href="{% url 'meeting_details' meeting.slug %}" class="btn-base btn-kaats-outline-secondary">
{% trans "Cancel" %}
</a>
</div>
</form>
</div>
{% endblock %}

View File

@ -149,59 +149,63 @@
</div>
<div class="card mb-4 shadow-sm no-hover">
<div class="card-body">
<div class="row">
<div class="col-md-6">
<label for="search" class="form-label small text-muted">{% trans "Search by Name or Email" %}</label>
<div class="input-group input-group-lg mb-3">
<form method="get" action="" class="w-100">
{% include 'includes/search_form.html' %}
</form>
</div>
</div>
<div class="col-md-6">
{% url 'candidate_list' as candidate_list_url %}
<form method="GET" class="row g-3 align-items-end" >
{% if search_query %}<input type="hidden" name="q" value="{{ search_query }}">{% endif %}
<div class="col-md-4">
<div class="col-md-4">
<label for="job_filter" class="form-label small text-muted">{% trans "Filter by Job" %}</label>
<div class="d-flex gap-2">
<select name="job" id="job_filter" class="form-select form-select-sm">
<option value="">{% trans "All Jobs" %}</option>
{% for job in available_jobs %}
<option value="{{ job.slug }}" {% if job_filter == job.slug %}selected{% endif %}>{{ job.title }}</option>
{% endfor %}
</select>
<select name="stage" id="stage_filter" class="form-select form-select-sm">
<option value="">{% trans "All Stages" %}</option>
<option value="Applied" {% if stage_filter == 'Applied' %}selected{% endif %}>{% trans "Applied" %}</option>
<option value="Exam" {% if stage_filter == 'Exam' %}selected{% endif %}>{% trans "Exam" %}</option>
<option value="Interview" {% if stage_filter == 'Interview' %}selected{% endif %}>{% trans "Interview" %}</option>
<option value="Offer" {% if stage_filter == 'Offer' %}selected{% endif %}>{% trans "Offer" %}</option>
</select>
</div>
</div>
<div class="col-md-8">
<div class="filter-buttons">
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-filter me-1"></i> {% trans "Apply Filters" %}
</button>
{% if job_filter or search_query %}
<a href="{% url 'candidate_list' %}" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-times me-1"></i> {% trans "Clear" %}
</a>
{% endif %}
</div>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-6">
<label for="search" class="form-label small text-muted">{% trans "Search by Name or Email" %}</label>
<div class="input-group input-group-lg">
<form method="get" action="" class="w-100">
{% include 'includes/search_form.html' %}
</form>
</div>
</div>
<div class="col-md-6">
{% url 'candidate_list' as candidate_list_url %}
<form method="GET" class="row g-3 align-items-end h-100">
{% if search_query %}<input type="hidden" name="q" value="{{ search_query }}">{% endif %}
{# Filter Group #}
<div class="col-md-4 col-6">
<label for="job_filter" class="form-label small text-muted">{% trans "Filter by Job" %}</label>
<select name="job" id="job_filter" class="form-select form-select-sm">
<option value="">{% trans "All Jobs" %}</option>
{% for job in available_jobs %}
<option value="{{ job.slug }}" {% if job_filter == job.slug %}selected{% endif %}>{{ job.title }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4 col-6">
<label for="stage_filter" class="form-label small text-muted">{% trans "Filter by Stage" %}</label>
<select name="stage" id="stage_filter" class="form-select form-select-sm">
<option value="">{% trans "All Stages" %}</option>
<option value="Applied" {% if stage_filter == 'Applied' %}selected{% endif %}>{% trans "Applied" %}</option>
<option value="Exam" {% if stage_filter == 'Exam' %}selected{% endif %}>{% trans "Exam" %}</option>
<option value="Interview" {% if stage_filter == 'Interview' %}selected{% endif %}>{% trans "Interview" %}</option>
<option value="Offer" {% if stage_filter == 'Offer' %}selected{% endif %}>{% trans "Offer" %}</option>
</select>
</div>
{# Buttons Group (pushed to the right/bottom) #}
<div class="col-md-4 d-flex justify-content-end align-self-end">
<div class="filter-buttons">
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-filter me-1"></i> {% trans "Apply" %}
</button>
{% if job_filter or stage_filter or search_query %}
<a href="{% url 'candidate_list' %}" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-times me-1"></i> {% trans "Clear" %}
</a>
{% endif %}
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% if candidates %}
<div id="candidate-list">
{# View Switcher - list_id must match the container ID #}

View File

@ -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 %}
<style>
/* Theme Variables for Consistency */
:root {
--color-primary: #007a88; /* Main Teal */
--color-primary-dark: #004d55; /* Dark Teal */
--color-background-light: #f4f6f9; /* Light Gray background */
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-light-bg: #f9fbfd;
/* Standard Bootstrap color overrides (for pagination/badges) */
--color-primary: var(--kaauh-teal);
--color-primary-dark: var(--kaauh-teal-dark);
--color-background-light: #f4f6f9; /* Retaining this lighter shade for page background */
}
/* Layout & Card Styling */
.container-fluid {
background-color: var(--color-background-light);
min-height: 100vh;
padding-top: 3rem;
padding-bottom: 3rem;
}
.dashboard-header {
color: var(--color-primary-dark);
border-bottom: 1px solid #e9ecef;
@ -27,21 +27,7 @@
margin-bottom: 2rem;
}
/* Admin Feature Card Styling */
.feature-card {
background-color: #ffffff;
border-radius: 0.75rem;
border: 1px solid #e9ecef;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transition: transform 0.2s, box-shadow 0.2s;
height: 100%; /* Important for grid consistency */
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
/* Accent Colors */
/* Standard Accent Colors/Icons */
.text-accent {
color: var(--color-primary) !important;
}
@ -63,8 +49,9 @@
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
padding: 1.5rem;
}
/* Adjusted table stripe color to use theme variable */
.table-striped > tbody > tr:nth-of-type(odd) > * {
background-color: rgba(0, 122, 136, 0.03); /* Light teal stripe */
background-color: rgba(0, 99, 110, 0.05); /* Light kaauh-teal stripe */
}
.page-link {
color: var(--color-primary-dark);
@ -74,13 +61,67 @@
border-color: var(--color-primary) !important;
color: #fff;
}
/* Action button container for better alignment and spacing */
.action-btns {
display: flex;
justify-content: center;
gap: 5px; /* Small space between icons */
}
/* --- Main Action Button Style (Teal Theme) --- */
.btn-main-action {
/* Use standard Bootstrap button styles as a base */
background-color: var(--kaauh-teal);
border: 1px solid var(--kaauh-teal);
color: white;
font-weight: 600;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem; /* Standard Bootstrap gap for icon/text */
padding: 0.5rem 1rem;
border-radius: 0.375rem; /* Standard Bootstrap border radius */
text-decoration: none;
}
/* Functional Hover State */
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
color: white;
text-decoration: none;
}
/* Active/Focus State (optional but good for accessibility) */
.btn-main-action:focus,
.btn-main-action:active {
box-shadow: 0 0 0 0.25rem rgba(0, 99, 110, 0.5); /* Teal focus ring */
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
color: white;
}
button i.fa-solid {
/* Re-enable display if it was hidden */
display: inline-block;
/* Ensure the correct font family is applied */
font-family: "Font Awesome 6 Free";
/* Inherit button color */
color: inherit;
}
.text-primary{
color:var(--kaauh-teal) !important;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="container-fluid mt-5">
<div class="row px-lg-4">
<div class="col-12">
<h1 class="h3 fw-bold dashboard-header">
@ -88,177 +129,116 @@
</h1>
</div>
</div>
{# --- User Management Section (Cards) --- #}
<div class="row px-lg-4 mb-5">
<div class="col-12">
<h4 class="text-secondary fw-bold mb-3">{% trans "User & Access Management" %}</h4>
</div>
{# 1. Manage/Update Users (Detail Page) - Placeholder link for the table below #}
<div class="col-md-6 col-lg-4 mb-4">
<a href="#user-list" class="text-decoration-none d-block">
<div class="feature-card p-4 text-center">
<i class="fas fa-users feature-icon"></i>
<h5 class="feature-title">{% trans "View Staff List" %}</h5>
<p class="text-muted small mb-0">{% trans "Scroll down to view and manage the paginated list of all staff users." %}</p>
</div>
</a>
</div>
{# 2. Create User #}
<div class="col-md-6 col-lg-4 mb-4">
<a href="{% url 'create_staff_user' %}" class="text-decoration-none d-block">
<div class="feature-card p-4 text-center">
<i class="fas fa-user-plus feature-icon"></i>
<h5 class="feature-title">{% trans "Create New User" %}</h5>
<p class="text-muted small mb-0">{% trans "Quickly add a new staff member to the Applicant Tracking System." %}</p>
</div>
</a>
</div>
{# 3. Change Password (Centralized/Admin-forced) #}
<div class="col-md-6 col-lg-4 mb-4">
<a href="#" class="text-decoration-none d-block">
<div class="feature-card p-4 text-center">
<i class="fas fa-key feature-icon"></i>
<h5 class="feature-title">{% trans "Reset User Passwords" %}</h5>
<p class="text-muted small mb-0">{% trans "Enforce password resets or change passwords for existing users." %}</p>
</div>
</a>
</div>
</div>
{# --- Paged User Table Section --- #}
<div class="row px-lg-4" id="user-list">
<div class="col-12">
<h4 class="text-secondary fw-bold mb-3">{% trans "Staff User List" %}</h4>
<div class="d-flex align-items-center justify-content-between mb-3">
<h4 class="text-secondary fw-bold mb-0">{% trans "Staff User List" %}</h4>
{# 1. Create User Button - Using the enhanced btn-main-action class #}
<a href="{% url 'create_staff_user' %}" class="btn btn-main-action">
<i class="fas fa-user-plus"></i>{% trans "Create New User" %}
</a>
</div>
<div class="col-12 table-card">
<div class="table-responsive">
{# Assumes 'page_obj' contains the paginated queryset from the view #}
<table class="table table-striped table-hover align-middle mb-0">
<thead>
<tr>
<th>{% trans "ID" %}</th>
<th>{% trans "Username" %}</th>
<th>{% trans "Full Name" %}</th>
<th>{% trans "Email" %}</th>
<th>{% trans "Status" %}</th>
<th class="text-center">{% trans "Actions" %}</th>
<th scope="col">{% trans "ID" %}</th>
<th scope="col">{% trans "Full Name" %}</th>
<th scope="col">{% trans "Email" %}</th>
<th scope="col" class="text-center">{% trans "Status" %}</th>
<th scope="col" class="text-center">{% trans "First Join" %}</th>
<th scope="col" class="text-center">{% trans "Last Login" %}</th>
<th scope="col" class="text-center">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for user in staffs %}
<tr>
<td>{{ user.pk }}</td>
<td>{{ user.username }}</td>
<td>{{ user.get_full_name|default:user.first_name }}</td>
<td>{{ user.email }}</td>
<td>
<td class="text-center">
{% if user.is_active %}
<span class="badge rounded-pill text-bg-success">{% trans "Active" %}</span>
<span class="badge rounded-pill bg-success-subtle text-success border border-success-subtle">{% trans "Active" %}</span>
{% else %}
<span class="badge rounded-pill text-bg-secondary">{% trans "Inactive" %}</span>
<span class="badge rounded-pill bg-secondary-subtle text-secondary border border-secondary-subtle">{% trans "Inactive" %}</span>
{% endif %}
</td>
<td class="text-center text-nowrap">
{# 1. Edit Button (Pencil Icon) #}
<a href="#" class="btn btn-sm btn-outline-secondary me-1" title="{% trans 'Edit User' %}">
<i class="fas fa-edit"></i>
</a>
{# Column: First Join Date #}
<td class="small text-muted text-center" title="{{ user.date_joined|date:'DATETIME_FORMAT' }}">
{{ user.date_joined|date:"d M Y" }}
</td>
{# Column: Last Login Date #}
<td class="small text-center">
{% if user.last_login %}
<span title="{{ user.last_login|date:'DATETIME_FORMAT' }}">{{ user.last_login|naturaltime }}</span>
{% else %}
{% trans "Never" %}
{% endif %}
</td>
<td class="text-center">
<div class="action-btns">
{# 2. Change Password Button (Key Icon) #}
{# NOTE: You must define a URL named 'user_password_change' that accepts the user ID #}
<a href="{% url 'set_staff_password' user.pk %}" class="btn btn-sm btn-outline-info me-1" title="{% trans 'Change Password' %}">
<i class="fas fa-key"></i>
</a>
{# 2. Change Password Button (Key Icon) #}
<a href="{% url 'set_staff_password' user.pk %}" class="btn btn-sm btn-outline-info" title="{% trans 'Change Password' %}">
<i class="fas fa-key"></i>
{% trans 'Change Password' %}
</a>
{# 3. Delete Button (Trash Icon) #}
{# NOTE: You must define a URL named 'user_delete' that accepts the user ID #}
<a href="#" class="btn btn-sm btn-outline-danger" title="{% trans 'Delete User' %}">
<i class="fas fa-trash-alt"></i>
</a>
{# 3. Delete Button (Trash Icon) #}
{% if user.is_active %}
<form method="post" action="{% url 'account_toggle_status' user.pk %}">
{% csrf_token %}
{# The button for DEACTIVATION #}
<button type="submit" >
<i class="fas fa-times-circle text-danger"></i> Deactivate
</button>
</form>
{% else %}
<form method="post" action="{% url 'account_toggle_status' user.pk %}">
{% csrf_token %}
{# The button for REACTIVATION #}
<button type="submit bg-primary" >
<i class="fas fa-check-circle text-primary"></i> Activate
</button>
</form>
{% endif %}
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center text-muted">{% trans "No staff users found." %}</td>
<td colspan="8" class="text-center text-muted py-4">{% trans "No staff users found." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{# --- Pagination Controls --- #}
{% if page_obj.has_other_pages %}
<nav class="mt-4" aria-label="Page navigation">
<ul class="pagination justify-content-center mb-0">
{# Previous Button #}
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">&laquo;</span></li>
{% endif %}
{# Page Numbers #}
{% for i in page_obj.paginator.page_range %}
{% if page_obj.number == i %}
<li class="page-item active" aria-current="page"><span class="page-link">{{ i }}</span></li>
{% elif i > page_obj.number|add:'-3' and i < page_obj.number|add:'3' %}
<li class="page-item"><a class="page-link" href="?page={{ i }}">{{ i }}</a></li>
{% endif %}
{% endfor %}
{# Next Button #}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">&raquo;</span></li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
{# --- Permissions & Group Management Section --- #}
<div class="row px-lg-4 mt-5">
<div class="col-12">
<h4 class="text-secondary fw-bold mb-3">{% trans "Role & Permissions" %}</h4>
</div>
{# 4. Manage Groups #}
<div class="col-md-6 col-lg-4 mb-4">
<a href="#" class="text-decoration-none d-block">
<div class="feature-card p-4 text-center">
<i class="fas fa-layer-group feature-icon"></i>
<h5 class="feature-title">{% trans "Manage User Groups" %}</h5>
<p class="text-muted small mb-0">{% trans "Edit, create, and assign logical permission groups (e.g., HR, Manager)." %}</p>
</div>
</a>
</div>
{# 5. Manage Permissions #}
<div class="col-md-6 col-lg-4 mb-4">
<a href="#" class="text-decoration-none d-block">
<div class="feature-card p-4 text-center">
<i class="fas fa-shield-alt feature-icon"></i>
<h5 class="feature-title">{% trans "View Permissions" %}</h5>
<p class="text-muted small mb-0">{% trans "Review all available content-level permissions in the system." %}</p>
</div>
</a>
</div>
</div>
</div>

View File

@ -29,7 +29,7 @@
{% endblock %}
{% block content %}
<div class="d-flex vh-80 w-100 justify-content-center align-items-center mt-4">
<div class="d-flex vh-80 w-100 justify-content-center align-items-center mt-5">
<div class="form-card">
@ -70,7 +70,7 @@
</button>
</form>
<div class="pt-5 mt-2 text-center border-top border-light-subtle">
<div class="pt-3 mt-1 text-center border-top border-light-subtle mb-4">
<p class="text-muted small mb-0">
<i class="fas fa-arrow-left me-1"></i>
<a href="{% url 'admin_settings' %}" class="text-accent text-decoration-none text-secondary">{% trans "Back to Settings" %}</a>

View File

@ -122,7 +122,7 @@
<div class="card p-5">
<h5 class="fw-bold mb-4 text-accent">{% trans "Personal Information" %}</h5>
<form method="post" action="{% url 'user_detail' user.pk %}">
<form method="POST" action="{% url 'user_detail' user.pk %}">
{% csrf_token %}
<div class="row g-4"> <div class="col-md-6">
@ -191,9 +191,11 @@
</div>
</div>
<!--modal for image upload-->
<!--modal class for image upload-->
<div class="modal fade mt-4" id="myModalForm" tabindex="-1" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
@ -204,14 +206,51 @@
<div class="modal-body">
<form method="post" action="{% url 'user_profile_image_update' user.pk %}" enctype="multipart/form-data" >
{% csrf_token %}
{{ profile_form.as_p}}
{% if image_upload_form.instance.post_image %}
<p>Current Image:</p>
<img src="{{ image_upload_form.instance.post_image.url }}" alt="Post Image" style="max-width: 200px;">
<div class="mb-3">
<label for="{{ profile_form.profile_image.id_for_label }}" class="form-label">Profile Image</label>
{# 1. Check if an image currently exists on the bound instance #}
{% if profile_form.instance.profile_image %}
<div class="mb-2">
<small class="text-muted d-block">Current Image:</small>
{# Display Link to View Current Image #}
<a href="{{ profile_form.instance.profile_image.url }}" target="_blank" class="d-inline-block me-3 text-info fw-bold">
View/Download ({{ profile_form.instance.profile_image.name }})
</a>
{# Image Preview #}
<div class="mt-2">
<img src="{{ profile_form.instance.profile_image.url }}"
alt="Profile Image"
style="max-width: 150px; height: auto; border: 1px solid #ccc; border-radius: 4px;">
</div>
</div>
{# 2. Explicitly render the 'Clear' checkbox and the Change input #}
<div class="form-check mt-3">
{# The ClearableFileInput widget renders itself here. It provides the "Clear" checkbox and the "Change" input field. #}
{{ profile_form.profile_image }}
</div>
{% else %}
{# If no image exists, just render the file input for upload #}
<div class="form-control p-0 border-0">
{{ profile_form.profile_image }}
</div>
{% endif %}
<div class="modal-footer mt-2">
<button type="button" class="btn btn-lg btn-secondary" data-bs-dismiss="modal">Close</button>
{# Display any validation errors #}
{% for error in profile_form.profile_image.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
{% endfor %}
</div>
<div class="modal-footer mt-4">
<button type="button" class="btn btn-lg btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save changes</button>
</div>
</form>
@ -219,5 +258,4 @@
</div>
</div>
</div>
{% endblock %}

View File

@ -8,7 +8,7 @@
{% block content %}
<div class="d-flex vh-80 w-100 justify-content-center align-items-center">
<div class="d-flex vh-80 w-100 justify-content-center align-items-center mt-5">
<div class="form-card">