frontend #18
Binary file not shown.
Binary file not shown.
@ -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/
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -693,3 +693,7 @@ class StaffUserCreationForm(UserCreationForm):
|
||||
|
||||
|
||||
|
||||
class ToggleAccountForm(forms.Form):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'},
|
||||
),
|
||||
]
|
||||
@ -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'},
|
||||
),
|
||||
]
|
||||
@ -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')
|
||||
|
||||
Binary file not shown.
@ -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'),
|
||||
|
||||
@ -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.")
|
||||
|
||||
|
||||
@ -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
|
||||
155
templates/account/account_inactive.html
Normal file
155
templates/account/account_inactive.html
Normal 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>
|
||||
125
templates/account/email.html
Normal file
125
templates/account/email.html
Normal 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 %}
|
||||
39
templates/account/email/password_reset_key_message.html
Normal file
39
templates/account/email/password_reset_key_message.html
Normal 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 %}
|
||||
71
templates/account/email_confirm.html
Normal file
71
templates/account/email_confirm.html
Normal 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 %}
|
||||
80
templates/account/logout.html
Normal file
80
templates/account/logout.html
Normal 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 %}
|
||||
@ -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">
|
||||
|
||||
|
||||
187
templates/account/password_reset_done.html
Normal file
187
templates/account/password_reset_done.html
Normal 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>
|
||||
165
templates/account/password_reset_from_key.html
Normal file
165
templates/account/password_reset_from_key.html
Normal 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>
|
||||
87
templates/account/password_reset_from_key_done.html
Normal file
87
templates/account/password_reset_from_key_done.html
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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%}
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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%}
|
||||
@ -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%}
|
||||
@ -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">
|
||||
|
||||
@ -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 %}
|
||||
@ -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 #}
|
||||
|
||||
@ -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">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">«</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">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">»</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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
@ -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">
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user