2148 lines
74 KiB
Python
2148 lines
74 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
|
|
|
|
# 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] # Monday to Friday
|
|
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:00"
|
|
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]
|
|
|
|
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"Than you, for your interest in the {self.job.title} role.",
|
|
f"We regret to inform you that you were not selected to move forward to the exam round at this time.",
|
|
f"We encourage you to check our career page for further updates and future opportunities:",
|
|
f"https://kaauh/careers",
|
|
f"Wishing you the best in your job search,",
|
|
f"The KAAUH Hiring team"
|
|
]
|
|
elif candidate and candidate.stage == 'Exam':
|
|
message_parts = [
|
|
f"Than you,for your interest in the {self.job.title} role.",
|
|
f"We're pleased to inform you that your initial screening was successful!",
|
|
f"The next step is the mandatory online assessment exam.",
|
|
f"Please complete the assessment by using the following link:",
|
|
f"https://kaauh/hire/exam",
|
|
f"We look forward to reviewing your results.",
|
|
f"Best regards, The KAAUH Hiring team"
|
|
]
|
|
|
|
elif candidate and candidate.stage == 'Interview':
|
|
message_parts = [
|
|
f"Than you, for your interest in the {self.job.title} role.",
|
|
f"We're pleased to inform you that you have cleared your exam!",
|
|
f"The next step is the mandatory interview.",
|
|
f"Please complete the assessment by using the following link:",
|
|
f"https://kaauh/hire/exam",
|
|
f"We look forward to reviewing your results.",
|
|
f"Best regards, The KAAUH Hiring team"
|
|
]
|
|
|
|
elif candidate and candidate.stage == 'Offer':
|
|
message_parts = [
|
|
f"Congratulations, ! We are delighted to inform you that we are extending a formal offer of employment for the {self.job.title} role.",
|
|
f"This is an exciting moment, and we look forward to having you join the KAAUH team.",
|
|
f"A detailed offer letter and compensation package will be sent to you via email within 24 hours.",
|
|
f"In the meantime, please contact our HR department at [HR Contact] if you have immediate questions.",
|
|
f"Welcome to the team!",
|
|
f"Best regards, The KAAUH Hiring team"
|
|
]
|
|
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].",
|
|
f"Best regards, The KAAUH Hiring team"
|
|
]
|
|
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.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":
|
|
|
|
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":
|
|
# Candidates can only see jobs they applied for
|
|
self.fields["job"].queryset = JobPosting.objects.filter(
|
|
applications__person=self.user.person_profile,
|
|
).distinct().order_by("-created_at")
|
|
else:
|
|
# 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)
|
|
|
|
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=False,
|
|
widget=forms.NumberInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Duration in minutes'
|
|
}),
|
|
label=_('Duration (minutes)')
|
|
)
|
|
|
|
|
|
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=False,
|
|
widget=forms.NumberInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Duration in minutes'
|
|
}),
|
|
label=_('Duration (minutes)')
|
|
)
|
|
|
|
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=False,
|
|
widget=forms.NumberInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Duration in minutes'
|
|
}),
|
|
label=_('Duration (minutes)')
|
|
)
|
|
|
|
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=False,
|
|
widget=forms.NumberInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Duration in minutes'
|
|
}),
|
|
label=_('Duration (minutes)')
|
|
)
|
|
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 = ['key', 'value']
|
|
widgets = {
|
|
'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.details_url} .\n\n"
|
|
else:
|
|
initial_message += "This is an onsite schedule. Please arrive 10 minutes early.\n\n"
|
|
|
|
initial_message += """
|
|
Best regards,
|
|
KAAUH Hiring Team
|
|
"""
|
|
|
|
self.fields['message'].initial = initial_message
|
|
|