dashboard #19

Closed
Faheed wants to merge 7 commits from frontend into main
43 changed files with 1553 additions and 2652 deletions

View File

@ -135,9 +135,9 @@ 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',
}

View File

@ -22,8 +22,8 @@ urlpatterns = [
# path('', include('recruitment.urls')),
path("ckeditor5/", include('django_ckeditor_5.urls')),
path('<slug:template_slug>/', views.form_wizard_view, name='form_wizard'),
path('<slug:template_slug>/submit/', views.submit_form, name='submit_form'),
path('form_wizard/<slug:template_slug>/', views.form_wizard_view, name='form_wizard'),
path('form/<slug:template_slug>/submit/', views.submit_form, name='submit_form'),
path('api/templates/', views.list_form_templates, name='list_form_templates'),
path('api/templates/save/', views.save_form_template, name='save_form_template'),

View File

@ -1426,178 +1426,3 @@ msgstr ""
#: templates/unfold/components/table.html:43
msgid "No data"
msgstr ""
# Source Management
msgid "Data Sources"
msgstr "مصادر البيانات"
msgid "Create New Source"
msgstr "إنشاء مصدر جديد"
msgid "Search by name, type, or description..."
msgstr "البحث بالاسم، النوع، أو الوصف..."
msgid "Filter"
msgstr "تصفية"
msgid "Available Sources"
msgstr "المصادر المتاحة"
msgid "sources"
msgstr "مصادر"
msgid "View Details"
msgstr "عرض التفاصيل"
msgid "Edit"
msgstr "تعديل"
msgid "Delete"
msgstr "حذف"
msgid "First"
msgstr "الأول"
msgid "Previous"
msgstr "السابق"
msgid "Next"
msgstr "التالي"
msgid "Last"
msgstr "الأخير"
msgid "No sources found"
msgstr "لم يتم العثور على مصادر"
msgid "Get started by creating your first data source."
msgstr "ابدأ بإنشاء أول مصدر بيانات لك."
msgid "Network Configuration"
msgstr "تكوين الشبكة"
msgid "Settings"
msgstr "الإعدادات"
msgid "API Configuration"
msgstr "تكوين واجهة برمجة التطبيقات"
msgid "API Keys"
msgstr "مفاتيح واجهة برمجة التطبيقات"
msgid "Generate secure API keys for external integrations"
msgstr "إنشاء مفاتيح واجهة برمجة التطبيقات الآمنة للتكاملات الخارجية"
msgid "Active"
msgstr "نشط"
msgid "Inactive"
msgstr "غير نشط"
msgid "Not generated"
msgstr "لم يتم إنشاؤه"
msgid "Created By"
msgstr "أنشأ بواسطة"
msgid "Created At"
msgstr "أنشأ في"
msgid "Updated At"
msgstr "حدث في"
msgid "IP Address"
msgstr "عنوان IP"
msgid "Trusted IPs"
msgstr "عناوين IP الموثوقة"
msgid "Integration Version"
msgstr "إصدار التكامل"
msgid "API Key"
msgstr "مفتاح واجهة برمجة التطبيقات"
msgid "API Secret"
msgstr "سر واجهة برمجة التطبيقات"
msgid "Recent Integration Logs"
msgstr "سجلات التكامل الأخيرة"
msgid "Time"
msgstr "الوقت"
msgid "Action"
msgstr "الإجراء"
msgid "Endpoint"
msgstr "نقطة النهاية"
msgid "Method"
msgstr "الطريقة"
msgid "Status"
msgstr "الحالة"
msgid "Success"
msgstr "نجح"
msgid "Failed"
msgstr "فشل"
msgid "No integration logs found"
msgstr "لم يتم العثور على سجلات تكامل"
msgid "Integration logs will appear here when this source is used for external integrations."
msgstr "ستظهر سجلات التكامل هنا عند استخدام هذا المصدر للتكاملات الخارجية."
msgid "Delete Source"
msgstr "حذف المصدر"
msgid "Confirm Deletion"
msgstr "تأكيد الحذف"
msgid "Are you sure you want to delete the following source? This action cannot be undone."
msgstr "هل أنت متأكد من رغبتك في حذف المصدر التالي؟ لا يمكن التراجع عن هذا الإجراء."
msgid "Important Note"
msgstr "ملاحظة هامة"
msgid "All associated API keys will be permanently deleted."
msgstr "ستتم حذف جميع مفاتيح واجهة برمجة التطبيقات المرتبطة بشكل دائم."
msgid "Integration logs related to this source will remain but will show 'Source deleted'."
msgstr "ستبقى سجلات التكامل المتعلقة بهذا المصدر ولكنها ستظهر 'تم حذف المصدر'."
msgid "Any active integrations using this source will be disconnected."
msgstr "سيتم فصل أي تكاملات نشطة تستخدم هذا المصدر."
msgid "Back to List"
msgstr "العودة إلى القائمة"
msgid "Delete Source"
msgstr "حذف المصدر"
msgid "Generate API Keys"
msgstr "إنشاء مفاتيح واجهة برمجة التطبيقات"
msgid "Copy to Clipboard"
msgstr "نسخ إلى الحافظة"
msgid "Failed to copy to clipboard"
msgstr "فشل نسخ إلى الحافظة"
msgid "Generate random API key"
msgstr "إنشاء مفتاح واجهة برمجة تطبيقات عشوائي"
msgid "Generate random API secret"
msgstr "إنشاء سر واجهة برمجة تطبيقات عشوائي"
msgid "Source updated successfully."
msgstr "تم تحديث المصدر بنجاح."
msgid "Source created successfully."
msgstr "تم إنشاء المصدر بنجاح."
msgid "Source deleted successfully."
msgstr "تم حذف المصدر بنجاح."

View File

@ -1425,178 +1425,3 @@ msgstr ""
#: templates/unfold/components/table.html:43
msgid "No data"
msgstr ""
# Source Management
msgid "Data Sources"
msgstr ""
msgid "Create New Source"
msgstr ""
msgid "Search by name, type, or description..."
msgstr ""
msgid "Filter"
msgstr ""
msgid "Available Sources"
msgstr ""
msgid "sources"
msgstr ""
msgid "View Details"
msgstr ""
msgid "Edit"
msgstr ""
msgid "Delete"
msgstr ""
msgid "First"
msgstr ""
msgid "Previous"
msgstr ""
msgid "Next"
msgstr ""
msgid "Last"
msgstr ""
msgid "No sources found"
msgstr ""
msgid "Get started by creating your first data source."
msgstr ""
msgid "Network Configuration"
msgstr ""
msgid "Settings"
msgstr ""
msgid "API Configuration"
msgstr ""
msgid "API Keys"
msgstr ""
msgid "Generate secure API keys for external integrations"
msgstr ""
msgid "Active"
msgstr ""
msgid "Inactive"
msgstr ""
msgid "Not generated"
msgstr ""
msgid "Created By"
msgstr ""
msgid "Created At"
msgstr ""
msgid "Updated At"
msgstr ""
msgid "IP Address"
msgstr ""
msgid "Trusted IPs"
msgstr ""
msgid "Integration Version"
msgstr ""
msgid "API Key"
msgstr ""
msgid "API Secret"
msgstr ""
msgid "Recent Integration Logs"
msgstr ""
msgid "Time"
msgstr ""
msgid "Action"
msgstr ""
msgid "Endpoint"
msgstr ""
msgid "Method"
msgstr ""
msgid "Status"
msgstr ""
msgid "Success"
msgstr ""
msgid "Failed"
msgstr ""
msgid "No integration logs found"
msgstr ""
msgid "Integration logs will appear here when this source is used for external integrations."
msgstr ""
msgid "Delete Source"
msgstr ""
msgid "Confirm Deletion"
msgstr ""
msgid "Are you sure you want to delete the following source? This action cannot be undone."
msgstr ""
msgid "Important Note"
msgstr ""
msgid "All associated API keys will be permanently deleted."
msgstr ""
msgid "Integration logs related to this source will remain but will show 'Source deleted'."
msgstr ""
msgid "Any active integrations using this source will be disconnected."
msgstr ""
msgid "Back to List"
msgstr ""
msgid "Delete Source"
msgstr ""
msgid "Generate API Keys"
msgstr ""
msgid "Copy to Clipboard"
msgstr ""
msgid "Failed to copy to clipboard"
msgstr ""
msgid "Generate random API key"
msgstr ""
msgid "Generate random API secret"
msgstr ""
msgid "Source updated successfully."
msgstr ""
msgid "Source created successfully."
msgstr ""
msgid "Source deleted successfully."
msgstr ""

View File

@ -1,171 +1,20 @@
from django import forms
from .validators import validate_hash_tags
from django.core.validators import URLValidator
from django.forms.formsets import formset_factory
from django.utils.translation import gettext_lazy as _
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit, Row, Column, Field, Div
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm
import re
from .models import (
ZoomMeeting, Candidate,TrainingMaterial,JobPosting,
FormTemplate,InterviewSchedule,BreakTime,JobPostingImage,
Profile,MeetingComment,ScheduledInterview,Source
Profile,MeetingComment,ScheduledInterview
)
# from django_summernote.widgets import SummernoteWidget
from django_ckeditor_5.widgets import CKEditor5Widget
import secrets
import string
from django.core.exceptions import ValidationError
def generate_api_key(length=32):
"""Generate a secure API key"""
alphabet = string.ascii_letters + string.digits
return ''.join(secrets.choice(alphabet) for _ in range(length))
def generate_api_secret(length=64):
"""Generate a secure API secret"""
alphabet = string.ascii_letters + string.digits + '-._~'
return ''.join(secrets.choice(alphabet) for _ in range(length))
class SourceForm(forms.ModelForm):
"""Form for creating and editing sources with API key generation"""
# Hidden field to trigger API key generation
generate_keys = forms.CharField(
widget=forms.HiddenInput(),
required=False,
help_text="Set to 'true' to generate new API keys"
)
# Display fields for generated keys (read-only)
api_key_generated = forms.CharField(
label="Generated API Key",
required=False,
widget=forms.TextInput(attrs={'readonly': True, 'class': 'form-control'})
)
api_secret_generated = forms.CharField(
label="Generated API Secret",
required=False,
widget=forms.TextInput(attrs={'readonly': True, 'class': 'form-control'})
)
class Meta:
model = Source
fields = [
'name', 'source_type', 'description', 'ip_address',
'trusted_ips', 'is_active', 'integration_version'
]
widgets = {
'name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., ATS System, ERP Integration',
'required': True
}),
'source_type': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., ATS, ERP, API',
'required': True
}),
'description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Brief description of the source system'
}),
'ip_address': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '192.168.1.100'
}),
'trusted_ips': forms.Textarea(attrs={
'class': 'form-control',
'rows': 2,
'placeholder': 'Comma-separated IP addresses (e.g., 192.168.1.100, 10.0.0.1)'
}),
'integration_version': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'v1.0, v2.1'
}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.form_class = 'form-horizontal'
self.helper.label_class = 'col-md-3'
self.helper.field_class = 'col-md-9'
# Add generate keys button
self.helper.layout = Layout(
Field('name', css_class='form-control'),
Field('source_type', css_class='form-control'),
Field('description', css_class='form-control'),
Field('ip_address', css_class='form-control'),
Field('trusted_ips', css_class='form-control'),
Field('integration_version', css_class='form-control'),
Field('is_active', css_class='form-check-input'),
# Hidden field for key generation trigger
Field('generate_keys', type='hidden'),
# Display fields for generated keys
Field('api_key_generated', css_class='form-control'),
Field('api_secret_generated', css_class='form-control'),
Submit('submit', 'Save Source', css_class='btn btn-primary mt-3')
)
def clean_name(self):
"""Ensure source name is unique"""
name = self.cleaned_data.get('name')
if name:
# Check for duplicates excluding current instance if editing
instance = self.instance
if not instance.pk: # Creating new instance
if Source.objects.filter(name=name).exists():
raise ValidationError('A source with this name already exists.')
else: # Editing existing instance
if Source.objects.filter(name=name).exclude(pk=instance.pk).exists():
raise ValidationError('A source with this name already exists.')
return name
def clean_trusted_ips(self):
"""Validate and format trusted IP addresses"""
trusted_ips = self.cleaned_data.get('trusted_ips')
if trusted_ips:
# Split by comma and strip whitespace
ips = [ip.strip() for ip in trusted_ips.split(',') if ip.strip()]
# Validate each IP address
for ip in ips:
try:
# Basic IP validation (can be enhanced)
if not (ip.replace('.', '').isdigit() and len(ip.split('.')) == 4):
raise ValidationError(f'Invalid IP address: {ip}')
except Exception:
raise ValidationError(f'Invalid IP address: {ip}')
return ', '.join(ips)
return trusted_ips
def clean(self):
"""Custom validation for the form"""
cleaned_data = super().clean()
# Check if we need to generate API keys
generate_keys = cleaned_data.get('generate_keys')
if generate_keys == 'true':
# Generate new API key and secret
cleaned_data['api_key'] = generate_api_key()
cleaned_data['api_secret'] = generate_api_secret()
# Set display fields for the frontend
cleaned_data['api_key_generated'] = cleaned_data['api_key']
cleaned_data['api_secret_generated'] = cleaned_data['api_secret']
return cleaned_data
class CandidateForm(forms.ModelForm):
class Meta:
@ -223,6 +72,51 @@ class CandidateStageForm(forms.ModelForm):
'stage': forms.Select(attrs={'class': 'form-select'}),
}
# def __init__(self, *args, **kwargs):
# # Get the current candidate instance for validation
# self.candidate = kwargs.pop('candidate', None)
# super().__init__(*args, **kwargs)
# # Dynamically filter stage choices based on current stage
# if self.candidate and self.candidate.pk:
# current_stage = self.candidate.stage
# available_stages = self.candidate.get_available_stages()
# # Filter choices to only include available stages
# choices = [(stage, self.candidate.Stage(stage).label)
# for stage in available_stages]
# self.fields['stage'].choices = choices
# # Set initial value to current stage
# self.fields['stage'].initial = current_stage
# else:
# # For new candidates, only show 'Applied' stage
# self.fields['stage'].choices = [('Applied', _('Applied'))]
# self.fields['stage'].initial = 'Applied'
# def clean_stage(self):
# """Validate stage transition"""
# new_stage = self.cleaned_data.get('stage')
# if not new_stage:
# raise forms.ValidationError(_('Please select a stage.'))
# # Use model validation for stage transitions
# if self.candidate and self.candidate.pk:
# current_stage = self.candidate.stage
# if new_stage != current_stage:
# if not self.candidate.can_transition_to(new_stage):
# allowed_stages = self.candidate.get_available_stages()
# raise forms.ValidationError(
# _('Cannot transition from "%(current)s" to "%(new)s". '
# 'Allowed transitions: %(allowed)s') % {
# 'current': current_stage,
# 'new': new_stage,
# 'allowed': ', '.join(allowed_stages) or 'None (final stage)'
# }
# )
# return new_stage
class ZoomMeetingForm(forms.ModelForm):
class Meta:
model = ZoomMeeting
@ -252,6 +146,8 @@ class ZoomMeetingForm(forms.ModelForm):
Submit('submit', _('Create Meeting'), css_class='btn btn-primary')
)
# Old JobForm removed - replaced by JobPostingForm
class TrainingMaterialForm(forms.ModelForm):
class Meta:
model = TrainingMaterial
@ -264,11 +160,13 @@ class TrainingMaterialForm(forms.ModelForm):
}
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Enter material title')}),
'content': CKEditor5Widget(attrs={'placeholder': _('Enter material content')}),
# 💡 Use SummernoteWidget here
# 'content': SummernoteWidget(attrs={'placeholder': _('Enter material content')}),
'video_link': forms.URLInput(attrs={'class': 'form-control', 'placeholder': _('https://www.youtube.com/watch?v=...')}),
'file': forms.FileInput(attrs={'class': 'form-control'}),
}
# The __init__ and FormHelper layout remains the same
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
@ -277,7 +175,7 @@ class TrainingMaterialForm(forms.ModelForm):
self.helper.layout = Layout(
'title',
'content',
'content', # Summernote is applied via the widgets dictionary
Row(
Column('video_link', css_class='col-md-6'),
Column('file', css_class='col-md-6'),
@ -290,6 +188,7 @@ class TrainingMaterialForm(forms.ModelForm):
)
)
class JobPostingForm(forms.ModelForm):
"""Form for creating and editing job postings"""
@ -298,12 +197,13 @@ class JobPostingForm(forms.ModelForm):
fields = [
'title', 'department', 'job_type', 'workplace_type',
'location_city', 'location_state', 'location_country',
'description', 'qualifications', 'salary_range', 'benefits','application_start_date'
,'application_deadline', 'application_instructions',
'position_number', 'reporting_to', 'joining_date',
'created_by','open_positions','hash_tags','max_applications'
'description', 'qualifications', 'salary_range', 'benefits',
'application_deadline', 'application_instructions',
'position_number', 'reporting_to',
'open_positions','hash_tags','max_applications'
]
widgets = {
# Basic Information
'title': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Assistant Professor of Computer Science',
@ -321,6 +221,8 @@ class JobPostingForm(forms.ModelForm):
'class': 'form-select',
'required': True
}),
# Location
'location_city': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Boston'
@ -333,18 +235,39 @@ class JobPostingForm(forms.ModelForm):
'class': 'form-control',
'value': 'United States'
}),
'salary_range': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '$60,000 - $80,000'
}),
# Application Information
# 'application_url': forms.URLInput(attrs={
# 'class': 'form-control',
# 'placeholder': 'https://university.edu/careers/job123',
# 'required': True
# }),
# Application Information
# 'application_url': forms.URLInput(attrs={
# 'class': 'form-control',
# 'placeholder': 'https://university.edu/careers/job123',
# 'required': True
# }),
'application_start_date': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
}),
'application_deadline': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
'type': 'date',
'required':True
}),
'open_positions': forms.NumberInput(attrs={
'class': 'form-control',
'min': 1,
@ -353,7 +276,10 @@ class JobPostingForm(forms.ModelForm):
'hash_tags': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '#hiring,#jobopening',
# 'validators':validate_hash_tags, # Assuming this is available
}),
# Internal Information
'position_number': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'UNIV-2025-001'
@ -362,14 +288,8 @@ class JobPostingForm(forms.ModelForm):
'class': 'form-control',
'placeholder': 'Department Chair, Director, etc.'
}),
'joining_date': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
}),
'created_by': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'University Administrator'
}),
'max_applications': forms.NumberInput(attrs={
'class': 'form-control',
'min': 1,
@ -378,15 +298,16 @@ class JobPostingForm(forms.ModelForm):
}
def __init__(self,*args,**kwargs):
self.is_anonymous_user = kwargs.pop('is_anonymous_user', False)
# Now call the parent __init__ with remaining args
super().__init__(*args, **kwargs)
if not self.instance.pk:
if not self.is_anonymous_user:
self.fields['created_by'].initial = 'University Administrator'
if not self.instance.pk:# Creating new job posting
# self.fields['status'].initial = 'Draft'
self.fields['location_city'].initial='Riyadh'
self.fields['location_state'].initial='Riyadh Province'
self.fields['location_country'].initial='Saudi Arabia'
def clean_hash_tags(self):
hash_tags=self.cleaned_data.get('hash_tags')
@ -396,7 +317,7 @@ class JobPostingForm(forms.ModelForm):
if not tag.startswith('#'):
raise forms.ValidationError("Each hashtag must start with '#' symbol and must be comma(,) sepearted.")
return ','.join(tags)
return hash_tags
return hash_tags # Allow blank
def clean_title(self):
title=self.cleaned_data.get('title')
@ -410,7 +331,43 @@ class JobPostingForm(forms.ModelForm):
description=self.cleaned_data.get('description')
if not description or len(description.strip())<20:
raise forms.ValidationError("Job description must be at least 20 characters long.")
return description.strip()
return description.strip() # to remove leading/trailing whitespace
def clean_application_url(self):
url=self.cleaned_data.get('application_url')
if url:
validator=URLValidator()
try:
validator(url)
except forms.ValidationError:
raise forms.ValidationError('Please enter a valid URL (e.g., https://example.com)')
return url
# def clean(self):
# """Cross-field validation"""
# cleaned_data = super().clean()
# # Validate dates
# start_date = cleaned_data.get('start_date')
# application_deadline = cleaned_data.get('application_deadline')
# # Perform cross-field validation only if both fields have values
# if start_date and application_deadline:
# if application_deadline > start_date:
# self.add_error('application_deadline',
# 'The application deadline must be set BEFORE the job start date.')
# # # Validate that if status is ACTIVE, we have required fields
# # status = cleaned_data.get('status')
# # if status == 'ACTIVE':
# # if not cleaned_data.get('application_url'):
# # self.add_error('application_url',
# # 'Application URL is required for active jobs.')
# # if not cleaned_data.get('description'):
# # self.add_error('description',
# # 'Job description is required for active jobs.')
# return cleaned_data
class JobPostingImageForm(forms.ModelForm):
class Meta:
@ -458,6 +415,90 @@ class FormTemplateForm(forms.ModelForm):
Field('is_active', css_class='form-check-input'),
Submit('submit', _('Create Template'), css_class='btn btn-primary mt-3')
)
# class BreakTimeForm(forms.ModelForm):
# class Meta:
# model = BreakTime
# fields = ['start_time', 'end_time']
# widgets = {
# 'start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
# 'end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
# }
# BreakTimeFormSet = formset_factory(BreakTimeForm, extra=1, can_delete=True)
# class InterviewScheduleForm(forms.ModelForm):
# candidates = forms.ModelMultipleChoiceField(
# queryset=Candidate.objects.none(),
# widget=forms.CheckboxSelectMultiple,
# required=True
# )
# working_days = forms.MultipleChoiceField(
# choices=[
# (0, 'Monday'),
# (1, 'Tuesday'),
# (2, 'Wednesday'),
# (3, 'Thursday'),
# (4, 'Friday'),
# (5, 'Saturday'),
# (6, 'Sunday'),
# ],
# widget=forms.CheckboxSelectMultiple,
# required=True
# )
# class Meta:
# model = InterviewSchedule
# fields = [
# 'candidates', 'start_date', 'end_date', 'working_days',
# 'start_time', 'end_time', 'interview_duration', 'buffer_time'
# ]
# widgets = {
# 'start_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
# 'end_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
# 'start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
# 'end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
# 'interview_duration': forms.NumberInput(attrs={'class': 'form-control'}),
# 'buffer_time': forms.NumberInput(attrs={'class': 'form-control'}),
# }
# def __init__(self, slug, *args, **kwargs):
# super().__init__(*args, **kwargs)
# # Filter candidates based on the selected job
# self.fields['candidates'].queryset = Candidate.objects.filter(
# job__slug=slug,
# stage='Interview'
# )
# def clean_working_days(self):
# working_days = self.cleaned_data.get('working_days')
# # Convert string values to integers
# return [int(day) for day in working_days]
class JobPostingCancelReasonForm(forms.ModelForm):
class Meta:
model = JobPosting
fields = ['cancel_reason']
class JobPostingStatusForm(forms.ModelForm):
class Meta:
model = JobPosting
fields = ['status']
widgets = {
'status': forms.Select(attrs={'class': 'form-select'}),
}
class FormTemplateIsActiveForm(forms.ModelForm):
class Meta:
model = FormTemplate
fields = ['is_active']
class CandidateExamDateForm(forms.ModelForm):
class Meta:
model = Candidate
fields = ['exam_date']
widgets = {
'exam_date': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}),
}
class BreakTimeForm(forms.Form):
"""
@ -474,8 +515,10 @@ class BreakTimeForm(forms.Form):
label="End Time"
)
# Use the non-model form for the formset factory
BreakTimeFormSet = formset_factory(BreakTimeForm, extra=1, can_delete=True)
# --- InterviewScheduleForm remains unchanged ---
class InterviewScheduleForm(forms.ModelForm):
candidates = forms.ModelMultipleChoiceField(
queryset=Candidate.objects.none(),
@ -511,6 +554,7 @@ class InterviewScheduleForm(forms.ModelForm):
def __init__(self, slug, *args, **kwargs):
super().__init__(*args, **kwargs)
# Filter candidates based on the selected job
self.fields['candidates'].queryset = Candidate.objects.filter(
job__slug=slug,
stage='Interview'
@ -518,6 +562,7 @@ class InterviewScheduleForm(forms.ModelForm):
def clean_working_days(self):
working_days = self.cleaned_data.get('working_days')
# Convert string values to integers
return [int(day) for day in working_days]
class MeetingCommentForm(forms.ModelForm):
@ -547,7 +592,26 @@ class MeetingCommentForm(forms.ModelForm):
Submit('submit', _('Add Comment'), css_class='btn btn-primary mt-3')
)
# --- ScheduleInterviewForCandiateForm remains unchanged ---
class ScheduleInterviewForCandiateForm(forms.ModelForm):
class Meta:
model = InterviewSchedule
fields = ['start_date', 'end_date', 'start_time', 'end_time', 'interview_duration', 'buffer_time']
widgets = {
'start_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'end_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
'end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
'interview_duration': forms.NumberInput(attrs={'class': 'form-control'}),
'buffer_time': forms.NumberInput(attrs={'class': 'form-control'}),
'break_start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
'break_end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
}
class InterviewForm(forms.ModelForm):
class Meta:
model = ScheduledInterview
fields = ['job','candidate']
@ -557,6 +621,36 @@ class ProfileImageUploadForm(forms.ModelForm):
model=Profile
fields=['profile_image']
# class UserEditForms(forms.ModelForm):
# class Meta:
# model = User
# fields = ['first_name', 'last_name']
from django.contrib.auth.forms import UserCreationForm
# class StaffUserCreationForm(UserCreationForm):
# email = forms.EmailField(required=True)
# first_name = forms.CharField(max_length=30)
# last_name = forms.CharField(max_length=150)
# class Meta:
# model = User
# fields = ("email", "first_name", "last_name", "password1", "password2")
# def save(self, commit=True):
# user = super().save(commit=False)
# user.email = self.cleaned_data["email"]
# user.first_name = self.cleaned_data["first_name"]
# user.last_name = self.cleaned_data["last_name"]
# user.username = self.cleaned_data["email"] # or generate
# user.is_staff = True
# if commit:
# user.save()
# return user
import re
class StaffUserCreationForm(UserCreationForm):
email = forms.EmailField(required=True)
first_name = forms.CharField(max_length=30, required=True)
@ -590,37 +684,15 @@ class StaffUserCreationForm(UserCreationForm):
user.email = self.cleaned_data["email"]
user.first_name = self.cleaned_data["first_name"]
user.last_name = self.cleaned_data["last_name"]
user.username = self.generate_username(user.email)
user.username = self.generate_username(user.email) # never use raw email if it has dots, etc.
user.is_staff = True
if commit:
user.save()
return user
class ToggleAccountForm(forms.Form):
pass
class JobPostingCancelReasonForm(forms.ModelForm):
class Meta:
model = JobPosting
fields = ['cancel_reason']
class JobPostingStatusForm(forms.ModelForm):
class Meta:
model = JobPosting
fields = ['status']
widgets = {
'status': forms.Select(attrs={'class': 'form-select'}),
}
class FormTemplateIsActiveForm(forms.ModelForm):
class Meta:
model = FormTemplate
fields = ['is_active']
class CandidateExamDateForm(forms.ModelForm):
class Meta:
model = Candidate
fields = ['exam_date']
widgets = {
'exam_date': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}),
}

View File

@ -0,0 +1,477 @@
# Generated by Django 5.2.7 on 2025-10-21 22:26
import django.core.validators
import django.db.models.deletion
import django_ckeditor_5.fields
import django_countries.fields
import django_extensions.db.fields
import recruitment.validators
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='BreakTime',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start_time', models.TimeField(verbose_name='Start Time')),
('end_time', models.TimeField(verbose_name='End Time')),
],
),
migrations.CreateModel(
name='FormStage',
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')),
('name', models.CharField(help_text='Name of the stage', max_length=200)),
('order', models.PositiveIntegerField(default=0, help_text='Order of the stage in the form')),
('is_predefined', models.BooleanField(default=False, help_text='Whether this is a default resume stage')),
],
options={
'verbose_name': 'Form Stage',
'verbose_name_plural': 'Form Stages',
'ordering': ['order'],
},
),
migrations.CreateModel(
name='HiringAgency',
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')),
('name', models.CharField(max_length=200, unique=True, verbose_name='Agency Name')),
('contact_person', models.CharField(blank=True, max_length=150, verbose_name='Contact Person')),
('email', models.EmailField(blank=True, max_length=254)),
('phone', models.CharField(blank=True, max_length=20)),
('website', models.URLField(blank=True)),
('notes', models.TextField(blank=True, help_text='Internal notes about the agency')),
('country', django_countries.fields.CountryField(blank=True, max_length=2, null=True)),
('address', models.TextField(blank=True, null=True)),
],
options={
'verbose_name': 'Hiring Agency',
'verbose_name_plural': 'Hiring Agencies',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Source',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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')),
('name', models.CharField(help_text='e.g., ATS, ERP ', max_length=100, unique=True, verbose_name='Source Name')),
('source_type', models.CharField(help_text='e.g., ATS, ERP ', max_length=100, verbose_name='Source Type')),
('description', models.TextField(blank=True, help_text='A description of the source', verbose_name='Description')),
('ip_address', models.GenericIPAddressField(blank=True, help_text='The IP address of the source', null=True, verbose_name='IP Address')),
('created_at', models.DateTimeField(auto_now_add=True)),
('api_key', models.CharField(blank=True, help_text='API key for authentication (will be encrypted)', max_length=255, null=True, verbose_name='API Key')),
('api_secret', models.CharField(blank=True, help_text='API secret for authentication (will be encrypted)', max_length=255, null=True, verbose_name='API Secret')),
('trusted_ips', models.TextField(blank=True, help_text='Comma-separated list of trusted IP addresses', null=True, verbose_name='Trusted IP Addresses')),
('is_active', models.BooleanField(default=True, help_text='Whether this source is active for integration', verbose_name='Active')),
('integration_version', models.CharField(blank=True, help_text='Version of the integration protocol', max_length=50, verbose_name='Integration Version')),
('last_sync_at', models.DateTimeField(blank=True, help_text='Timestamp of the last successful synchronization', null=True, verbose_name='Last Sync At')),
('sync_status', models.CharField(blank=True, choices=[('IDLE', 'Idle'), ('SYNCING', 'Syncing'), ('ERROR', 'Error'), ('DISABLED', 'Disabled')], default='IDLE', max_length=20, verbose_name='Sync Status')),
],
options={
'verbose_name': 'Source',
'verbose_name_plural': 'Sources',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='ZoomMeeting',
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')),
('topic', models.CharField(max_length=255, verbose_name='Topic')),
('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')),
('participant_video', models.BooleanField(default=True, verbose_name='Participant Video')),
('password', models.CharField(blank=True, max_length=20, null=True, verbose_name='Password')),
('join_before_host', models.BooleanField(default=False, verbose_name='Join Before Host')),
('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, db_index=True, default='waiting', max_length=20, null=True, verbose_name='Status')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='FormField',
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')),
('label', models.CharField(help_text='Label for the field', max_length=200)),
('field_type', models.CharField(choices=[('text', 'Text Input'), ('email', 'Email'), ('phone', 'Phone'), ('textarea', 'Text Area'), ('file', 'File Upload'), ('date', 'Date Picker'), ('select', 'Dropdown'), ('radio', 'Radio Buttons'), ('checkbox', 'Checkboxes')], help_text='Type of the field', max_length=20)),
('placeholder', models.CharField(blank=True, help_text='Placeholder text', max_length=200)),
('required', models.BooleanField(default=False, help_text='Whether the field is required')),
('order', models.PositiveIntegerField(default=0, help_text='Order of the field in the stage')),
('is_predefined', models.BooleanField(default=False, help_text='Whether this is a default field')),
('options', models.JSONField(blank=True, default=list, help_text='Options for selection fields (stored as JSON array)')),
('file_types', models.CharField(blank=True, help_text="Allowed file types (comma-separated, e.g., '.pdf,.doc,.docx')", max_length=200)),
('max_file_size', models.PositiveIntegerField(default=5, help_text='Maximum file size in MB (default: 5MB)')),
('multiple_files', models.BooleanField(default=False, help_text='Allow multiple files to be uploaded')),
('max_files', models.PositiveIntegerField(default=1, help_text='Maximum number of files allowed (when multiple_files is True)')),
('stage', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='recruitment.formstage')),
],
options={
'verbose_name': 'Form Field',
'verbose_name_plural': 'Form Fields',
'ordering': ['order'],
},
),
migrations.CreateModel(
name='FormTemplate',
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')),
('name', models.CharField(help_text='Name of the form template', max_length=200)),
('description', models.TextField(blank=True, help_text='Description of the form template')),
('is_active', models.BooleanField(default=False, help_text='Whether this template is active')),
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='form_templates', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Form Template',
'verbose_name_plural': 'Form Templates',
'ordering': ['-created_at'],
},
),
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',
name='template',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stages', to='recruitment.formtemplate'),
),
migrations.CreateModel(
name='Candidate',
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')),
('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(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')),
('is_resume_parsed', models.BooleanField(default=False, verbose_name='Resume Parsed')),
('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')], 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=[('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')),
('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={
'verbose_name': 'Candidate',
'verbose_name_plural': 'Candidates',
},
),
migrations.CreateModel(
name='JobPosting',
fields=[
('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')),
('title', models.CharField(max_length=200)),
('department', models.CharField(blank=True, max_length=100)),
('job_type', models.CharField(choices=[('FULL_TIME', 'Full-time'), ('PART_TIME', 'Part-time'), ('CONTRACT', 'Contract'), ('INTERNSHIP', 'Internship'), ('FACULTY', 'Faculty'), ('TEMPORARY', 'Temporary')], default='FULL_TIME', max_length=20)),
('workplace_type', models.CharField(choices=[('ON_SITE', 'On-site'), ('REMOTE', 'Remote'), ('HYBRID', 'Hybrid')], default='ON_SITE', max_length=20)),
('location_city', models.CharField(blank=True, max_length=100)),
('location_state', models.CharField(blank=True, max_length=100)),
('location_country', models.CharField(default='Saudia Arabia', max_length=100)),
('description', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Description')),
('qualifications', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)),
('salary_range', models.CharField(blank=True, help_text='e.g., $60,000 - $80,000', max_length=200)),
('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()),
('application_deadline', models.DateField(db_index=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')], 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, 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)),
('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)),
('hiring_agency', models.ManyToManyField(blank=True, help_text='External agency responsible for sourcing candidates for this role', related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency')),
('source', models.ForeignKey(blank=True, help_text='The system or channel from which this job posting originated or was first published.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='job_postings', to='recruitment.source')),
],
options={
'verbose_name': 'Job Posting',
'verbose_name_plural': 'Job Postings',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
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(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')),
('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)')),
('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')),
],
),
migrations.AddField(
model_name='formtemplate',
name='job',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='form_template', to='recruitment.jobposting'),
),
migrations.AddField(
model_name='candidate',
name='job',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.jobposting', verbose_name='Job'),
),
migrations.CreateModel(
name='JobPostingImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('post_image', models.ImageField(upload_to='post/', validators=[recruitment.validators.validate_image_size])),
('job', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='post_images', to='recruitment.jobposting')),
],
),
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/', validators=[recruitment.validators.validate_image_size])),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='SharedFormTemplate',
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')),
('is_public', models.BooleanField(default=False, help_text='Whether this template is publicly available')),
('shared_with', models.ManyToManyField(blank=True, related_name='shared_templates', to=settings.AUTH_USER_MODEL)),
('template', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='recruitment.formtemplate')),
],
options={
'verbose_name': 'Shared Form Template',
'verbose_name_plural': 'Shared Form Templates',
},
),
migrations.CreateModel(
name='IntegrationLog',
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')),
('action', models.CharField(choices=[('REQUEST', 'Request'), ('RESPONSE', 'Response'), ('ERROR', 'Error'), ('SYNC', 'Sync'), ('CREATE_JOB', 'Create Job'), ('UPDATE_JOB', 'Update Job')], max_length=20, verbose_name='Action')),
('endpoint', models.CharField(blank=True, max_length=255, verbose_name='Endpoint')),
('method', models.CharField(blank=True, max_length=10, verbose_name='HTTP Method')),
('request_data', models.JSONField(blank=True, null=True, verbose_name='Request Data')),
('response_data', models.JSONField(blank=True, null=True, verbose_name='Response Data')),
('status_code', models.CharField(blank=True, max_length=10, verbose_name='Status Code')),
('error_message', models.TextField(blank=True, verbose_name='Error Message')),
('ip_address', models.GenericIPAddressField(verbose_name='IP Address')),
('user_agent', models.CharField(blank=True, max_length=255, verbose_name='User Agent')),
('processing_time', models.FloatField(blank=True, null=True, verbose_name='Processing Time (seconds)')),
('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integration_logs', to='recruitment.source', verbose_name='Source')),
],
options={
'verbose_name': 'Integration Log',
'verbose_name_plural': 'Integration Logs',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='TrainingMaterial',
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')),
('title', models.CharField(max_length=255, verbose_name='Title')),
('content', django_ckeditor_5.fields.CKEditor5Field(blank=True, verbose_name='Content')),
('video_link', models.URLField(blank=True, verbose_name='Video Link')),
('file', models.FileField(blank=True, upload_to='training_materials/', verbose_name='File')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Created by')),
],
options={
'verbose_name': 'Training Material',
'verbose_name_plural': 'Training Materials',
},
),
migrations.CreateModel(
name='ScheduledInterview',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('interview_date', models.DateField(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')], 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')),
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.jobposting')),
('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={
'verbose_name': 'Meeting Comment',
'verbose_name_plural': 'Meeting Comments',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='FieldResponse',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('value', models.JSONField(blank=True, help_text='Response value (stored as JSON)', null=True)),
('uploaded_file', models.FileField(blank=True, null=True, upload_to='form_uploads/')),
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formfield')),
('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formsubmission')),
],
options={
'verbose_name': 'Field Response',
'verbose_name_plural': 'Field Responses',
'indexes': [models.Index(fields=['submission'], name='recruitment_submiss_474130_idx'), models.Index(fields=['field'], name='recruitment_field_i_097e5b_idx')],
},
),
migrations.AddIndex(
model_name='formsubmission',
index=models.Index(fields=['submitted_at'], name='recruitment_submitt_7946c8_idx'),
),
migrations.AddIndex(
model_name='interviewschedule',
index=models.Index(fields=['start_date'], name='recruitment_start_d_15d55e_idx'),
),
migrations.AddIndex(
model_name='interviewschedule',
index=models.Index(fields=['end_date'], name='recruitment_end_dat_aeb00e_idx'),
),
migrations.AddIndex(
model_name='interviewschedule',
index=models.Index(fields=['created_by'], name='recruitment_created_d0bdcc_idx'),
),
migrations.AddIndex(
model_name='formtemplate',
index=models.Index(fields=['created_at'], name='recruitment_created_c21775_idx'),
),
migrations.AddIndex(
model_name='formtemplate',
index=models.Index(fields=['is_active'], name='recruitment_is_acti_ae5efb_idx'),
),
migrations.AddIndex(
model_name='candidate',
index=models.Index(fields=['stage'], name='recruitment_stage_f1c6eb_idx'),
),
migrations.AddIndex(
model_name='candidate',
index=models.Index(fields=['created_at'], name='recruitment_created_73590f_idx'),
),
migrations.AddIndex(
model_name='jobposting',
index=models.Index(fields=['status', 'created_at', 'title'], name='recruitment_status_8b77aa_idx'),
),
migrations.AddIndex(
model_name='jobposting',
index=models.Index(fields=['slug'], name='recruitment_slug_004045_idx'),
),
migrations.AddIndex(
model_name='scheduledinterview',
index=models.Index(fields=['job', 'status'], name='recruitment_job_id_f09e22_idx'),
),
migrations.AddIndex(
model_name='scheduledinterview',
index=models.Index(fields=['interview_date', 'interview_time'], name='recruitment_intervi_7f5877_idx'),
),
migrations.AddIndex(
model_name='scheduledinterview',
index=models.Index(fields=['candidate', 'job'], name='recruitment_candida_43d5b0_idx'),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 5.2.7 on 2025-10-21 23:25
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='jobposting',
name='application_start_date',
),
]

View File

@ -84,8 +84,8 @@ class JobPosting(Base):
null=True,
blank=True,
)
application_start_date=models.DateField(null=True, blank=True)
application_deadline = models.DateField(db_index=True, null=True, blank=True) # Added index
application_deadline = models.DateField(db_index=True) # Added index
application_instructions =CKEditor5Field(
blank=True, null=True,config_name='extends'
)
@ -137,7 +137,7 @@ class JobPosting(Base):
reporting_to = models.CharField(
max_length=100, blank=True, help_text="Who this position reports to"
)
joining_date = models.DateField(null=True, blank=True, help_text="Desired start date")
open_positions = models.PositiveIntegerField(
default=1, help_text="Number of open positions for this job"
)
@ -732,8 +732,10 @@ class FormTemplate(Base):
blank=True, help_text="Description of the form template"
)
created_by = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="form_templates",null=True,blank=True, db_index=True
)
User, on_delete=models.CASCADE, related_name="form_templates",null=True,blank=True, db_index=True
)
# FIXME: on Delete model SETNULl
is_active = models.BooleanField(
default=False, help_text="Whether this template is active"
)

View File

@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
@receiver(post_save, sender=JobPosting)
def format_job(sender, instance, created, **kwargs):
if created:
FormTemplate.objects.create(job=instance, is_active=True, name=instance.title)
# FormTemplate.objects.create(job=instance, is_active=True, name=instance.title)
async_task(
'recruitment.tasks.format_job_description',
instance.pk,

View File

@ -2,7 +2,6 @@ from django.urls import path
from . import views_frontend
from . import views
from . import views_integration
from . import views_source
urlpatterns = [
path('', views_frontend.dashboard_view, name='dashboard'),
@ -127,15 +126,6 @@ urlpatterns = [
# Source URLs
path('sources/', views_source.SourceListView.as_view(), name='source_list'),
path('sources/create/', views_source.SourceCreateView.as_view(), name='source_create'),
path('sources/<int:pk>/', views_source.SourceDetailView.as_view(), name='source_detail'),
path('sources/<int:pk>/update/', views_source.SourceUpdateView.as_view(), name='source_update'),
path('sources/<int:pk>/delete/', views_source.SourceDeleteView.as_view(), name='source_delete'),
path('sources/api/generate-keys/', views_source.generate_api_keys_view, name='generate_api_keys'),
path('sources/api/copy-to-clipboard/', views_source.copy_to_clipboard_view, name='copy_to_clipboard'),
# Meeting Comments URLs
path('meetings/<slug:slug>/comments/add/', views.add_meeting_comment, name='add_meeting_comment'),

View File

@ -273,21 +273,12 @@ def create_job(request):
if request.method == "POST":
form = JobPostingForm(
request.POST, is_anonymous_user=not request.user.is_authenticated
request.POST
)
# to check user is authenticated or not
if form.is_valid():
try:
job = form.save(commit=False)
if request.user.is_authenticated:
job.created_by = (
request.user.get_full_name() or request.user.username
)
else:
job.created_by = request.POST.get("created_by", "").strip()
if not job.created_by:
job.created_by = request.user.username
job.save()
job_apply_url_relative=reverse('job_detail_candidate',kwargs={'slug':job.slug})
job_apply_url_absolute=request.build_absolute_uri(job_apply_url_relative)
@ -302,7 +293,7 @@ def create_job(request):
else:
messages.error(request, f"Please correct the errors below.{form.errors}")
else:
form = JobPostingForm(is_anonymous_user=not request.user.is_authenticated)
form = JobPostingForm()
return render(request, "jobs/create_job.html", {"form": form})
@ -313,21 +304,11 @@ def edit_job(request, slug):
if request.method == "POST":
form = JobPostingForm(
request.POST,
instance=job,
is_anonymous_user=not request.user.is_authenticated,
instance=job
)
if form.is_valid():
try:
job = form.save(commit=False)
if request.user.is_authenticated:
job.created_by = (
request.user.get_full_name() or request.user.username
)
else:
job.created_by = request.POST.get("created_by", "").strip()
if not job.created_by:
job.created_by = "University Administrator"
job.save()
form.save()
messages.success(request, f'Job "{job.title}" updated successfully!')
return redirect("job_list")
except Exception as e:
@ -338,7 +319,7 @@ def edit_job(request, slug):
else:
job = get_object_or_404(JobPosting, slug=slug)
form = JobPostingForm(
instance=job, is_anonymous_user=not request.user.is_authenticated
instance=job
)
return render(request, "jobs/edit_job.html", {"form": form, "job": job})
@ -347,6 +328,7 @@ def edit_job(request, slug):
def job_detail(request, slug):
"""View details of a specific job"""
job = get_object_or_404(JobPosting, slug=slug)
# Get all candidates for this job, ordered by most recent
applicants = job.candidates.all().order_by("-created_at")
@ -632,15 +614,15 @@ def application_success(request,slug):
@ensure_csrf_cookie
@login_required
def form_builder(request, template_slug=None):
def form_builder(request, template_id=None):
"""Render the form builder interface"""
context = {}
if template_slug:
if template_id:
template = get_object_or_404(
FormTemplate, slug=template_slug
FormTemplate, id=template_id, created_by=request.user
)
context['template']=template
context["template_slug"] = template.slug
context["template_id"] = template.id
context["template_name"] = template.name
return render(request, "forms/form_builder.html", context)
@ -653,12 +635,12 @@ def save_form_template(request):
data = json.loads(request.body)
template_name = data.get("name", "Untitled Form")
stages_data = data.get("stages", [])
template_slug = data.get("template_slug")
template_id = data.get("template_id")
if template_slug:
if template_id:
# Update existing template
template = get_object_or_404(
FormTemplate, slug=template_slug
FormTemplate, id=template_id, created_by=request.user
)
template.name = template_name
template.save()
@ -667,7 +649,7 @@ def save_form_template(request):
else:
# Create new template
template = FormTemplate.objects.create(
name=template_name
name=template_name, created_by=request.user
)
# Create stages and fields
@ -703,7 +685,7 @@ def save_form_template(request):
return JsonResponse(
{
"success": True,
"template_slug": template.slug,
"template_id": template.id,
"message": "Form template saved successfully!",
}
)
@ -712,9 +694,9 @@ def save_form_template(request):
@require_http_methods(["GET"])
def load_form_template(request, template_slug):
def load_form_template(request, template_id):
"""Load an existing form template"""
template = get_object_or_404(FormTemplate, slug=template_slug)
template = get_object_or_404(FormTemplate, id=template_id, created_by=request.user)
stages = []
for stage in template.stages.all():
@ -747,7 +729,6 @@ def load_form_template(request, template_slug):
"success": True,
"template": {
"id": template.id,
"template_slug": template.slug,
"name": template.name,
"description": template.description,
"is_active": template.is_active,
@ -762,7 +743,7 @@ def load_form_template(request, template_slug):
def form_templates_list(request):
"""List all form templates for the current user"""
query = request.GET.get("q", "")
templates = FormTemplate.objects.filter()
templates = FormTemplate.objects.filter(created_by=request.user)
if query:
templates = templates.filter(
@ -802,7 +783,7 @@ def create_form_template(request):
@require_http_methods(["GET"])
def list_form_templates(request):
"""List all form templates for the current user"""
templates = FormTemplate.objects.filter().values(
templates = FormTemplate.objects.filter(created_by=request.user).values(
"id", "name", "description", "created_at", "updated_at"
)
return JsonResponse({"success": True, "templates": list(templates)})
@ -812,25 +793,26 @@ def list_form_templates(request):
@require_http_methods(["DELETE"])
def delete_form_template(request, template_id):
"""Delete a form template"""
template = get_object_or_404(FormTemplate, id=template_id)
template = get_object_or_404(FormTemplate, id=template_id, created_by=request.user)
template.delete()
return JsonResponse(
{"success": True, "message": "Form template deleted successfully!"}
)
def form_wizard_view(request, template_slug):
def form_wizard_view(request, template_id):
"""Display the form as a step-by-step wizard"""
template = get_object_or_404(FormTemplate, slug=template_slug, is_active=True)
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
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,
@ -841,16 +823,14 @@ def form_wizard_view(request, template_slug):
return render(
request,
"forms/form_wizard.html",
{"template_slug": template_slug, "job_id": job_id},
{"template_id": template_id, "job_id": job_id},
)
@csrf_exempt
@require_POST
def submit_form(request, template_slug):
def submit_form(request, template_id):
"""Handle form submission"""
template = get_object_or_404(FormTemplate, slug=template_slug)
job = template.job
template = get_object_or_404(FormTemplate, id=template_id)
if request.method == "POST":
try:
with transaction.atomic():
@ -864,7 +844,6 @@ def submit_form(request, template_slug):
{"success": False, "message": "Application limit reached for this job."}
)
submission = FormSubmission.objects.create(template=template)
# Process field responses
for field_id, value in request.POST.items():
if field_id.startswith("field_"):
@ -909,7 +888,7 @@ def submit_form(request, template_slug):
)
submission.applicant_email = email.display_value
submission.save()
# time=timezone.now()
time=timezone.now()
Candidate.objects.create(
first_name=first_name.display_value,
last_name=last_name.display_value,
@ -917,16 +896,10 @@ def submit_form(request, template_slug):
phone=phone.display_value,
address=address.display_value,
resume=resume.get_file if resume.is_file else None,
job=job
job=submission.template.job,
)
return JsonResponse(
{
"success": True,
"message": "Form submitted successfully!",
"redirect_url": reverse('application_success',kwargs={'slug':job.slug}),
}
)
# return redirect('application_success',slug=job.slug)
return redirect('application_success',slug=job.slug)
except Exception as e:
logger.error(f"Candidate creation failed,{e}")
@ -2071,6 +2044,7 @@ 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:
@ -2224,10 +2198,6 @@ def account_toggle_status(request,pk):
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')
@csrf_exempt

View File

@ -394,6 +394,27 @@ def dashboard_view(request):
).count()
high_potential_ratio = round((high_potential_count / total_candidates) * 100, 1) if total_candidates > 0 else 0
#donut chart data
jobs=models.JobPosting.objects.all()
selected_job_id=request.GET.get('selected_job_id')
print(jobs)
print(selected_job_id)
apply_counts,exam_counts,interview_counts,offer_counts=[0]*4
if selected_job_id:
job=jobs.filter(internal_job_id=selected_job_id)
apply_counts=job.screening_candidates_count or 0
exam_counts=job.exam_candidates_count or 0
interview_counts=job.interview_candidates_count or 0
offer_counts=job.offer_candidates_count or 0
applicant_stages=['APPLIED','EXAM','INTERVIEW','OFFER']
stage_counts=[apply_counts,exam_counts,interview_counts,offer_counts]
context = {
'total_jobs': total_jobs,
@ -409,8 +430,13 @@ def dashboard_view(request):
'high_potential_count': high_potential_count,
'high_potential_ratio': high_potential_ratio,
'scored_ratio': scored_ratio,
'applicant_stages':json.dumps(applicant_stages),
'stage_counts':json.dumps(stage_counts),
'jobs':'jobs',
'selected_job_id':selected_job_id
}
return render(request, 'recruitment/dashboard.html', context)
@login_required
def candidate_offer_view(request, slug):
"""View for candidates in the Offer stage"""

View File

@ -1,212 +0,0 @@
from django.shortcuts import render, get_object_or_404, redirect
from django.views.generic import ListView, CreateView, UpdateView, DetailView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.urls import reverse_lazy
from django.contrib import messages
from django.db import transaction
from django.http import JsonResponse
from django.db import models
import secrets
import string
from .models import Source, IntegrationLog
from .forms import SourceForm, generate_api_key, generate_api_secret
class SourceListView(LoginRequiredMixin, UserPassesTestMixin, ListView):
"""List all sources"""
model = Source
template_name = 'recruitment/source_list.html'
context_object_name = 'sources'
paginate_by = 10
def test_func(self):
return self.request.user.is_staff
def get_queryset(self):
queryset = super().get_queryset().order_by('name')
# Search functionality
search_query = self.request.GET.get('search', '')
if search_query:
queryset = queryset.filter(
models.Q(name__icontains=search_query) |
models.Q(source_type__icontains=search_query) |
models.Q(description__icontains=search_query)
)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['search_query'] = self.request.GET.get('search', '')
return context
class SourceCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView):
"""Create a new source"""
model = Source
form_class = SourceForm
template_name = 'recruitment/source_form.html'
success_url = reverse_lazy('source_list')
def test_func(self):
return self.request.user.is_staff
def form_valid(self, form):
# Set initial values
form.instance.created_by = self.request.user.get_full_name() or self.request.user.username
# Check if we need to generate API keys
if form.cleaned_data.get('generate_keys') == 'true':
form.instance.api_key = generate_api_key()
form.instance.api_secret = generate_api_secret()
# Log the key generation
IntegrationLog.objects.create(
source=form.instance,
action=IntegrationLog.ActionChoices.CREATE,
endpoint='/api/sources/',
method='POST',
request_data={'name': form.instance.name},
ip_address=self.request.META.get('REMOTE_ADDR'),
user_agent=self.request.META.get('HTTP_USER_AGENT', '')
)
response = super().form_valid(form)
# Add success message
messages.success(self.request, f'Source "{form.instance.name}" created successfully!')
return response
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Create New Source'
context['generate_keys'] = self.request.GET.get('generate_keys', 'false')
return context
class SourceDetailView(LoginRequiredMixin, UserPassesTestMixin, DetailView):
"""View source details"""
model = Source
template_name = 'recruitment/source_detail.html'
context_object_name = 'source'
def test_func(self):
return self.request.user.is_staff
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Mask API keys in display
source = self.object
if source.api_key:
masked_key = source.api_key[:8] + '*' * 24
context['masked_api_key'] = masked_key
else:
context['masked_api_key'] = 'Not generated'
if source.api_secret:
masked_secret = source.api_secret[:12] + '*' * 52
context['masked_api_secret'] = masked_secret
else:
context['masked_api_secret'] = 'Not generated'
# Get recent integration logs
context['recent_logs'] = IntegrationLog.objects.filter(
source=source
).order_by('-created_at')[:10]
return context
class SourceUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
"""Update an existing source"""
model = Source
form_class = SourceForm
template_name = 'recruitment/source_form.html'
success_url = reverse_lazy('source_list')
def test_func(self):
return self.request.user.is_staff
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = f'Edit Source: {self.object.name}'
context['generate_keys'] = self.request.GET.get('generate_keys', 'false')
return context
def form_valid(self, form):
# Check if we need to generate new API keys
if form.cleaned_data.get('generate_keys') == 'true':
form.instance.api_key = generate_api_key()
form.instance.api_secret = generate_api_secret()
# Log the key regeneration
IntegrationLog.objects.create(
source=self.object,
action=IntegrationLog.ActionChoices.CREATE,
endpoint=f'/api/sources/{self.object.pk}/',
method='PUT',
request_data={'name': form.instance.name, 'regenerated_keys': True},
ip_address=self.request.META.get('REMOTE_ADDR'),
user_agent=self.request.META.get('HTTP_USER_AGENT', '')
)
messages.success(self.request, 'New API keys generated successfully!')
response = super().form_valid(form)
messages.success(self.request, f'Source "{form.instance.name}" updated successfully!')
return response
class SourceDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
"""Delete a source"""
model = Source
template_name = 'recruitment/source_confirm_delete.html'
success_url = reverse_lazy('source_list')
def test_func(self):
return self.request.user.is_staff
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
success_url = self.get_success_url()
# Log the deletion
IntegrationLog.objects.create(
source=self.object,
action=IntegrationLog.ActionChoices.SYNC, # Using SYNC for deletion
endpoint=f'/api/sources/{self.object.pk}/',
method='DELETE',
request_data={'name': self.object.name},
ip_address=self.request.META.get('REMOTE_ADDR'),
user_agent=self.request.META.get('HTTP_USER_AGENT', '')
)
messages.success(request, f'Source "{self.object.name}" deleted successfully!')
return super().delete(request, *args, **kwargs)
def generate_api_keys_view(request):
"""API endpoint to generate API keys"""
if not request.user.is_staff:
return JsonResponse({'error': 'Permission denied'}, status=403)
if request.method == 'POST':
api_key = generate_api_key()
api_secret = generate_api_secret()
return JsonResponse({
'success': True,
'api_key': api_key,
'api_secret': api_secret,
'message': 'API keys generated successfully'
})
return JsonResponse({'error': 'Invalid request method'}, status=405)
def copy_to_clipboard_view(request):
"""HTMX endpoint to copy text to clipboard"""
if request.method == 'POST':
text_to_copy = request.POST.get('text', '')
return render(request, 'includes/copy_to_clipboard.html', {
'text': text_to_copy
})
return JsonResponse({'error': 'Invalid request method'}, status=405)

View File

@ -16,9 +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.6.0/css/all.min.css" integrity="sha512-Kc323vGBEqzTmouAECnVceyQqyqdsSiqLQISBL29aUW4U/M7pSPA/gEUZQqv1cwx4OnYxTxve5UMg5GT6L4JJg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="{% static 'css/main.css' %}">
@ -44,7 +44,7 @@
<div class="en text-xs">King Abdullah bin Abdulaziz University Hospital</div>
</div>
</div>
<img src="{% static 'image/hospital_logo.png' %}" alt="KAAUH Logo" style="max-height: 40px; max-width: 40px;">
<img src="{% static 'image/kaauh.png' %}" alt="KAAUH Logo" style="max-height: 40px; max-width: 40px;">
</div>
</div>
</div>
@ -55,11 +55,11 @@
{# --- MOBILE BRAND LOGIC: Show small logo on mobile, large on desktop (lg) --- #}
<a class="navbar-brand text-white d-block d-lg-none" href="{% url 'dashboard' %}" aria-label="Home">
<img src="{% static 'image/hospital_logo_1.png' %}" alt="{% trans 'kaauh logo green bg' %}" class="navbar-brand-mobile">
<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 me-4 pe-4" href="{% url 'dashboard' %}" aria-label="Home">
<img src="{% static 'image/hospital_logo_1.png' %}" alt="{% trans 'kaauh logo green bg' %}" style="width: 60px; height: 60px;">
<img src="{% static 'image/kaauh_green1.png' %}" alt="{% trans 'kaauh logo green bg' %}" style="width: 60px; height: 60px;">
</a>
{# Toggler: order-lg-0 ensures it's before navigation links on desktop, but it stays where it is on mobile #}
@ -120,7 +120,7 @@
{% endif %}
</button>
<ul
class="dropdown-menu dropdown-menu-end py-0 shadow border-0 rounded-3"
style="min-width: 240px;"
>
@ -216,21 +216,13 @@
<li class="nav-item me-lg-4">
<a class="nav-link {% if request.resolver_match.url_name == 'list_meetings' %}active{% endif %}" href="{% url 'list_meetings' %}">
<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">
<path stroke-linecap="round" stroke-linejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" />
</svg>
{% trans "Meetings" %}
</span>
</a>
</li>
<li class="nav-item me-lg-4">
<a class="nav-link {% if request.resolver_match.url_name == 'source_list' %}active{% endif %}" href="{% url 'source_list' %}">
<span class="d-flex align-items-center gap-2">
{% include "icons/sources.html" %}
{% trans "Sources" %}
</span>
</a>
</li>
{% 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">
@ -332,36 +324,8 @@
});
}
});
function form_loader(){
const forms = document.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('submit', function(e) {
const submitButton = form.querySelector('button[type="submit"], input[type="submit"]');
if (submitButton) {
submitButton.disabled = true;
submitButton.classList.add('loading');
window.addEventListener('unload', function() {
submitButton.disabled = false;
submitButton.classList.remove('loading');
});
}
});
});
}
form_loader();
try{
document.addEventListener('htmx:afterSwap', form_loader);
}catch(e){
console.error(e)
}
</script>
{% block customJS %}{% endblock %}
</body>
</html>
</html>

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 14.25h13.5a.75.75 0 0 1 0 1.5H5.25a.75.75 0 0 1 0-1.5zM5.25 17.25h13.5a.75.75 0 0 1 0 1.5H5.25a.75.75 0 0 1 0-1.5zM5.25 11.25h13.5a.75.75 0 0 1 0 1.5H5.25a.75.75 0 0 1 0-1.5zM3.75 6.75h16.5a.75.75 0 0 1 0 1.5H3.75a.75.75 0 0 1 0-1.5z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 2.25h15a2.25 2.25 0 0 1 2.25 2.25v15a2.25 2.25 0 0 1-2.25 2.25h-15A2.25 2.25 0 0 0 2.25 19.5v-15A2.25 2.25 0 0 1 4.5 2.25z" />
</svg>

Before

Width:  |  Height:  |  Size: 628 B

View File

@ -1,20 +0,0 @@
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="copyToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<i class="fas fa-check-circle text-success me-2"></i>
<strong class="me-auto">{% trans "Success" %}</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
{% blocktrans with text=text %}Copied "{{ text }}" to clipboard!{% endblocktrans %}
</div>
</div>
</div>
<script>
// Show toast notification
document.addEventListener('DOMContentLoaded', function() {
const toast = new bootstrap.Toast(document.getElementById('copyToast'));
toast.show();
});
</script>

View File

@ -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.email|default:"Anonymous" }}</td>
<td>{{ log.user.email|default:"Anonymous" }}</td>
<td>
<span class="badge rounded-pill badge-request-method">{{ log.method }}</span>
</td>

View File

@ -150,8 +150,113 @@
</div>
</div>
{# ================================================= #}
{# SECTION 2: JOB CONTENT (CKEDITOR 5 Fields) #}
{# SECTION 2: INTERNAL AND PROMOTION #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
<h5><i class="fas fa-tags"></i> {% trans "Internal & Promotion" %}</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-6">
<div>
<label for="{{ form.position_number.id_for_label }}" class="form-label">{% trans "Position Number" %}</label>
{{ form.position_number }}
{% if form.position_number.errors %}<div class="text-danger small mt-1">{{ form.position_number.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div>
<label for="{{ form.reporting_to.id_for_label }}" class="form-label">{% trans "Reports To" %}</label>
{{ form.reporting_to }}
{% 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>
<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>
<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-12">
<div>
<label for="{{ form.hash_tags.id_for_label }}" class="form-label">{% trans "Hashtags (For Promotion/Search on Linkedin)" %}</label>
{{ form.hash_tags }}
{% if form.hash_tags.errors %}<div class="text-danger small mt-1">{{ form.hash_tags.errors }}</div>{% endif %}
<div class="form-text">{% trans "Comma-separated list of hashtags, e.g., #hiring, #professor" %}</div>
</div>
</div>
</div>
</div>
</div>
{# ================================================= #}
{# SECTION 3: LOCATION AND DATES #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
<h5><i class="fas fa-map-marker-alt"></i> {% trans "Location, Dates, & Salary" %}</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-4">
<div>
<label for="{{ form.location_city.id_for_label }}" class="form-label">{% trans "City" %}</label>
{{ form.location_city }}
{% if form.location_city.errors %}<div class="text-danger small mt-1">{{ form.location_city.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.location_state.id_for_label }}" class="form-label">{% trans "State/Province" %}</label>
{{ form.location_state }}
{% if form.location_state.errors %}<div class="text-danger small mt-1">{{ form.location_state.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.location_country.id_for_label }}" class="form-label">{% trans "Country" %}</label>
{{ form.location_country }}
{% if form.location_country.errors %}<div class="text-danger small mt-1">{{ form.location_country.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" %}<span class="text-danger">*</span></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.salary_range.id_for_label }}" class="form-label">{% trans "Salary Range" %}</label>
{{ form.salary_range }}
{% if form.salary_range.errors %}<div class="text-danger small mt-1">{{ form.salary_range.errors }}</div>{% endif %}
</div>
</div>
</div>
</div>
</div>
{# ================================================= #}
{# SECTION 4: JOB CONTENT (CKEDITOR 5 Fields) #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
@ -179,21 +284,15 @@
</div>
{# ================================================= #}
{# SECTION 3: COMPENSATION AND APPLICATION #}
{# SECTION 5: APPLICATION Instructions #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
<h5><i class="fas fa-dollar-sign"></i> {% trans "Compensation & Application" %}</h5>
<h5><i class="fas fa-dollar-sign"></i> {% trans "Benefits & Application Instructions" %}</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-6">
<div>
<label for="{{ form.salary_range.id_for_label }}" class="form-label">{% trans "Salary Range" %}</label>
{{ form.salary_range }}
{% if form.salary_range.errors %}<div class="text-danger small mt-1">{{ form.salary_range.errors }}</div>{% endif %}
</div>
</div>
{% comment %} (application_url comment removed for brevity) {% endcomment %}
<div class="col-12">
@ -215,117 +314,7 @@
</div>
</div>
{# ================================================= #}
{# SECTION 4: LOCATION AND DATES #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
<h5><i class="fas fa-map-marker-alt"></i> {% trans "Location, Dates, & Status" %}</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-4">
<div>
<label for="{{ form.location_city.id_for_label }}" class="form-label">{% trans "City" %}</label>
{{ form.location_city }}
{% if form.location_city.errors %}<div class="text-danger small mt-1">{{ form.location_city.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.location_state.id_for_label }}" class="form-label">{% trans "State/Province" %}</label>
{{ form.location_state }}
{% if form.location_state.errors %}<div class="text-danger small mt-1">{{ form.location_state.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.location_country.id_for_label }}" class="form-label">{% trans "Country" %}</label>
{{ form.location_country }}
{% 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.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>
</div>
</div>
{# ================================================= #}
{# SECTION 5: INTERNAL AND PROMOTION #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
<h5><i class="fas fa-tags"></i> {% trans "Internal & Promotion" %}</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-6">
<div>
<label for="{{ form.position_number.id_for_label }}" class="form-label">{% trans "Position Number" %}</label>
{{ form.position_number }}
{% if form.position_number.errors %}<div class="text-danger small mt-1">{{ form.position_number.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div>
<label for="{{ form.reporting_to.id_for_label }}" class="form-label">{% trans "Reports To" %}</label>
{{ form.reporting_to }}
{% if form.reporting_to.errors %}<div class="text-danger small mt-1">{{ form.reporting_to.errors }}</div>{% endif %}
</div>
</div>
<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-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 }}
{% if form.created_by.errors %}<div class="text-danger small mt-1">{{ form.created_by.errors }}</div>{% endif %}
</div>
</div>
<div class="col-12">
<div>
<label for="{{ form.hash_tags.id_for_label }}" class="form-label">{% trans "Hashtags (For Promotion/Search)" %}</label>
{{ form.hash_tags }}
{% if form.hash_tags.errors %}<div class="text-danger small mt-1">{{ form.hash_tags.errors }}</div>{% endif %}
<div class="form-text">{% trans "Comma-separated list of hashtags, e.g., #hiring, #professor" %}</div>
</div>
</div>
</div>
</div>
</div>
{# ================================================= #}
{# ACTION BUTTONS #}

View File

@ -150,8 +150,113 @@
</div>
</div>
{# ================================================= #}
{# SECTION 2: JOB CONTENT (CKEDITOR 5 Fields) #}
{# SECTION 2: INTERNAL AND PROMOTION #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
<h5><i class="fas fa-tags"></i> {% trans "Internal & Promotion" %}</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-6">
<div>
<label for="{{ form.position_number.id_for_label }}" class="form-label">{% trans "Position Number" %}</label>
{{ form.position_number }}
{% if form.position_number.errors %}<div class="text-danger small mt-1">{{ form.position_number.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div>
<label for="{{ form.reporting_to.id_for_label }}" class="form-label">{% trans "Reports To" %}</label>
{{ form.reporting_to }}
{% 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>
<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>
<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-12">
<div>
<label for="{{ form.hash_tags.id_for_label }}" class="form-label">{% trans "Hashtags (For Promotion/Search on Linkedin)" %}</label>
{{ form.hash_tags }}
{% if form.hash_tags.errors %}<div class="text-danger small mt-1">{{ form.hash_tags.errors }}</div>{% endif %}
<div class="form-text">{% trans "Comma-separated list of hashtags, e.g., #hiring, #professor" %}</div>
</div>
</div>
</div>
</div>
</div>
{# ================================================= #}
{# SECTION 3: LOCATION AND DATES #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
<h5><i class="fas fa-map-marker-alt"></i> {% trans "Location, Dates, & Salary" %}</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-4">
<div>
<label for="{{ form.location_city.id_for_label }}" class="form-label">{% trans "City" %}</label>
{{ form.location_city }}
{% if form.location_city.errors %}<div class="text-danger small mt-1">{{ form.location_city.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.location_state.id_for_label }}" class="form-label">{% trans "State/Province" %}</label>
{{ form.location_state }}
{% if form.location_state.errors %}<div class="text-danger small mt-1">{{ form.location_state.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.location_country.id_for_label }}" class="form-label">{% trans "Country" %}</label>
{{ form.location_country }}
{% if form.location_country.errors %}<div class="text-danger small mt-1">{{ form.location_country.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" %}<span class="text-danger">*</span></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.salary_range.id_for_label }}" class="form-label">{% trans "Salary Range" %}</label>
{{ form.salary_range }}
{% if form.salary_range.errors %}<div class="text-danger small mt-1">{{ form.salary_range.errors }}</div>{% endif %}
</div>
</div>
</div>
</div>
</div>
{# ================================================= #}
{# SECTION 4: JOB CONTENT (CKEDITOR 5 Fields) #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
@ -179,21 +284,15 @@
</div>
{# ================================================= #}
{# SECTION 3: COMPENSATION AND APPLICATION #}
{# SECTION 5: APPLICATION Instructions #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
<h5><i class="fas fa-dollar-sign"></i> {% trans "Compensation & Application" %}</h5>
<h5><i class="fas fa-dollar-sign"></i> {% trans "Benefits & Application Instructions" %}</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-6">
<div>
<label for="{{ form.salary_range.id_for_label }}" class="form-label">{% trans "Salary Range" %}</label>
{{ form.salary_range }}
{% if form.salary_range.errors %}<div class="text-danger small mt-1">{{ form.salary_range.errors }}</div>{% endif %}
</div>
</div>
{% comment %} (application_url comment removed for brevity) {% endcomment %}
<div class="col-12">
@ -215,117 +314,7 @@
</div>
</div>
{# ================================================= #}
{# SECTION 4: LOCATION AND DATES #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
<h5><i class="fas fa-map-marker-alt"></i> {% trans "Location, Dates, & Status" %}</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-4">
<div>
<label for="{{ form.location_city.id_for_label }}" class="form-label">{% trans "City" %}</label>
{{ form.location_city }}
{% if form.location_city.errors %}<div class="text-danger small mt-1">{{ form.location_city.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.location_state.id_for_label }}" class="form-label">{% trans "State/Province" %}</label>
{{ form.location_state }}
{% if form.location_state.errors %}<div class="text-danger small mt-1">{{ form.location_state.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.location_country.id_for_label }}" class="form-label">{% trans "Country" %}</label>
{{ form.location_country }}
{% 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.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>
</div>
</div>
{# ================================================= #}
{# SECTION 5: INTERNAL AND PROMOTION #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
<h5><i class="fas fa-tags"></i> {% trans "Internal & Promotion" %}</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-6">
<div>
<label for="{{ form.position_number.id_for_label }}" class="form-label">{% trans "Position Number" %}</label>
{{ form.position_number }}
{% if form.position_number.errors %}<div class="text-danger small mt-1">{{ form.position_number.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div>
<label for="{{ form.reporting_to.id_for_label }}" class="form-label">{% trans "Reports To" %}</label>
{{ form.reporting_to }}
{% if form.reporting_to.errors %}<div class="text-danger small mt-1">{{ form.reporting_to.errors }}</div>{% endif %}
</div>
</div>
<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-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 }}
{% if form.created_by.errors %}<div class="text-danger small mt-1">{{ form.created_by.errors }}</div>{% endif %}
</div>
</div>
<div class="col-12">
<div>
<label for="{{ form.hash_tags.id_for_label }}" class="form-label">{% trans "Hashtags (For Promotion/Search)" %}</label>
{{ form.hash_tags }}
{% if form.hash_tags.errors %}<div class="text-danger small mt-1">{{ form.hash_tags.errors }}</div>{% endif %}
<div class="form-text">{% trans "Comma-separated list of hashtags, e.g., #hiring, #professor" %}</div>
</div>
</div>
</div>
</div>
</div>
{# ================================================= #}
{# ACTION BUTTONS #}

View File

@ -1,4 +1,3 @@
{% extends "base.html" %}
{% load i18n static %}
@ -217,8 +216,17 @@
padding-top: 0.5rem;
}
/* Custom CSS for simplified stat card (from previous answer) */
.stats-grid .kpi-card {
border-left: 4px solid var(--kaauh-teal);
background-color: #f0faff;
}
.stats-grid .card-body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>
{% endblock %}
@ -236,7 +244,7 @@
<div class="row g-4">
{# LEFT COLUMN: JOB DETAILS WITH TABS #}
<div class="col-lg-8">
<div class="col-lg-7">
<div class="card shadow-sm no-hover">
{# HEADER SECTION #}
@ -248,6 +256,7 @@
<div class="d-flex align-items-center gap-2">
<span class="badge status-badge">
{# Corrected status badge logic to close the span correctly #}
{% if job.status == "ACTIVE" %}
<span class="badge bg-success status-badge">
{% elif job.status == "DRAFT" %}
@ -262,6 +271,7 @@
<span class="badge bg-secondary status-badge">
{% endif %}
{{ job.get_status_display }}
</span>
<button type="button" class="btn btn-outline-light btn-sm ms-2" data-bs-toggle="modal" data-bs-target="#editStatusModal">
<i class="fas fa-edit text-primary"></i>
</button>
@ -281,13 +291,12 @@
<i class="fas fa-file-alt me-1"></i> {% trans "Description & Requirements" %}
</button>
</li>
{% if job.application_instructions %}
<li class="nav-item" role="presentation">
<button class="nav-link" id="instructions-tab" data-bs-toggle="tab" data-bs-target="#instructions" type="button" role="tab" aria-controls="instructions" aria-selected="false">
<i class="fas fa-paper-plane me-1"></i> {% trans "Application" %}
<button class="nav-link" id="kpis-tab" data-bs-toggle="tab" data-bs-target="#kpis" type="button" role="tab" aria-controls="kpis" aria-selected="false">
<i class="fas fa-chart-line me-1"></i> {% trans "Application KPIs" %}
</button>
</li>
{% endif %}
</ul>
<div class="card-body">
@ -323,21 +332,20 @@
</div>
<div class="col-md-4">
<button
type="button"
class="btn btn-main-action btn-sm"
id="copyJobLinkButton"
data-url="{{ job.application_url }}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 heroicon">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25ZM6.75 12h.008v.008H6.75V12Zm0 3h.008v.008H6.75V15Zm0 3h.008v.008H6.75V18Z" />
</svg>
{% trans "Share Public Link" %}
</button>
<button
type="button"
class="btn btn-main-action btn-sm"
id="copyJobLinkButton"
data-url="{{ job.application_url }}">
{# Replaced bulky SVG with simpler Font Awesome icon #}
<i class="fas fa-link"></i>
{% trans "Share Public Link" %}
</button>
<span id="copyFeedback" class="text-success ms-2 small" style="display:none;">
{% trans "Copied!" %}
</span>
</div>
<span id="copyFeedback" class="text-success ms-2 small" style="display:none;">
{% trans "Copied!" %}
</span>
</div>
</div>
<h5 class="text-muted mb-3">{% trans "Financial & Timeline" %}</h5>
@ -392,19 +400,74 @@
<div class="text-secondary">{{ job.benefits|safe}}</div>
</div>
{% endif %}
</div>
{# TAB 3 CONTENT: APPLICATION INSTRUCTIONS #}
{% if job.application_instructions %}
<div class="tab-pane fade" id="instructions" role="tabpanel" aria-labelledby="instructions-tab">
<div class="mb-4">
{% if job.application_instructions %}
<div class="mb-4">
<h5>{% trans "Application Instructions" %}</h5>
<div class="text-secondary">{{ job.application_instructions|safe }}</div>
</div>
</div>
{% endif %}
{% endif %}
</div>
{# TAB 3 CONTENT: APPLICATION KPIS #}
<div class="tab-pane fade" id="kpis" role="tabpanel" aria-labelledby="kpis-tab">
<div class="row g-3 stats-grid">
{# 1. Job Avg. Score #}
<div class="col-6 col-md-3">
<div class="card text-center h-100 kpi-card">
<div class="card-body p-2">
<i class="fas fa-star text-primary mb-1 d-block" style="font-size: 1.2rem;"></i>
<div class="h4 mb-0 text-primary fw-bold">{{ avg_match_score|floatformat:1 }}</div>
<small class="text-muted d-block">{% trans "Avg. AI Score" %}</small>
</div>
</div>
</div>
{# 2. High Potential Count #}
<div class="col-6 col-md-3">
<div class="card text-center h-100">
<div class="card-body p-2">
<i class="fas fa-trophy text-success mb-1 d-block" style="font-size: 1.2rem;"></i>
<div class="h4 mb-0 text-success fw-bold">{{ high_potential_count }}</div>
<small class="text-muted d-block">{% trans "High Potential" %}</small>
</div>
</div>
</div>
{# 3. Avg. Time to Interview #}
<div class="col-6 col-md-3">
<div class="card text-center h-100">
<div class="card-body p-2">
<i class="fas fa-calendar-alt text-info mb-1 d-block" style="font-size: 1.2rem;"></i>
<div class="h4 mb-0 text-info fw-bold">{{ avg_t2i_days|floatformat:1 }}d</div>
<small class="text-muted d-block">{% trans "Time to Interview" %}</small>
</div>
</div>
</div>
{# 4. Avg. Exam Review Time #}
<div class="col-6 col-md-3">
<div class="card text-center h-100">
<div class="card-body p-2">
<i class="fas fa-hourglass-half text-secondary mb-1 d-block" style="font-size: 1.2rem;"></i>
<div class="h4 mb-0 text-secondary fw-bold">{{ avg_t_in_exam_days|floatformat:1 }}d</div>
<small class="text-muted d-block">{% trans "Avg. Exam Review" %}</small>
</div>
</div>
</div>
</div>
<p class="text-end text-muted small mt-3 me-2">
<i class="fas fa-info-circle me-1"></i> {% trans "KPIs based on completed applicant data." %}
</p>
</div>
</div>
</div>
{# FOOTER ACTIONS #}
@ -423,7 +486,7 @@
</div>
{# RIGHT COLUMN: TABBED CARDS #}
<div class="col-lg-4 ">
<div class="col-lg-5">
{# New Card for Candidate Category Chart #}
<div class="card shadow-sm no-hover mb-4">
@ -440,44 +503,9 @@
</div>
</div>
<div class="card shadow-sm no-hover mb-4">
<div class="card-body p-4">
<h6 class="text-muted mb-4">{% trans "Applicant Tracking" %}</h6>
{% include 'jobs/partials/applicant_tracking.html' %}
</div>
</div>
<div class="stats">
<div class="card">
<div class="card-header">
<h3><i class="fas fa-star stat-icon"></i> {% trans "Job Avg. Score" %}</h3>
</div>
<div class="stat-value">{{ avg_match_score|floatformat:1 }}</div>
<div class="stat-caption">{% trans "Average AI Match Score (0-100)" %}</div>
</div>
<div class="card">
<div class="card-header">
<h3><i class="fas fa-trophy stat-icon" style="color: var(--color-success);"></i> {% trans "High Potential Count" %}</h3>
</div>
<div class="stat-value">{{ high_potential_count }}</div>
<div class="stat-caption">{% trans "Candidates with Score ≥ 75%" %}</div>
</div>
<div class="card">
<div class="card-header">
<h3><i class="fas fa-calendar-alt stat-icon" style="color: var(--kaauh-teal-light);"></i> {% trans "Avg. Time to Interview" %}</h3>
</div>
<div class="stat-value">{{ avg_t2i_days|floatformat:1 }}d</div>
<div class="stat-caption">{% trans "Applied to Interview (Total Funnel Speed)" %}</div>
</div>
<div class="card">
<div class="card-header">
<h3><i class="fas fa-hourglass-half stat-icon" style="color: var(--color-info);"></i> {% trans "Avg. Exam Review Time" %}</h3>
</div>
<div class="stat-value">{{ avg_t_in_exam_days|floatformat:1 }}d</div>
<div class="stat-caption">{% trans "Days spent between Exam and Interview" %}</div>
</div>
</div>
<div class="card shadow-sm no-hover" style="height:350px;">
{# REMOVED: Standalone Applicant Tracking Card (It is now in a tab) #}
<div class="card shadow-sm no-hover">
{# RIGHT TABS NAVIGATION #}
<ul class="nav nav-tabs right-column-tabs" id="rightJobTabs" role="tablist">
@ -486,14 +514,19 @@
<i class="fas fa-users me-1 text-primary"></i> {% trans "Applicants" %}
</button>
</li>
<li class="nav-item flex-fill" role="presentation">
<button class="nav-link" id="tracking-tab" data-bs-toggle="tab" data-bs-target="#tracking-pane" type="button" role="tab" aria-controls="tracking-pane" aria-selected="false">
<i class="fas fa-project-diagram me-1 text-primary"></i> {% trans "Tracking" %}
</button>
</li>
<li class="nav-item flex-fill" role="presentation">
<button class="nav-link" id="manage-tab" data-bs-toggle="tab" data-bs-target="#manage-pane" type="button" role="tab" aria-controls="manage-pane" aria-selected="false">
<i class="fas fa-cogs me-1 text-secondary"></i> {% trans "Form Template" %}
</button>
</li>
<li class="nav-item flex-fill" role="presentation">
<button class="nav-link" id="internal-tab" data-bs-toggle="tab" data-bs-target="#internal-pane" type="button" role="tab" aria-controls="internal-pane" aria-selected="false">
<i class="fas fa-info me-1 text-muted"></i> {% trans "Linkedin" %}
<button class="nav-link" id="linkedin-tab" data-bs-toggle="tab" data-bs-target="#linkedin-pane" type="button" role="tab" aria-controls="linkedin-pane" aria-selected="false">
<i class="fab fa-linkedin me-1 text-info"></i> {% trans "LinkedIn" %}
</button>
</li>
</ul>
@ -503,35 +536,7 @@
{# TAB 1: APPLICANTS CONTENT #}
<div class="tab-pane fade show active" id="applicants-pane" role="tabpanel" aria-labelledby="applicants-tab">
<h5 class="mb-3">{% trans "Total Applicants" %} (<span id="total_candidates">{{ total_applicants }}</span>)</h5>
{% comment %} {% if total_applicants > 0 %}
<div class="row mb-4 applicant-stats">
<div class="col-4">
<div class="stat-item">
<div class="text-primary">{{ applied_count }}</div>
<small class="text-muted">{% trans "Applied" %}</small>
</div>
</div>
<div class="col-4">
<div class="stat-item">
<div class="text-info">{{ interview_count }}</div>
<small class="text-muted">{% trans "Interview" %}</small>
</div>
</div>
<div class="col-4">
<div class="stat-item">
<div class="text-success">{{ offer_count }}</div>
<small class="text-muted">{% trans "Offer" %}</small>
</div>
</div>
</div>
<div class="col-12 mb-2">
<a href="{% url 'job_candidates_list' job.slug %}" class="btn btn-outline-secondary w-100">
{% trans "View All Applicants" %} ({{ total_applicants }})
</a>
</div>
{% endif %} {% endcomment %}
<div class="d-grid gap-4">
<a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action">
<i class="fas fa-user-plus"></i> {% trans "Create Applicant" %}
@ -542,13 +547,16 @@
</div>
</div>
{# TAB 2: MANAGEMENT (LinkedIn & Forms) CONTENT #}
{# NEW TAB 2: APPLICANT TRACKING CONTENT #}
<div class="tab-pane fade" id="tracking-pane" role="tabpanel" aria-labelledby="tracking-tab">
<h5 class="mb-3"><i class="fas fa-project-diagram me-2 text-primary"></i>{% trans "Pipeline Stages" %}</h5>
{% include 'jobs/partials/applicant_tracking.html' %}
<p class="text-muted small mt-3">{% trans "View the number of candidates currently in each stage of the hiring pipeline." %}</p>
</div>
{# TAB 3: MANAGEMENT (Form Template) CONTENT #}
<div class="tab-pane fade" id="manage-pane" role="tabpanel" aria-labelledby="manage-tab">
{# LinkedIn Integration (Content from old card) #}
{# Applicant Form Management (Content from old card) #}
<h5 class="mb-3"><i class="fas fa-clipboard-list me-2 text-primary"></i>{% trans "Form Management" %}</h5>
<div class="d-grid gap-2">
<p class="text-muted small mb-3">
@ -569,8 +577,8 @@
</div>
</div>
{# TAB 3: INTERNAL INFO CONTENT #}
<div class="tab-pane fade" id="internal-pane" role="tabpanel" aria-labelledby="internal-tab">
{# TAB 4: LINKEDIN INTEGRATION CONTENT #}
<div class="tab-pane fade" id="linkedin-pane" role="tabpanel" aria-labelledby="linkedin-tab">
<h5 class="mb-3"><i class="fab fa-linkedin me-2 text-info"></i>{% trans "LinkedIn Integration" %}</h5>
<div class="mb-4">
{% if job.posted_to_linkedin %}
@ -611,48 +619,6 @@
</div>
{% endif %}
</div>
{% comment %} {# Applicant Form Management (Content from old card) #}
<h5 class="mb-3"><i class="fas fa-clipboard-list me-2 text-primary"></i>{% trans "Form Management" %}</h5> {% endcomment %}
{% comment %} <div class="d-grid gap-2">
<p class="text-muted small mb-3">
{% trans "Manage the custom application forms associated with this job posting." %}
</p> {% endcomment %}
{% comment %} <a href="{% url 'create_form_template' %}" class="btn btn-main-action">
<i class="fas fa-plus-circle me-2"></i> {% trans "Create New Form" %}
</a>
<a href="" class="btn btn-outline-secondary">
<i class="fas fa-list-alt me-1"></i> {% trans "View All Existing Forms" %}
</a> {% endcomment %}
{% comment %} <a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action">
<i class="fas fa-user-plus"></i> {% trans "Create Candidate" %}
</a>
<a href="{% url 'candidate_screening_view' job.slug %}" class="btn btn-main-action">
<i class="fas fa-layer-group"></i> {% trans "Manage Tiers" %}
</a> {% endcomment %}
</div>
</div>
{# TAB 3: INTERNAL INFO CONTENT #}
<div class="tab-pane fade" id="internal-pane" role="tabpanel" aria-labelledby="internal-tab">
<h5 class="mb-3"><i class="fas fa-info-circle me-2 text-secondary"></i>{% trans "Internal Information" %}</h5>
<div class="small">
<p class="mb-1"><strong>{% trans "Internal Job ID:" %}</strong> {{ job.internal_job_id }}</p>
<p class="mb-1"><strong>{% trans "Created:" %}</strong> {{ job.created_at|date:"M d, Y" }}</p>
<p class="mb-1"><strong>{% trans "Last Updated:" %}</strong> {{ job.updated_at|date:"M d, Y" }}</p>
{% if job.reporting_to %}
<p class="mb-0"><strong>{% trans "Reports To:" %}</strong> {{ job.reporting_to }}</p>
{% endif %}
</div>
<div class="mt-4">
<a href="{% url 'job_list' %}" class="btn btn-outline-secondary w-100">
<i class="fas fa-arrow-left"></i> {% trans "Back to Jobs" %}
</a>
</div>
</div>
</div>
</div>
@ -660,10 +626,8 @@
</div>
</div>
</div>
<!--image modal class-->
{% include "jobs/partials/image_upload.html" %}
<!-- JOB STATUS MODAL-->
<div class="modal fade" id="editStatusModal" tabindex="-1" aria-labelledby="editStatusModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
@ -811,4 +775,4 @@
});
</script>
{% endblock %}
{% endblock %}

View File

@ -278,7 +278,7 @@
</tr>
<tr class="nested-metrics-row">
<th style="width: calc(50% / 7);">{% trans "All Applicants" %}</th>
<th style="width: calc(50% / 7);">{% trans "All" %}</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>
@ -386,7 +386,7 @@
{# --- END OF JOB LIST CONTAINER --- #}
{% include "includes/paginator.html" %}
{% if not jobs and not job_list_data and not page_obj %}
<div class="text-center py-5 card shadow-sm">
<div class="text-center py-5 card shadow-sm mt-4">
<div class="card-body">
<i class="fas fa-briefcase fa-3x mb-3" style="color: var(--kaauh-teal-dark);"></i>
<h3>{% trans "No job postings found" %}</h3>

View File

@ -1,10 +1,9 @@
{% extends 'base.html' %}
{% load static i18n %}
{% block customCSS %}
<style>
/* -------------------------------------------------------------------------- */
/* KAAT-S Redesign CSS */
/* KAAT-S Redesign CSS - Optimized Compact Detail View (Settings Removed) */
/* -------------------------------------------------------------------------- */
:root {
@ -20,93 +19,56 @@
}
body {
background-color: #f0f2f5; /* Off-white page background */
font-family: 'Inter', sans-serif; /* Use a modern font stack */
background-color: #f0f2f5;
font-family: 'Inter', sans-serif;
}
/* ------------------ General Layout & Card Styles ------------------ */
.container {
width:auto;
padding: 3rem 1.5rem;
}
.card {
border: none; /* Remove default border */
border: none;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.08), 0 4px 10px rgba(0,0,0,0.05); /* Deep, soft shadow */
background-color: white;
margin-bottom: 2.5rem;
transition: all 0.3s ease;
box-shadow: 0 5px 15px rgba(0,0,0,0.05), 0 2px 5px rgba(0,0,0,0.03);
margin-bottom: 1.5rem;
transition: all 0.2s ease;
}
.card:not(.no-hover):hover {
transform: translateY(-3px);
box-shadow: 0 15px 40px rgba(0,0,0,0.1), 0 6px 15px rgba(0,0,0,0.08);
}
.card.no-hover:hover {
transform: none;
box-shadow: 0 10px 30px rgba(0,0,0,0.08), 0 4px 10px rgba(0,0,0,0.05);
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0,0,0,0.08);
}
.card-body {
padding: 2rem;
padding: 1.5rem;
}
/* ------------------ Header & Title Styles ------------------ */
/* ------------------ Main Header & Title Styles ------------------ */
.card-header {
background-color: var(--kaauh-gray-light);
border-bottom: 1px solid var(--kaauh-border);
padding: 2rem;
.main-title-card {
padding: 1.5rem 2rem;
background-color: white;
border-bottom: 3px solid var(--kaauh-teal);
border-radius: 12px 12px 0 0;
display: flex;
justify-content: space-between;
align-items: flex-start; /* Align title group to the top */
flex-wrap: wrap;
}
.card-header-title-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.card-header h1 {
color: var(--kaauh-teal-dark);
font-weight: 800; /* Extra bold for prominence */
margin: 0;
display: flex;
align-items: center;
gap: 1rem;
font-size: 2.5rem;
}
.card-header .heroicon {
width: 2.5rem;
height: 2.5rem;
color: var(--kaauh-teal);
}
.card-header .btn-secondary-back {
/* Subtle Back Button */
align-self: flex-start;
background-color: transparent;
border: none;
color: var(--kaauh-secondary-text);
font-weight: 600;
font-size: 1rem;
padding: 0.5rem 0.75rem;
transition: color 0.2s;
}
.card-header .btn-secondary-back:hover {
color: var(--kaauh-teal);
text-decoration: underline;
}
/* ------------------ Status Badge Styles ------------------ */
.main-title-card h1 {
color: var(--kaauh-teal-dark);
font-weight: 800;
margin: 0;
font-size: 2rem;
}
.main-title-card .heroicon {
width: 2rem;
height: 2rem;
color: var(--kaauh-teal);
}
.status-badge {
font-size: 0.85rem;
padding: 0.5em 1em;
border-radius: 20px; /* Pill shape */
font-size: 0.75rem;
padding: 0.35em 0.8em;
border-radius: 15px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 0.5rem;
}
.bg-waiting { background-color: #ffc107 !important; color: var(--kaauh-primary-text) !important;}
.bg-started { background-color: var(--kaauh-teal) !important; color: white !important;}
@ -114,13 +76,13 @@ body {
/* ------------------ Detail Row & Content Styles ------------------ */
.card h2 {
.detail-section h2 {
color: var(--kaauh-teal-dark);
font-weight: 700;
padding: 1.5rem 2rem 1rem;
margin: 0;
font-size: 1.5rem;
border-bottom: 1px solid var(--kaauh-border);
font-size: 1.25rem;
margin-bottom: 1rem;
border-bottom: 2px solid var(--kaauh-teal-light);
padding-bottom: 0.5rem;
}
.detail-row-group {
@ -128,10 +90,10 @@ body {
}
.detail-row {
display: grid;
grid-template-columns: minmax(150px, 40%) 1fr;
padding: 1rem 2rem;
border-bottom: 1px solid var(--kaauh-border);
display: flex;
justify-content: space-between;
padding: 0.75rem 0;
border-bottom: 1px dashed var(--kaauh-border);
align-items: center;
}
.detail-row:last-child {
@ -139,273 +101,194 @@ body {
}
.detail-label {
font-weight: 600;
color: var(--kaauh-teal-dark);
text-align: left;
font-size: 0.95rem;
color: var(--kaauh-teal);
font-size: 0.9rem;
flex-basis: 45%;
}
.detail-value {
text-align: right;
color: var(--kaauh-primary-text);
word-wrap: break-word;
font-weight: 500;
text-align: right;
font-size: 0.9rem;
flex-basis: 55%;
}
/* ------------------ Join Info & Copy Button ------------------ */
.join-info-card .card-body {
padding-top: 2rem;
.join-info-card {
border-left: 5px solid var(--kaauh-teal); /* Highlight join info */
}
.btn-primary {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
padding: 0.75rem 1.5rem;
border-radius: 8px;
transition: all 0.2s ease;
padding: 0.6rem 1.25rem;
border-radius: 6px;
}
.btn-primary:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
box-shadow: 0 4px 8px rgba(0, 99, 110, 0.3);
}
.join-url-container {
display: flex;
gap: 1rem;
align-items: center;
margin-top: 1.5rem;
position: relative;
padding: 1rem 0; /* Add padding for clear space around the copy area */
margin-top: 1rem;
}
.join-url-display {
flex-grow: 1;
background-color: var(--kaauh-gray-light);
border: 1px solid var(--kaauh-border);
border-radius: 8px;
padding: 0.75rem 1rem;
word-break: break-all;
font-size: 0.9rem;
color: var(--kaauh-secondary-text);
font-family: monospace; /* Monospace for links/code */
padding: 0.5rem 0.75rem;
font-size: 0.85rem;
}
.join-url-display strong {
color: var(--kaauh-teal-dark);
font-family: 'Inter', sans-serif;
}
.btn-copy {
flex-shrink: 0;
background-color: var(--kaauh-teal-dark); /* Darker teal for a clean utility look */
border: none;
color: white;
padding: 0.75rem 1rem;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-copy:hover {
background-color: var(--kaauh-teal);
}
.btn-copy i {
margin-right: 0.25rem;
}
/* 🎯 Copy Message Pill Style */
#copy-message {
position: absolute;
top: -5px;
right: 0;
background-color: var(--kaauh-success);
color: white;
padding: 0.2rem 0.6rem;
border-radius: 20px; /* Pill shape */
opacity: 0;
transition: opacity 0.4s ease-in-out;
z-index: 10;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
padding: 0.5rem 0.75rem;
background-color: var(--kaauh-teal-dark);
}
/* ------------------ Footer & Actions ------------------ */
.card-footer {
.action-bar-footer {
border-top: 1px solid var(--kaauh-border);
padding: 1.5rem 2rem;
display: flex;
flex-wrap: wrap;
gap: 1rem;
justify-content: flex-start;
padding: 1rem 1.5rem;
gap: 0.75rem;
background-color: var(--kaauh-gray-light);
border-radius: 0 0 12px 12px;
}
.btn-danger {
background-color: var(--kaauh-danger);
border-color: var(--kaauh-danger);
color: white;
.btn-footer-action {
font-weight: 600;
padding: 0.75rem 1.5rem;
border-radius: 8px;
transition: all 0.2s ease;
}
.btn-danger:hover {
background-color: #c82333;
border-color: #bd2130;
box-shadow: 0 4px 8px rgba(220, 53, 69, 0.3);
padding: 0.5rem 1rem;
border-radius: 6px;
font-size: 0.9rem;
}
.btn-secondary {
background-color: #6c757d;
border-color: #6c757d;
color: white;
font-weight: 600;
padding: 0.75rem 1.5rem;
border-radius: 8px;
transition: all 0.2s ease;
}
.btn-secondary:hover {
background-color: #5a6268;
border-color: #545b62;
}
/* ------------------ API Response Styling ------------------ */
#gateway-response-card {
border-left: 5px solid var(--kaauh-teal); /* Prominent left border */
}
#gateway-response-card .card-body {
padding: 1.5rem;
}
#gateway-response-card h3 {
/* ------------------ Comments Section ------------------ */
#comments-card .card-header {
background-color: var(--kaauh-teal-light);
color: var(--kaauh-teal-dark);
font-weight: 700;
font-size: 1.35rem;
margin-bottom: 1rem;
}
#gateway-response-card pre {
background-color: #fff;
border: 1px solid var(--kaauh-border);
border-radius: 8px;
padding: 1rem;
font-size: 0.8rem;
color: var(--kaauh-primary-text);
white-space: pre-wrap;
word-wrap: break-word;
padding: 1rem 1.5rem;
font-weight: 600;
border-radius: 12px 12px 0 0;
}
/* Comment card body/item styling is kept compact */
</style>
{% endblock %}
{% block content %}
<div class="container">
<div class="container-fluid">
<div class="card no-hover">
<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](http://www.w3.org/2000/svg)">
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0 8.268-2.943-9.542-7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
{{ meeting.topic }}
</h1>
<div class="col-auto">
<span class="status-badge bg-{{ meeting.status }}">
{{ meeting.status|title }}
</span>
{# --- TOP BAR / BACK BUTTON --- #}
<div class="d-flex justify-content-between align-items-center mb-3">
<a href="{% url 'list_meetings' %}" class="btn btn-secondary-back">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Meetings" %}
</a>
</div>
<div class="row g-4">
{# --- LEFT COLUMN (MAIN DETAILS) --- #}
<div class="col-lg-6">
<div class="card no-hover h-100">
{# --- CONSOLIDATED HEADER --- #}
<div class="main-title-card">
<div class="d-flex justify-content-between align-items-start">
<div class="card-header-title-group">
<h1 class="mb-1">
<svg class="heroicon me-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0 8.268-2.943-9.542-7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
{{ meeting.topic }}
</h1>
<div class="d-flex align-items-center gap-3">
<span class="status-badge bg-{{ meeting.status }}">
{{ meeting.status|title }}
</span>
{% if meeting.interview %}
<span class="text-muted small">
{% trans "Candidate" %}: <a class="text-primary-theme fw-bold text-decoration-none" href="{% url 'candidate_detail' meeting.interview.candidate.slug %}">{{ meeting.interview.candidate.name }} </a>
</span>
{% endif %}
</div>
</div>
</div>
</div>
{% if meeting.interview %}
<div class="col-auto">
<span class="status-badge">
Candidate Name : <a class="text-primary-theme" href="{% url 'candidate_detail' meeting.interview.candidate.slug %}">{{ meeting.interview.candidate.name }} </a>
</span>
{# --- MAIN DETAIL BODY --- #}
<div class="card-body detail-section">
<h2>{% trans "Core Details" %}</h2>
<div class="detail-row-group">
<div class="detail-row"><div class="detail-label">{% trans "Meeting ID" %}:</div><div class="detail-value">{{ meeting.meeting_id }}</div></div>
<div class="detail-row"><div class="detail-label">{% trans "Start Time" %}:</div><div class="detail-value">{{ meeting.start_time|date:"M d, Y H:i" }}</div></div>
<div class="detail-row"><div class="detail-label">{% trans "Duration" %}:</div><div class="detail-value">{{ meeting.duration }} {% trans "minutes" %}</div></div>
<div class="detail-row"><div class="detail-label">{% trans "Timezone" %}:</div><div class="detail-value">{{ meeting.timezone|default:"UTC" }}</div></div>
<div class="detail-row"><div class="detail-label">{% trans "Host Email" %}:</div><div class="detail-value">{{ meeting.host_email|default:"N/A" }}</div></div>
</div>
{% endif %}
</div>
<a href="{% url 'list_meetings' %}" class="btn btn-secondary-back">
<i class="fas fa-arrow-left"></i> {% trans "Back to Meetings" %}
</a>
</div>
</div>
<div class="card no-hover">
<h2>{% trans "Meeting Information" %}</h2>
<div class="card-body detail-row-group">
<div class="detail-row"><div class="detail-label">{% trans "Meeting ID" %}:</div><div class="detail-value">{{ meeting.meeting_id }}</div></div>
<div class="detail-row"><div class="detail-label">{% trans "Topic" %}:</div><div class="detail-value">{{ meeting.topic }}</div></div>
<div class="detail-row"><div class="detail-label">{% trans "Start Time" %}:</div><div class="detail-value">{{ meeting.start_time|date:"M d, Y H:i" }}</div></div>
<div class="detail-row"><div class="detail-label">{% trans "Duration" %}:</div><div class="detail-value">{{ meeting.duration }} {% trans "minutes" %}</div></div>
<div class="detail-row"><div class="detail-label">{% trans "Timezone" %}:</div><div class="detail-value">{{ meeting.timezone|default:"UTC" }}</div></div>
<div class="detail-row"><div class="detail-label">{% trans "Host Email" %}:</div><div class="detail-value">{{ meeting.host_email|default:"N/A" }}</div></div>
</div>
</div>
{% if meeting.join_url %}
<div class="card no-hover join-info-card">
<h2>{% trans "Join Information" %}</h2>
<div class="card-body">
<a href="{{ meeting.join_url }}" class="btn btn-primary" target="_blank">
<i class="fas fa-video"></i> {% trans "Join Meeting Now" %}
</a>
<div class="join-url-container">
<div id="copy-message" style="opacity: 0;">{% trans "Copied!" %}</div>
<div class="join-url-display" id="join-url-display">
<strong>{% trans "Join URL" %}:</strong> <span id="meeting-join-url">{{ meeting.join_url }}</span>
</div>
<button class="btn-copy" onclick="copyLink()">
<i class="fas fa-copy"></i>
</div>
{# --- ACTION BAR AT THE BOTTOM OF THE MAIN CARD --- #}
<div class="card-footer action-bar-footer d-flex justify-content-end">
<a href="{% url 'update_meeting' meeting.slug %}" class="btn btn-primary btn-footer-action">
<i class="fas fa-edit me-1"></i> {% trans "Update" %}
</a>
{% if meeting.zoom_gateway_response %}
<button type="button" class="btn btn-secondary btn-footer-action" onclick="toggleGateway()">
<i class="fas fa-code me-1"></i> {% trans "API Response" %}
</button>
{% endif %}
<button type="button" class="btn btn-danger btn-footer-action" title="{% trans 'Delete' %}"
data-bs-toggle="modal" data-bs-target="#deleteModal"
hx-post="{% url 'delete_meeting' meeting.slug %}"
hx-target="#deleteModalBody"
hx-swap="outerHTML"
data-item-name="{{ meeting.topic }}">
<i class="fas fa-trash-alt me-1"></i>
Delete
</button>
</div>
{% if meeting.password %}
<div class="detail-row" style="border: none; padding: 1rem 0 0 0;">
<div class="detail-label" style="font-size: 1rem;">{% trans "Password" %}:</div>
<div class="detail-value" style="font-weight: 700; color: var(--kaauh-danger);">{{ meeting.password }}</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
<div class="card no-hover">
<h2>{% trans "Settings" %}</h2>
<div class="card-body detail-row-group">
<div class="detail-row"><div class="detail-label">{% trans "Participant Video" %}:</div><div class="detail-value">{{ meeting.participant_video|yesno:"Yes,No" }}</div></div>
<div class="detail-row"><div class="detail-label">{% trans "Join Before Host" %}:</div><div class="detail-value">{{ meeting.join_before_host|yesno:"Yes,No" }}</div></div>
<div class="detail-row"><div class="detail-label">{% trans "Mute Upon Entry" %}:</div><div class="detail-value">{{ meeting.mute_upon_entry|yesno:"Yes,No" }}</div></div>
<div class="detail-row"><div class="detail-label">{% trans "Waiting Room" %}:</div><div class="detail-value">{{ meeting.waiting_room|yesno:"Yes,No" }}</div></div>
</div>
</div>
{# --- RIGHT COLUMN (JOIN INFO) --- #}
<div class="col-lg-6">
{% if meeting.join_url %}
<div class="card no-hover join-info-card detail-section h-100">
<div class="card-body">
<h2>{% trans "Join Information" %}</h2>
<a href="{{ meeting.join_url }}" class="btn btn-primary w-100 mb-4" target="_blank">
<i class="fas fa-video me-1"></i> {% trans "Join Meeting Now" %}
</a>
<div class="card no-hover">
<div class="card-footer">
<a href="{% url 'update_meeting' meeting.slug %}" class="btn btn-primary">
<i class="fas fa-edit"></i> {% trans "Update Meeting" %}
</a>
<div class="join-url-container">
<div id="copy-message" style="opacity: 0;">{% trans "Copied!" %}</div>
{% if meeting.zoom_gateway_response %}
<button type="button" class="btn btn-secondary" onclick="toggleGateway()">
<i class="fas fa-code"></i> {% trans "View API Response" %}
</button>
<div class="join-url-display d-flex justify-content-between align-items-center">
<div class="text-truncate">
<strong>{% trans "Join URL" %}:</strong>
<span id="meeting-join-url">{{ meeting.join_url }}</span>
</div>
<button class="btn-copy ms-2" onclick="copyLink()" title="{% trans 'Copy URL' %}">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
{% if meeting.password %}
<div class="detail-row" style="border: none; padding-top: 1rem;">
<div class="detail-label" style="font-size: 1rem;">{% trans "Password" %}:</div>
<div class="detail-value fw-bolder text-danger">{{ meeting.password }}</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
<button type="button" class="btn btn-danger" title="{% trans 'Delete' %}"
data-bs-toggle="modal" data-bs-target="#deleteModal"
hx-post="{% url 'delete_meeting' meeting.slug %}"
hx-target="#deleteModalBody"
hx-swap="outerHTML"
data-item-name="{{ meeting.topic }}">
<i class="fas fa-trash-alt"></i>
Delete Meeting
</button>
</div>
</div>
{# --- API RESPONSE CARD (Full width, hidden by default) --- #}
{% if meeting.zoom_gateway_response %}
<div id="gateway-response-card" class="card" style="display: none;">
<div id="gateway-response-card" class="card mt-4" style="display: none;">
<div class="card-body">
<h3>{% trans "API Gateway Response" %}</h3>
<pre>{{ meeting.zoom_gateway_response|safe }}</pre>
@ -413,72 +296,68 @@ body {
</div>
{% endif %}
<!-- Comments Section -->
<div class="card no-hover" id="comments-card">
<div class="card-header text-primary-theme d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
{# --- Comments Section (Full Width, below main content) --- #}
<div class="card no-hover mt-4" id="comments-card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-comments me-2"></i>
Comments ({{ meeting.comments.count }})
{% trans "Comments" %} ({{ meeting.comments.count }})
</h5>
{% if user.is_authenticated %}
<button type="button" class="btn btn-primary btn-sm"
hx-get="{% url 'add_meeting_comment' meeting.slug %}"
hx-target="#comment-section"
>
<i class="fas fa-plus"></i> Add Comment
<i class="fas fa-plus me-1"></i> {% trans "Add Comment" %}
</button>
{% endif %}
</div>
<div class="card-body">
<div id="comment-section">
{% if meeting.comments.all %}
<div class="row">
{% for comment in meeting.comments.all|dictsortreversed:"created_at" %}
<div class="col-12 mb-3">
<div class="card ">
<div class="card-header d-flex justify-content-between align-items-start">
<div>
<strong>{{ comment.author.get_full_name|default:comment.author.username }}</strong>
{% if comment.author != user %}
<span class="badge bg-secondary ms-2">Comment</span>
{% endif %}
</div>
<small class="text-muted">{{ comment.created_at|date:"M d, Y P" }}</small>
</div>
<div class="card-body">
<p class="card-text">{{ comment.content|safe }}</p>
</div>
<div class="card-footer">
{% if comment.author == user or user.is_staff %}
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-primary"
hx-get="{% url 'edit_meeting_comment' meeting.slug comment.id %}"
hx-target="#comment-section"
title="Edit Comment">
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn btn-outline-danger"
hx-get="{% url 'delete_meeting_comment' meeting.slug comment.id %}"
hx-target="#comment-section"
title="Delete Comment">
<i class="fas fa-trash"></i>
</button>
</div>
{% endif %}
</div>
{% for comment in meeting.comments.all|dictsortreversed:"created_at" %}
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<strong class="me-2">{{ comment.author.get_full_name|default:comment.author.username }}</strong>
{% if comment.author != user %}
<span class="badge bg-secondary ms-1">{% trans "Comment" %}</span>
{% endif %}
</div>
<small class="text-muted">{{ comment.created_at|date:"M d, Y P" }}</small>
</div>
{% endfor %}
</div>
<div class="card-body">
<p class="card-text">{{ comment.content|safe }}</p>
</div>
<div class="card-footer">
{% if comment.author == user or user.is_staff %}
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-primary"
hx-get="{% url 'edit_meeting_comment' meeting.slug comment.id %}"
hx-target="#comment-section"
title="{% trans 'Edit Comment' %}">
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn btn-outline-danger"
hx-get="{% url 'delete_meeting_comment' meeting.slug comment.id %}"
hx-target="#comment-section"
title="{% trans 'Delete Comment' %}">
<i class="fas fa-trash"></i>
</button>
</div>
{% endif %}
</div>
</div>
{% endfor %}
{% else %}
<p class="text-muted">No comments yet. Be the first to comment!</p>
<p class="text-muted">{% trans "No comments yet. Be the first to comment!" %}</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Comment Modal (for Add/Edit) -->
<div class="modal fade" id="commentModal" tabindex="-1" aria-labelledby="commentModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
@ -487,8 +366,7 @@ body {
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="commentModalBody">
<!-- HTMX will load the form here -->
</div>
</div>
</div>
</div>
</div>
@ -505,41 +383,36 @@ body {
}
}
// CopyLink function remains the same (as provided in the original code)
function copyLink() {
const urlElement = document.getElementById('meeting-join-url');
const messageElement = document.getElementById('copy-message');
const textToCopy = urlElement.textContent || urlElement.innerText;
// Clear any existing message
clearTimeout(window.copyMessageTimeout);
// Function to show the message
function showMessage(success) {
messageElement.textContent = success ? '{% trans "Copied!" %}' : '{% trans "Copy Failed." %}';
messageElement.style.backgroundColor = success ? 'var(--kaauh-success)' : 'var(--kaauh-danger)';
messageElement.style.opacity = '1';
// Hide the message after 2 seconds
window.copyMessageTimeout = setTimeout(() => {
messageElement.style.opacity = '0';
}, 2000);
}
// Use the modern clipboard API
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(textToCopy).then(() => {
showMessage(true); // Show success message
showMessage(true);
}).catch(err => {
console.error('Could not copy text: ', err);
fallbackCopyTextToClipboard(textToCopy, showMessage); // Try fallback on failure
fallbackCopyTextToClipboard(textToCopy, showMessage);
});
} else {
// Fallback for older browsers
fallbackCopyTextToClipboard(textToCopy, showMessage);
}
}
// Fallback function for older browsers
function fallbackCopyTextToClipboard(text, callback) {
const textArea = document.createElement("textarea");
textArea.value = text;
@ -560,7 +433,7 @@ body {
}
document.body.removeChild(textArea);
callback(success); // Call the message function with the result
callback(success);
}
</script>
{% endblock %}
{% endblock %}

View File

@ -294,15 +294,15 @@
<div class="tab-pane fade" id="resume-pane" role="tabpanel" aria-labelledby="resume-tab">
<h5 class="text-primary mb-4">{% trans "Resume Document" %}</h5>
<div class="d-flex align-items-center justify-content-between p-3 border rounded">
<div>
{% comment %} <div>
<p class="mb-1"><strong>{{ candidate.resume.name }}</strong></p>
<small class="text-muted">{{ candidate.resume.name|truncatechars:30 }}</small>
</div>
</div> {% endcomment %}
<div class="d-flex flex-column gap-2">
<div class="d-flex gap-2">
<a href="{{ candidate.resume.url }}" target="_blank" class="btn btn-outline-primary">
<i class="fas fa-eye me-1"></i>
{% trans "View Resume" %}
{% trans "View Actual Resume" %}
</a>
<a href="{{ candidate.resume.url }}" download class="btn btn-main-action">
<i class="fas fa-download me-1"></i>
@ -311,7 +311,7 @@
</div>
<a href="{% url 'candidate_resume_template' candidate.slug %}" class="btn btn-outline-info">
<i class="fas fa-file-alt me-1"></i>
{% trans "View Formatted Resume" %}
{% trans "View Resume AI Overview" %}
</a>
</div>
</div>

View File

@ -65,6 +65,11 @@
</th>
<th style="width: 15%"><i class="fas fa-user me-1"></i> {% trans "Name" %}</th>
<th style="width: 15%"><i class="fas fa-phone me-1"></i> {% trans "Contact" %}</th>
<th style="width: 15%"><i class="fas fa-tag me-1"></i> {% trans "Topic" %}</th>
<th style="width: 10%"><i class="fas fa-clock me-1"></i> {% trans "Duration" %}</th>
<th style="width: 15%"><i class="fas fa-calendar me-1"></i> {% trans "Meeting Date" %}</th>
<th style="width: 10%"><i class="fas fa-video me-1"></i> {% trans "Meeting Link" %}</th>
<th style="width: 10%"><i class="fas fa-check-circle me-1"></i> {% trans "Meeting Status" %}</th>
<th style="width: 10%"><i class="fas fa-check-circle me-1"></i> {% trans "Offer" %}</th>
<th style="width: 15%"><i class="fas fa-cog me-1"></i> {% trans "Actions" %}</th>
</tr>
@ -88,6 +93,57 @@
<i class="fas fa-phone me-1"></i> {{ candidate.phone }}
</div>
</td>
<td class="candidate-details text-muted">
{% if candidate.get_latest_meeting.topic %}
{{ candidate.get_latest_meeting.topic }}
{% else %}
--
{% endif %}
</td>
<td class="candidate-details text-muted"><div class="d-block">
{% if candidate.get_latest_meeting.duration %}
{{ candidate.get_latest_meeting.duration }} {% trans _("Minutes") %}
{% else %}
--
{% endif %}
</div></td>
<td class="candidate-details text-muted">
{% with latest_meeting=candidate.get_latest_meeting %}
{% if latest_meeting %}
{{ latest_meeting.start_time|date:"d-m-Y h:i A" }}
{% else %}
<span class="text-muted">--</span>
{% endif %}
{% endwith %}
</td>
<td>
{% with latest_meeting=candidate.get_latest_meeting %}
{% if latest_meeting and latest_meeting.join_url %}
<a href="{{ latest_meeting.join_url }}" target="_blank" class="btn btn-sm bg-primary-theme text-white" title="Join Interview"
{% if latest_meeting.status == 'ended' %}disabled{% endif %}>
click to join
<i class="fas fa-video me-1"></i>
</a>
{% else %}
<span class="text-muted">--</span>
{% endif %}
{% endwith %}
</td>
<td>
{{ latest_meeting.status }}
{% with latest_meeting=candidate.get_latest_meeting %}
{% if latest_meeting %}
<span class="badge {% if latest_meeting.status == 'waiting' %}bg-warning{% elif latest_meeting.status == 'started' %}bg-success{% elif latest_meeting.status == 'ended' %}bg-danger{% endif %}">
{% if latest_meeting.status == 'started' %}
<i class="fas fa-circle me-1 text-success"></i>
{% endif %}
{{ latest_meeting.status|title }}
</span>
{% else %}
<span class="text-muted">--</span>
{% endif %}
{% endwith %}
</td>
<td class="text-center">
{% if not candidate.offer_status %}
<button type="button" class="btn btn-warning btn-sm"

View File

@ -547,6 +547,33 @@
{% endif %}
</div>
</div>
<div class="mb-3" style="font-size: 0.9rem;">
<span style="display: inline-flex; align-items: center;">
<a href="{% url 'job_list' %}"
style="color: white; text-decoration: none; padding-right: 8px;">
JOBS
</a>
<span style="color: #6c757d; padding-right: 8px;">/</span>
<a href="{% url 'candidate_list' %}"
style="color: white; text-decoration: none; padding-right: 8px;">
CANDIDATES
</a>
<span style="color: #6c757d; padding-right: 8px;">/</span>
<a href="{% url 'candidate_detail' candidate.id %}"
style="color: white; text-decoration: none; padding-right: 8px;">
CANDIDATE
</a>
<span style="color: #6c757d; padding-right: 8px;">/</span>
<span style="color:gray; font-weight: 600;">
RESUME Overview
</span>
</span>
</div>
<div class="score-box">
<div class="score-value">{{ candidate.analysis_data.match_score|default:0 }}%</div>
<div class="score-text">Match Score</div>

View File

@ -151,6 +151,26 @@
<canvas id="applicationsChart"></canvas>
</div>
</div>
<div class="card shadow-lg">
<div class="card-header">
<h2 class="d-flex align-items-center mb-0">
Select a job from the drop
</h2>
<form method="GET" action="{% url 'dashboard' %}">
<select name="selected_job_id" onchange="this.form.submit()">
<option value="">Show All</option>
{% for job in jobs%}
<option value="job" {% if request.GET.selected_job_id == selected_job_id %}selected{% endif %}>job</option>
{% endfor %}
</select>
</form>
</div>
<div style="width: 75%; margin: auto;">
<canvas id="job-doughnut-chart"></canvas>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
@ -218,6 +238,54 @@
}
}
});
const applicant_stages=JSON.parse('{{applicant_stages|safe}}');
const stage_counts=JSON.parse('{{stage_counts|safe}}');
console.log(applicant_stages)
console.log(stage_counts)
const job_data = {
labels: applicant_stages,
datasets: [{
label: 'My Doughnut Dataset',
data: stage_counts,
backgroundColor: [ // Define colors for your slices
'rgb(255, 99, 132)', // Red
'rgb(54, 162, 235)', // Blue
'rgb(255, 205, 86)', // Yellow
'rgb(200,200,100)',
'rgb(30,40,80)',
],
hoverOffset: 4
}]
};
const dnt_config = {
// Set type to 'doughnut'
type: 'doughnut',
data: job_data,
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
},
title: {
display: true,
text: 'Django Data Doughnut Chart'
}
},
// The 'cutout' option is what makes it a doughnut chart (defaults to '50%')
// cutout: '50%',
}
};
// 4. Initialize and render the chart
const ctx_dnt = document.getElementById('job-doughnut-chart').getContext('2d');
new Chart(ctx_dnt, dnt_config);
</script>
{% endblock %}

View File

@ -49,6 +49,7 @@
{% endblock %}
{% block content %}
jhashkdhkashkdhkash
<div class="container mt-4">
<div class="detail-header">
<div class="d-flex justify-content-between align-items-center">

View File

@ -1,128 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}
{% trans "Delete Source" %} | {% trans "Recruitment System" %}
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="row">
<div class="col-12">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
{% trans "Delete Source" %}
</h6>
</div>
<div class="card-body">
<div class="alert alert-warning">
<h5 class="alert-heading">{% trans "Confirm Deletion" %}</h5>
<p>{% trans "Are you sure you want to delete the following source? This action cannot be undone." %}</p>
</div>
<!-- Source Information -->
<div class="row mb-4">
<div class="col-12">
<h5>{% trans "Source Information" %}</h5>
<div class="table-responsive">
<table class="table table-borderless">
<tr>
<th width="30%">{% trans "Name" %}</th>
<td>{{ source.name }}</td>
</tr>
<tr>
<th>{% trans "Type" %}</th>
<td>
<span class="badge bg-info">{{ source.source_type }}</span>
</td>
</tr>
<tr>
<th>{% trans "Status" %}</th>
<td>
{% if source.is_active %}
<span class="badge bg-success">
<i class="fas fa-check-circle me-1"></i>
{% trans "Active" %}
</span>
{% else %}
<span class="badge bg-danger">
<i class="fas fa-times-circle me-1"></i>
{% trans "Inactive" %}
</span>
{% endif %}
</td>
</tr>
<tr>
<th>{% trans "Created By" %}</th>
<td>{{ source.created_by }}</td>
</tr>
<tr>
<th>{% trans "Created At" %}</th>
<td>{{ source.created_at|date:"M d, Y H:i" }}</td>
</tr>
</table>
</div>
</div>
</div>
<!-- Warning Messages -->
<div class="row mb-4">
<div class="col-12">
<div class="alert alert-danger">
<h5 class="alert-heading">
<i class="fas fa-exclamation-circle me-2"></i>
{% trans "Important Note" %}
</h5>
<ul>
<li>{% trans "All associated API keys will be permanently deleted." %}</li>
<li>{% trans "Integration logs related to this source will remain but will show 'Source deleted'." %}</li>
<li>{% trans "Any active integrations using this source will be disconnected." %}</li>
</ul>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between">
<div>
<a href="{% url 'source_detail' source.pk %}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-2"></i>
{% trans "Cancel" %}
</a>
</div>
<form method="post" action="">
{% csrf_token %}
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash me-2"></i>
{% trans "Delete Source" %}
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Add confirmation dialog
document.addEventListener('DOMContentLoaded', function() {
const deleteForm = document.querySelector('form[action*="delete"]');
if (deleteForm) {
deleteForm.addEventListener('submit', function(e) {
if (!confirm('{% trans "Are you sure you want to delete this source? This action cannot be undone." %}')) {
e.preventDefault();
}
});
}
});
</script>
{% endblock %}

View File

@ -1,285 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}
{{ source.name }} | {% trans "Source Details" %} | {% trans "Recruitment System" %}
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-1">{{ source.name }}</h1>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{% url 'source_list' %}">{% trans "Sources" %}</a>
</li>
<li class="breadcrumb-item active" aria-current="page">{{ source.name }}</li>
</ol>
</nav>
</div>
<div class="btn-group">
<a href="{% url 'source_update' source.pk %}" class="btn btn-primary">
<i class="fas fa-edit me-2"></i>
{% trans "Edit" %}
</a>
<a href="{% url 'source_delete' source.pk %}" class="btn btn-danger">
<i class="fas fa-trash me-2"></i>
{% trans "Delete" %}
</a>
</div>
</div>
</div>
</div>
<!-- Source Information Card -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-info-circle me-2"></i>
{% trans "Source Information" %}
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<table class="table table-borderless">
<tr>
<th width="30%">{% trans "Name" %}</th>
<td>{{ source.name }}</td>
</tr>
<tr>
<th>{% trans "Type" %}</th>
<td>
<span class="badge bg-info">{{ source.source_type }}</span>
</td>
</tr>
<tr>
<th>{% trans "Status" %}</th>
<td>
{% if source.is_active %}
<span class="badge bg-success">
<i class="fas fa-check-circle me-1"></i>
{% trans "Active" %}
</span>
{% else %}
<span class="badge bg-danger">
<i class="fas fa-times-circle me-1"></i>
{% trans "Inactive" %}
</span>
{% endif %}
</td>
</tr>
</table>
</div>
<div class="col-md-6">
<table class="table table-borderless">
<tr>
<th width="30%">{% trans "Created By" %}</th>
<td>{{ source.created_by }}</td>
</tr>
<tr>
<th>{% trans "Created At" %}</th>
<td>{{ source.created_at|date:"M d, Y H:i" }}</td>
</tr>
<tr>
<th>{% trans "Updated At" %}</th>
<td>{{ source.updated_at|date:"M d, Y H:i" }}</td>
</tr>
</table>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<h6>{% trans "Description" %}</h6>
<p class="text-muted">{{ source.description|default:"-" }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Network Configuration -->
<div class="col-lg-6 mb-4">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-network-wired me-2"></i>
{% trans "Network Configuration" %}
</h6>
</div>
<div class="card-body">
<div class="mb-3">
<strong>{% trans "IP Address" %}:</strong>
<code>{{ source.ip_address|default:"Not specified" }}</code>
</div>
<div class="mb-3">
<strong>{% trans "Trusted IPs" %}:</strong>
<div class="mt-2">
{% if source.trusted_ips %}
<div class="d-flex flex-wrap gap-2">
{% for ip in source.trusted_ips|split:"," %}
<span class="badge bg-secondary">{{ ip|strip }}</span>
{% endfor %}
</div>
{% else %}
<span class="text-muted">Not specified</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- API Configuration -->
<div class="col-lg-6 mb-4">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-key me-2"></i>
{% trans "API Configuration" %}
</h6>
</div>
<div class="card-body">
<div class="mb-3">
<strong>{% trans "Integration Version" %}:</strong>
<span class="badge bg-primary">{{ source.integration_version|default:"Not specified" }}</span>
</div>
<div class="mb-3">
<strong>{% trans "API Key" %}:</strong>
<div class="input-group mt-2">
<input type="text" class="form-control" id="apiKey" value="{{ masked_api_key }}" readonly>
<button class="btn btn-outline-secondary" type="button"
onclick="copyToClipboard('apiKey')">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
<div class="mb-3">
<strong>{% trans "API Secret" %}:</strong>
<div class="input-group mt-2">
<input type="text" class="form-control" id="apiSecret" value="{{ masked_api_secret }}" readonly>
<button class="btn btn-outline-secondary" type="button"
onclick="copyToClipboard('apiSecret')">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Integration Logs -->
<div class="row">
<div class="col-12">
<div class="card shadow">
<div class="card-header py-3 d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-history me-2"></i>
{% trans "Recent Integration Logs" %}
</h6>
<a href="{% url 'source_list' %}" class="btn btn-sm btn-secondary">
<i class="fas fa-arrow-left me-1"></i>
{% trans "Back to List" %}
</a>
</div>
<div class="card-body">
{% if recent_logs %}
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th>{% trans "Time" %}</th>
<th>{% trans "Action" %}</th>
<th>{% trans "Endpoint" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "Status" %}</th>
</tr>
</thead>
<tbody>
{% for log in recent_logs %}
<tr>
<td>{{ log.created_at|date:"M d, Y H:i:s" }}</td>
<td>
<span class="badge bg-info">{{ log.get_action_display }}</span>
</td>
<td>
<code class="text-muted">{{ log.endpoint }}</code>
</td>
<td>
<span class="badge bg-secondary">{{ log.method }}</span>
</td>
<td>
{% if log.success %}
<span class="badge bg-success">Success</span>
{% else %}
<span class="badge bg-danger">Failed</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if recent_logs.has_previous %}
<div class="text-center mt-3">
<a href="?page={{ recent_logs.previous_page_number }}" class="btn btn-sm btn-secondary">
<i class="fas fa-chevron-left"></i> {% trans "Previous" %}
</a>
</div>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
<h5 class="text-muted">{% trans "No integration logs found" %}</h5>
<p class="text-muted">
{% trans "Integration logs will appear here when this source is used for external integrations." %}
</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Make function available globally
window.copyToClipboard = function(elementId) {
const element = document.getElementById(elementId);
if (element) {
const text = element.value;
navigator.clipboard.writeText(text).then(function() {
// Show success message
const button = event.target.closest('button');
const originalContent = button.innerHTML;
button.innerHTML = '<i class="fas fa-check"></i>';
button.classList.add('btn-success');
button.classList.remove('btn-outline-secondary');
setTimeout(function() {
button.innerHTML = originalContent;
button.classList.remove('btn-success');
button.classList.add('btn-outline-secondary');
}, 2000);
}).catch(function(err) {
console.error('Failed to copy text: ', err);
alert('{% trans "Failed to copy to clipboard" %}');
});
} else {
console.error('Element not found:', elementId);
}
}
</script>
{% endblock %}

View File

@ -1,376 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}
{% if title %}{{ title }} | {% endif %}{% trans "Source" %} | {% trans "Recruitment System" %}
{% endblock %}
{% block content %}
<!-- Script to define functions globally before buttons are rendered -->
<script>
function copyToClipboard(elementId) {
const element = document.getElementById(elementId);
if (element) {
const text = element.value;
navigator.clipboard.writeText(text).then(function() {
const button = event.target.closest('button');
if (button) {
const orig = button.innerHTML;
button.innerHTML = '<i class="fas fa-check"></i>';
button.classList.add('btn-success');
button.classList.remove('btn-outline-secondary');
setTimeout(function() {
button.innerHTML = orig;
button.classList.remove('btn-success');
button.classList.add('btn-outline-secondary');
}, 2000);
}
}).catch(function(err) {
console.error('Failed to copy text: ', err);
alert('{% trans "Failed to copy to clipboard" %}');
});
} else {
console.error('Element not found:', elementId);
}
}
function generateRandomKey(elementId, length) {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
let result = '';
for (let i = 0; i < length; i++) {
result += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
}
const element = document.getElementById(elementId);
if (element) {
element.value = result;
const button = event.target.closest('button');
if (button) {
const orig = button.innerHTML;
button.innerHTML = '<i class="fas fa-check"></i>';
button.classList.add('btn-success');
button.classList.remove('btn-outline-secondary');
setTimeout(function() {
button.innerHTML = orig;
button.classList.remove('btn-success');
button.classList.add('btn-outline-secondary');
}, 1500);
}
} else {
console.error('Element not found:', elementId);
}
}
// Make functions globally available
window.copyToClipboard = copyToClipboard;
window.generateRandomKey = generateRandomKey;
</script>
<div class="container-fluid py-4">
<div class="row">
<div class="col-12">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
{% if title %}{{ title }}{% else %}{% trans "Create New Source" %}{% endif %}
</h6>
</div>
<div class="card-body">
<form method="post" id="sourceForm">
{% csrf_token %}
<!-- Form Messages -->
{% if form.errors %}
<div class="alert alert-danger">
<h5 class="alert-heading">{% trans "Please correct the errors below:" %}</h5>
{% for field in form %}
{% if field.errors %}
<p class="mb-0">{{ field.label }}: {{ field.errors|join:", " }}</p>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<!-- Basic Information -->
<div class="row mb-4">
<div class="col-12">
<h5 class="mb-3">{% trans "Basic Information" %}</h5>
</div>
<div class="col-md-6 mb-3">
{{ form.name.label_tag }}
{{ form.name }}
{% if form.name.help_text %}
<small class="form-text text-muted">{{ form.name.help_text }}</small>
{% endif %}
{% if form.name.errors %}
<div class="invalid-feedback d-block">{{ form.name.errors }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
{{ form.source_type.label_tag }}
{{ form.source_type }}
{% if form.source_type.help_text %}
<small class="form-text text-muted">{{ form.source_type.help_text }}</small>
{% endif %}
{% if form.source_type.errors %}
<div class="invalid-feedback d-block">{{ form.source_type.errors }}</div>
{% endif %}
</div>
<div class="col-12 mb-3">
{{ form.description.label_tag }}
{{ form.description }}
{% if form.description.help_text %}
<small class="form-text text-muted">{{ form.description.help_text }}</small>
{% endif %}
{% if form.description.errors %}
<div class="invalid-feedback d-block">{{ form.description.errors }}</div>
{% endif %}
</div>
</div>
<!-- Network Configuration -->
<div class="row mb-4">
<div class="col-12">
<h5 class="mb-3">{% trans "Network Configuration" %}</h5>
</div>
<div class="col-md-6 mb-3">
{{ form.ip_address.label_tag }}
{{ form.ip_address }}
{% if form.ip_address.help_text %}
<small class="form-text text-muted">{{ form.ip_address.help_text }}</small>
{% endif %}
{% if form.ip_address.errors %}
<div class="invalid-feedback d-block">{{ form.ip_address.errors }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
{{ form.trusted_ips.label_tag }}
{{ form.trusted_ips }}
{% if form.trusted_ips.help_text %}
<small class="form-text text-muted">{{ form.trusted_ips.help_text }}</small>
{% endif %}
{% if form.trusted_ips.errors %}
<div class="invalid-feedback d-block">{{ form.trusted_ips.errors }}</div>
{% endif %}
</div>
</div>
<!-- Settings -->
<div class="row mb-4">
<div class="col-12">
<h5 class="mb-3">{% trans "Settings" %}</h5>
</div>
<div class="col-md-6 mb-3">
{{ form.integration_version.label_tag }}
{{ form.integration_version }}
{% if form.integration_version.help_text %}
<small class="form-text text-muted">{{ form.integration_version.help_text }}</small>
{% endif %}
{% if form.integration_version.errors %}
<div class="invalid-feedback d-block">{{ form.integration_version.errors }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<div class="form-check form-switch">
{{ form.is_active }}
{{ form.is_active.label_tag }}
</div>
{% if form.is_active.help_text %}
<small class="form-text text-muted">{{ form.is_active.help_text }}</small>
{% endif %}
</div>
</div>
<!-- API Configuration -->
<div class="row mb-4">
<div class="col-12">
<h5 class="mb-3">{% trans "API Configuration" %}</h5>
</div>
<div class="col-12 mb-3">
<div class="card">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-8">
<h6 class="mb-0">{% trans "API Keys" %}</h6>
<small class="text-muted">{% trans "Generate secure API keys for external integrations" %}</small>
</div>
<div class="col-md-4 text-end">
<div class="form-check form-switch">
{{ form.generate_keys }}
{{ form.generate_keys.label_tag }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Generated API Key -->
<div class="col-md-6 mb-3">
<label class="form-label">{% trans "API Key" %}</label>
<div class="input-group">
{{ form.api_key_generated }}
<button class="btn btn-outline-secondary" type="button"
id="generateApiKey"
onclick="generateRandomKey('id_api_key_generated', 32)"
title="{% trans 'Generate random API key' %}">
<i class="fas fa-sync-alt"></i>
</button>
<button class="btn btn-outline-secondary" type="button"
id="copyApiKey"
onclick="copyToClipboard('id_api_key_generated')"
title="{% trans 'Copy to clipboard' %}">
<i class="fas fa-copy"></i>
</button>
</div>
{% if form.api_key_generated.errors %}
<div class="invalid-feedback d-block">{{ form.api_key_generated.errors }}</div>
{% endif %}
</div>
<!-- Generated API Secret -->
<div class="col-md-6 mb-3">
<label class="form-label">{% trans "API Secret" %}</label>
<div class="input-group">
{{ form.api_secret_generated }}
<button class="btn btn-outline-secondary" type="button"
id="generateApiSecret"
onclick="generateRandomKey('id_api_secret_generated', 64)"
title="{% trans 'Generate random API secret' %}">
<i class="fas fa-sync-alt"></i>
</button>
<button class="btn btn-outline-secondary" type="button"
id="copyApiSecret"
onclick="copyToClipboard('id_api_secret_generated')"
title="{% trans 'Copy to clipboard' %}">
<i class="fas fa-copy"></i>
</button>
</div>
{% if form.api_secret_generated.errors %}
<div class="invalid-feedback d-block">{{ form.api_secret_generated.errors }}</div>
{% endif %}
</div>
</div>
<!-- Form Actions -->
<div class="row">
<div class="col-12">
<hr>
<div class="d-flex justify-content-between">
<a href="{% url 'source_list' %}" class="btn btn-secondary">
<i class="fas fa-times me-2"></i>
{% trans "Cancel" %}
</a>
<div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>
{% trans "Save Source" %}
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Function to copy text to clipboard
function copyToClipboard(elementId) {
const element = document.getElementById(elementId);
if (element) {
const text = element.value;
navigator.clipboard.writeText(text).then(function() {
// Show success message
const button = event.target.closest('button');
if (button) {
const originalContent = button.innerHTML;
button.innerHTML = '<i class="fas fa-check"></i>';
button.classList.add('btn-success');
button.classList.remove('btn-outline-secondary');
setTimeout(function() {
button.innerHTML = originalContent;
button.classList.remove('btn-success');
button.classList.add('btn-outline-secondary');
}, 2000);
}
}).catch(function(err) {
console.error('Failed to copy text: ', err);
alert('{% trans "Failed to copy to clipboard" %}');
});
} else {
console.error('Element not found:', elementId);
}
}
// Function to generate random key
function generateRandomKey(elementId, length) {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
let result = '';
for (let i = 0; i < length; i++) {
result += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
}
console.log(elementId);
const element = document.getElementById(elementId);
if (element) {
element.value = result;
// Show success animation on the generate button
const button = event.target.closest('button');
if (button) {
const originalContent = button.innerHTML;
button.innerHTML = '<i class="fas fa-check"></i>';
button.classList.add('btn-success');
button.classList.remove('btn-outline-secondary');
setTimeout(function() {
button.innerHTML = originalContent;
button.classList.remove('btn-success');
button.classList.add('btn-outline-secondary');
}, 1500);
}
} else {
console.error('Element not found:', elementId);
}
}
// Initialize after DOM is fully loaded
document.addEventListener('DOMContentLoaded', function() {
const generateKeysCheckbox = document.getElementById('id_generate_keys');
if (generateKeysCheckbox) {
// If API keys are already generated, show them
const apiKeyField = document.getElementById('id_api_key_generated');
const apiSecretField = document.getElementById('id_api_secret_generated');
if (apiKeyField && apiSecretField && (apiKeyField.value || apiSecretField.value)) {
generateKeysCheckbox.checked = true;
}
}
});
</script>
{% endblock %}

View File

@ -1,209 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Sources" %} | {% trans "Recruitment System" %}{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0 text-gray-800">
<i class="fas fa-database me-2"></i>
{% trans "Data Sources" %}
</h1>
<a href="{% url 'source_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>
{% trans "Create New Source" %}
</a>
</div>
<!-- Search Bar -->
<div class="card shadow mb-4">
<div class="card-body">
<form method="get" class="row g-3">
<div class="col-md-10">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-search"></i>
</span>
<input type="text" name="search" class="form-control"
placeholder="{% trans 'Search by name, type, or description...' %}"
value="{{ search_query }}">
</div>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-secondary w-100">
<i class="fas fa-filter me-2"></i>
{% trans "Filter" %}
</button>
</div>
</form>
</div>
</div>
<!-- Sources List -->
<div class="card shadow">
<div class="card-header py-3 d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary">
{% trans "Available Sources" %}
</h6>
<span class="badge bg-secondary">
{{ page_obj.paginator.count }} {% trans "sources" %}
</span>
</div>
<div class="card-body">
{% if sources %}
<div class="table-responsive">
<table class="table table-bordered" id="dataTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Type" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "API Key" %}</th>
<th>{% trans "Created By" %}</th>
<th>{% trans "Created At" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for source in sources %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="me-3">
<div class="icon-circle bg-primary text-white">
<i class="fas fa-server"></i>
</div>
</div>
<div>
<div class="fw-bold">{{ source.name }}</div>
<small class="text-muted">{{ source.description|truncatewords:10 }}</small>
</div>
</div>
</td>
<td>
<span class="badge bg-info">{{ source.source_type }}</span>
</td>
<td>
{% if source.is_active %}
<span class="badge bg-success">
<i class="fas fa-check-circle me-1"></i>
{% trans "Active" %}
</span>
{% else %}
<span class="badge bg-danger">
<i class="fas fa-times-circle me-1"></i>
{% trans "Inactive" %}
</span>
{% endif %}
</td>
<td>
{% if source.api_key %}
<code class="text-muted">{{ source.api_key|slice:":8" }}...</code>
{% else %}
<span class="text-muted">
<i class="fas fa-key me-1"></i>
{% trans "Not generated" %}
</span>
{% endif %}
</td>
<td>{{ source.created_by }}</td>
<td>{{ source.created_at|date:"M d, Y" }}</td>
<td>
<div class="btn-group" role="group">
<a href="{% url 'source_detail' source.pk %}"
class="btn btn-sm btn-outline-primary"
title="{% trans 'View Details' %}">
<i class="fas fa-eye"></i>
</a>
<a href="{% url 'source_update' source.pk %}"
class="btn btn-sm btn-outline-secondary"
title="{% trans 'Edit' %}">
<i class="fas fa-edit"></i>
</a>
<a href="{% url 'source_delete' source.pk %}"
class="btn btn-sm btn-outline-danger"
title="{% trans 'Delete' %}">
<i class="fas fa-trash"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if is_paginated %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1{% if search_query %}&search={{ search_query }}{% endif %}"
aria-label="{% trans 'First' %}">
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}"
aria-label="{% trans 'Previous' %}">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}{% if search_query %}&search={{ search_query }}{% endif %}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}"
aria-label="{% trans 'Next' %}">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if search_query %}&search={{ search_query }}{% endif %}"
aria-label="{% trans 'Last' %}">
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
<h5 class="text-muted">{% trans "No sources found" %}</h5>
<p class="text-muted mb-4">
{% trans "Get started by creating your first data source." %}
</p>
<a href="{% url 'source_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>
{% trans "Create Source" %}
</a>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
$(document).ready(function() {
// Add any DataTables initialization or other JavaScript here
});
</script>
{% endblock %}