1612 lines
56 KiB
Python
1612 lines
56 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,
|
|
)
|
|
|
|
# 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
|