2025-12-17 19:06:45 +03:00

2275 lines
81 KiB
Python

import re
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 import get_user_model
from django.contrib.auth.forms import UserCreationForm
from .models import (
Application,
JobPosting,
FormTemplate,
BulkInterviewTemplate,
JobPostingImage,
Note,
ScheduledInterview,
Source,
HiringAgency,
AgencyJobAssignment,
AgencyAccessLink,
Message,
Person,
Document,
CustomUser,
Settings,
Interview
)
from django_ckeditor_5.widgets import CKEditor5Widget
import secrets
import string
from django.core.exceptions import ValidationError
from django.utils import timezone
User = get_user_model()
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","trusted_ips", "is_active"]
widgets = {
"name": forms.TextInput(
attrs={
"class": "form-control",
"required": True,
}
),
"source_type": forms.TextInput(
attrs={
"class": "form-control",
"required": True,
}
),
"description": forms.Textarea(
attrs={
"class": "form-control",
"rows": 3,
}
),
"ip_address": forms.TextInput(
attrs={"class": "form-control",
"required":True},
),
"trusted_ips":forms.TextInput(
attrs={"class": "form-control", "required": False}
),
"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
def clean_ip_address(self):
ip_address=self.cleaned_data.get('ip_address')
if not ip_address:
raise ValidationError(_("Ip address should not be empty"))
return ip_address
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 PersonForm(forms.ModelForm):
class Meta:
model = Person
fields = ["first_name","middle_name", "last_name", "email", "phone","date_of_birth","gpa","national_id","nationality","gender","address"]
widgets = {
"first_name": forms.TextInput(attrs={'class': 'form-control'}),
"middle_name": forms.TextInput(attrs={'class': 'form-control'}),
"last_name": forms.TextInput(attrs={'class': 'form-control'}),
"email": forms.EmailInput(attrs={'class': 'form-control'}),
"phone": forms.TextInput(attrs={'class': 'form-control'}),
"gender": forms.Select(attrs={'class': 'form-control'}),
"date_of_birth": forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
"nationality": forms.Select(attrs={'class': 'form-control select2'}),
"address": forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
"gpa": forms.TextInput(attrs={'class': 'custom-decimal-input'}),
"national_id":forms.NumberInput(attrs={'min': 0, 'step': 1}),
}
def clean_email(self):
email = self.cleaned_data.get('email')
if not email:
return email
if email:
instance = self.instance
qs = CustomUser.objects.filter(email=email) | CustomUser.objects.filter(username=email)
if not instance.pk: # Creating new instance
if qs.exists():
raise ValidationError(_("A user account with this email address already exists. Please use a different email."))
else: # Editing existing instance
# if (
# qs
# .exclude(pk=instance.user.pk)
# .exists()
# ):
# raise ValidationError(_("An user with this email already exists."))
pass
return email.strip()
class ApplicationForm(forms.ModelForm):
class Meta:
model = Application
fields = [
'person',
"job",
"hiring_source",
"hiring_agency",
"resume",
]
labels = {
"person":_("Applicant"),
"resume": _("Resume"),
"hiring_source": _("Hiring Type"),
"hiring_agency": _("Hiring Agency"),
}
widgets = {
"hiring_source": forms.Select(attrs={"class": "form-select"}),
"hiring_agency": forms.Select(attrs={"class": "form-select"}),
}
def __init__(self, *args,current_agency=None,current_job=None,**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"
if current_agency:
# IMPORTANT: Replace 'agency' below with the actual field name
# on your Person model that links it back to the Agency model.
self.fields['person'].queryset = self.fields['person'].queryset.filter(
agency=current_agency
)
self.fields['job'].queryset = self.fields['job'].queryset.filter(
pk=current_job.id
)
self.fields['job'].initial = current_job
self.fields['job'].widget.attrs['readonly'] = True
else:
self.fields['job'].queryset = self.fields['job'].queryset.filter(
status="ACTIVE"
)
# 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("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"),
)
# def clean(self):
# cleaned_data = super().clean()
# job = cleaned_data.get("job")
# agency = cleaned_data.get("hiring_agency")
# person = cleaned_data.get("person")
# if Application.objects.filter(person=person,job=job, hiring_agency=agency).exists():
# raise forms.ValidationError("You have already applied for this job.")
# return cleaned_data
# def save(self, commit=True):
# """Override save to handle person creation/update"""
# instance = super().save(commit=False)
# # Get or create person
# if instance.person:
# person = instance.person
# else:
# # Create new person
# from .models import Person
# person = Person()
# # Update person fields
# person.first_name = self.cleaned_data['first_name']
# person.last_name = self.cleaned_data['last_name']
# person.email = self.cleaned_data['email']
# person.phZoomone = self.cleaned_data['phone']
# if commit:
# person.save()
# instance.person = person
# instance.save()
# return instance
class ApplicationStageForm(forms.ModelForm):
"""Form specifically for updating candidate stage with validation"""
class Meta:
model = Application
fields = ["stage"]
labels = {
"stage": _("New Application Stage"),
}
widgets = {
"stage": forms.Select(attrs={"class": "form-select"}),
}
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,
}
),
}
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 BulkInterviewTemplateForm(forms.ModelForm):
applications = forms.ModelMultipleChoiceField(
queryset=Application.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 = BulkInterviewTemplate
fields = [
'schedule_interview_type',
'topic',
'physical_address',
"applications",
"start_date",
"end_date",
"working_days",
"start_time",
"end_time",
"interview_duration",
"buffer_time",
"break_start_time",
"break_end_time",
]
widgets = {
"topic": forms.TextInput(attrs={"class": "form-control"}),
"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"}
),
"schedule_interview_type":forms.RadioSelect(),
"physical_address": forms.Textarea(
attrs={"class": "form-control", "rows": 3, "placeholder": "Enter physical address if 'In-Person' is selected"}
),
}
def __init__(self, slug, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["applications"].queryset = Application.objects.filter(
job__slug=slug, stage="Interview"
)
self.fields["topic"].initial = "Interview for " + str(
self.fields["applications"].queryset.first().job.title
)
self.fields["start_date"].initial = timezone.now().date()
working_days_initial = [0, 1, 2, 3, 6]
self.fields["working_days"].initial = working_days_initial
self.fields["start_time"].initial = "08:00"
self.fields["end_time"].initial = "14:00"
self.fields["interview_duration"].initial = 30
self.fields["buffer_time"].initial = 10
self.fields["break_start_time"].initial = "11:30"
self.fields["break_end_time"].initial = "12:30"
self.fields["physical_address"].initial = "Airport Road, King Khalid International Airport, Riyadh 11564, Saudi Arabia"
def clean_working_days(self):
working_days = self.cleaned_data.get("working_days")
return [int(day) for day in working_days]
def clean_start_date(self):
start_date = self.cleaned_data.get("start_date")
if start_date and start_date < timezone.now().date():
raise forms.ValidationError(_("Start date must be in the future"))
return start_date
def clean_end_date(self):
start_date = self.cleaned_data.get("start_date")
end_date = self.cleaned_data.get("end_date")
if end_date and start_date and end_date < start_date:
raise forms.ValidationError(_("End date must be after start date"))
return end_date
def clean_end_time(self):
start_time = self.cleaned_data.get("start_time")
end_time = self.cleaned_data.get("end_time")
if end_time and start_time and end_time < start_time:
raise forms.ValidationError(_("End time must be after start time"))
return end_time
class InterviewCancelForm(forms.ModelForm):
class Meta:
model = ScheduledInterview
fields = ["cancelled_reason","cancelled_at"]
widgets = {
"cancelled_reason": forms.Textarea(
attrs={"class": "form-control", "rows": 3}
),
"cancelled_at": forms.DateTimeInput(
attrs={"class": "form-control", "type": "datetime-local"}
),
}
class NoteForm(forms.ModelForm):
"""Form for creating and editing meeting comments"""
class Meta:
model = Note
fields = "__all__"
widgets = {
"content": CKEditor5Widget(
attrs={
"class": "form-control",
"placeholder": _("Enter your comment or note"),
},
config_name="extends",
),
}
labels = {
"content": _("Note"),
}
class ProfileImageUploadForm(forms.ModelForm):
class Meta:
model = User
fields = ["profile_image"]
class StaffUserCreationForm(UserCreationForm):
email = forms.EmailField(label=_("Email"), required=True)
first_name = forms.CharField(label=_("First Name"),max_length=30, required=True)
last_name = forms.CharField(label=_("Last Name"),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.password1=self.cleaned_data["password1"]
user.password2=self.cleaned_data["password2"]
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"}),
}
def clean_status(self):
status = self.cleaned_data.get("status")
if status == "ACTIVE":
if self.instance and self.instance.pk:
print(self.instance.assigned_to)
if not self.instance.assigned_to:
raise ValidationError("Please assign the job posting before setting it to Active.")
return status
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 ApplicationExamDateForm(forms.ModelForm):
class Meta:
model = Application
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",
"required": True,
}
),
"contact_person": forms.TextInput(
attrs={
"class": "form-control",
}
),
"email": forms.EmailInput(
attrs={"class": "form-control","required": True}
),
"phone": forms.TextInput(
attrs={"class": "form-control"}
),
"website": forms.URLInput(
attrs={"class": "form-control"}
),
"country": forms.Select(attrs={"class": "form-select"}),
"address": forms.Textarea(
attrs={
"class": "form-control",
"rows": 3,
}
),
"notes": forms.Textarea(
attrs={
"class": "form-control",
"rows": 3,
}
),
}
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.fields['email'].required=True
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")
instance=self.instance
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
email = email.lower().strip()
if not instance.pk: # Creating new instance
if HiringAgency.objects.filter(email=email).exists() or User.objects.filter(email=email):
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.")
# 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,
}
),
"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,
}
),
}
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 AgencyApplicationSubmissionForm(forms.ModelForm):
"""Form for agencies to submit candidates"""
class Meta:
model = Application
fields = ["person", "resume"]
labels = {
"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(
Field("person", css_class="form-control"),
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_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 = Application.Stage.APPLIED
instance.applicant_status = Application.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,
)
class PortalLoginForm(forms.Form):
"""Unified login form for agency and candidate"""
USER_TYPE_CHOICES = [
("", _("Select User Type")),
("agency", _("Agency")),
("candidate", _("Candidate")),
]
email = forms.EmailField(
widget=forms.EmailInput(
attrs={"class": "form-control", "placeholder": "Enter your email"}
),
label=_("Email"),
required=True,
)
password = forms.CharField(
widget=forms.PasswordInput(
attrs={"class": "form-control", "placeholder": "Enter your password"}
),
label=_("Password"),
required=True,
)
user_type = forms.ChoiceField(
choices=USER_TYPE_CHOICES,
widget=forms.Select(attrs={"class": "form-control"}),
label=_("User Type"),
required=True,
)
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
class CandidateEmailForm(forms.Form):
"""Form for composing emails to participants about a candidate"""
to = forms.MultipleChoiceField(
widget=forms.CheckboxSelectMultiple(attrs={
'class': 'form-check'
}),
label=_('Select Candidates'), # Use a descriptive label
required=False
)
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
)
def __init__(self, job, candidates, *args, **kwargs):
super().__init__(*args, **kwargs)
self.job = job
self.candidates=candidates
candidate_choices=[]
for candidate in candidates:
candidate_choices.append(
(f'candidate_{candidate.id}', f'{candidate.email}')
)
self.fields['to'].choices =candidate_choices
self.fields['to'].initial = [choice[0] for choice in candidate_choices]
# 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"""
candidate=self.candidates.first()
message_parts=[]
if candidate and candidate.stage == 'Applied':
message_parts = [
f"Thank you for your interest in the {self.job.title} position at KAAUH and for taking the time to submit your application.",
f"We have carefully reviewed your qualifications; however, we regret to inform you that your application was not selected to proceed to the examination round at this time.",
f"The selection process was highly competitive, and we had a large number of highly qualified applicants.",
f"We encourage you to review other opportunities and apply for roles that align with your skills on our career portal:",
f"[settings.CAREER_PAGE_URL]", # Use a Django setting for the URL for flexibility
f"We wish you the best of luck in your current job search and future career endeavors.",
f"Sincerely,",
f"The KAAUH Recruitment Team",
]
elif candidate and candidate.stage == 'Exam':
message_parts = [
f"Dear Candidate,",
f"Thank you once again for your continued interest in the **{self.job.title}** position.",
f"We are pleased to inform you that, following a careful review of your application, you have been **selected to proceed to the next phase** of our recruitment process.",
f"The next mandatory step is the **Online Assessment Examination** designed to evaluate essential skills for this role.",
f"\n**Action Required:**",
f"Please click on the link below to access and complete the assessment:",
f"[settings.EXAM_LINK_URL]", # Using a settings variable is a professional best practice
f"\n**Important Details:**",
f"* **Deadline:** The exam must be completed within **72 hours** of receiving this notification.",
f"* **Duration:** The assessment is timed and will take approximately [Insert Time e.g., 60 minutes] to complete.",
f"* **Technical Note:** Please ensure you have a stable internet connection before beginning.",
f"We appreciate your dedication to this process and look forward to reviewing your results.",
f"Sincerely,",
f"The KAAUH Recruitment Team",
]
elif candidate and candidate.stage == 'Interview':
message_parts = [
f"Dear Candidate,",
f"Thank you for your performance in the recent assessment for the **{self.job.title}** role.",
f"We are pleased to inform you that you have **successfully cleared the examination phase** and have been selected to proceed to an interview.",
f"The interview is a mandatory step that allows us to learn more about your experience and fit for the role.",
f"\n**Next Steps:**",
f"Our recruitment coordinator will contact you directly within the next 1-2 business days to schedule your interview time and provide the necessary details (such as the interview panel, format, and location/virtual meeting link).",
f"\n**Please ensure your phone number and email address are current.**",
f"We look forward to speaking with you and discussing this exciting opportunity further.",
f"Sincerely,",
f"The KAAUH Recruitment Team",
]
elif candidate and candidate.stage == 'Offer':
message_parts = [
f"Dear Candidate,",
f"We are delighted to extend to you a **formal offer of employment** for the position of **{self.job.title}** at KAAUH.",
f"Congratulations! This is an exciting moment, and we are very enthusiastic about the prospect of you joining our team.",
f"\n**Next Steps & Documentation:**",
f"A comprehensive offer package, detailing your compensation, benefits, and the full terms of employment, will be transmitted to your email address within the next **24 hours**.",
f"Please review this document carefully.",
f"\n**Questions and Support:**",
f"Should you have any immediate questions regarding the offer or the next steps, please do not hesitate to contact our Human Resources department directly at [HR Contact Email/Phone].",
f"\nWe eagerly anticipate your favorable response and officially welcoming you to the KAAUH team!",
f"Sincerely,",
f"The KAAUH Recruitment Team",
]
elif candidate and candidate.stage == 'Document Review':
message_parts = [
f"Congratulations on progressing to the final stage for the {self.job.title} role!",
f"The next critical step is to complete your application by uploading the required employment verification documents.",
f"**Please log into the Candidate Portal immediately** to access the 'Document Upload' section.",
f"Required documents typically include: National ID/Iqama, Academic Transcripts, and Professional Certifications.",
f"You have **7 days** to upload all documents. Failure to do so may delay or invalidate your candidacy.",
f"If you encounter any technical issues, please contact our support team at [Support Email/Phone] immediately.",
f"We appreciate your cooperation as we finalize your employment process.",
]
elif candidate and candidate.stage == 'Hired':
message_parts = [
f"Welcome aboard,!",
f"We are thrilled to officially confirm your employment as our new {self.job.title}.",
f"You will receive a separate email shortly with details regarding your start date, first-day instructions, and onboarding documents.",
f"We look forward to seeing you at KAAUH.",
f"If you have any questions before your start date, please contact [Onboarding Contact].",
]
elif candidate:
message_parts=""
return '\n'.join(message_parts)
def get_email_addresses(self):
"""Extract email addresses from selected recipients"""
email_addresses = []
candidates=self.cleaned_data.get('to',[])
print(f"candidates are {candidates}")
if candidates:
for candidate in candidates:
if candidate.startswith('candidate_'):
print("candidadte: {candidate}")
candidate_id = candidate.split('_')[1]
try:
candidate = Application.objects.get(id=candidate_id)
email_addresses.append(candidate.email)
except Application.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', '')
return message
class MessageForm(forms.ModelForm):
"""Form for creating and editing messages between users"""
class Meta:
model = Message
fields = ["job","recipient", "subject", "content", "message_type"]
widgets = {
"recipient": forms.Select(
attrs={"class": "form-select", "placeholder": "Select recipient","required": True,}
),
"job": forms.Select(
attrs={"class": "form-select", "placeholder": "Select job"}
),
"subject": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "Enter message subject",
"required": True,
}
),
"content": forms.Textarea(
attrs={
"class": "form-control",
"rows": 6,
"placeholder": "Enter your message here...",
"required": True,
'spellcheck': 'true',
}
),
"message_type": forms.Select(attrs={"class": "form-select"}),
}
labels = {
"recipient": _("Recipient"),
"job": _("Related Job"),
"subject": _("Subject"),
"content": _("Message"),
"message_type": _("Message Type"),
}
def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = user
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_class = "g-3"
self._filter_recipient_field()
self._filter_job_field()
self.helper.layout = Layout(
Row(
Column("recipient", css_class="col-md-6"),
Column("job", css_class="col-md-6"),
css_class="g-3 mb-3",
),
Field("subject", css_class="form-control"),
Field("message_type", css_class="form-control"),
Field("content", css_class="form-control"),
Div(
Submit("submit", _("Send Message"), css_class="btn btn-main-action"),
css_class="col-12 mt-4",
),
)
def _filter_job_field(self):
"""Filter job options based on user type"""
if self.user.user_type == "agency":
print("jhjkshfjksd")
job_assignments =AgencyJobAssignment.objects.filter(
agency__user=self.user,
job__status="ACTIVE"
)
job_ids = job_assignments.values_list('job__id', flat=True)
self.fields["job"].queryset = JobPosting.objects.filter(
id__in=job_ids
).order_by("-created_at")
print("Agency user job queryset:", self.fields["job"].queryset)
elif self.user.user_type == "candidate":
print("sjhdakjhsdkjashkdjhskd")
# Candidates can only see jobs they applied for
person=self.user.person_profile
print(person)
applications=person.applications.all()
print(applications)
self.fields["job"].queryset = JobPosting.objects.filter(
applications__in=applications,
).distinct().order_by("-created_at")
else:
print("shhadjkhkd")
# Staff can see all jobs
self.fields["job"].queryset = JobPosting.objects.filter(
status="ACTIVE"
).order_by("-created_at")
def _filter_recipient_field(self):
"""Filter recipient options based on user type"""
if self.user.user_type == "staff":
# Staff can message anyone
self.fields["recipient"].queryset = User.objects.all().order_by("username")
elif self.user.user_type == "agency":
# Agency can message staff and their candidates
from django.db.models import Q
self.fields["recipient"].queryset = User.objects.filter(
user_type="staff"
).distinct().order_by("username")
elif self.user.user_type == "candidate":
# Candidates can only message staff
self.fields["recipient"].queryset = User.objects.filter(
user_type="staff"
).order_by("username")
def clean(self):
"""Validate message form data"""
cleaned_data = super().clean()
job = cleaned_data.get("job")
recipient = cleaned_data.get("recipient")
# If job is selected but no recipient, auto-assign to job.assigned_to
if job and not recipient:
if job.assigned_to:
cleaned_data["recipient"] = job.assigned_to
# Set message type to job_related
cleaned_data["message_type"] = Message.MessageType.JOB_RELATED
else:
raise forms.ValidationError(
_("Selected job is not assigned to any user. Please assign the job first.")
)
# Validate messaging permissions
if self.user and cleaned_data.get("recipient"):
self._validate_messaging_permissions(cleaned_data)
if self.cleaned_data.get('recipient')==self.user:
raise forms.ValidationError(_("You cannot message yourself"))
return cleaned_data
def _validate_messaging_permissions(self, cleaned_data):
"""Validate if user can message the recipient"""
recipient = cleaned_data.get("recipient")
job = cleaned_data.get("job")
# Staff can message anyone
if self.user.user_type == "staff":
return
# Agency users validation
if self.user.user_type == "agency":
if recipient.user_type not in ["staff", "candidate"]:
raise forms.ValidationError(
_("Agencies can only message staff or candidates.")
)
# If messaging a candidate, ensure candidate is from their agency
if recipient.user_type == "candidate" and job:
if not job.hiring_agency.filter(user=self.user).exists():
raise forms.ValidationError(
_("You can only message candidates from your assigned jobs.")
)
# Candidate users validation
if self.user.user_type == "candidate":
if recipient.user_type != "staff":
raise forms.ValidationError(
_("Candidates can only message staff.")
)
# If job-related, ensure candidate applied for the job
if job:
if not Application.objects.filter(job=job, person=self.user.person_profile).exists():
raise forms.ValidationError(
_("You can only message about jobs you have applied for.")
)
class ApplicantSignupForm(forms.ModelForm):
password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'form-control'}))
confirm_password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'form-control'}))
class Meta:
model = Person
fields = ["first_name","middle_name","last_name", "email","phone","gpa","nationality","national_id", "date_of_birth","gender","address"]
widgets = {
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
'middle_name': forms.TextInput(attrs={'class': 'form-control'}),
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'phone': forms.TextInput(attrs={'class': 'form-control'}),
"nationality": forms.Select(attrs={'class': 'form-control select2'}),
'date_of_birth': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'gender': forms.Select(attrs={'class': 'form-control'}),
'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'national_id': forms.TextInput(attrs={'class': 'form-control'}),
}
def clean(self):
cleaned_data = super().clean()
password = cleaned_data.get("password")
confirm_password = cleaned_data.get("confirm_password")
if password and confirm_password and password != confirm_password:
raise forms.ValidationError("Passwords do not match.")
return cleaned_data
def email_clean(self):
email = self.cleaned_data.get('email')
if CustomUser.objects.filter(email=email).exists():
raise forms.ValidationError("Email is already in use.")
return email
class DocumentUploadForm(forms.ModelForm):
"""Form for uploading documents for candidates"""
class Meta:
model = Document
fields = ['document_type', 'description', 'file']
widgets = {
'document_type': forms.Select(
choices=Document.DocumentType.choices,
attrs={'class': 'form-control'}
),
'description': forms.Textarea(
attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Enter document description (optional)'
}
),
'file': forms.FileInput(
attrs={
'class': 'form-control',
'accept': '.pdf,.doc,.docx,.jpg,.jpeg,.png'
}
),
}
labels = {
'document_type': _('Document Type'),
'description': _('Description'),
'file': _('Document File'),
}
def clean_file(self):
"""Validate uploaded file"""
file = self.cleaned_data.get('file')
if file:
# Check file size (max 10MB)
if file.size > 10 * 1024 * 1024: # 10MB
raise forms.ValidationError(
_('File size must be less than 10MB.')
)
# Check file extension
allowed_extensions = ['.pdf', '.doc', '.docx', '.jpg', '.jpeg', '.png']
file_extension = file.name.lower().split('.')[-1]
if f'.{file_extension}' not in allowed_extensions:
raise forms.ValidationError(
_('File type must be one of: PDF, DOC, DOCX, JPG, JPEG, PNG.')
)
return file
def clean(self):
"""Custom validation for document upload"""
cleaned_data = super().clean()
self.clean_file()
return cleaned_data
class PasswordResetForm(forms.Form):
old_password = forms.CharField(
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
label=_('Old Password')
)
new_password1 = forms.CharField(
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
label=_('New Password')
)
new_password2 = forms.CharField(
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
label=_('Confirm New Password')
)
def clean(self):
"""Custom validation for password reset"""
cleaned_data = super().clean()
old_password = cleaned_data.get('old_password')
new_password1 = cleaned_data.get('new_password1')
new_password2 = cleaned_data.get('new_password2')
if old_password:
if not self.data.get('old_password'):
raise forms.ValidationError(_('Old password is incorrect.'))
if new_password1 and new_password2:
if new_password1 != new_password2:
raise forms.ValidationError(_('New passwords do not match.'))
return cleaned_data
class StaffAssignmentForm(forms.ModelForm):
"""Form for assigning staff to a job posting"""
class Meta:
model = JobPosting
fields = ['assigned_to']
widgets = {
'assigned_to': forms.Select(attrs={
'class': 'form-select',
'placeholder': _('Select staff member'),
'required': True
}),
}
labels = {
'assigned_to': _('Assign Staff Member'),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Filter users to only show staff members
self.fields['assigned_to'].queryset = User.objects.filter(
user_type='staff',is_superuser=False
).order_by('first_name', 'last_name')
# Add empty choice for unassigning
self.fields['assigned_to'].required = False
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.form_class = 'g-3'
self.helper.form_id = 'staff-assignment-form'
self.helper.layout = Layout(
Field('assigned_to', css_class='form-control'),
Div(
Submit('submit', _('Assign Staff'), css_class='btn btn-primary'),
css_class='col-12 mt-3'
),
)
def clean_assigned_to(self):
"""Validate that assigned user is a staff member"""
assigned_to = self.cleaned_data.get('assigned_to')
if assigned_to and assigned_to.user_type != 'staff':
raise forms.ValidationError(_('Only staff members can be assigned to jobs.'))
return assigned_to
class RemoteInterviewForm(forms.Form):
"""Form for creating remote interviews"""
# Add Interview model fields to the form
topic = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., Software Interview'
}),
label=_('Meeting Topic')
)
interview_date = forms.DateField(
required=False,
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
}),
label=_('Interview Date')
)
interview_time = forms.TimeField(
required=False,
widget=forms.TimeInput(attrs={
'class': 'form-control',
'type': 'time'
}),
label=_('Interview Time')
)
duration = forms.IntegerField(
min_value=1,
required=True,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'placeholder': 'Duration in minutes'
}),
label=_('Duration (minutes)'),
error_messages={
'required': _('Please enter how long the interview will last.'),
'min_value': _('Duration must be at least 1 minute.')
}
)
class OnsiteInterviewForm(forms.Form):
"""Form for creating onsite interviews"""
# Add Interview model fields to the form
topic = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., In-person Interview'
}),
label=_('Interview Topic')
)
physical_address = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Physical address'
}),
label=_('Physical Address')
)
room_number = forms.CharField(
max_length=50,
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Room number'
}),
label=_('Room Number')
)
interview_date = forms.DateField(
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date',
'required': True
}),
label=_('Interview Date')
)
interview_time = forms.TimeField(
widget=forms.TimeInput(attrs={
'class': 'form-control',
'type': 'time',
'required': True
}),
label=_('Interview Time')
)
duration = forms.IntegerField(
min_value=1,
required=True,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'placeholder': 'Duration in minutes'
}),
label=_('Duration (minutes)'),
error_messages={
'required': _('Please enter how long the interview will last.'),
'min_value': _('Duration must be at least 1 minute.')
}
)
class ScheduledInterviewForm(forms.Form):
topic = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., Interview Topic'
}),
label=_('Interview Topic')
)
start_time = forms.DateTimeField(
widget=forms.DateTimeInput(attrs={
'class': 'form-control',
'type': 'datetime-local',
'required': True
}),
label=_('Start Time')
)
duration = forms.IntegerField(
min_value=1,
required=True,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'placeholder': 'Duration in minutes'
}),
label=_('Duration (minutes)'),
error_messages={
'required': _('Please enter how long the interview will last.'),
'min_value': _('Duration must be at least 1 minute.')
}
)
def clean_start_time(self):
"""Validate start time is not in the past"""
start_time = self.cleaned_data.get('start_time')
if start_time and start_time < timezone.now():
raise forms.ValidationError(_('Start time cannot be in the past.'))
return start_time
class OnsiteScheduleInterviewUpdateForm(forms.Form):
topic = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., Interview Topic'
}),
label=_('Interview Topic')
)
start_time = forms.DateTimeField(
widget=forms.DateTimeInput(attrs={
'class': 'form-control',
'type': 'datetime-local',
'required': True
}),
label=_('Start Time')
)
duration = forms.IntegerField(
min_value=1,
required=True,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'placeholder': 'Duration in minutes'
}),
label=_('Duration (minutes)'),
error_messages={
'required': _('Please enter how long the interview will last.'),
'min_value': _('Duration must be at least 1 minute.')
}
)
physical_address = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Physical address'
}),
label=_('Physical Address')
)
room_number = forms.CharField(
max_length=50,
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Room number'
}),
label=_('Room Number')
)
class ScheduledInterviewUpdateStatusForm(forms.Form):
status = forms.ChoiceField(
choices=ScheduledInterview.InterviewStatus.choices,
widget=forms.Select(attrs={
'class': 'form-control',
'required': True
}),
label=_('Interview Status')
)
class Meta:
model = ScheduledInterview
fields = ['status']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Filter the choices here
EXCLUDED_STATUS = ScheduledInterview.InterviewStatus.CANCELLED
filtered_choices = [
choice for choice in ScheduledInterview.InterviewStatus.choices
if choice[0]!= EXCLUDED_STATUS
]
# Apply the filtered list back to the field
self.fields['status'].choices = filtered_choices
class SettingsForm(forms.ModelForm):
"""Form for creating and editing settings"""
class Meta:
model = Settings
fields = ['name','key', 'value']
widgets = {
'name': forms.TextInput(attrs={
'class': 'form-control mb-3',
'placeholder': 'e.g., Zoom',
'required': True
}),
'key': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter setting key',
'required': True
}),
'value': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Enter setting value',
'required': True
}),
}
labels = {
'key': _('Setting Key'),
'value': _('Setting Value'),
}
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('key', css_class='form-control'),
Field('value', css_class='form-control'),
Div(
Submit('submit', _('Save Setting'), css_class='btn btn-main-action'),
css_class='col-12 mt-4',
),
)
def clean_key(self):
"""Ensure key is unique and properly formatted"""
key = self.cleaned_data.get('key')
if key:
# Convert to uppercase for consistency
key = key.upper().strip()
# Check for duplicates excluding current instance if editing
instance = self.instance
if not instance.pk: # Creating new instance
if Settings.objects.filter(key=key).exists():
raise forms.ValidationError("A setting with this key already exists.")
else: # Editing existing instance
if Settings.objects.filter(key=key).exclude(pk=instance.pk).exists():
raise forms.ValidationError("A setting with this key already exists.")
# Validate key format (alphanumeric and underscores only)
import re
if not re.match(r'^[A-Z][A-Z0-9_]*$', key):
raise forms.ValidationError(
"Setting key must start with a letter and contain only uppercase letters, numbers, and underscores."
)
return key
def clean_value(self):
"""Validate setting value"""
value = self.cleaned_data.get('value')
if value:
value = value.strip()
if not value:
raise forms.ValidationError("Setting value cannot be empty.")
return value
class InterviewEmailForm(forms.Form):
"""Form for composing emails to participants about a candidate"""
to = forms.CharField(
label=_('To'), # Use a descriptive label
required=True,
)
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
)
def __init__(self, job, application,schedule, *args, **kwargs):
applicant=application.person.user
interview=schedule.interview
super().__init__(*args, **kwargs)
if application.hiring_agency:
self.fields['to'].initial=application.hiring_agency.email
self.fields['to'].disabled= True
else:
self.fields['to'].initial=application.person.email
self.fields['to'].disabled= True
# Set initial message with candidate and meeting info
initial_message = f"""
Dear {applicant.first_name} {applicant.last_name},
Your interview details are as follows:
Date: {interview.start_time.strftime("%d-%m-%Y")}
Time: {interview.start_time.strftime("%I:%M %p")}
Interview Duration: {interview.duration} minutes
Job: {job.title}
"""
if interview.location_type == 'Remote':
initial_message += f"Pease join using meeting link {interview.join_url} \n\n"
else:
initial_message += "This is an onsite schedule. Please arrive 10 minutes early.\n\n"
self.fields['message'].initial = initial_message
# class InterviewResultForm(forms.ModelForm):
# class Meta:
# model = Interview
# fields = ['interview_result', 'result_comments']
# widgets = {
# 'interview_result': forms.Select(attrs={
# 'class': 'form-select', # Standard Bootstrap class
# 'required': True
# }),
# 'result_comments': forms.Textarea(attrs={
# 'class': 'form-control',
# 'rows': 3,
# 'placeholder': 'Enter setting value',
# 'required': True
# }),
# }
RESULT_CHOICES = (
('passed', 'Passed'),
('failed', 'Failed'),
('on_hold', 'On Hold'),
)
class InterviewResultForm(forms.Form):
interview_result = forms.ChoiceField(
choices=RESULT_CHOICES,
widget=forms.Select(attrs={
'class': 'form-select',
})
)
result_comments = forms.CharField(
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Enter result comment',
})
)