648 lines
24 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
)
# 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:
model = Candidate
fields = ['job', 'first_name', 'last_name', 'phone', 'email', 'resume',]
labels = {
'first_name': _('First Name'),
'last_name': _('Last Name'),
'phone': _('Phone'),
'email': _('Email'),
'resume': _('Resume'),
}
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'}),
}
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('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': 'Assistant Professor of Computer Science',
'required': True
}),
'department': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Computer Science, Human Resources, etc.'
}),
'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 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'}),
}