update on the models and forms
This commit is contained in:
parent
579cc085e2
commit
a23c96cc17
Binary file not shown.
Binary file not shown.
@ -196,7 +196,6 @@ SOCIALACCOUNT_PROVIDERS = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ZOOM_ACCOUNT_ID = 'HoGikHXsQB2GNDC5Rvyw9A'
|
ZOOM_ACCOUNT_ID = 'HoGikHXsQB2GNDC5Rvyw9A'
|
||||||
ZOOM_CLIENT_ID = 'brC39920R8C8azfudUaQgA'
|
ZOOM_CLIENT_ID = 'brC39920R8C8azfudUaQgA'
|
||||||
ZOOM_CLIENT_SECRET = 'rvfhjlbID4ychXPOvZ2lYsoAC0B0Ny2L'
|
ZOOM_CLIENT_SECRET = 'rvfhjlbID4ychXPOvZ2lYsoAC0B0Ny2L'
|
||||||
@ -215,7 +214,6 @@ CELERY_TASK_SERIALIZER = 'json'
|
|||||||
CELERY_RESULT_SERIALIZER = 'json'
|
CELERY_RESULT_SERIALIZER = 'json'
|
||||||
CELERY_TIMEZONE = 'UTC'
|
CELERY_TIMEZONE = 'UTC'
|
||||||
|
|
||||||
|
|
||||||
LINKEDIN_CLIENT_ID = '867jwsiyem1504'
|
LINKEDIN_CLIENT_ID = '867jwsiyem1504'
|
||||||
LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw=='
|
LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw=='
|
||||||
LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/jobs/linkedin/callback/'
|
LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/jobs/linkedin/callback/'
|
||||||
BIN
db.sqlite3
BIN
db.sqlite3
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.
@ -470,3 +470,17 @@ class InterviewScheduleForm(forms.ModelForm):
|
|||||||
working_days = self.cleaned_data.get('working_days')
|
working_days = self.cleaned_data.get('working_days')
|
||||||
# Convert string values to integers
|
# Convert string values to integers
|
||||||
return [int(day) for day in working_days]
|
return [int(day) for day in working_days]
|
||||||
|
|
||||||
|
|
||||||
|
class JobPostingCancelReasonForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = JobPosting
|
||||||
|
fields = ['cancel_reason']
|
||||||
|
class JobPostingStatusForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = JobPosting
|
||||||
|
fields = ['status']
|
||||||
|
class FormTemplateIsActiveForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = FormTemplate
|
||||||
|
fields = ['is_active']
|
||||||
@ -2,12 +2,13 @@ import requests
|
|||||||
|
|
||||||
LINKEDIN_API_BASE = "https://api.linkedin.com/v2"
|
LINKEDIN_API_BASE = "https://api.linkedin.com/v2"
|
||||||
|
|
||||||
|
|
||||||
class LinkedInService:
|
class LinkedInService:
|
||||||
def __init__(self, access_token):
|
def __init__(self, access_token):
|
||||||
self.headers = {
|
self.headers = {
|
||||||
'Authorization': f'Bearer {access_token}',
|
"Authorization": f"Bearer {access_token}",
|
||||||
'X-Restli-Protocol-Version': '2.0.0',
|
"X-Restli-Protocol-Version": "2.0.0",
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
def post_job(self, organization_id, job_data):
|
def post_job(self, organization_id, job_data):
|
||||||
@ -17,10 +18,10 @@ class LinkedInService:
|
|||||||
"lifecycleState": "PUBLISHED",
|
"lifecycleState": "PUBLISHED",
|
||||||
"specificContent": {
|
"specificContent": {
|
||||||
"com.linkedin.ugc.ShareContent": {
|
"com.linkedin.ugc.ShareContent": {
|
||||||
"shareCommentary": {"text": job_data['text']},
|
"shareCommentary": {"text": job_data["text"]},
|
||||||
"shareMediaCategory": "NONE"
|
"shareMediaCategory": "NONE",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visibility": {"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"}
|
"visibility": {"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"},
|
||||||
}
|
}
|
||||||
return requests.post(url, json=data, headers=self.headers)
|
return requests.post(url, json=data, headers=self.headers)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.2.6 on 2025-10-08 15:48
|
# Generated by Django 5.2.6 on 2025-10-09 10:10
|
||||||
|
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@ -213,6 +213,7 @@ class Migration(migrations.Migration):
|
|||||||
('last_name', models.CharField(max_length=255, verbose_name='Last Name')),
|
('last_name', models.CharField(max_length=255, verbose_name='Last Name')),
|
||||||
('email', models.EmailField(max_length=254, verbose_name='Email')),
|
('email', models.EmailField(max_length=254, verbose_name='Email')),
|
||||||
('phone', models.CharField(max_length=20, verbose_name='Phone')),
|
('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')),
|
('resume', models.FileField(upload_to='resumes/', verbose_name='Resume')),
|
||||||
('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')),
|
('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')),
|
||||||
('applied', models.BooleanField(default=False, verbose_name='Applied')),
|
('applied', models.BooleanField(default=False, verbose_name='Applied')),
|
||||||
@ -311,6 +312,14 @@ class Migration(migrations.Migration):
|
|||||||
name='job',
|
name='job',
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.jobposting', verbose_name='Job'),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.jobposting', verbose_name='Job'),
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Profile',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/')),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='SharedFormTemplate',
|
name='SharedFormTemplate',
|
||||||
fields=[
|
fields=[
|
||||||
@ -374,6 +383,7 @@ class Migration(migrations.Migration):
|
|||||||
name='ScheduledInterview',
|
name='ScheduledInterview',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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(verbose_name='Interview Date')),
|
||||||
('interview_time', models.TimeField(verbose_name='Interview Time')),
|
('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')], default='scheduled', max_length=20)),
|
||||||
@ -384,5 +394,8 @@ class Migration(migrations.Migration):
|
|||||||
('schedule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interviews', to='recruitment.interviewschedule')),
|
('schedule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interviews', to='recruitment.interviewschedule')),
|
||||||
('zoom_meeting', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='interview', to='recruitment.zoommeeting')),
|
('zoom_meeting', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='interview', to='recruitment.zoommeeting')),
|
||||||
],
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
# Generated by Django 5.2.6 on 2025-10-08 17:46
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('recruitment', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='candidate',
|
|
||||||
name='address',
|
|
||||||
field=models.TextField(default='', max_length=200, verbose_name='Address'),
|
|
||||||
preserve_default=False,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 5.2.6 on 2025-10-09 10:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('recruitment', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='jobposting',
|
||||||
|
name='cancel_reason',
|
||||||
|
field=models.TextField(blank=True, help_text='Reason for canceling the job posting', verbose_name='Cancel Reason'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='jobposting',
|
||||||
|
name='cancelled_at',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='jobposting',
|
||||||
|
name='cancelled_by',
|
||||||
|
field=models.CharField(blank=True, help_text='Name of person who cancelled this job', max_length=100, verbose_name='Cancelled By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='jobposting',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(blank=True, choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], default='DRAFT', max_length=20, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.2.6 on 2025-10-09 12:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('recruitment', '0002_jobposting_cancel_reason_jobposting_cancelled_at_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='candidate',
|
||||||
|
name='is_resume_parsed',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Resume Parsed'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='formtemplate',
|
||||||
|
name='is_active',
|
||||||
|
field=models.BooleanField(default=False, help_text='Whether this template is active'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -1,19 +0,0 @@
|
|||||||
# Generated by Django 5.2.6 on 2025-10-08 17:47
|
|
||||||
|
|
||||||
import django_extensions.db.fields
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('recruitment', '0002_candidate_address'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='scheduledinterview',
|
|
||||||
name='slug',
|
|
||||||
field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2025-10-08 13:01
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('recruitment', '0026_interviewschedule_scheduledinterview'),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Profile',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/')),
|
|
||||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
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.
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.
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.
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.
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.
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.
@ -11,8 +11,10 @@ from django.urls import reverse
|
|||||||
|
|
||||||
|
|
||||||
class Profile(models.Model):
|
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/")
|
||||||
user=models.OneToOneField(User,on_delete=models.CASCADE,related_name='profile')
|
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
|
||||||
|
|
||||||
|
|
||||||
class Base(models.Model):
|
class Base(models.Model):
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
|
||||||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated at"))
|
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated at"))
|
||||||
@ -105,8 +107,9 @@ class JobPosting(Base):
|
|||||||
# Status Fields
|
# Status Fields
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
("DRAFT", "Draft"),
|
("DRAFT", "Draft"),
|
||||||
("PUBLISHED", "Published"),
|
("ACTIVE", "Active"),
|
||||||
("CLOSED", "Closed"),
|
("CLOSED", "Closed"),
|
||||||
|
("CANCELLED", "Cancelled"),
|
||||||
("ARCHIVED", "Archived"),
|
("ARCHIVED", "Archived"),
|
||||||
]
|
]
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
@ -165,6 +168,18 @@ class JobPosting(Base):
|
|||||||
"External agency responsible for sourcing candidates for this role"
|
"External agency responsible for sourcing candidates for this role"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
cancel_reason = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
help_text=_("Reason for canceling the job posting"),
|
||||||
|
verbose_name=_("Cancel Reason"),
|
||||||
|
)
|
||||||
|
cancelled_by = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True,
|
||||||
|
help_text=_("Name of person who cancelled this job"),
|
||||||
|
verbose_name=_("Cancelled By"),
|
||||||
|
)
|
||||||
|
cancelled_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["-created_at"]
|
ordering = ["-created_at"]
|
||||||
@ -197,7 +212,7 @@ class JobPosting(Base):
|
|||||||
else:
|
else:
|
||||||
next_num = 1
|
next_num = 1
|
||||||
|
|
||||||
self.internal_job_id = f"{prefix}-{year}-{next_num:04d}"
|
self.internal_job_id = f"{prefix}-{year}-{next_num:06d}"
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@ -260,8 +275,11 @@ class Candidate(Base):
|
|||||||
last_name = models.CharField(max_length=255, verbose_name=_("Last Name"))
|
last_name = models.CharField(max_length=255, verbose_name=_("Last Name"))
|
||||||
email = models.EmailField(verbose_name=_("Email"))
|
email = models.EmailField(verbose_name=_("Email"))
|
||||||
phone = models.CharField(max_length=20, verbose_name=_("Phone"))
|
phone = models.CharField(max_length=20, verbose_name=_("Phone"))
|
||||||
address = models.TextField(max_length=200,verbose_name=_("Address"))
|
address = models.TextField(max_length=200, verbose_name=_("Address"))
|
||||||
resume = models.FileField(upload_to="resumes/", verbose_name=_("Resume"))
|
resume = models.FileField(upload_to="resumes/", verbose_name=_("Resume"))
|
||||||
|
is_resume_parsed = models.BooleanField(
|
||||||
|
default=False, verbose_name=_("Resume Parsed")
|
||||||
|
)
|
||||||
parsed_summary = models.TextField(blank=True, verbose_name=_("Parsed Summary"))
|
parsed_summary = models.TextField(blank=True, verbose_name=_("Parsed Summary"))
|
||||||
applied = models.BooleanField(default=False, verbose_name=_("Applied"))
|
applied = models.BooleanField(default=False, verbose_name=_("Applied"))
|
||||||
stage = models.CharField(
|
stage = models.CharField(
|
||||||
@ -331,6 +349,7 @@ class Candidate(Base):
|
|||||||
if self.resume:
|
if self.resume:
|
||||||
return self.resume.size
|
return self.resume.size
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Validate stage transitions"""
|
"""Validate stage transitions"""
|
||||||
# Only validate if this is an existing record (not being created)
|
# Only validate if this is an existing record (not being created)
|
||||||
@ -376,6 +395,14 @@ class Candidate(Base):
|
|||||||
old_stage = self.__class__.objects.get(pk=self.pk).stage
|
old_stage = self.__class__.objects.get(pk=self.pk).stage
|
||||||
return self.STAGE_SEQUENCE.get(old_stage, [])
|
return self.STAGE_SEQUENCE.get(old_stage, [])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def submission(self):
|
||||||
|
return FormSubmission.objects.filter(template__job=self.job).first()
|
||||||
|
@property
|
||||||
|
def responses(self):
|
||||||
|
if self.submission:
|
||||||
|
return self.submission.responses.all()
|
||||||
|
return []
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.full_name
|
return self.full_name
|
||||||
|
|
||||||
@ -449,7 +476,7 @@ class FormTemplate(Base):
|
|||||||
User, on_delete=models.CASCADE, related_name="form_templates"
|
User, on_delete=models.CASCADE, related_name="form_templates"
|
||||||
)
|
)
|
||||||
is_active = models.BooleanField(
|
is_active = models.BooleanField(
|
||||||
default=True, help_text="Whether this template is active"
|
default=False, help_text="Whether this template is active"
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -595,6 +622,9 @@ class FormField(Base):
|
|||||||
if self.order < 0:
|
if self.order < 0:
|
||||||
raise ValidationError("Order must be a positive integer")
|
raise ValidationError("Order must be a positive integer")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.stage.template.name} - {self.stage.name} - {self.label}"
|
||||||
|
|
||||||
|
|
||||||
class FormSubmission(Base):
|
class FormSubmission(Base):
|
||||||
"""
|
"""
|
||||||
@ -658,16 +688,19 @@ class FieldResponse(Base):
|
|||||||
if self.uploaded_file:
|
if self.uploaded_file:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def get_file(self):
|
def get_file(self):
|
||||||
if self.is_file:
|
if self.is_file:
|
||||||
return self.uploaded_file
|
return self.uploaded_file
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def get_file_size(self):
|
def get_file_size(self):
|
||||||
if self.is_file:
|
if self.is_file:
|
||||||
return self.uploaded_file.size
|
return self.uploaded_file.size
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def display_value(self):
|
def display_value(self):
|
||||||
"""Return a human-readable representation of the response value"""
|
"""Return a human-readable representation of the response value"""
|
||||||
@ -885,9 +918,7 @@ class InterviewSchedule(Base):
|
|||||||
job = models.ForeignKey(
|
job = models.ForeignKey(
|
||||||
JobPosting, on_delete=models.CASCADE, related_name="interview_schedules"
|
JobPosting, on_delete=models.CASCADE, related_name="interview_schedules"
|
||||||
)
|
)
|
||||||
candidates = models.ManyToManyField(
|
candidates = models.ManyToManyField(Candidate, related_name="interview_schedules")
|
||||||
Candidate, related_name="interview_schedules"
|
|
||||||
)
|
|
||||||
start_date = models.DateField(verbose_name=_("Start Date"))
|
start_date = models.DateField(verbose_name=_("Start Date"))
|
||||||
end_date = models.DateField(verbose_name=_("End Date"))
|
end_date = models.DateField(verbose_name=_("End Date"))
|
||||||
working_days = models.JSONField(
|
working_days = models.JSONField(
|
||||||
@ -895,9 +926,7 @@ class InterviewSchedule(Base):
|
|||||||
) # Store days of week as [0,1,2,3,4] for Mon-Fri
|
) # Store days of week as [0,1,2,3,4] for Mon-Fri
|
||||||
start_time = models.TimeField(verbose_name=_("Start Time"))
|
start_time = models.TimeField(verbose_name=_("Start Time"))
|
||||||
end_time = models.TimeField(verbose_name=_("End Time"))
|
end_time = models.TimeField(verbose_name=_("End Time"))
|
||||||
breaks = models.ManyToManyField(
|
breaks = models.ManyToManyField(BreakTime, blank=True, related_name="schedules")
|
||||||
BreakTime, blank=True, related_name="schedules"
|
|
||||||
)
|
|
||||||
interview_duration = models.PositiveIntegerField(
|
interview_duration = models.PositiveIntegerField(
|
||||||
verbose_name=_("Interview Duration (minutes)")
|
verbose_name=_("Interview Duration (minutes)")
|
||||||
)
|
)
|
||||||
|
|||||||
@ -24,6 +24,8 @@ import asyncio
|
|||||||
|
|
||||||
@receiver(post_save, sender=models.Candidate)
|
@receiver(post_save, sender=models.Candidate)
|
||||||
def score_candidate_resume(sender, instance, created, **kwargs):
|
def score_candidate_resume(sender, instance, created, **kwargs):
|
||||||
|
if instance.is_resume_parsed:
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
# Get absolute file path
|
# Get absolute file path
|
||||||
file_path = instance.resume.path
|
file_path = instance.resume.path
|
||||||
@ -108,12 +110,12 @@ def score_candidate_resume(sender, instance, created, **kwargs):
|
|||||||
instance.weaknesses = result1.get('weaknesses', '')
|
instance.weaknesses = result1.get('weaknesses', '')
|
||||||
instance.criteria_checklist = result1.get('criteria_checklist', {})
|
instance.criteria_checklist = result1.get('criteria_checklist', {})
|
||||||
|
|
||||||
|
instance.is_resume_parsed = True
|
||||||
|
|
||||||
# Save only scoring-related fields to avoid recursion
|
# Save only scoring-related fields to avoid recursion
|
||||||
instance.save(update_fields=[
|
instance.save(update_fields=[
|
||||||
'match_score', 'strengths', 'weaknesses',
|
'match_score', 'strengths', 'weaknesses',
|
||||||
'criteria_checklist','parsed_summary'
|
'criteria_checklist','parsed_summary', 'is_resume_parsed'
|
||||||
])
|
])
|
||||||
|
|
||||||
logger.info(f"Successfully scored resume for candidate {instance.id}")
|
logger.info(f"Successfully scored resume for candidate {instance.id}")
|
||||||
|
|||||||
@ -536,3 +536,18 @@ def get_available_time_slots(schedule, breaks=None):
|
|||||||
|
|
||||||
print(f"Total slots generated: {len(slots)}")
|
print(f"Total slots generated: {len(slots)}")
|
||||||
return slots
|
return slots
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def json_to_markdown_table(data_list):
|
||||||
|
if not data_list:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
headers = data_list[0].keys()
|
||||||
|
markdown = "| " + " | ".join(headers) + " |\n"
|
||||||
|
markdown += "| " + " | ".join(["---"] * len(headers)) + " |\n"
|
||||||
|
|
||||||
|
for row in data_list:
|
||||||
|
values = [str(row.get(header, "")) for header in headers]
|
||||||
|
markdown += "| " + " | ".join(values) + " |\n"
|
||||||
|
return markdown
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,8 @@
|
|||||||
|
import json
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
|
|
||||||
|
from recruitment.utils import json_to_markdown_table
|
||||||
from . import models
|
from . import models
|
||||||
from django.utils.translation import get_language
|
from django.utils.translation import get_language
|
||||||
from . import forms
|
from . import forms
|
||||||
@ -19,6 +22,8 @@ from datastar_py.django import (
|
|||||||
ServerSentEventGenerator as SSE,
|
ServerSentEventGenerator as SSE,
|
||||||
read_signals,
|
read_signals,
|
||||||
)
|
)
|
||||||
|
# from rich import print
|
||||||
|
from rich.markdown import CodeBlock
|
||||||
|
|
||||||
class JobListView(LoginRequiredMixin, ListView):
|
class JobListView(LoginRequiredMixin, ListView):
|
||||||
model = models.JobPosting
|
model = models.JobPosting
|
||||||
@ -201,6 +206,7 @@ def training_list(request):
|
|||||||
|
|
||||||
|
|
||||||
def candidate_detail(request, slug):
|
def candidate_detail(request, slug):
|
||||||
|
from rich.json import JSON
|
||||||
candidate = get_object_or_404(models.Candidate, slug=slug)
|
candidate = get_object_or_404(models.Candidate, slug=slug)
|
||||||
try:
|
try:
|
||||||
parsed = ast.literal_eval(candidate.parsed_summary)
|
parsed = ast.literal_eval(candidate.parsed_summary)
|
||||||
@ -212,6 +218,8 @@ def candidate_detail(request, slug):
|
|||||||
if request.user.is_staff:
|
if request.user.is_staff:
|
||||||
stage_form = forms.CandidateStageForm(candidate=candidate)
|
stage_form = forms.CandidateStageForm(candidate=candidate)
|
||||||
|
|
||||||
|
# parsed = JSON(json.dumps(parsed), indent=2, highlight=True, skip_keys=False, ensure_ascii=False, check_circular=True, allow_nan=True, default=None, sort_keys=False)
|
||||||
|
parsed = json_to_markdown_table([parsed])
|
||||||
return render(request, 'recruitment/candidate_detail.html', {
|
return render(request, 'recruitment/candidate_detail.html', {
|
||||||
'candidate': candidate,
|
'candidate': candidate,
|
||||||
'parsed': parsed,
|
'parsed': parsed,
|
||||||
|
|||||||
@ -1,50 +1,238 @@
|
|||||||
{% extends "base.html" %}
|
{% extends 'base.html' %}
|
||||||
|
{% load static i18n crispy_forms_tags %}
|
||||||
|
{% load partials %}
|
||||||
|
|
||||||
{% block title %}Submissions for {{ template.name }}{% endblock %}
|
{% block title %}Submissions for {{ template.name }} - ATS{% endblock %}
|
||||||
|
|
||||||
|
{% block customCSS %}
|
||||||
|
<style>
|
||||||
|
/* ================================================= */
|
||||||
|
/* UI Variables (Matching Form Templates List) */
|
||||||
|
/* ================================================= */
|
||||||
|
:root {
|
||||||
|
--kaauh-teal: #00636e;
|
||||||
|
--kaauh-teal-dark: #004a53;
|
||||||
|
--kaauh-border: #eaeff3;
|
||||||
|
--kaauh-primary-text: #343a40;
|
||||||
|
--kaauh-gray-light: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Typography and Color Overrides --- */
|
||||||
|
.text-primary { color: var(--kaauh-teal) !important; }
|
||||||
|
|
||||||
|
/* --- Button Base Styles (Matching Form Templates List) --- */
|
||||||
|
.btn-main-action {
|
||||||
|
background-color: var(--kaauh-teal);
|
||||||
|
border-color: var(--kaauh-teal);
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.btn-main-action:hover {
|
||||||
|
background-color: var(--kaauh-teal-dark);
|
||||||
|
border-color: var(--kaauh-teal-dark);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Secondary Button Style (for Edit/Preview) */
|
||||||
|
.btn-outline-secondary {
|
||||||
|
color: var(--kaauh-teal-dark);
|
||||||
|
border-color: var(--kaauh-teal);
|
||||||
|
}
|
||||||
|
.btn-outline-secondary:hover {
|
||||||
|
background-color: var(--kaauh-teal-dark);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--kaauh-teal-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Size Utilities (matching Bootstrap convention) */
|
||||||
|
.btn-lg {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.btn-sm {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Card and Layout Styles (Matching Form Templates List) --- */
|
||||||
|
.card {
|
||||||
|
border: 1px solid var(--kaauh-border);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||||
|
background-color: white;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background-color: var(--kaauh-teal-dark) !important;
|
||||||
|
border-bottom: 1px solid var(--kaauh-border);
|
||||||
|
color: white !important;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-radius: 0.75rem 0.75rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h1 {
|
||||||
|
color: white !important;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
.card-header .fas {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
.card-header .small {
|
||||||
|
color: rgba(255, 255, 255, 0.7) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Table Styles --- */
|
||||||
|
.table-responsive {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.table {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.table thead th {
|
||||||
|
background-color: var(--kaauh-teal-dark);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--kaauh-border);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.table tbody td {
|
||||||
|
padding: 1rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-color: var(--kaauh-border);
|
||||||
|
}
|
||||||
|
.table tbody tr {
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
.table tbody tr:hover {
|
||||||
|
background-color: var(--kaauh-gray-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Pagination Styling (Matching Form Templates List) --- */
|
||||||
|
.pagination .page-item .page-link {
|
||||||
|
color: var(--kaauh-teal-dark);
|
||||||
|
border-color: var(--kaauh-border);
|
||||||
|
}
|
||||||
|
.pagination .page-item.active .page-link {
|
||||||
|
background-color: var(--kaauh-teal);
|
||||||
|
border-color: var(--kaauh-teal);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.pagination .page-item:hover .page-link:not(.active) {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
.pagination-info {
|
||||||
|
color: var(--kaauh-primary-text);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Empty State Theming --- */
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
color: var(--kaauh-primary-text);
|
||||||
|
border: 2px dashed var(--kaauh-border);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background-color: var(--kaauh-gray-light);
|
||||||
|
}
|
||||||
|
.empty-state i {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--kaauh-teal-dark);
|
||||||
|
}
|
||||||
|
.empty-state .btn-main-action .fas {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Breadcrumb --- */
|
||||||
|
.breadcrumb {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.breadcrumb-item a {
|
||||||
|
color: var(--kaauh-teal-dark);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.breadcrumb-item a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.breadcrumb-item.active {
|
||||||
|
color: var(--kaauh-primary-text);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container mx-auto px-4 py-8">
|
<div class="container py-4">
|
||||||
<nav class="mb-6">
|
<nav aria-label="breadcrumb">
|
||||||
<a href="{% url 'form_templates_list' %}" class="text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white flex items-center">
|
<ol class="breadcrumb">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" viewBox="0 0 20 20" fill="currentColor">
|
<li class="breadcrumb-item"><a href="{% url 'dashboard' %}">{% trans "Dashboard" %}</a></li>
|
||||||
<path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd" />
|
<li class="breadcrumb-item"><a href="{% url 'form_templates_list' %}">{% trans "Form Templates" %}</a></li>
|
||||||
</svg>
|
<li class="breadcrumb-item active">{% trans "Submissions" %}</li>
|
||||||
Back to Form Templates
|
</ol>
|
||||||
</a>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 shadow-md rounded-lg overflow-hidden">
|
<div class="card shadow-sm">
|
||||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Submissions for: <span class="text-blue-600 dark:text-blue-400">{{ template.name }}</span></h1>
|
<div>
|
||||||
<p class="text-gray-600 dark:text-gray-400 mt-1">Template ID: {{ template.id }}</p>
|
<h1 class="h3 mb-1 d-flex align-items-center">
|
||||||
|
<i class="fas fa-file-alt me-2"></i>
|
||||||
|
{% trans "Submissions for" %}: <span class="text-white ms-2">{{ template.name }}</span>
|
||||||
|
</h1>
|
||||||
|
<small class="text-white-50">Template ID: #{{ template.id }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
<a href="{% url 'form_templates_list' %}" class="btn btn-outline-light btn-sm">
|
||||||
<div class="p-6">
|
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Templates" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
{% if page_obj.object_list %}
|
{% if page_obj.object_list %}
|
||||||
<div class="overflow-x-auto">
|
<div id="form-template-submissions-list">
|
||||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
{# View Switcher #}
|
||||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
{% include "includes/_list_view_switcher.html" with list_id="form-template-submissions-list" %}
|
||||||
|
|
||||||
|
{# Table View (Default) #}
|
||||||
|
<div class="table-view active">
|
||||||
|
<div class="table-responsive mb-4">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Submission ID</th>
|
<th scope="col">{% trans "Submission ID" %}</th>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Applicant Name</th>
|
<th scope="col">{% trans "Applicant Name" %}</th>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Applicant Email</th>
|
<th scope="col">{% trans "Applicant Email" %}</th>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Submitted At</th>
|
<th scope="col">{% trans "Submitted At" %}</th>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Actions</th>
|
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
<tbody>
|
||||||
{% for submission in page_obj %}
|
{% for submission in page_obj %}
|
||||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-750">
|
<tr>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{{ submission.id }}</td>
|
<td class="fw-medium">{{ submission.id }}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">{{ submission.applicant_name|default:"N/A" }}</td>
|
<td>{{ submission.applicant_name|default:"N/A" }}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">{{ submission.applicant_email|default:"N/A" }}</td>
|
<td>{{ submission.applicant_email|default:"N/A" }}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">{{ submission.submitted_at|date:"Y-m-d H:i:s" }}</td>
|
<td>{{ submission.submitted_at|date:"M d, Y H:i" }}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
<td class="text-end">
|
||||||
<a href="{% url 'form_submission_details' template_id=template.id submission_id=submission.id %}" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300 inline-flex items-center">
|
<a href="{% url 'form_submission_details' template_id=template.id submission_id=submission.id %}" class="btn btn-sm btn-outline-primary">
|
||||||
View Details
|
<i class="fas fa-eye me-1"></i> {% trans "View Details" %}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -52,50 +240,93 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Card View #}
|
||||||
|
<div class="card-view">
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for submission in page_obj %}
|
||||||
|
<div class="col-lg-4 col-md-6">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="h5 mb-2">{% trans "Submission" %} #{{ submission.id }}</h3>
|
||||||
|
<small class="text-white-50">{{ template.name }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="card-text">
|
||||||
|
<strong>{% trans "Applicant Name" %}:</strong> {{ submission.applicant_name|default:"N/A" }}<br>
|
||||||
|
<strong>{% trans "Applicant Email" %}:</strong> {{ submission.applicant_email|default:"N/A" }}<br>
|
||||||
|
<strong>{% trans "Submitted At" %}:</strong> {{ submission.submitted_at|date:"M d, Y H:i" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<a href="{% url 'form_submission_details' template_id=template.id submission_id=submission.id %}" class="btn btn-sm btn-outline-primary w-100">
|
||||||
|
<i class="fas fa-eye me-1"></i> {% trans "View Details" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
{% if page_obj.has_other_pages %}
|
{% if page_obj.has_other_pages %}
|
||||||
<nav class="mt-6 flex items-center justify-between" aria-label="Pagination">
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center">
|
||||||
<div class="hidden sm:block">
|
<div class="pagination-info mb-3 mb-md-0">
|
||||||
<p class="text-sm text-gray-700 dark:text-gray-300">
|
{% blocktrans with start=page_obj.start_index end=page_obj.end_index total=page_obj.paginator.count %}
|
||||||
Showing <span class="font-medium">{{ page_obj.start_index }}</span> to <span class="font-medium">{{ page_obj.end_index }}</span> of <span class="font-medium">{{ page_obj.paginator.count }}</span> results.
|
Showing {{ start }} to {{ end }} of {{ total }} results.
|
||||||
</p>
|
{% endblocktrans %}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 flex justify-between sm:justify-end mt-4 sm:mt-0">
|
<nav aria-label="Page navigation">
|
||||||
|
<ul class="pagination pagination-sm mb-0">
|
||||||
{% if page_obj.has_previous %}
|
{% if page_obj.has_previous %}
|
||||||
<a href="?page={{ page_obj.previous_page_number }}" class="relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700">
|
<li class="page-item">
|
||||||
Previous
|
<a class="page-link" href="?page=1" aria-label="First">
|
||||||
|
<span aria-hidden="true">«</span>
|
||||||
</a>
|
</a>
|
||||||
|
</li>
|
||||||
|
<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>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm font-medium text-gray-700 dark:text-gray-300">
|
<li class="page-item active">
|
||||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
<span class="page-link">
|
||||||
|
{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}
|
||||||
</span>
|
</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
{% if page_obj.has_next %}
|
{% if page_obj.has_next %}
|
||||||
<a href="?page={{ page_obj.next_page_number }}" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700">
|
<li class="page-item">
|
||||||
Next
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="Next">
|
||||||
|
<span aria-hidden="true">›</span>
|
||||||
</a>
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}" aria-label="Last">
|
||||||
|
<span aria-hidden="true">»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center py-12">
|
<div class="empty-state">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<i class="fas fa-inbox"></i>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
<h3 class="h5 mb-3">{% trans "No Submissions Found" %}</h3>
|
||||||
</svg>
|
<p class="text-muted mb-4">
|
||||||
<h3 class="mt-2 text-lg font-medium text-gray-900 dark:text-white">No submissions found</h3>
|
{% trans "There are no submissions for this form template yet." %}
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">There are no submissions for this form template yet.</p>
|
</p>
|
||||||
<div class="mt-6">
|
<a href="{% url 'form_templates_list' %}" class="btn btn-main-action">
|
||||||
<a href="{% url 'form_templates_list' %}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Templates" %}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="mr-2 -ml-1 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
|
|
||||||
</svg>
|
|
||||||
Back to Templates
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -231,7 +231,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if templates %}
|
{% if templates %}
|
||||||
<div class="row g-4">
|
<div id="form-templates-list">
|
||||||
|
{# View Switcher #}
|
||||||
|
{% include "includes/_list_view_switcher.html" with list_id="form-templates-list" %}
|
||||||
|
|
||||||
|
{# Card View (Default) #}
|
||||||
|
<div class="card-view active row g-4">
|
||||||
{% for template in templates %}
|
{% for template in templates %}
|
||||||
<div class="col-lg-4 col-md-6">
|
<div class="col-lg-4 col-md-6">
|
||||||
<div class="card template-card h-100">
|
<div class="card template-card h-100">
|
||||||
@ -292,6 +297,54 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# Table View #}
|
||||||
|
<div class="table-view">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{% trans "Template Name" %}</th>
|
||||||
|
<th scope="col">{% trans "Job" %}</th>
|
||||||
|
<th scope="col">{% trans "Stages" %}</th>
|
||||||
|
<th scope="col">{% trans "Fields" %}</th>
|
||||||
|
<th scope="col">{% trans "Created" %}</th>
|
||||||
|
<th scope="col">{% trans "Last Updated" %}</th>
|
||||||
|
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for template in templates %}
|
||||||
|
<tr>
|
||||||
|
<td class="fw-medium">{{ template.name }}</td>
|
||||||
|
<td>{{ template.job }}</td>
|
||||||
|
<td>{{ template.get_stage_count }}</td>
|
||||||
|
<td>{{ template.get_field_count }}</td>
|
||||||
|
<td>{{ template.created_at|date:"M d, Y" }}</td>
|
||||||
|
<td>{{ template.updated_at|date:"M d, Y" }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<a href="{% url 'form_wizard' template.id %}" class="btn btn-outline-primary" title="{% trans 'Preview' %}">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'form_builder' template.id %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'form_template_submissions_list' template.slug %}" class="btn btn-outline-info" title="{% trans 'Submissions' %}">
|
||||||
|
<i class="fas fa-file-alt"></i>
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-outline-danger delete-btn" data-bs-toggle="modal" data-bs-target="#deleteModal" data-template-id="{{ template.id }}" data-template-name="{{ template.name }}" title="{% trans 'Delete' %}">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if templates.has_other_pages %}
|
{% if templates.has_other_pages %}
|
||||||
<nav aria-label="Page navigation" class="mt-4">
|
<nav aria-label="Page navigation" class="mt-4">
|
||||||
<ul class="pagination justify-content-center">
|
<ul class="pagination justify-content-center">
|
||||||
|
|||||||
@ -1,12 +1,18 @@
|
|||||||
{% load static i18n %}
|
{% load static i18n %}
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>{% translate "Application Form" %}</title>
|
<title>{% translate "Application Form" %}</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||||
|
/>
|
||||||
<style>
|
<style>
|
||||||
/* KAAT-S Theme Variables */
|
/* KAAT-S Theme Variables */
|
||||||
:root {
|
:root {
|
||||||
@ -57,14 +63,20 @@
|
|||||||
padding-top: 56px; /* Space for the sticky navbar */
|
padding-top: 56px; /* Space for the sticky navbar */
|
||||||
|
|
||||||
/* Dark gradient background to match the theme */
|
/* Dark gradient background to match the theme */
|
||||||
background: linear-gradient(135deg, var(--kaauh-teal-dark) 0%, #1e3a47 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--kaauh-teal-dark) 0%,
|
||||||
|
#1e3a47 100%
|
||||||
|
);
|
||||||
background-image: url("{% static 'image/vision.svg' %}");
|
background-image: url("{% static 'image/vision.svg' %}");
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: 60px;
|
background-position: 60px;
|
||||||
background-size: 320px auto;
|
background-size: 320px auto;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 0; /* Remove padding from body */
|
padding: 0; /* Remove padding from body */
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
|
"Helvetica Neue", Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Wrapper to center the wizard content below the navbar */
|
/* Wrapper to center the wizard content below the navbar */
|
||||||
@ -73,7 +85,9 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20px; /* Re-apply padding here for the content area */
|
padding: 20px; /* Re-apply padding here for the content area */
|
||||||
min-height: calc(100vh - 56px); /* Adjust height to account for navbar */
|
min-height: calc(
|
||||||
|
100vh - 56px
|
||||||
|
); /* Adjust height to account for navbar */
|
||||||
}
|
}
|
||||||
|
|
||||||
.wizard-container {
|
.wizard-container {
|
||||||
@ -486,36 +500,63 @@
|
|||||||
top: 72px;
|
top: 72px;
|
||||||
/* The z-index is already 1030 in the inline style, which is correct */
|
/* The z-index is already 1030 in the inline style, which is correct */
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<nav
|
||||||
<nav id="topNavbar" class="navbar navbar-expand-lg sticky-top" style="background-color: white; z-index: 1030;">
|
id="topNavbar"
|
||||||
|
class="navbar navbar-expand-lg sticky-top"
|
||||||
|
style="background-color: white; z-index: 1030"
|
||||||
|
>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand text-white fw-bold" href="/">
|
<a class="navbar-brand text-white fw-bold" href="/">
|
||||||
<img src="{% static 'image/kaauh.jpeg' %}" alt="{% translate 'KAAUH IMAGE' %}" style="height: 50px; margin-right: 10px;">
|
<img
|
||||||
|
src="{% static 'image/kaauh.jpeg' %}"
|
||||||
|
alt="{% translate 'KAAUH IMAGE' %}"
|
||||||
|
style="height: 50px; margin-right: 10px"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
|
<button
|
||||||
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
class="navbar-toggler"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#navbarNav"
|
||||||
|
aria-controls="navbarNav"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-label="Toggle navigation"
|
||||||
|
>
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
<ul class="navbar-nav ms-auto">
|
<ul class="navbar-nav ms-auto">
|
||||||
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link text-secondary" href="/applications/">{% translate "Applications" %}</a>
|
<a
|
||||||
|
class="nav-link text-secondary"
|
||||||
|
href="/applications/"
|
||||||
|
>{% translate "Applications" %}</a
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link text-secondary" href="/profile/">{% translate "Profile" %}</a>
|
<a class="nav-link text-secondary" href="/profile/"
|
||||||
|
>{% translate "Profile" %}</a
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link text-secondary" href="https://kaauh.edu.sa/career">{% translate "Careers" %}</a>
|
<a
|
||||||
|
class="nav-link text-secondary"
|
||||||
|
href="https://kaauh.edu.sa/career"
|
||||||
|
>{% translate "Careers" %}</a
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<nav id="bottomNavbar" class="navbar navbar-expand-lg sticky-top" style="background-color: var(--kaauh-teal); z-index: 1030;">
|
<nav
|
||||||
|
id="bottomNavbar"
|
||||||
|
class="navbar navbar-expand-lg sticky-top"
|
||||||
|
style="background-color: var(--kaauh-teal); z-index: 1030"
|
||||||
|
>
|
||||||
<span class="ms-2 text-white">JOB ID: {{job_id}}</span>
|
<span class="ms-2 text-white">JOB ID: {{job_id}}</span>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@ -528,55 +569,58 @@
|
|||||||
<div class="wizard-header">
|
<div class="wizard-header">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<i class="fas fa-file-alt"></i>
|
<i class="fas fa-file-alt"></i>
|
||||||
<span id="formTitle">{% translate "Application Form" %}</span>
|
<span id="formTitle"
|
||||||
|
>{% translate "Application Form" %}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-text" id="progressText">1 of 1</div>
|
<div class="progress-text" id="progressText">1 of 1</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="wizard-content">
|
<div class="wizard-content">
|
||||||
<div class="stage-container" id="stageContainer">
|
<div class="stage-container" id="stageContainer"></div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="preview-container" id="previewContainer" style="display: none;">
|
<div
|
||||||
<h3 class="mb-4">{% translate "Review Your Application" %}</h3>
|
class="preview-container"
|
||||||
|
id="previewContainer"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
<h3 class="mb-4">
|
||||||
|
{% translate "Review Your Application" %}
|
||||||
|
</h3>
|
||||||
<div id="previewContent"></div>
|
<div id="previewContent"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="wizard-footer">
|
<div class="wizard-footer">
|
||||||
<button id="backBtn" class="nav-btn btn-back" style="display: none;">
|
<button
|
||||||
|
id="backBtn"
|
||||||
|
class="nav-btn btn-back"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
<i class="fas fa-arrow-left"></i> {% translate "Back" %}
|
<i class="fas fa-arrow-left"></i> {% translate "Back" %}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button id="nextBtn" class="nav-btn btn-next">
|
<button id="nextBtn" class="nav-btn btn-next">
|
||||||
{% translate "Next" %} <i class="fas fa-arrow-right"></i>
|
{% translate "Next" %}
|
||||||
|
<i class="fas fa-arrow-right"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button id="submitBtn" class="nav-btn btn-submit" style="display: none;">
|
<button
|
||||||
{% translate "Submit Application" %} <i class="fas fa-paper-plane"></i>
|
id="submitBtn"
|
||||||
|
class="nav-btn btn-submit"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
{% translate "Submit Application" %}
|
||||||
|
<i class="fas fa-paper-plane"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> <script>
|
</div>
|
||||||
// Placeholder for the complete JavaScript logic (omitted for brevity, but required for functionality)
|
|
||||||
// This script block should contain the Application State, DOM Elements, Validation, API, Rendering, and Event Handlers
|
|
||||||
console.log("JavaScript logic for form wizard needs to be included here.");
|
|
||||||
|
|
||||||
// --- COMPLETE JAVASCRIPT LOGIC GOES HERE ---
|
|
||||||
// (The logic provided in the previous turn, including the completed createFieldElement and renderPreview functions)
|
|
||||||
|
|
||||||
// Example structure for reference:
|
|
||||||
// function validateEmail(email) { ... }
|
|
||||||
// function loadFormTemplate() { ... }
|
|
||||||
// function renderCurrentStage() { ... }
|
|
||||||
// function createFieldElement(field) { ... }
|
|
||||||
// function renderPreview() { ... }
|
|
||||||
// document.addEventListener('DOMContentLoaded', loadFormTemplate);
|
|
||||||
</script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Application State
|
// Application State
|
||||||
const csrfToken = '{{ csrf_token }}';
|
const csrfToken = '{{ csrf_token }}';
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
templateId: {{ template_id }},
|
templateId: {{ template_id }},
|
||||||
stages: [],
|
stages: [],
|
||||||
@ -803,6 +847,7 @@
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const templateData = result.template;
|
const templateData = result.template;
|
||||||
|
console.log(templateData);
|
||||||
state.stages = templateData.stages;
|
state.stages = templateData.stages;
|
||||||
elements.formTitle.textContent = templateData.name;
|
elements.formTitle.textContent = templateData.name;
|
||||||
updateProgress();
|
updateProgress();
|
||||||
@ -831,11 +876,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
formData.append('csrfmiddlewaretoken', csrfToken);
|
||||||
// Add applicant info
|
// Add applicant info
|
||||||
//formData.append('applicant_name', state.formData.applicant_name || '');
|
//formData.append('applicant_name', state.formData.applicant_name || '');
|
||||||
//formData.append('applicant_email', state.formData.applicant_email || '');
|
//formData.append('applicant_email', state.formData.applicant_email || '');
|
||||||
|
console.log(state.formData)
|
||||||
// Add field responses
|
// Add field responses
|
||||||
state.stages.forEach(stage => {
|
state.stages.forEach(stage => {
|
||||||
stage.fields.forEach(field => {
|
stage.fields.forEach(field => {
|
||||||
@ -855,9 +900,6 @@
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(`/forms/form/${state.templateId}/submit/`, {
|
const response = await fetch(`/forms/form/${state.templateId}/submit/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1279,5 +1321,5 @@
|
|||||||
// Start the application
|
// Start the application
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
159
templates/includes/_list_view_switcher.html
Normal file
159
templates/includes/_list_view_switcher.html
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm view-toggle active" data-view="table" data-list-id="{{ list_id }}">
|
||||||
|
<i class="fas fa-table me-1"></i> Table
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm view-toggle" data-view="card" data-list-id="{{ list_id }}">
|
||||||
|
<i class="fas fa-th me-1"></i> Card
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* View Toggle Styles */
|
||||||
|
.view-toggle {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
.view-toggle.active {
|
||||||
|
background-color: var(--kaauh-teal);
|
||||||
|
border-color: var(--kaauh-teal);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.view-toggle.active:hover {
|
||||||
|
background-color: var(--kaauh-teal-dark);
|
||||||
|
border-color: var(--kaauh-teal-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide elements by default */
|
||||||
|
.table-view,
|
||||||
|
.card-view {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show active view */
|
||||||
|
.table-view.active,
|
||||||
|
.card-view.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card View Styles */
|
||||||
|
.card-view .card {
|
||||||
|
border: 1px solid var(--kaauh-border);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||||
|
background-color: white;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.card-view .card-header {
|
||||||
|
background-color: var(--kaauh-teal-dark);
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-radius: 0.75rem 0.75rem 0 0;
|
||||||
|
}
|
||||||
|
.card-view .card-body {
|
||||||
|
padding: 1.25rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.card-view .card-title {
|
||||||
|
color: var(--kaauh-teal-dark);
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.card-view .card-text {
|
||||||
|
color: var(--kaauh-primary-text);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.card-view .card-footer {
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-top: 1px solid var(--kaauh-border);
|
||||||
|
border-radius: 0 0 0.75rem 0.75rem;
|
||||||
|
}
|
||||||
|
.card-view .btn-sm {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table View Styles */
|
||||||
|
.table-view .table-responsive {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.table-view .table {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.table-view .table thead th {
|
||||||
|
background-color: var(--kaauh-teal-dark);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--kaauh-border);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.table-view .table tbody td {
|
||||||
|
padding: 1rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-color: var(--kaauh-border);
|
||||||
|
}
|
||||||
|
.table-view .table tbody tr {
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
.table-view .table tbody tr:hover {
|
||||||
|
background-color: var(--kaauh-gray-light);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Get the list ID from the data attribute
|
||||||
|
const listId = document.querySelector('.view-toggle').getAttribute('data-list-id');
|
||||||
|
const listContainer = document.getElementById(listId);
|
||||||
|
|
||||||
|
// Get saved view preference from localStorage
|
||||||
|
const savedView = localStorage.getItem(`list_view_${listId}`) || 'table';
|
||||||
|
|
||||||
|
// Set initial view
|
||||||
|
setView(savedView);
|
||||||
|
|
||||||
|
// Add click event listeners to view toggle buttons
|
||||||
|
document.querySelectorAll('.view-toggle').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const view = this.getAttribute('data-view');
|
||||||
|
setView(view);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setView(view) {
|
||||||
|
// Update button states
|
||||||
|
document.querySelectorAll('.view-toggle').forEach(button => {
|
||||||
|
if (button.getAttribute('data-view') === view) {
|
||||||
|
button.classList.add('active');
|
||||||
|
} else {
|
||||||
|
button.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update view visibility
|
||||||
|
const tableView = listContainer.querySelector('.table-view');
|
||||||
|
const cardView = listContainer.querySelector('.card-view');
|
||||||
|
|
||||||
|
if (view === 'table') {
|
||||||
|
tableView.classList.add('active');
|
||||||
|
cardView.classList.remove('active');
|
||||||
|
} else {
|
||||||
|
tableView.classList.remove('active');
|
||||||
|
cardView.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save preference to localStorage
|
||||||
|
localStorage.setItem(`list_view_${listId}`, view);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -97,8 +97,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Candidates Table -->
|
<!-- Candidates -->
|
||||||
{% if candidates %}
|
{% if candidates %}
|
||||||
|
<div id="job-candidates-list">
|
||||||
|
{# View Switcher #}
|
||||||
|
{% include "includes/_list_view_switcher.html" with list_id="job-candidates-list" %}
|
||||||
|
|
||||||
|
{# Table View (Default) #}
|
||||||
|
<div class="table-view active">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
@ -219,6 +225,51 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Card View #}
|
||||||
|
<div class="card-view">
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for candidate in candidates %}
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="h5 mb-1">{{ candidate.first_name }} {{ candidate.last_name }}</h5>
|
||||||
|
<small class="text-white-50">{{ candidate.email }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="card-text">
|
||||||
|
<strong>{% trans "Phone" %}:</strong> {{ candidate.phone|default:"N/A" }}<br>
|
||||||
|
<strong>{% trans "Stage" %}:</strong> <span class="badge bg-{% if candidate.stage == 'Applied' %}primary{% elif candidate.stage == 'Interview' %}info{% elif candidate.stage == 'Offer' %}success{% else %}secondary{% endif %}">{{ candidate.stage }}</span><br>
|
||||||
|
<strong>{% trans "Applied Date" %}:</strong> {{ candidate.created_at|date:"M d, Y" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-sm btn-outline-primary w-100">
|
||||||
|
<i class="fas fa-eye"></i> {% trans "View" %}
|
||||||
|
</a>
|
||||||
|
{% if user.is_staff %}
|
||||||
|
<div class="btn-group w-100" role="group">
|
||||||
|
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-sm btn-outline-secondary" title="Edit">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-sm" title="Delete"
|
||||||
|
data-bs-toggle="deleteModal"
|
||||||
|
data-delete-url="{% url 'candidate_delete' candidate.slug %}"
|
||||||
|
data-item-name="{{ candidate.first_name }} {{ candidate.last_name }}">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center py-5">
|
<div class="text-center py-5">
|
||||||
<i class="fas fa-user-slash fa-3x text-muted mb-3"></i>
|
<i class="fas fa-user-slash fa-3x text-muted mb-3"></i>
|
||||||
|
|||||||
@ -250,9 +250,10 @@
|
|||||||
{# HEADER SECTION #}
|
{# HEADER SECTION #}
|
||||||
<div class="job-header-card d-flex justify-content-between align-items-center flex-wrap">
|
<div class="job-header-card d-flex justify-content-between align-items-center flex-wrap">
|
||||||
<h2>{{job}}</h2>
|
<h2>{{job}}</h2>
|
||||||
<span class="badge bg-{{ job.status|lower|striptags|yesno:'success,warning,secondary,danger' }} status-badge">
|
<button class="badge bg-success status-badge">
|
||||||
|
{% include "icons/edit.html" %}
|
||||||
{{ job.get_status_display }}
|
{{ job.get_status_display }}
|
||||||
</span>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# LEFT TABS NAVIGATION #}
|
{# LEFT TABS NAVIGATION #}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static i18n %}
|
||||||
|
|
||||||
{% block title %}Job Postings - University ATS{% endblock %}
|
{% block title %}Job Postings - University ATS{% endblock %}
|
||||||
|
|
||||||
@ -164,7 +164,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if page_obj %}
|
{% if page_obj %}
|
||||||
<div class="row">
|
<div id="job-list">
|
||||||
|
{# View Switcher #}
|
||||||
|
{% include "includes/_list_view_switcher.html" with list_id="job-list" %}
|
||||||
|
|
||||||
|
{# Card View (Default) #}
|
||||||
|
<div class="card-view active row">
|
||||||
{% for job in page_obj %}
|
{% for job in page_obj %}
|
||||||
<div class="col-md-6 col-lg-4 mb-4">
|
<div class="col-md-6 col-lg-4 mb-4">
|
||||||
<div class="card job-card h-100">
|
<div class="card job-card h-100">
|
||||||
@ -205,6 +210,48 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# Table View #}
|
||||||
|
<div class="table-view">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{% trans "Job Title" %}</th>
|
||||||
|
<th scope="col">{% trans "Department" %}</th>
|
||||||
|
<th scope="col">{% trans "Location" %}</th>
|
||||||
|
<th scope="col">{% trans "Job Type" %}</th>
|
||||||
|
<th scope="col">{% trans "Status" %}</th>
|
||||||
|
<th scope="col">{% trans "Source" %}</th>
|
||||||
|
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for job in page_obj %}
|
||||||
|
<tr>
|
||||||
|
<td class="fw-medium">{{ job.title }}</td>
|
||||||
|
<td>{{ job.department|default:"N/A" }}</td>
|
||||||
|
<td>{{ job.get_location_display }}</td>
|
||||||
|
<td>{{ job.get_job_type_display }}</td>
|
||||||
|
<td><span class="badge bg-{{ job.status|lower|striptags|yesno:'active,draft,closed,archived' }} status-badge">{{ job.get_status_display }}</span></td>
|
||||||
|
<td>{{ job.get_source }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-primary" title="View">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'job_update' job.slug %}" class="btn btn-outline-secondary" title="Edit">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if page_obj.has_other_pages %}
|
{% if page_obj.has_other_pages %}
|
||||||
<nav aria-label="Job pagination" class="mt-4">
|
<nav aria-label="Job pagination" class="mt-4">
|
||||||
<ul class="pagination justify-content-center">
|
<ul class="pagination justify-content-center">
|
||||||
|
|||||||
@ -246,6 +246,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if meetings %}
|
{% if meetings %}
|
||||||
|
<div id="meetings-list">
|
||||||
|
{# View Switcher #}
|
||||||
|
{% include "includes/_list_view_switcher.html" with list_id="meetings-list" %}
|
||||||
|
|
||||||
|
{# Card View (Default) #}
|
||||||
|
<div class="card-view active">
|
||||||
<div class="meetings-grid">
|
<div class="meetings-grid">
|
||||||
{% for meeting in meetings %}
|
{% for meeting in meetings %}
|
||||||
<div class="meeting-card">
|
<div class="meeting-card">
|
||||||
@ -316,6 +322,63 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Table View #}
|
||||||
|
<div class="table-view">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{% trans "Topic" %}</th>
|
||||||
|
<th scope="col">{% trans "ID" %}</th>
|
||||||
|
<th scope="col">{% trans "Start Time" %}</th>
|
||||||
|
<th scope="col">{% trans "Duration" %}</th>
|
||||||
|
<th scope="col">{% trans "Status" %}</th>
|
||||||
|
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for meeting in meetings %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ meeting.topic }}</strong></td>
|
||||||
|
<td>{{ meeting.meeting_id|default:meeting.id }}</td>
|
||||||
|
<td>{{ meeting.start_time|date:"M d, Y H:i" }}</td>
|
||||||
|
<td>{{ meeting.duration }} minutes</td>
|
||||||
|
<td>
|
||||||
|
<span class="status-badge {% if meeting.status == 'waiting' %}bg-warning{% elif meeting.status == 'started' %}bg-success{% elif meeting.status == 'ended' %}bg-danger{% endif %}">
|
||||||
|
{% if meeting.status == 'waiting' %}
|
||||||
|
{% trans "Waiting" %}
|
||||||
|
{% elif meeting.status == 'started' %}
|
||||||
|
{% trans "Started" %}
|
||||||
|
{% elif meeting.status == 'ended' %}
|
||||||
|
{% trans "Ended" %}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<a href="{% url 'meeting_details' meeting.pk %}" class="btn btn-outline-primary" title="{% trans 'View' %}">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'update_meeting' meeting.pk %}" class="btn btn-outline-secondary" title="{% trans 'Update' %}">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}"
|
||||||
|
data-bs-toggle="deleteModal"
|
||||||
|
data-delete-url="{% url 'delete_meeting' meeting.pk %}"
|
||||||
|
data-item-name="{{ meeting.topic }}">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if is_paginated %}
|
{% if is_paginated %}
|
||||||
<nav aria-label="Page navigation" class="mt-4">
|
<nav aria-label="Page navigation" class="mt-4">
|
||||||
|
|||||||
@ -143,6 +143,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if candidates %}
|
{% if candidates %}
|
||||||
|
<div id="candidate-list">
|
||||||
|
{# View Switcher #}
|
||||||
|
{% include "includes/_list_view_switcher.html" with list_id="candidate-list" %}
|
||||||
|
|
||||||
|
{# Table View (Default) #}
|
||||||
|
<div class="table-view active">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
@ -192,6 +198,52 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Card View #}
|
||||||
|
<div class="card-view">
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for candidate in candidates %}
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="h5 mb-1">{{ candidate.name }}</h5>
|
||||||
|
<small class="text-white-50">{{ candidate.email }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="card-text">
|
||||||
|
<strong>{% trans "Phone" %}:</strong> {{ candidate.phone|default:"N/A" }}<br>
|
||||||
|
<strong>{% trans "Job" %}:</strong> <span class="badge bg-primary">{{ candidate.job.title }}</span><br>
|
||||||
|
<strong>{% trans "Stage" %}:</strong> <span class="badge bg-primary">{{ candidate.stage }}</span><br>
|
||||||
|
<strong>{% trans "Created" %}:</strong> {{ candidate.created_at|date:"M d, Y" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-sm btn-outline-primary w-100">
|
||||||
|
<i class="fas fa-eye"></i> {% trans "View" %}
|
||||||
|
</a>
|
||||||
|
{% if user.is_staff %}
|
||||||
|
<div class="btn-group w-100" role="group">
|
||||||
|
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-sm btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}"
|
||||||
|
data-bs-toggle="deleteModal"
|
||||||
|
data-delete-url="{% url 'candidate_delete' candidate.slug %}"
|
||||||
|
data-item-name="{{ candidate.name }}">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if is_paginated %}
|
{% if is_paginated %}
|
||||||
<div class="card-footer bg-white border-top">
|
<div class="card-footer bg-white border-top">
|
||||||
|
|||||||
@ -143,6 +143,12 @@
|
|||||||
|
|
||||||
|
|
||||||
{% if materials %}
|
{% if materials %}
|
||||||
|
<div id="training-materials-list">
|
||||||
|
{# View Switcher #}
|
||||||
|
{% include "includes/_list_view_switcher.html" with list_id="training-materials-list" %}
|
||||||
|
|
||||||
|
{# Table View (Default) #}
|
||||||
|
<div class="table-view active">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
@ -182,6 +188,49 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Card View #}
|
||||||
|
<div class="card-view">
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for material in materials %}
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="h5 mb-1">{{ material.title }}</h5>
|
||||||
|
<small class="text-white-50">{{ material.created_by.username|default:"Anonymous" }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="card-text">
|
||||||
|
<strong>{% trans "Created" %}:</strong> {{ material.created_at|date:"M d, Y" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{% url 'training_detail' material.pk %}" class="btn btn-sm btn-outline-primary w-100">
|
||||||
|
<i class="fas fa-eye"></i> {% trans "View" %}
|
||||||
|
</a>
|
||||||
|
{% if user.is_authenticated and material.created_by == user %}
|
||||||
|
<div class="btn-group w-100" role="group">
|
||||||
|
<a href="{% url 'training_update' material.pk %}" class="btn btn-sm btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}"
|
||||||
|
data-bs-toggle="deleteModal"
|
||||||
|
data-delete-url="{% url 'training_delete' material.pk %}"
|
||||||
|
data-item-name="{{ material.title }}">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if is_paginated %}
|
{% if is_paginated %}
|
||||||
<div class="card-footer bg-light border-top">
|
<div class="card-footer bg-light border-top">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user