1426 lines
52 KiB
Python
1426 lines
52 KiB
Python
from django import forms
|
|
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,HiringAgency,
|
|
AgencyJobAssignment, AgencyAccessLink,Participants
|
|
)
|
|
# from django_summernote.widgets import SummernoteWidget
|
|
from django_ckeditor_5.widgets import CKEditor5Widget
|
|
import secrets
|
|
import string
|
|
from django.core.exceptions import ValidationError
|
|
from django.utils import timezone
|
|
|
|
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):
|
|
"""Simple form for creating and editing sources"""
|
|
|
|
class Meta:
|
|
model = Source
|
|
fields = [
|
|
'name', 'source_type', 'description', 'ip_address', 'is_active'
|
|
]
|
|
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'
|
|
}),
|
|
'is_active': forms.CheckboxInput(attrs={
|
|
'class': 'form-check-input'
|
|
}),
|
|
}
|
|
|
|
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'
|
|
|
|
self.helper.layout = Layout(
|
|
Field('name', css_class='form-control'),
|
|
Field('source_type', css_class='form-control'),
|
|
Field('ip_address', css_class='form-control'),
|
|
Field('is_active', css_class='form-check-input'),
|
|
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
|
|
|
|
class SourceAdvancedForm(forms.ModelForm):
|
|
"""Advanced 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:
|
|
model = Candidate
|
|
fields = ['job', 'first_name', 'last_name', 'phone', 'email','hiring_source','hiring_agency', 'resume',]
|
|
labels = {
|
|
'first_name': _('First Name'),
|
|
'last_name': _('Last Name'),
|
|
'phone': _('Phone'),
|
|
'email': _('Email'),
|
|
'resume': _('Resume'),
|
|
'hiring_source': _('Hiring Type'),
|
|
'hiring_agency': _('Hiring Agency'),
|
|
}
|
|
widgets = {
|
|
'first_name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Enter first name')}),
|
|
'last_name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Enter last name')}),
|
|
'phone': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Enter phone number')}),
|
|
'email': forms.EmailInput(attrs={'class': 'form-control', 'placeholder': _('Enter email')}),
|
|
'stage': forms.Select(attrs={'class': 'form-select'}),
|
|
'hiring_source': forms.Select(attrs={'class': 'form-select'}),
|
|
'hiring_agency': forms.Select(attrs={'class': 'form-select'}),
|
|
}
|
|
|
|
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'
|
|
|
|
# Make job field read-only if it's being pre-populated
|
|
job_value = self.initial.get('job')
|
|
if job_value:
|
|
self.fields['job'].widget.attrs['readonly'] = True
|
|
|
|
self.helper.layout = Layout(
|
|
Field('job', css_class='form-control'),
|
|
Field('first_name', css_class='form-control'),
|
|
Field('last_name', css_class='form-control'),
|
|
Field('phone', css_class='form-control'),
|
|
Field('email', css_class='form-control'),
|
|
Field('stage', css_class='form-control'),
|
|
Field('hiring_source', css_class='form-control'),
|
|
Field('hiring_agency', css_class='form-control'),
|
|
Field('resume', css_class='form-control'),
|
|
Submit('submit', _('Submit'), css_class='btn btn-primary')
|
|
|
|
)
|
|
|
|
class CandidateStageForm(forms.ModelForm):
|
|
"""Form specifically for updating candidate stage with validation"""
|
|
|
|
class Meta:
|
|
model = Candidate
|
|
fields = ['stage']
|
|
labels = {
|
|
'stage': _('New Application Stage'),
|
|
}
|
|
widgets = {
|
|
'stage': forms.Select(attrs={'class': 'form-select'}),
|
|
}
|
|
|
|
class ZoomMeetingForm(forms.ModelForm):
|
|
class Meta:
|
|
model = ZoomMeeting
|
|
fields = ['topic', 'start_time', 'duration']
|
|
labels = {
|
|
'topic': _('Topic'),
|
|
'start_time': _('Start Time'),
|
|
'duration': _('Duration'),
|
|
}
|
|
widgets = {
|
|
'topic': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Enter meeting topic'),}),
|
|
'start_time': forms.DateTimeInput(attrs={'class': 'form-control','type': 'datetime-local'}),
|
|
'duration': forms.NumberInput(attrs={'class': 'form-control','min': 1, 'placeholder': _('60')}),
|
|
}
|
|
|
|
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'
|
|
self.helper.layout = Layout(
|
|
Field('topic', css_class='form-control'),
|
|
Field('start_time', css_class='form-control'),
|
|
Field('duration', css_class='form-control'),
|
|
Submit('submit', _('Create Meeting'), css_class='btn btn-primary')
|
|
)
|
|
|
|
class TrainingMaterialForm(forms.ModelForm):
|
|
class Meta:
|
|
model = TrainingMaterial
|
|
fields = ['title', 'content', 'video_link', 'file']
|
|
labels = {
|
|
'title': _('Title'),
|
|
'content': _('Content'),
|
|
'video_link': _('Video Link'),
|
|
'file': _('File'),
|
|
}
|
|
widgets = {
|
|
'title': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Enter material title')}),
|
|
'content': CKEditor5Widget(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'}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.helper = FormHelper()
|
|
self.helper.form_method = 'post'
|
|
self.helper.form_class = 'g-3'
|
|
|
|
self.helper.layout = Layout(
|
|
'title',
|
|
'content',
|
|
Row(
|
|
Column('video_link', css_class='col-md-6'),
|
|
Column('file', css_class='col-md-6'),
|
|
css_class='g-3 mb-4'
|
|
),
|
|
Div(
|
|
Submit('submit', _('Create Material'),
|
|
css_class='btn btn-main-action'),
|
|
css_class='col-12 mt-4'
|
|
)
|
|
)
|
|
|
|
|
|
class JobPostingForm(forms.ModelForm):
|
|
"""Form for creating and editing job postings"""
|
|
|
|
class Meta:
|
|
model = JobPosting
|
|
fields = [
|
|
'title', 'department', 'job_type', 'workplace_type',
|
|
'location_city', 'location_state', 'location_country',
|
|
'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': '',
|
|
'required': True
|
|
}),
|
|
'department': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': ''
|
|
}),
|
|
'job_type': forms.Select(attrs={
|
|
'class': 'form-select',
|
|
'required': True
|
|
}),
|
|
'workplace_type': forms.Select(attrs={
|
|
'class': 'form-select',
|
|
'required': True
|
|
}),
|
|
|
|
# Location
|
|
'location_city': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Boston'
|
|
}),
|
|
'location_state': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'MA'
|
|
}),
|
|
'location_country': forms.TextInput(attrs={
|
|
'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_deadline': forms.DateInput(attrs={
|
|
'class': 'form-control',
|
|
'type': 'date',
|
|
'required': True
|
|
}),
|
|
|
|
'open_positions': forms.NumberInput(attrs={
|
|
'class': 'form-control',
|
|
'min': 1,
|
|
'placeholder': 'Number of open positions'
|
|
}),
|
|
'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'
|
|
}),
|
|
'reporting_to': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Department Chair, Director, etc.'
|
|
}),
|
|
|
|
'max_applications': forms.NumberInput(attrs={
|
|
'class': 'form-control',
|
|
'min': 1,
|
|
'placeholder': 'Maximum number of applicants'
|
|
}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
# Now call the parent __init__ with remaining args
|
|
super().__init__(*args, **kwargs)
|
|
|
|
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')
|
|
if hash_tags:
|
|
tags = [tag.strip() for tag in hash_tags.split(',') if tag.strip()]
|
|
for tag in tags:
|
|
if not tag.startswith('#'):
|
|
raise forms.ValidationError(
|
|
"Each hashtag must start with '#' symbol and must be comma(,) sepearted.")
|
|
return ','.join(tags)
|
|
return hash_tags # Allow blank
|
|
|
|
def clean_title(self):
|
|
title = self.cleaned_data.get('title')
|
|
if not title or len(title.strip()) < 3:
|
|
raise forms.ValidationError("Job title must be at least 3 characters long.")
|
|
if len(title) > 200:
|
|
raise forms.ValidationError("Job title cannot exceed 200 characters.")
|
|
return title.strip()
|
|
|
|
def clean_description(self):
|
|
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() # 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
|
|
|
|
class JobPostingImageForm(forms.ModelForm):
|
|
class Meta:
|
|
model=JobPostingImage
|
|
fields=['post_image']
|
|
|
|
class FormTemplateForm(forms.ModelForm):
|
|
"""Form for creating form templates"""
|
|
class Meta:
|
|
model = FormTemplate
|
|
fields = ['job','name', 'description', 'is_active']
|
|
labels = {
|
|
'job': _('Job'),
|
|
'name': _('Template Name'),
|
|
'description': _('Description'),
|
|
'is_active': _('Active'),
|
|
}
|
|
widgets = {
|
|
'name': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': _('Enter template name'),
|
|
'required': True
|
|
}),
|
|
'description': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 3,
|
|
'placeholder': _('Enter template description (optional)')
|
|
}),
|
|
'is_active': forms.CheckboxInput(attrs={
|
|
'class': 'form-check-input'
|
|
})
|
|
}
|
|
|
|
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'
|
|
self.helper.layout = Layout(
|
|
Field('job', css_class='form-control'),
|
|
Field('name', css_class='form-control'),
|
|
Field('description', css_class='form-control'),
|
|
Field('is_active', css_class='form-check-input'),
|
|
Submit('submit', _('Create Template'), css_class='btn btn-primary mt-3')
|
|
)
|
|
|
|
class BreakTimeForm(forms.Form):
|
|
"""
|
|
A simple Form used for the BreakTimeFormSet.
|
|
It is not a ModelForm because the data is stored directly in InterviewSchedule's JSONField,
|
|
not in a separate BreakTime model instance.
|
|
"""
|
|
start_time = forms.TimeField(
|
|
widget=forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
|
|
label="Start Time"
|
|
)
|
|
end_time = forms.TimeField(
|
|
widget=forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
|
|
label="End Time"
|
|
)
|
|
|
|
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',
|
|
'break_start_time', 'break_end_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'}),
|
|
}
|
|
|
|
def __init__(self, slug, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
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')
|
|
return [int(day) for day in working_days]
|
|
|
|
class MeetingCommentForm(forms.ModelForm):
|
|
"""Form for creating and editing meeting comments"""
|
|
class Meta:
|
|
model = MeetingComment
|
|
fields = ['content']
|
|
widgets = {
|
|
'content': CKEditor5Widget(
|
|
attrs={'class': 'form-control', 'placeholder': _('Enter your comment or note')},
|
|
config_name='extends'
|
|
),
|
|
}
|
|
labels = {
|
|
'content': _('Comment'),
|
|
}
|
|
|
|
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'
|
|
self.helper.layout = Layout(
|
|
Field('content', css_class='form-control'),
|
|
Submit('submit', _('Add Comment'), css_class='btn btn-primary mt-3')
|
|
)
|
|
|
|
class InterviewForm(forms.ModelForm):
|
|
class Meta:
|
|
model = ScheduledInterview
|
|
fields = ['job','candidate']
|
|
|
|
class ProfileImageUploadForm(forms.ModelForm):
|
|
class Meta:
|
|
model=Profile
|
|
fields=['profile_image']
|
|
|
|
class StaffUserCreationForm(UserCreationForm):
|
|
email = forms.EmailField(required=True)
|
|
first_name = forms.CharField(max_length=30, required=True)
|
|
last_name = forms.CharField(max_length=150, required=True)
|
|
|
|
class Meta:
|
|
model = User
|
|
fields = ("email", "first_name", "last_name", "password1", "password2")
|
|
|
|
def clean_email(self):
|
|
email = self.cleaned_data["email"]
|
|
if User.objects.filter(email=email).exists():
|
|
raise forms.ValidationError("A user with this email already exists.")
|
|
return email
|
|
|
|
def generate_username(self, email):
|
|
"""Generate a valid, unique username from email."""
|
|
prefix = email.split('@')[0].lower()
|
|
username = re.sub(r'[^a-z0-9._]', '', prefix)
|
|
if not username:
|
|
username = 'user'
|
|
base = username
|
|
counter = 1
|
|
while User.objects.filter(username=username).exists():
|
|
username = f"{base}{counter}"
|
|
counter += 1
|
|
return username
|
|
|
|
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.generate_username(user.email)
|
|
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 LinkedPostContentForm(forms.ModelForm):
|
|
class Meta:
|
|
model = JobPosting
|
|
fields = ['linkedin_post_formated_data']
|
|
|
|
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 HiringAgencyForm(forms.ModelForm):
|
|
"""Form for creating and editing hiring agencies"""
|
|
|
|
class Meta:
|
|
model = HiringAgency
|
|
fields = [
|
|
'name', 'contact_person', 'email', 'phone',
|
|
'website', 'country', 'address', 'notes'
|
|
]
|
|
widgets = {
|
|
'name': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Enter agency name',
|
|
'required': True
|
|
}),
|
|
'contact_person': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Enter contact person name'
|
|
}),
|
|
'email': forms.EmailInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'agency@example.com'
|
|
}),
|
|
'phone': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': '+966 50 123 4567'
|
|
}),
|
|
'website': forms.URLInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'https://www.agency.com'
|
|
}),
|
|
'country': forms.Select(attrs={
|
|
'class': 'form-select'
|
|
}),
|
|
'address': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 3,
|
|
'placeholder': 'Enter agency address'
|
|
}),
|
|
'notes': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 3,
|
|
'placeholder': 'Internal notes about the agency'
|
|
}),
|
|
}
|
|
labels = {
|
|
'name': _('Agency Name'),
|
|
'contact_person': _('Contact Person'),
|
|
'email': _('Email Address'),
|
|
'phone': _('Phone Number'),
|
|
'website': _('Website'),
|
|
'country': _('Country'),
|
|
'address': _('Address'),
|
|
'notes': _('Internal Notes'),
|
|
}
|
|
|
|
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'
|
|
|
|
self.helper.layout = Layout(
|
|
Field('name', css_class='form-control'),
|
|
Field('contact_person', css_class='form-control'),
|
|
Row(
|
|
Column('email', css_class='col-md-6'),
|
|
Column('phone', css_class='col-md-6'),
|
|
css_class='g-3 mb-3'
|
|
),
|
|
Field('website', css_class='form-control'),
|
|
Field('country', css_class='form-control'),
|
|
Field('address', css_class='form-control'),
|
|
Field('notes', css_class='form-control'),
|
|
Div(
|
|
Submit('submit', _('Save Agency'), css_class='btn btn-main-action'),
|
|
css_class='col-12 mt-4'
|
|
)
|
|
)
|
|
|
|
def clean_name(self):
|
|
"""Ensure agency name is unique"""
|
|
name = self.cleaned_data.get('name')
|
|
if name:
|
|
instance = self.instance
|
|
if not instance.pk: # Creating new instance
|
|
if HiringAgency.objects.filter(name=name).exists():
|
|
raise ValidationError('An agency with this name already exists.')
|
|
else: # Editing existing instance
|
|
if HiringAgency.objects.filter(name=name).exclude(pk=instance.pk).exists():
|
|
raise ValidationError('An agency with this name already exists.')
|
|
return name.strip()
|
|
|
|
def clean_email(self):
|
|
"""Validate email format and uniqueness"""
|
|
email = self.cleaned_data.get('email')
|
|
if email:
|
|
# Check email format
|
|
if not '@' in email or '.' not in email.split('@')[1]:
|
|
raise ValidationError('Please enter a valid email address.')
|
|
|
|
# Check uniqueness (optional - remove if multiple agencies can have same email)
|
|
instance = self.instance
|
|
if not instance.pk: # Creating new instance
|
|
if HiringAgency.objects.filter(email=email).exists():
|
|
raise ValidationError('An agency with this email already exists.')
|
|
else: # Editing existing instance
|
|
if HiringAgency.objects.filter(email=email).exclude(pk=instance.pk).exists():
|
|
raise ValidationError('An agency with this email already exists.')
|
|
return email.lower().strip() if email else email
|
|
|
|
def clean_phone(self):
|
|
"""Validate phone number format"""
|
|
phone = self.cleaned_data.get('phone')
|
|
if phone:
|
|
# Remove common formatting characters
|
|
clean_phone = ''.join(c for c in phone if c.isdigit() or c in '+')
|
|
if len(clean_phone) < 10:
|
|
raise ValidationError('Phone number must be at least 10 digits long.')
|
|
return phone.strip() if phone else phone
|
|
|
|
def clean_website(self):
|
|
"""Validate website URL"""
|
|
website = self.cleaned_data.get('website')
|
|
if website:
|
|
if not website.startswith(('http://', 'https://')):
|
|
website = 'https://' + website
|
|
validator = URLValidator()
|
|
try:
|
|
validator(website)
|
|
except ValidationError:
|
|
raise ValidationError('Please enter a valid website URL.')
|
|
return website
|
|
|
|
|
|
class AgencyJobAssignmentForm(forms.ModelForm):
|
|
"""Form for creating and editing agency job assignments"""
|
|
|
|
class Meta:
|
|
model = AgencyJobAssignment
|
|
fields = [
|
|
'agency', 'job', 'max_candidates', 'deadline_date','admin_notes'
|
|
]
|
|
widgets = {
|
|
'agency': forms.Select(attrs={'class': 'form-select'}),
|
|
'job': forms.Select(attrs={'class': 'form-select'}),
|
|
'max_candidates': forms.NumberInput(attrs={
|
|
'class': 'form-control',
|
|
'min': 1,
|
|
'placeholder': 'Maximum number of candidates'
|
|
}),
|
|
'deadline_date': forms.DateTimeInput(attrs={
|
|
'class': 'form-control',
|
|
'type': 'datetime-local'
|
|
}),
|
|
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
'status': forms.Select(attrs={'class': 'form-select'}),
|
|
'admin_notes': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 3,
|
|
'placeholder': 'Internal notes about this assignment'
|
|
}),
|
|
}
|
|
labels = {
|
|
'agency': _('Agency'),
|
|
'job': _('Job Posting'),
|
|
'max_candidates': _('Maximum Candidates'),
|
|
'deadline_date': _('Deadline Date'),
|
|
'is_active': _('Is Active'),
|
|
'status': _('Status'),
|
|
'admin_notes': _('Admin Notes'),
|
|
}
|
|
|
|
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'
|
|
|
|
# Filter jobs to only show active jobs
|
|
self.fields['job'].queryset = JobPosting.objects.filter(
|
|
status='ACTIVE'
|
|
).order_by('-created_at')
|
|
|
|
self.helper.layout = Layout(
|
|
Row(
|
|
Column('agency', css_class='col-md-6'),
|
|
Column('job', css_class='col-md-6'),
|
|
css_class='g-3 mb-3'
|
|
),
|
|
Row(
|
|
Column('max_candidates', css_class='col-md-6'),
|
|
Column('deadline_date', css_class='col-md-6'),
|
|
css_class='g-3 mb-3'
|
|
),
|
|
Row(
|
|
Column('is_active', css_class='col-md-6'),
|
|
Column('status', css_class='col-md-6'),
|
|
css_class='g-3 mb-3'
|
|
),
|
|
Field('admin_notes', css_class='form-control'),
|
|
Div(
|
|
Submit('submit', _('Save Assignment'), css_class='btn btn-main-action'),
|
|
css_class='col-12 mt-4'
|
|
)
|
|
)
|
|
|
|
def clean_deadline_date(self):
|
|
"""Validate deadline date is in the future"""
|
|
deadline_date = self.cleaned_data.get('deadline_date')
|
|
if deadline_date and deadline_date <= timezone.now():
|
|
raise ValidationError('Deadline date must be in the future.')
|
|
return deadline_date
|
|
|
|
def clean_max_candidates(self):
|
|
"""Validate maximum candidates is positive"""
|
|
max_candidates = self.cleaned_data.get('max_candidates')
|
|
if max_candidates and max_candidates <= 0:
|
|
raise ValidationError('Maximum candidates must be greater than 0.')
|
|
return max_candidates
|
|
|
|
def clean(self):
|
|
"""Check for duplicate assignments"""
|
|
cleaned_data = super().clean()
|
|
agency = cleaned_data.get('agency')
|
|
job = cleaned_data.get('job')
|
|
|
|
if agency and job:
|
|
# Check if this assignment already exists
|
|
existing = AgencyJobAssignment.objects.filter(
|
|
agency=agency, job=job
|
|
).exclude(pk=self.instance.pk).first()
|
|
|
|
if existing:
|
|
raise ValidationError(
|
|
f'This job is already assigned to {agency.name}. '
|
|
f'Current status: {existing.get_status_display()}'
|
|
)
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class AgencyAccessLinkForm(forms.ModelForm):
|
|
"""Form for creating and managing agency access links"""
|
|
|
|
class Meta:
|
|
model = AgencyAccessLink
|
|
fields = [
|
|
'assignment', 'expires_at', 'is_active'
|
|
]
|
|
widgets = {
|
|
'assignment': forms.Select(attrs={'class': 'form-select'}),
|
|
'expires_at': forms.DateTimeInput(attrs={
|
|
'class': 'form-control',
|
|
'type': 'datetime-local'
|
|
}),
|
|
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
}
|
|
labels = {
|
|
'assignment': _('Assignment'),
|
|
'expires_at': _('Expires At'),
|
|
'is_active': _('Is Active'),
|
|
}
|
|
|
|
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'
|
|
|
|
# Filter assignments to only show active ones without existing links
|
|
self.fields['assignment'].queryset = AgencyJobAssignment.objects.filter(
|
|
is_active=True,
|
|
status='ACTIVE'
|
|
).exclude(
|
|
access_link__isnull=False
|
|
).order_by('-created_at')
|
|
|
|
self.helper.layout = Layout(
|
|
Field('assignment', css_class='form-control'),
|
|
Field('expires_at', css_class='form-control'),
|
|
Field('is_active', css_class='form-check-input'),
|
|
Div(
|
|
Submit('submit', _('Create Access Link'), css_class='btn btn-main-action'),
|
|
css_class='col-12 mt-4'
|
|
)
|
|
)
|
|
|
|
def clean_expires_at(self):
|
|
"""Validate expiration date is in the future"""
|
|
expires_at = self.cleaned_data.get('expires_at')
|
|
if expires_at and expires_at <= timezone.now():
|
|
raise ValidationError('Expiration date must be in the future.')
|
|
return expires_at
|
|
|
|
|
|
# Agency messaging forms removed - AgencyMessage model has been deleted
|
|
|
|
|
|
class AgencyCandidateSubmissionForm(forms.ModelForm):
|
|
"""Form for agencies to submit candidates (simplified - resume + basic info)"""
|
|
|
|
class Meta:
|
|
model = Candidate
|
|
fields = [
|
|
'first_name', 'last_name', 'email', 'phone', 'resume'
|
|
]
|
|
widgets = {
|
|
'first_name': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'First Name',
|
|
'required': True
|
|
}),
|
|
'last_name': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Last Name',
|
|
'required': True
|
|
}),
|
|
'email': forms.EmailInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'email@example.com',
|
|
'required': True
|
|
}),
|
|
'phone': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': '+966 50 123 4567',
|
|
'required': True
|
|
}),
|
|
'resume': forms.FileInput(attrs={
|
|
'class': 'form-control',
|
|
'accept': '.pdf,.doc,.docx',
|
|
'required': True
|
|
}),
|
|
}
|
|
labels = {
|
|
'first_name': _('First Name'),
|
|
'last_name': _('Last Name'),
|
|
'email': _('Email Address'),
|
|
'phone': _('Phone Number'),
|
|
'resume': _('Resume'),
|
|
}
|
|
|
|
def __init__(self, assignment, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.assignment = assignment
|
|
self.helper = FormHelper()
|
|
self.helper.form_method = 'post'
|
|
self.helper.form_class = 'g-3'
|
|
self.helper.enctype = 'multipart/form-data'
|
|
|
|
self.helper.layout = Layout(
|
|
Row(
|
|
Column('first_name', css_class='col-md-6'),
|
|
Column('last_name', css_class='col-md-6'),
|
|
css_class='g-3 mb-3'
|
|
),
|
|
Row(
|
|
Column('email', css_class='col-md-6'),
|
|
Column('phone', css_class='col-md-6'),
|
|
css_class='g-3 mb-3'
|
|
),
|
|
Field('resume', css_class='form-control'),
|
|
Div(
|
|
Submit('submit', _('Submit Candidate'), css_class='btn btn-main-action'),
|
|
css_class='col-12 mt-4'
|
|
)
|
|
)
|
|
|
|
def clean_email(self):
|
|
"""Validate email format and check for duplicates in the same job"""
|
|
email = self.cleaned_data.get('email')
|
|
if email:
|
|
# Check if candidate with this email already exists for this job
|
|
existing_candidate = Candidate.objects.filter(
|
|
email=email.lower().strip(),
|
|
job=self.assignment.job
|
|
).first()
|
|
|
|
if existing_candidate:
|
|
raise ValidationError(
|
|
f'A candidate with this email has already applied for {self.assignment.job.title}.'
|
|
)
|
|
return email.lower().strip() if email else email
|
|
|
|
def clean_resume(self):
|
|
"""Validate resume file"""
|
|
resume = self.cleaned_data.get('resume')
|
|
if resume:
|
|
# Check file size (max 5MB)
|
|
if resume.size > 5 * 1024 * 1024:
|
|
raise ValidationError('Resume file size must be less than 5MB.')
|
|
|
|
# Check file extension
|
|
allowed_extensions = ['.pdf', '.doc', '.docx']
|
|
file_extension = resume.name.lower().split('.')[-1]
|
|
if f'.{file_extension}' not in allowed_extensions:
|
|
raise ValidationError(
|
|
'Resume must be in PDF, DOC, or DOCX format.'
|
|
)
|
|
return resume
|
|
|
|
def save(self, commit=True):
|
|
"""Override save to set additional fields"""
|
|
instance = super().save(commit=False)
|
|
|
|
# Set required fields for agency submission
|
|
instance.job = self.assignment.job
|
|
instance.hiring_agency = self.assignment.agency
|
|
instance.stage = Candidate.Stage.APPLIED
|
|
instance.applicant_status = Candidate.ApplicantType.CANDIDATE
|
|
instance.applied = True
|
|
|
|
if commit:
|
|
instance.save()
|
|
# Increment the assignment's submitted count
|
|
self.assignment.increment_submission_count()
|
|
return instance
|
|
|
|
|
|
class AgencyLoginForm(forms.Form):
|
|
"""Form for agencies to login with token and password"""
|
|
|
|
token = forms.CharField(
|
|
widget=forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Enter your access token'
|
|
}),
|
|
label=_('Access Token'),
|
|
required=True
|
|
)
|
|
|
|
password = forms.CharField(
|
|
widget=forms.PasswordInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Enter your password'
|
|
}),
|
|
label=_('Password'),
|
|
required=True
|
|
)
|
|
|
|
# def __init__(self, *args, **kwargs):
|
|
# super().__init__(*args, **kwargs)
|
|
# self.helper = FormHelper()
|
|
# self.helper.form_method = 'post'
|
|
# self.helper.form_class = 'g-3'
|
|
|
|
# self.helper.layout = Layout(
|
|
# Field('token', css_class='form-control'),
|
|
# Field('password', css_class='form-control'),
|
|
# Div(
|
|
# Submit('submit', _('Login'), css_class='btn btn-main-action w-100'),
|
|
# css_class='col-12 mt-4'
|
|
# )
|
|
# )
|
|
|
|
def clean(self):
|
|
"""Validate token and password combination"""
|
|
cleaned_data = super().clean()
|
|
token = cleaned_data.get('token')
|
|
password = cleaned_data.get('password')
|
|
if token and password:
|
|
try:
|
|
access_link = AgencyAccessLink.objects.get(
|
|
unique_token=token,
|
|
is_active=True
|
|
)
|
|
|
|
if not access_link.is_valid:
|
|
if access_link.is_expired:
|
|
raise ValidationError('This access link has expired.')
|
|
else:
|
|
raise ValidationError('This access link is no longer active.')
|
|
|
|
if access_link.access_password != password:
|
|
raise ValidationError('Invalid password.')
|
|
|
|
# Store the access_link for use in the view
|
|
self.validated_access_link = access_link
|
|
except AgencyAccessLink.DoesNotExist:
|
|
print("Access link does not exist")
|
|
raise ValidationError('Invalid access token.')
|
|
|
|
return cleaned_data
|
|
|
|
|
|
|
|
|
|
|
|
#participants form
|
|
class ParticipantsForm(forms.ModelForm):
|
|
"""Form for creating and editing Participants"""
|
|
|
|
class Meta:
|
|
model = Participants
|
|
fields = ['name', 'email', 'phone', 'designation']
|
|
widgets = {
|
|
'name': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Enter participant name',
|
|
'required': True
|
|
}),
|
|
'email': forms.EmailInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Enter email address',
|
|
'required': True
|
|
}),
|
|
'phone': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Enter phone number'
|
|
}),
|
|
'designation': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Enter designation'
|
|
}),
|
|
# 'jobs': forms.CheckboxSelectMultiple(),
|
|
}
|
|
|
|
|
|
class ParticipantsSelectForm(forms.ModelForm):
|
|
"""Form for selecting Participants"""
|
|
|
|
participants=forms.ModelMultipleChoiceField(
|
|
queryset=Participants.objects.all(),
|
|
widget=forms.CheckboxSelectMultiple,
|
|
required=False,
|
|
label=_("Select Participants"))
|
|
|
|
users=forms.ModelMultipleChoiceField(
|
|
queryset=User.objects.all(),
|
|
widget=forms.CheckboxSelectMultiple,
|
|
required=False,
|
|
label=_("Select Users"))
|
|
|
|
class Meta:
|
|
model = JobPosting
|
|
fields = ['participants','users'] # No direct fields from Participants model
|
|
|
|
|
|
class CandidateEmailForm(forms.Form):
|
|
"""Form for composing emails to participants about a candidate"""
|
|
|
|
subject = forms.CharField(
|
|
max_length=200,
|
|
widget=forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Enter email subject',
|
|
'required': True
|
|
}),
|
|
label=_('Subject'),
|
|
required=True
|
|
)
|
|
|
|
message = forms.CharField(
|
|
widget=forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 8,
|
|
'placeholder': 'Enter your message here...',
|
|
'required': True
|
|
}),
|
|
label=_('Message'),
|
|
required=True
|
|
)
|
|
|
|
recipients = forms.MultipleChoiceField(
|
|
widget=forms.CheckboxSelectMultiple(attrs={
|
|
'class': 'form-check'
|
|
}),
|
|
label=_('Recipients'),
|
|
required=True
|
|
)
|
|
|
|
include_candidate_info = forms.BooleanField(
|
|
widget=forms.CheckboxInput(attrs={
|
|
'class': 'form-check-input'
|
|
}),
|
|
label=_('Include candidate information'),
|
|
initial=True,
|
|
required=False
|
|
)
|
|
|
|
include_meeting_details = forms.BooleanField(
|
|
widget=forms.CheckboxInput(attrs={
|
|
'class': 'form-check-input'
|
|
}),
|
|
label=_('Include meeting details'),
|
|
initial=True,
|
|
required=False
|
|
)
|
|
|
|
|
|
def __init__(self, job, candidate, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.job = job
|
|
self.candidate = candidate
|
|
|
|
# Get all participants and users for this job
|
|
recipient_choices = []
|
|
|
|
# Add job participants
|
|
for participant in job.participants.all():
|
|
recipient_choices.append(
|
|
(f'participant_{participant.id}', f'{participant.name} - {participant.designation} (Participant)')
|
|
)
|
|
|
|
# Add job users
|
|
for user in job.users.all():
|
|
recipient_choices.append(
|
|
(f'user_{user.id}', f'{user.get_full_name() or user.username} - {user.email} (User)')
|
|
)
|
|
|
|
self.fields['recipients'].choices = recipient_choices
|
|
self.fields['recipients'].initial = [choice[0] for choice in recipient_choices] # Select all by default
|
|
|
|
# Set initial subject
|
|
self.fields['subject'].initial = f'Interview Update: {candidate.name} - {job.title}'
|
|
|
|
# Set initial message with candidate and meeting info
|
|
initial_message = self._get_initial_message()
|
|
if initial_message:
|
|
self.fields['message'].initial = initial_message
|
|
|
|
def _get_initial_message(self):
|
|
"""Generate initial message with candidate and meeting information"""
|
|
message_parts = []
|
|
|
|
# Add candidate information
|
|
if self.candidate:
|
|
message_parts.append(f"Candidate Information:")
|
|
message_parts.append(f"Name: {self.candidate.name}")
|
|
message_parts.append(f"Email: {self.candidate.email}")
|
|
message_parts.append(f"Phone: {self.candidate.phone}")
|
|
|
|
# Add latest meeting information if available
|
|
latest_meeting = self.candidate.get_latest_meeting
|
|
if latest_meeting:
|
|
message_parts.append(f"\nMeeting Information:")
|
|
message_parts.append(f"Topic: {latest_meeting.topic}")
|
|
message_parts.append(f"Date & Time: {latest_meeting.start_time.strftime('%B %d, %Y at %I:%M %p')}")
|
|
message_parts.append(f"Duration: {latest_meeting.duration} minutes")
|
|
if latest_meeting.join_url:
|
|
message_parts.append(f"Join URL: {latest_meeting.join_url}")
|
|
|
|
return '\n'.join(message_parts)
|
|
|
|
def clean_recipients(self):
|
|
"""Ensure at least one recipient is selected"""
|
|
recipients = self.cleaned_data.get('recipients')
|
|
if not recipients:
|
|
raise forms.ValidationError(_('Please select at least one recipient.'))
|
|
return recipients
|
|
|
|
def get_email_addresses(self):
|
|
"""Extract email addresses from selected recipients"""
|
|
email_addresses = []
|
|
recipients = self.cleaned_data.get('recipients', [])
|
|
for recipient in recipients:
|
|
if recipient.startswith('participant_'):
|
|
participant_id = recipient.split('_')[1]
|
|
try:
|
|
participant = Participants.objects.get(id=participant_id)
|
|
email_addresses.append(participant.email)
|
|
except Participants.DoesNotExist:
|
|
continue
|
|
elif recipient.startswith('user_'):
|
|
user_id = recipient.split('_')[1]
|
|
try:
|
|
user = User.objects.get(id=user_id)
|
|
email_addresses.append(user.email)
|
|
except User.DoesNotExist:
|
|
continue
|
|
|
|
return list(set(email_addresses)) # Remove duplicates
|
|
|
|
def get_formatted_message(self):
|
|
"""Get the formatted message with optional additional information"""
|
|
message = self.cleaned_data.get('message', '')
|
|
|
|
# Add candidate information if requested
|
|
if self.cleaned_data.get('include_candidate_info') and self.candidate:
|
|
candidate_info = f"\n\n--- Candidate Information ---\n"
|
|
candidate_info += f"Name: {self.candidate.name}\n"
|
|
candidate_info += f"Email: {self.candidate.email}\n"
|
|
candidate_info += f"Phone: {self.candidate.phone}\n"
|
|
message += candidate_info
|
|
|
|
# Add meeting details if requested
|
|
if self.cleaned_data.get('include_meeting_details') and self.candidate:
|
|
latest_meeting = self.candidate.get_latest_meeting
|
|
if latest_meeting:
|
|
meeting_info = f"\n\n--- Meeting Details ---\n"
|
|
meeting_info += f"Topic: {latest_meeting.topic}\n"
|
|
meeting_info += f"Date & Time: {latest_meeting.start_time.strftime('%B %d, %Y at %I:%M %p')}\n"
|
|
meeting_info += f"Duration: {latest_meeting.duration} minutes\n"
|
|
if latest_meeting.join_url:
|
|
meeting_info += f"Join URL: {latest_meeting.join_url}\n"
|
|
message += meeting_info
|
|
|
|
return message
|