2025-11-10 16:21:29 +03:00

1796 lines
63 KiB
Python

from django import forms
from django.core.validators import URLValidator
from django.forms.formsets import formset_factory
from django.utils.translation import gettext_lazy as _
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit, Row, Column, Field, Div
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm
User = get_user_model()
import re
from .models import (
ZoomMeeting,
Candidate,
TrainingMaterial,
JobPosting,
FormTemplate,
InterviewSchedule,
BreakTime,
JobPostingImage,
Profile,
MeetingComment,
ScheduledInterview,
Source,
HiringAgency,
AgencyJobAssignment,
AgencyAccessLink,
Participants,
Message,
)
# from django_summernote.widgets import SummernoteWidget
from django_ckeditor_5.widgets import CKEditor5Widget
import secrets
import string
from django.core.exceptions import ValidationError
from django.utils import timezone
def generate_api_key(length=32):
"""Generate a secure API key"""
alphabet = string.ascii_letters + string.digits
return "".join(secrets.choice(alphabet) for _ in range(length))
def generate_api_secret(length=64):
"""Generate a secure API secret"""
alphabet = string.ascii_letters + string.digits + "-._~"
return "".join(secrets.choice(alphabet) for _ in range(length))
class SourceForm(forms.ModelForm):
"""Simple form for creating and editing sources"""
class Meta:
model = Source
fields = ["name", "source_type", "description", "ip_address", "is_active"]
widgets = {
"name": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "e.g., ATS System, ERP Integration",
"required": True,
}
),
"source_type": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "e.g., ATS, ERP, API",
"required": True,
}
),
"description": forms.Textarea(
attrs={
"class": "form-control",
"rows": 3,
"placeholder": "Brief description of the source system",
}
),
"ip_address": forms.TextInput(
attrs={"class": "form-control", "placeholder": "192.168.1.100"}
),
"is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-md-3"
self.helper.field_class = "col-md-9"
self.helper.layout = Layout(
Field("name", css_class="form-control"),
Field("source_type", css_class="form-control"),
Field("ip_address", css_class="form-control"),
Field("is_active", css_class="form-check-input"),
Submit("submit", "Save Source", css_class="btn btn-primary mt-3"),
)
def clean_name(self):
"""Ensure source name is unique"""
name = self.cleaned_data.get("name")
if name:
# Check for duplicates excluding current instance if editing
instance = self.instance
if not instance.pk: # Creating new instance
if Source.objects.filter(name=name).exists():
raise ValidationError("A source with this name already exists.")
else: # Editing existing instance
if Source.objects.filter(name=name).exclude(pk=instance.pk).exists():
raise ValidationError("A source with this name already exists.")
return name
class SourceAdvancedForm(forms.ModelForm):
"""Advanced form for creating and editing sources with API key generation"""
# Hidden field to trigger API key generation
generate_keys = forms.CharField(
widget=forms.HiddenInput(),
required=False,
help_text="Set to 'true' to generate new API keys",
)
# Display fields for generated keys (read-only)
api_key_generated = forms.CharField(
label="Generated API Key",
required=False,
widget=forms.TextInput(attrs={"readonly": True, "class": "form-control"}),
)
api_secret_generated = forms.CharField(
label="Generated API Secret",
required=False,
widget=forms.TextInput(attrs={"readonly": True, "class": "form-control"}),
)
class Meta:
model = Source
fields = [
"name",
"source_type",
"description",
"ip_address",
"trusted_ips",
"is_active",
"integration_version",
]
widgets = {
"name": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "e.g., ATS System, ERP Integration",
"required": True,
}
),
"source_type": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "e.g., ATS, ERP, API",
"required": True,
}
),
"description": forms.Textarea(
attrs={
"class": "form-control",
"rows": 3,
"placeholder": "Brief description of the source system",
}
),
"ip_address": forms.TextInput(
attrs={"class": "form-control", "placeholder": "192.168.1.100"}
),
"trusted_ips": forms.Textarea(
attrs={
"class": "form-control",
"rows": 2,
"placeholder": "Comma-separated IP addresses (e.g., 192.168.1.100, 10.0.0.1)",
}
),
"integration_version": forms.TextInput(
attrs={"class": "form-control", "placeholder": "v1.0, v2.1"}
),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-md-3"
self.helper.field_class = "col-md-9"
# Add generate keys button
self.helper.layout = Layout(
Field("name", css_class="form-control"),
Field("source_type", css_class="form-control"),
Field("description", css_class="form-control"),
Field("ip_address", css_class="form-control"),
Field("trusted_ips", css_class="form-control"),
Field("integration_version", css_class="form-control"),
Field("is_active", css_class="form-check-input"),
# Hidden field for key generation trigger
Field("generate_keys", type="hidden"),
# Display fields for generated keys
Field("api_key_generated", css_class="form-control"),
Field("api_secret_generated", css_class="form-control"),
Submit("submit", "Save Source", css_class="btn btn-primary mt-3"),
)
def clean_name(self):
"""Ensure source name is unique"""
name = self.cleaned_data.get("name")
if name:
# Check for duplicates excluding current instance if editing
instance = self.instance
if not instance.pk: # Creating new instance
if Source.objects.filter(name=name).exists():
raise ValidationError("A source with this name already exists.")
else: # Editing existing instance
if Source.objects.filter(name=name).exclude(pk=instance.pk).exists():
raise ValidationError("A source with this name already exists.")
return name
def clean_trusted_ips(self):
"""Validate and format trusted IP addresses"""
trusted_ips = self.cleaned_data.get("trusted_ips")
if trusted_ips:
# Split by comma and strip whitespace
ips = [ip.strip() for ip in trusted_ips.split(",") if ip.strip()]
# Validate each IP address
for ip in ips:
try:
# Basic IP validation (can be enhanced)
if not (ip.replace(".", "").isdigit() and len(ip.split(".")) == 4):
raise ValidationError(f"Invalid IP address: {ip}")
except Exception:
raise ValidationError(f"Invalid IP address: {ip}")
return ", ".join(ips)
return trusted_ips
def clean(self):
"""Custom validation for the form"""
cleaned_data = super().clean()
# Check if we need to generate API keys
generate_keys = cleaned_data.get("generate_keys")
if generate_keys == "true":
# Generate new API key and secret
cleaned_data["api_key"] = generate_api_key()
cleaned_data["api_secret"] = generate_api_secret()
# Set display fields for the frontend
cleaned_data["api_key_generated"] = cleaned_data["api_key"]
cleaned_data["api_secret_generated"] = cleaned_data["api_secret"]
return cleaned_data
class CandidateForm(forms.ModelForm):
class Meta:
model = Candidate
fields = [
"job",
"first_name",
"last_name",
"phone",
"email",
"hiring_source",
"hiring_agency",
"resume",
]
labels = {
"first_name": _("First Name"),
"last_name": _("Last Name"),
"phone": _("Phone"),
"email": _("Email"),
"resume": _("Resume"),
"hiring_source": _("Hiring Type"),
"hiring_agency": _("Hiring Agency"),
}
widgets = {
"first_name": forms.TextInput(
attrs={"class": "form-control", "placeholder": _("Enter first name")}
),
"last_name": forms.TextInput(
attrs={"class": "form-control", "placeholder": _("Enter last name")}
),
"phone": forms.TextInput(
attrs={"class": "form-control", "placeholder": _("Enter phone number")}
),
"email": forms.EmailInput(
attrs={"class": "form-control", "placeholder": _("Enter email")}
),
"stage": forms.Select(attrs={"class": "form-select"}),
"hiring_source": forms.Select(attrs={"class": "form-select"}),
"hiring_agency": forms.Select(attrs={"class": "form-select"}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-md-3"
self.helper.field_class = "col-md-9"
# Make job field read-only if it's being pre-populated
job_value = self.initial.get("job")
if job_value:
self.fields["job"].widget.attrs["readonly"] = True
self.helper.layout = Layout(
Field("job", css_class="form-control"),
Field("first_name", css_class="form-control"),
Field("last_name", css_class="form-control"),
Field("phone", css_class="form-control"),
Field("email", css_class="form-control"),
Field("stage", css_class="form-control"),
Field("hiring_source", css_class="form-control"),
Field("hiring_agency", css_class="form-control"),
Field("resume", css_class="form-control"),
Submit("submit", _("Submit"), css_class="btn btn-primary"),
)
class CandidateStageForm(forms.ModelForm):
"""Form specifically for updating candidate stage with validation"""
class Meta:
model = Candidate
fields = ["stage"]
labels = {
"stage": _("New Application Stage"),
}
widgets = {
"stage": forms.Select(attrs={"class": "form-select"}),
}
class ZoomMeetingForm(forms.ModelForm):
class Meta:
model = ZoomMeeting
fields = ["topic", "start_time", "duration"]
labels = {
"topic": _("Topic"),
"start_time": _("Start Time"),
"duration": _("Duration"),
}
widgets = {
"topic": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": _("Enter meeting topic"),
}
),
"start_time": forms.DateTimeInput(
attrs={"class": "form-control", "type": "datetime-local"}
),
"duration": forms.NumberInput(
attrs={"class": "form-control", "min": 1, "placeholder": _("60")}
),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-md-3"
self.helper.field_class = "col-md-9"
self.helper.layout = Layout(
Field("topic", css_class="form-control"),
Field("start_time", css_class="form-control"),
Field("duration", css_class="form-control"),
Submit("submit", _("Create Meeting"), css_class="btn btn-primary"),
)
class TrainingMaterialForm(forms.ModelForm):
class Meta:
model = TrainingMaterial
fields = ["title", "content", "video_link", "file"]
labels = {
"title": _("Title"),
"content": _("Content"),
"video_link": _("Video Link"),
"file": _("File"),
}
widgets = {
"title": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": _("Enter material title"),
}
),
"content": CKEditor5Widget(
attrs={"placeholder": _("Enter material content")}
),
"video_link": forms.URLInput(
attrs={
"class": "form-control",
"placeholder": _("https://www.youtube.com/watch?v=..."),
}
),
"file": forms.FileInput(attrs={"class": "form-control"}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_class = "g-3"
self.helper.layout = Layout(
"title",
"content",
Row(
Column("video_link", css_class="col-md-6"),
Column("file", css_class="col-md-6"),
css_class="g-3 mb-4",
),
Div(
Submit("submit", _("Create Material"), css_class="btn btn-main-action"),
css_class="col-12 mt-4",
),
)
class JobPostingForm(forms.ModelForm):
"""Form for creating and editing job postings"""
class Meta:
model = JobPosting
fields = [
"title",
"department",
"job_type",
"workplace_type",
"location_city",
"location_state",
"location_country",
"description",
"qualifications",
"salary_range",
"benefits",
"application_deadline",
"application_instructions",
"position_number",
"reporting_to",
"open_positions",
"hash_tags",
"max_applications",
]
widgets = {
# Basic Information
"title": forms.TextInput(
attrs={"class": "form-control", "placeholder": "", "required": True}
),
"department": forms.TextInput(
attrs={"class": "form-control", "placeholder": ""}
),
"job_type": forms.Select(attrs={"class": "form-select", "required": True}),
"workplace_type": forms.Select(
attrs={"class": "form-select", "required": True}
),
# Location
"location_city": forms.TextInput(
attrs={"class": "form-control", "placeholder": "Boston"}
),
"location_state": forms.TextInput(
attrs={"class": "form-control", "placeholder": "MA"}
),
"location_country": forms.TextInput(
attrs={"class": "form-control", "value": "United States"}
),
"salary_range": forms.TextInput(
attrs={"class": "form-control", "placeholder": "$60,000 - $80,000"}
),
# Application Information
# 'application_url': forms.URLInput(attrs={
# 'class': 'form-control',
# 'placeholder': 'https://university.edu/careers/job123',
# 'required': True
# }),
"application_deadline": forms.DateInput(
attrs={"class": "form-control", "type": "date", "required": True}
),
"open_positions": forms.NumberInput(
attrs={
"class": "form-control",
"min": 1,
"placeholder": "Number of open positions",
}
),
"hash_tags": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "#hiring,#jobopening",
# 'validators':validate_hash_tags, # Assuming this is available
}
),
# Internal Information
"position_number": forms.TextInput(
attrs={"class": "form-control", "placeholder": "UNIV-2025-001"}
),
"reporting_to": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "Department Chair, Director, etc.",
}
),
"max_applications": forms.NumberInput(
attrs={
"class": "form-control",
"min": 1,
"placeholder": "Maximum number of applicants",
}
),
}
def __init__(self, *args, **kwargs):
# Now call the parent __init__ with remaining args
super().__init__(*args, **kwargs)
if not self.instance.pk: # Creating new job posting
# self.fields['status'].initial = 'Draft'
self.fields["location_city"].initial = "Riyadh"
self.fields["location_state"].initial = "Riyadh Province"
self.fields["location_country"].initial = "Saudi Arabia"
def clean_hash_tags(self):
hash_tags = self.cleaned_data.get("hash_tags")
if hash_tags:
tags = [tag.strip() for tag in hash_tags.split(",") if tag.strip()]
for tag in tags:
if not tag.startswith("#"):
raise forms.ValidationError(
"Each hashtag must start with '#' symbol and must be comma(,) sepearted."
)
return ",".join(tags)
return hash_tags # Allow blank
def clean_title(self):
title = self.cleaned_data.get("title")
if not title or len(title.strip()) < 3:
raise forms.ValidationError("Job title must be at least 3 characters long.")
if len(title) > 200:
raise forms.ValidationError("Job title cannot exceed 200 characters.")
return title.strip()
def clean_description(self):
description = self.cleaned_data.get("description")
if not description or len(description.strip()) < 20:
raise forms.ValidationError(
"Job description must be at least 20 characters long."
)
return description.strip() # to remove leading/trailing whitespace
def clean_application_url(self):
url = self.cleaned_data.get("application_url")
if url:
validator = URLValidator()
try:
validator(url)
except forms.ValidationError:
raise forms.ValidationError(
"Please enter a valid URL (e.g., https://example.com)"
)
return url
class JobPostingImageForm(forms.ModelForm):
class Meta:
model = JobPostingImage
fields = ["post_image"]
class FormTemplateForm(forms.ModelForm):
"""Form for creating form templates"""
class Meta:
model = FormTemplate
fields = ["job", "name", "description", "is_active"]
labels = {
"job": _("Job"),
"name": _("Template Name"),
"description": _("Description"),
"is_active": _("Active"),
}
widgets = {
"name": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": _("Enter template name"),
"required": True,
}
),
"description": forms.Textarea(
attrs={
"class": "form-control",
"rows": 3,
"placeholder": _("Enter template description (optional)"),
}
),
"is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-md-3"
self.helper.field_class = "col-md-9"
self.helper.layout = Layout(
Field("job", css_class="form-control"),
Field("name", css_class="form-control"),
Field("description", css_class="form-control"),
Field("is_active", css_class="form-check-input"),
Submit("submit", _("Create Template"), css_class="btn btn-primary mt-3"),
)
class BreakTimeForm(forms.Form):
"""
A simple Form used for the BreakTimeFormSet.
It is not a ModelForm because the data is stored directly in InterviewSchedule's JSONField,
not in a separate BreakTime model instance.
"""
start_time = forms.TimeField(
widget=forms.TimeInput(attrs={"type": "time", "class": "form-control"}),
label="Start Time",
)
end_time = forms.TimeField(
widget=forms.TimeInput(attrs={"type": "time", "class": "form-control"}),
label="End Time",
)
BreakTimeFormSet = formset_factory(BreakTimeForm, extra=1, can_delete=True)
class InterviewScheduleForm(forms.ModelForm):
candidates = forms.ModelMultipleChoiceField(
queryset=Candidate.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=True,
)
working_days = forms.MultipleChoiceField(
choices=[
(0, "Monday"),
(1, "Tuesday"),
(2, "Wednesday"),
(3, "Thursday"),
(4, "Friday"),
(5, "Saturday"),
(6, "Sunday"),
],
widget=forms.CheckboxSelectMultiple,
required=True,
)
class Meta:
model = InterviewSchedule
fields = [
"candidates",
"start_date",
"end_date",
"working_days",
"start_time",
"end_time",
"interview_duration",
"buffer_time",
"break_start_time",
"break_end_time",
]
widgets = {
"start_date": forms.DateInput(
attrs={"type": "date", "class": "form-control"}
),
"end_date": forms.DateInput(
attrs={"type": "date", "class": "form-control"}
),
"start_time": forms.TimeInput(
attrs={"type": "time", "class": "form-control"}
),
"end_time": forms.TimeInput(
attrs={"type": "time", "class": "form-control"}
),
"interview_duration": forms.NumberInput(attrs={"class": "form-control"}),
"buffer_time": forms.NumberInput(attrs={"class": "form-control"}),
"break_start_time": forms.TimeInput(
attrs={"type": "time", "class": "form-control"}
),
"break_end_time": forms.TimeInput(
attrs={"type": "time", "class": "form-control"}
),
}
def __init__(self, slug, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["candidates"].queryset = Candidate.objects.filter(
job__slug=slug, stage="Interview"
)
def clean_working_days(self):
working_days = self.cleaned_data.get("working_days")
return [int(day) for day in working_days]
class MeetingCommentForm(forms.ModelForm):
"""Form for creating and editing meeting comments"""
class Meta:
model = MeetingComment
fields = ["content"]
widgets = {
"content": CKEditor5Widget(
attrs={
"class": "form-control",
"placeholder": _("Enter your comment or note"),
},
config_name="extends",
),
}
labels = {
"content": _("Comment"),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-md-3"
self.helper.field_class = "col-md-9"
self.helper.layout = Layout(
Field("content", css_class="form-control"),
Submit("submit", _("Add Comment"), css_class="btn btn-primary mt-3"),
)
class InterviewForm(forms.ModelForm):
class Meta:
model = ScheduledInterview
fields = ["job", "candidate"]
class ProfileImageUploadForm(forms.ModelForm):
class Meta:
model = Profile
fields = ["profile_image"]
class StaffUserCreationForm(UserCreationForm):
email = forms.EmailField(required=True)
first_name = forms.CharField(max_length=30, required=True)
last_name = forms.CharField(max_length=150, required=True)
class Meta:
model = User
fields = ("email", "first_name", "last_name", "password1", "password2")
def clean_email(self):
email = self.cleaned_data["email"]
if User.objects.filter(email=email).exists():
raise forms.ValidationError("A user with this email already exists.")
return email
def generate_username(self, email):
"""Generate a valid, unique username from email."""
prefix = email.split("@")[0].lower()
username = re.sub(r"[^a-z0-9._]", "", prefix)
if not username:
username = "user"
base = username
counter = 1
while User.objects.filter(username=username).exists():
username = f"{base}{counter}"
counter += 1
return username
def save(self, commit=True):
user = super().save(commit=False)
user.email = self.cleaned_data["email"]
user.first_name = self.cleaned_data["first_name"]
user.last_name = self.cleaned_data["last_name"]
user.username = self.generate_username(user.email)
user.is_staff = True
if commit:
user.save()
return user
class ToggleAccountForm(forms.Form):
pass
class JobPostingCancelReasonForm(forms.ModelForm):
class Meta:
model = JobPosting
fields = ["cancel_reason"]
class JobPostingStatusForm(forms.ModelForm):
class Meta:
model = JobPosting
fields = ["status"]
widgets = {
"status": forms.Select(attrs={"class": "form-select"}),
}
class LinkedPostContentForm(forms.ModelForm):
class Meta:
model = JobPosting
fields = ["linkedin_post_formated_data"]
class FormTemplateIsActiveForm(forms.ModelForm):
class Meta:
model = FormTemplate
fields = ["is_active"]
class CandidateExamDateForm(forms.ModelForm):
class Meta:
model = Candidate
fields = ["exam_date"]
widgets = {
"exam_date": forms.DateTimeInput(
attrs={"type": "datetime-local", "class": "form-control"}
),
}
class HiringAgencyForm(forms.ModelForm):
"""Form for creating and editing hiring agencies"""
class Meta:
model = HiringAgency
fields = [
"name",
"contact_person",
"email",
"phone",
"website",
"country",
"address",
"notes",
]
widgets = {
"name": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "Enter agency name",
"required": True,
}
),
"contact_person": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "Enter contact person name",
}
),
"email": forms.EmailInput(
attrs={"class": "form-control", "placeholder": "agency@example.com"}
),
"phone": forms.TextInput(
attrs={"class": "form-control", "placeholder": "+966 50 123 4567"}
),
"website": forms.URLInput(
attrs={"class": "form-control", "placeholder": "https://www.agency.com"}
),
"country": forms.Select(attrs={"class": "form-select"}),
"address": forms.Textarea(
attrs={
"class": "form-control",
"rows": 3,
"placeholder": "Enter agency address",
}
),
"notes": forms.Textarea(
attrs={
"class": "form-control",
"rows": 3,
"placeholder": "Internal notes about the agency",
}
),
}
labels = {
"name": _("Agency Name"),
"contact_person": _("Contact Person"),
"email": _("Email Address"),
"phone": _("Phone Number"),
"website": _("Website"),
"country": _("Country"),
"address": _("Address"),
"notes": _("Internal Notes"),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-md-3"
self.helper.field_class = "col-md-9"
self.helper.layout = Layout(
Field("name", css_class="form-control"),
Field("contact_person", css_class="form-control"),
Row(
Column("email", css_class="col-md-6"),
Column("phone", css_class="col-md-6"),
css_class="g-3 mb-3",
),
Field("website", css_class="form-control"),
Field("country", css_class="form-control"),
Field("address", css_class="form-control"),
Field("notes", css_class="form-control"),
Div(
Submit("submit", _("Save Agency"), css_class="btn btn-main-action"),
css_class="col-12 mt-4",
),
)
def clean_name(self):
"""Ensure agency name is unique"""
name = self.cleaned_data.get("name")
if name:
instance = self.instance
if not instance.pk: # Creating new instance
if HiringAgency.objects.filter(name=name).exists():
raise ValidationError("An agency with this name already exists.")
else: # Editing existing instance
if (
HiringAgency.objects.filter(name=name)
.exclude(pk=instance.pk)
.exists()
):
raise ValidationError("An agency with this name already exists.")
return name.strip()
def clean_email(self):
"""Validate email format and uniqueness"""
email = self.cleaned_data.get("email")
if email:
# Check email format
if not "@" in email or "." not in email.split("@")[1]:
raise ValidationError("Please enter a valid email address.")
# Check uniqueness (optional - remove if multiple agencies can have same email)
instance = self.instance
if not instance.pk: # Creating new instance
if HiringAgency.objects.filter(email=email).exists():
raise ValidationError("An agency with this email already exists.")
else: # Editing existing instance
if (
HiringAgency.objects.filter(email=email)
.exclude(pk=instance.pk)
.exists()
):
raise ValidationError("An agency with this email already exists.")
return email.lower().strip() if email else email
def clean_phone(self):
"""Validate phone number format"""
phone = self.cleaned_data.get("phone")
if phone:
# Remove common formatting characters
clean_phone = "".join(c for c in phone if c.isdigit() or c in "+")
if len(clean_phone) < 10:
raise ValidationError("Phone number must be at least 10 digits long.")
return phone.strip() if phone else phone
def clean_website(self):
"""Validate website URL"""
website = self.cleaned_data.get("website")
if website:
if not website.startswith(("http://", "https://")):
website = "https://" + website
validator = URLValidator()
try:
validator(website)
except ValidationError:
raise ValidationError("Please enter a valid website URL.")
return website
class AgencyJobAssignmentForm(forms.ModelForm):
"""Form for creating and editing agency job assignments"""
class Meta:
model = AgencyJobAssignment
fields = ["agency", "job", "max_candidates", "deadline_date", "admin_notes"]
widgets = {
"agency": forms.Select(attrs={"class": "form-select"}),
"job": forms.Select(attrs={"class": "form-select"}),
"max_candidates": forms.NumberInput(
attrs={
"class": "form-control",
"min": 1,
"placeholder": "Maximum number of candidates",
}
),
"deadline_date": forms.DateTimeInput(
attrs={"class": "form-control", "type": "datetime-local"}
),
"is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}),
"status": forms.Select(attrs={"class": "form-select"}),
"admin_notes": forms.Textarea(
attrs={
"class": "form-control",
"rows": 3,
"placeholder": "Internal notes about this assignment",
}
),
}
labels = {
"agency": _("Agency"),
"job": _("Job Posting"),
"max_candidates": _("Maximum Candidates"),
"deadline_date": _("Deadline Date"),
"is_active": _("Is Active"),
"status": _("Status"),
"admin_notes": _("Admin Notes"),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-md-3"
self.helper.field_class = "col-md-9"
# Filter jobs to only show active jobs
self.fields["job"].queryset = JobPosting.objects.filter(
status="ACTIVE"
).order_by("-created_at")
self.helper.layout = Layout(
Row(
Column("agency", css_class="col-md-6"),
Column("job", css_class="col-md-6"),
css_class="g-3 mb-3",
),
Row(
Column("max_candidates", css_class="col-md-6"),
Column("deadline_date", css_class="col-md-6"),
css_class="g-3 mb-3",
),
Row(
Column("is_active", css_class="col-md-6"),
Column("status", css_class="col-md-6"),
css_class="g-3 mb-3",
),
Field("admin_notes", css_class="form-control"),
Div(
Submit("submit", _("Save Assignment"), css_class="btn btn-main-action"),
css_class="col-12 mt-4",
),
)
def clean_deadline_date(self):
"""Validate deadline date is in the future"""
deadline_date = self.cleaned_data.get("deadline_date")
if deadline_date and deadline_date <= timezone.now():
raise ValidationError("Deadline date must be in the future.")
return deadline_date
def clean_max_candidates(self):
"""Validate maximum candidates is positive"""
max_candidates = self.cleaned_data.get("max_candidates")
if max_candidates and max_candidates <= 0:
raise ValidationError("Maximum candidates must be greater than 0.")
return max_candidates
def clean(self):
"""Check for duplicate assignments"""
cleaned_data = super().clean()
agency = cleaned_data.get("agency")
job = cleaned_data.get("job")
if agency and job:
# Check if this assignment already exists
existing = (
AgencyJobAssignment.objects.filter(agency=agency, job=job)
.exclude(pk=self.instance.pk)
.first()
)
if existing:
raise ValidationError(
f"This job is already assigned to {agency.name}. "
f"Current status: {existing.get_status_display()}"
)
return cleaned_data
class AgencyAccessLinkForm(forms.ModelForm):
"""Form for creating and managing agency access links"""
class Meta:
model = AgencyAccessLink
fields = ["assignment", "expires_at", "is_active"]
widgets = {
"assignment": forms.Select(attrs={"class": "form-select"}),
"expires_at": forms.DateTimeInput(
attrs={"class": "form-control", "type": "datetime-local"}
),
"is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}),
}
labels = {
"assignment": _("Assignment"),
"expires_at": _("Expires At"),
"is_active": _("Is Active"),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-md-3"
self.helper.field_class = "col-md-9"
# Filter assignments to only show active ones without existing links
self.fields["assignment"].queryset = (
AgencyJobAssignment.objects.filter(is_active=True, status="ACTIVE")
.exclude(access_link__isnull=False)
.order_by("-created_at")
)
self.helper.layout = Layout(
Field("assignment", css_class="form-control"),
Field("expires_at", css_class="form-control"),
Field("is_active", css_class="form-check-input"),
Div(
Submit(
"submit", _("Create Access Link"), css_class="btn btn-main-action"
),
css_class="col-12 mt-4",
),
)
def clean_expires_at(self):
"""Validate expiration date is in the future"""
expires_at = self.cleaned_data.get("expires_at")
if expires_at and expires_at <= timezone.now():
raise ValidationError("Expiration date must be in the future.")
return expires_at
# Agency messaging forms removed - AgencyMessage model has been deleted
class AgencyCandidateSubmissionForm(forms.ModelForm):
"""Form for agencies to submit candidates (simplified - resume + basic info)"""
class Meta:
model = Candidate
fields = ["first_name", "last_name", "email", "phone", "resume"]
widgets = {
"first_name": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "First Name",
"required": True,
}
),
"last_name": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "Last Name",
"required": True,
}
),
"email": forms.EmailInput(
attrs={
"class": "form-control",
"placeholder": "email@example.com",
"required": True,
}
),
"phone": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "+966 50 123 4567",
"required": True,
}
),
"resume": forms.FileInput(
attrs={
"class": "form-control",
"accept": ".pdf,.doc,.docx",
"required": True,
}
),
}
labels = {
"first_name": _("First Name"),
"last_name": _("Last Name"),
"email": _("Email Address"),
"phone": _("Phone Number"),
"resume": _("Resume"),
}
def __init__(self, assignment, *args, **kwargs):
super().__init__(*args, **kwargs)
self.assignment = assignment
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_class = "g-3"
self.helper.enctype = "multipart/form-data"
self.helper.layout = Layout(
Row(
Column("first_name", css_class="col-md-6"),
Column("last_name", css_class="col-md-6"),
css_class="g-3 mb-3",
),
Row(
Column("email", css_class="col-md-6"),
Column("phone", css_class="col-md-6"),
css_class="g-3 mb-3",
),
Field("resume", css_class="form-control"),
Div(
Submit(
"submit", _("Submit Candidate"), css_class="btn btn-main-action"
),
css_class="col-12 mt-4",
),
)
def clean_email(self):
"""Validate email format and check for duplicates in the same job"""
email = self.cleaned_data.get("email")
if email:
# Check if candidate with this email already exists for this job
existing_candidate = Candidate.objects.filter(
email=email.lower().strip(), job=self.assignment.job
).first()
if existing_candidate:
raise ValidationError(
f"A candidate with this email has already applied for {self.assignment.job.title}."
)
return email.lower().strip() if email else email
def clean_resume(self):
"""Validate resume file"""
resume = self.cleaned_data.get("resume")
if resume:
# Check file size (max 5MB)
if resume.size > 5 * 1024 * 1024:
raise ValidationError("Resume file size must be less than 5MB.")
# Check file extension
allowed_extensions = [".pdf", ".doc", ".docx"]
file_extension = resume.name.lower().split(".")[-1]
if f".{file_extension}" not in allowed_extensions:
raise ValidationError("Resume must be in PDF, DOC, or DOCX format.")
return resume
def save(self, commit=True):
"""Override save to set additional fields"""
instance = super().save(commit=False)
# Set required fields for agency submission
instance.job = self.assignment.job
instance.hiring_agency = self.assignment.agency
instance.stage = Candidate.Stage.APPLIED
instance.applicant_status = Candidate.ApplicantType.CANDIDATE
instance.applied = True
if commit:
instance.save()
# Increment the assignment's submitted count
self.assignment.increment_submission_count()
return instance
class AgencyLoginForm(forms.Form):
"""Form for agencies to login with token and password"""
token = forms.CharField(
widget=forms.TextInput(
attrs={"class": "form-control", "placeholder": "Enter your access token"}
),
label=_("Access Token"),
required=True,
)
password = forms.CharField(
widget=forms.PasswordInput(
attrs={"class": "form-control", "placeholder": "Enter your password"}
),
label=_("Password"),
required=True,
)
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 __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
# self.helper = FormHelper()
# self.helper.form_method = 'post'
# self.helper.form_class = 'g-3'
# self.helper.layout = Layout(
# Field('token', css_class='form-control'),
# Field('password', css_class='form-control'),
# Div(
# Submit('submit', _('Login'), css_class='btn btn-main-action w-100'),
# css_class='col-12 mt-4'
# )
# )
def clean(self):
"""Validate token and password combination"""
cleaned_data = super().clean()
token = cleaned_data.get("token")
password = cleaned_data.get("password")
if token and password:
try:
access_link = AgencyAccessLink.objects.get(
unique_token=token, is_active=True
)
if not access_link.is_valid:
if access_link.is_expired:
raise ValidationError("This access link has expired.")
else:
raise ValidationError("This access link is no longer active.")
if access_link.access_password != password:
raise ValidationError("Invalid password.")
# Store the access_link for use in the view
self.validated_access_link = access_link
except AgencyAccessLink.DoesNotExist:
print("Access link does not exist")
raise ValidationError("Invalid access token.")
return cleaned_data
# participants form
class ParticipantsForm(forms.ModelForm):
"""Form for creating and editing Participants"""
class Meta:
model = Participants
fields = ["name", "email", "phone", "designation"]
widgets = {
"name": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "Enter participant name",
"required": True,
}
),
"email": forms.EmailInput(
attrs={
"class": "form-control",
"placeholder": "Enter email address",
"required": True,
}
),
"phone": forms.TextInput(
attrs={"class": "form-control", "placeholder": "Enter phone number"}
),
"designation": forms.TextInput(
attrs={"class": "form-control", "placeholder": "Enter designation"}
),
# 'jobs': forms.CheckboxSelectMultiple(),
}
class ParticipantsSelectForm(forms.ModelForm):
"""Form for selecting Participants"""
participants = forms.ModelMultipleChoiceField(
queryset=Participants.objects.all(),
widget=forms.CheckboxSelectMultiple,
required=False,
label=_("Select Participants"),
)
users = forms.ModelMultipleChoiceField(
queryset=User.objects.all(),
widget=forms.CheckboxSelectMultiple,
required=False,
label=_("Select Users"),
)
class Meta:
model = JobPosting
fields = ["participants", "users"] # No direct fields from Participants model
class CandidateEmailForm(forms.Form):
"""Form for composing emails to participants about a candidate"""
subject = forms.CharField(
max_length=200,
widget=forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "Enter email subject",
"required": True,
}
),
label=_("Subject"),
required=True,
)
message = forms.CharField(
widget=forms.Textarea(
attrs={
"class": "form-control",
"rows": 8,
"placeholder": "Enter your message here...",
"required": True,
}
),
label=_("Message"),
required=True,
)
recipients = forms.MultipleChoiceField(
widget=forms.CheckboxSelectMultiple(attrs={"class": "form-check"}),
label=_("Recipients"),
required=True,
)
include_candidate_info = forms.BooleanField(
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
label=_("Include candidate information"),
initial=True,
required=False,
)
include_meeting_details = forms.BooleanField(
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
label=_("Include meeting details"),
initial=True,
required=False,
)
def __init__(self, job, candidate, *args, **kwargs):
super().__init__(*args, **kwargs)
self.job = job
self.candidate = candidate
# Get all participants and users for this job
recipient_choices = []
# Add job participants
for participant in job.participants.all():
recipient_choices.append(
(
f"participant_{participant.id}",
f"{participant.name} - {participant.designation} (Participant)",
)
)
# Add job users
for user in job.users.all():
recipient_choices.append(
(
f"user_{user.id}",
f"{user.get_full_name() or user.username} - {user.email} (User)",
)
)
self.fields["recipients"].choices = recipient_choices
self.fields["recipients"].initial = [
choice[0] for choice in recipient_choices
] # Select all by default
# Set initial subject
self.fields[
"subject"
].initial = f"Interview Update: {candidate.name} - {job.title}"
# Set initial message with candidate and meeting info
initial_message = self._get_initial_message()
if initial_message:
self.fields["message"].initial = initial_message
def _get_initial_message(self):
"""Generate initial message with candidate and meeting information"""
message_parts = []
# Add candidate information
if self.candidate:
message_parts.append(f"Candidate Information:")
message_parts.append(f"Name: {self.candidate.name}")
message_parts.append(f"Email: {self.candidate.email}")
message_parts.append(f"Phone: {self.candidate.phone}")
# Add latest meeting information if available
latest_meeting = self.candidate.get_latest_meeting
if latest_meeting:
message_parts.append(f"\nMeeting Information:")
message_parts.append(f"Topic: {latest_meeting.topic}")
message_parts.append(
f"Date & Time: {latest_meeting.start_time.strftime('%B %d, %Y at %I:%M %p')}"
)
message_parts.append(f"Duration: {latest_meeting.duration} minutes")
if latest_meeting.join_url:
message_parts.append(f"Join URL: {latest_meeting.join_url}")
return "\n".join(message_parts)
def clean_recipients(self):
"""Ensure at least one recipient is selected"""
recipients = self.cleaned_data.get("recipients")
if not recipients:
raise forms.ValidationError(_("Please select at least one recipient."))
return recipients
def get_email_addresses(self):
"""Extract email addresses from selected recipients"""
email_addresses = []
recipients = self.cleaned_data.get("recipients", [])
for recipient in recipients:
if recipient.startswith("participant_"):
participant_id = recipient.split("_")[1]
try:
participant = Participants.objects.get(id=participant_id)
email_addresses.append(participant.email)
except Participants.DoesNotExist:
continue
elif recipient.startswith("user_"):
user_id = recipient.split("_")[1]
try:
user = User.objects.get(id=user_id)
email_addresses.append(user.email)
except User.DoesNotExist:
continue
return list(set(email_addresses)) # Remove duplicates
def get_formatted_message(self):
"""Get the formatted message with optional additional information"""
message = self.cleaned_data.get("message", "")
# Add candidate information if requested
if self.cleaned_data.get("include_candidate_info") and self.candidate:
candidate_info = f"\n\n--- Candidate Information ---\n"
candidate_info += f"Name: {self.candidate.name}\n"
candidate_info += f"Email: {self.candidate.email}\n"
candidate_info += f"Phone: {self.candidate.phone}\n"
message += candidate_info
# Add meeting details if requested
if self.cleaned_data.get("include_meeting_details") and self.candidate:
latest_meeting = self.candidate.get_latest_meeting
if latest_meeting:
meeting_info = f"\n\n--- Meeting Details ---\n"
meeting_info += f"Topic: {latest_meeting.topic}\n"
meeting_info += f"Date & Time: {latest_meeting.start_time.strftime('%B %d, %Y at %I:%M %p')}\n"
meeting_info += f"Duration: {latest_meeting.duration} minutes\n"
if latest_meeting.join_url:
meeting_info += f"Join URL: {latest_meeting.join_url}\n"
message += meeting_info
return message
class MessageForm(forms.ModelForm):
"""Form for creating and editing messages between users"""
class Meta:
model = Message
fields = ["recipient", "job", "subject", "content", "message_type"]
widgets = {
"recipient": forms.Select(
attrs={"class": "form-select", "placeholder": "Select recipient"}
),
"job": forms.Select(
attrs={"class": "form-select", "placeholder": "Select job (optional)"}
),
"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,
}
),
"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"
# Filter job options based on user type
self._filter_job_field()
# Filter recipient options based on user type
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":
# Agency users can only see jobs assigned to their agency
self.fields["job"].queryset = JobPosting.objects.filter(
hiring_agency__user=self.user,
status="ACTIVE"
).order_by("-created_at")
elif self.user.user_type == "candidate":
# Candidates can only see jobs they applied for
self.fields["job"].queryset = JobPosting.objects.filter(
candidates__user=self.user
).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(
Q(user_type="staff") |
Q(candidate_profile__job__hiring_agency__user=self.user)
).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 Candidate.objects.filter(job=job, user=self.user).exists():
raise forms.ValidationError(
_("You can only message about jobs you have applied for.")
)
class CandidateSignupForm(forms.Form):
first_name = forms.CharField(max_length=30, required=True)
middle_name = forms.CharField(max_length=30, required=False)
last_name = forms.CharField(max_length=30, required=True)
email = forms.EmailField(max_length=254, required=True)
phone = forms.CharField(max_length=30, required=True)
password = forms.CharField(widget=forms.PasswordInput, required=True)
confirm_password = forms.CharField(widget=forms.PasswordInput, required=True)
def clean(self):
cleaned_data = super().clean()
password = cleaned_data.get("password")
confirm_password = cleaned_data.get("confirm_password")
if password != confirm_password:
raise forms.ValidationError("Passwords do not match.")
return cleaned_data