2025-11-27 16:25:34 +03:00

2790 lines
101 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (
#ZoomMeetingDetails,
Application,
TrainingMaterial,
JobPosting,
FormTemplate,
BulkInterviewTemplate,
BreakTime,
JobPostingImage,
InterviewNote,
ScheduledInterview,
Source,
HiringAgency,
AgencyJobAssignment,
AgencyAccessLink,
Participants,
Message,
Person,
Document
)
# 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","trusted_ips", "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"}
),
"trusted_ips":forms.TextInput(
attrs={"class": "form-control", "placeholder": "192.168.1.100","required": False}
),
"is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-md-3"
self.helper.field_class = "col-md-9"
self.helper.layout = Layout(
Field("name", css_class="form-control"),
Field("source_type", css_class="form-control"),
Field("ip_address", css_class="form-control"),
Field("is_active", css_class="form-check-input"),
Submit("submit", "Save Source", css_class="btn btn-primary mt-3"),
)
def clean_name(self):
"""Ensure source name is unique"""
name = self.cleaned_data.get("name")
if name:
# Check for duplicates excluding current instance if editing
instance = self.instance
if not instance.pk: # Creating new instance
if Source.objects.filter(name=name).exists():
raise ValidationError("A source with this name already exists.")
else: # Editing existing instance
if Source.objects.filter(name=name).exclude(pk=instance.pk).exists():
raise ValidationError("A source with this name already exists.")
return name
class SourceAdvancedForm(forms.ModelForm):
"""Advanced form for creating and editing sources with API key generation"""
# Hidden field to trigger API key generation
generate_keys = forms.CharField(
widget=forms.HiddenInput(),
required=False,
help_text="Set to 'true' to generate new API keys",
)
# Display fields for generated keys (read-only)
api_key_generated = forms.CharField(
label="Generated API Key",
required=False,
widget=forms.TextInput(attrs={"readonly": True, "class": "form-control"}),
)
api_secret_generated = forms.CharField(
label="Generated API Secret",
required=False,
widget=forms.TextInput(attrs={"readonly": True, "class": "form-control"}),
)
class Meta:
model = Source
fields = [
"name",
"source_type",
"description",
"ip_address",
"trusted_ips",
"is_active",
"integration_version",
]
widgets = {
"name": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "e.g., ATS System, ERP Integration",
"required": True,
}
),
"source_type": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "e.g., ATS, ERP, API",
"required": True,
}
),
"description": forms.Textarea(
attrs={
"class": "form-control",
"rows": 3,
"placeholder": "Brief description of the source system",
}
),
"ip_address": forms.TextInput(
attrs={"class": "form-control", "placeholder": "192.168.1.100"}
),
"trusted_ips": forms.Textarea(
attrs={
"class": "form-control",
"rows": 2,
"placeholder": "Comma-separated IP addresses (e.g., 192.168.1.100, 10.0.0.1)",
}
),
"integration_version": forms.TextInput(
attrs={"class": "form-control", "placeholder": "v1.0, v2.1"}
),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-md-3"
self.helper.field_class = "col-md-9"
# Add generate keys button
self.helper.layout = Layout(
Field("name", css_class="form-control"),
Field("source_type", css_class="form-control"),
Field("description", css_class="form-control"),
Field("ip_address", css_class="form-control"),
Field("trusted_ips", css_class="form-control"),
Field("integration_version", css_class="form-control"),
Field("is_active", css_class="form-check-input"),
# Hidden field for key generation trigger
Field("generate_keys", type="hidden"),
# Display fields for generated keys
Field("api_key_generated", css_class="form-control"),
Field("api_secret_generated", css_class="form-control"),
Submit("submit", "Save Source", css_class="btn btn-primary mt-3"),
)
def clean_name(self):
"""Ensure source name is unique"""
name = self.cleaned_data.get("name")
if name:
# Check for duplicates excluding current instance if editing
instance = self.instance
if not instance.pk: # Creating new instance
if Source.objects.filter(name=name).exists():
raise ValidationError("A source with this name already exists.")
else: # Editing existing instance
if Source.objects.filter(name=name).exclude(pk=instance.pk).exists():
raise ValidationError("A source with this name already exists.")
return name
def clean_trusted_ips(self):
"""Validate and format trusted IP addresses"""
trusted_ips = self.cleaned_data.get("trusted_ips")
if trusted_ips:
# Split by comma and strip whitespace
ips = [ip.strip() for ip in trusted_ips.split(",") if ip.strip()]
# Validate each IP address
for ip in ips:
try:
# Basic IP validation (can be enhanced)
if not (ip.replace(".", "").isdigit() and len(ip.split(".")) == 4):
raise ValidationError(f"Invalid IP address: {ip}")
except Exception:
raise ValidationError(f"Invalid IP address: {ip}")
return ", ".join(ips)
return trusted_ips
def clean(self):
"""Custom validation for the form"""
cleaned_data = super().clean()
# Check if we need to generate API keys
generate_keys = cleaned_data.get("generate_keys")
if generate_keys == "true":
# Generate new API key and secret
cleaned_data["api_key"] = generate_api_key()
cleaned_data["api_secret"] = generate_api_secret()
# Set display fields for the frontend
cleaned_data["api_key_generated"] = cleaned_data["api_key"]
cleaned_data["api_secret_generated"] = cleaned_data["api_secret"]
return cleaned_data
class PersonForm(forms.ModelForm):
class Meta:
model = Person
fields = ["first_name","middle_name", "last_name", "email", "phone","date_of_birth","nationality","gender","address"]
widgets = {
"first_name": forms.TextInput(attrs={'class': 'form-control'}),
"middle_name": forms.TextInput(attrs={'class': 'form-control'}),
"last_name": forms.TextInput(attrs={'class': 'form-control'}),
"email": forms.EmailInput(attrs={'class': 'form-control'}),
"phone": forms.TextInput(attrs={'class': 'form-control'}),
"gender": forms.Select(attrs={'class': 'form-control'}),
"date_of_birth": forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
"nationality": forms.Select(attrs={'class': 'form-control select2'}),
"address": forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
class ApplicationForm(forms.ModelForm):
class Meta:
model = Application
fields = [
'person',
"job",
"hiring_source",
"hiring_agency",
"resume",
]
labels = {
"person":_("Applicant"),
"resume": _("Resume"),
"hiring_source": _("Hiring Type"),
"hiring_agency": _("Hiring Agency"),
}
widgets = {
"hiring_source": forms.Select(attrs={"class": "form-select"}),
"hiring_agency": forms.Select(attrs={"class": "form-select"}),
}
def __init__(self, *args,current_agency=None,current_job=None,**kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-md-3"
self.helper.field_class = "col-md-9"
if current_agency:
# IMPORTANT: Replace 'agency' below with the actual field name
# on your Person model that links it back to the Agency model.
self.fields['person'].queryset = self.fields['person'].queryset.filter(
agency=current_agency
)
self.fields['job'].queryset = self.fields['job'].queryset.filter(
pk=current_job.id
)
self.fields['job'].initial = current_job
self.fields['job'].widget.attrs['readonly'] = True
# Make job field read-only if it's being pre-populated
job_value = self.initial.get("job")
if job_value:
self.fields["job"].widget.attrs["readonly"] = True
self.helper.layout = Layout(
Field("job", css_class="form-control"),
Field("hiring_source", css_class="form-control"),
Field("hiring_agency", css_class="form-control"),
Field("resume", css_class="form-control"),
Submit("submit", _("Submit"), css_class="btn btn-primary"),
)
# def clean(self):
# cleaned_data = super().clean()
# job = cleaned_data.get("job")
# agency = cleaned_data.get("hiring_agency")
# person = cleaned_data.get("person")
# if Application.objects.filter(person=person,job=job, hiring_agency=agency).exists():
# raise forms.ValidationError("You have already applied for this job.")
# return cleaned_data
# def save(self, commit=True):
# """Override save to handle person creation/update"""
# instance = super().save(commit=False)
# # Get or create person
# if instance.person:
# person = instance.person
# else:
# # Create new person
# from .models import Person
# person = Person()
# # Update person fields
# person.first_name = self.cleaned_data['first_name']
# person.last_name = self.cleaned_data['last_name']
# person.email = self.cleaned_data['email']
# person.phZoomone = self.cleaned_data['phone']
# if commit:
# person.save()
# instance.person = person
# instance.save()
# return instance
class ApplicationStageForm(forms.ModelForm):
"""Form specifically for updating candidate stage with validation"""
class Meta:
model = Application
fields = ["stage"]
labels = {
"stage": _("New Application Stage"),
}
widgets = {
"stage": forms.Select(attrs={"class": "form-select"}),
}
# class ZoomMeetingForm(forms.ModelForm):
# class Meta:
# model = ZoomMeetingDetails
# 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 MeetingForm(forms.ModelForm):
# class Meta:
# model = ZoomMeetingDetails
# 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,
}
),
}
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 BulkInterviewTemplate'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 BulkInterviewTemplateForm(forms.ModelForm):
# applications = forms.ModelMultipleChoiceField(
# queryset=Application.objects.none(),
# widget=forms.CheckboxSelectMultiple,
# required=True,
# )
# working_days = forms.MultipleChoiceField(
# choices=[
# (0, "Monday"),
# (1, "Tuesday"),
# (2, "Wednesday"),
# (3, "Thursday"),
# (4, "Friday"),
# (5, "Saturday"),
# (6, "Sunday"),
# ],
# widget=forms.CheckboxSelectMultiple,
# required=True,
# )
# class Meta:
# model = BulkInterviewTemplate
# fields = [
# 'schedule_interview_type',
# "applications",
# "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"}
# ),
# "schedule_interview_type":forms.RadioSelect()
# }
# def __init__(self, slug, *args, **kwargs):
# super().__init__(*args, **kwargs)
# self.fields["applications"].queryset = Application.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 InterviewNoteForm(forms.ModelForm):
# """Form for creating and editing meeting comments"""
# class Meta:
# model = InterviewNote
# 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", "application"]
class ProfileImageUploadForm(forms.ModelForm):
class Meta:
model = User
fields = ["profile_image"]
class StaffUserCreationForm(UserCreationForm):
email = forms.EmailField(label=_("Email"), required=True)
first_name = forms.CharField(label=_("First Name"),max_length=30, required=True)
last_name = forms.CharField(label=_("Last Name"),max_length=150, required=True)
class Meta:
model = User
fields = ("email", "first_name", "last_name", "password1", "password2")
def clean_email(self):
email = self.cleaned_data["email"]
if User.objects.filter(email=email).exists():
raise forms.ValidationError("A user with this email already exists.")
return email
def generate_username(self, email):
"""Generate a valid, unique username from email."""
prefix = email.split("@")[0].lower()
username = re.sub(r"[^a-z0-9._]", "", prefix)
if not username:
username = "user"
base = username
counter = 1
while User.objects.filter(username=username).exists():
username = f"{base}{counter}"
counter += 1
return username
def save(self, commit=True):
user = super().save(commit=False)
user.email = self.cleaned_data["email"]
user.first_name = self.cleaned_data["first_name"]
user.last_name = self.cleaned_data["last_name"]
user.username = self.generate_username(user.email)
user.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 ApplicationExamDateForm(forms.ModelForm):
class Meta:
model = Application
fields = ["exam_date"]
widgets = {
"exam_date": forms.DateTimeInput(
attrs={"type": "datetime-local", "class": "form-control"}
),
}
class HiringAgencyForm(forms.ModelForm):
"""Form for creating and editing hiring agencies"""
class Meta:
model = HiringAgency
fields = [
"name",
"contact_person",
"email",
"phone",
"website",
"country",
"address",
"notes",
]
widgets = {
"name": forms.TextInput(
attrs={
"class": "form-control",
"required": True,
}
),
"contact_person": forms.TextInput(
attrs={
"class": "form-control",
}
),
"email": forms.EmailInput(
attrs={"class": "form-control"}
),
"phone": forms.TextInput(
attrs={"class": "form-control"}
),
"website": forms.URLInput(
attrs={"class": "form-control"}
),
"country": forms.Select(attrs={"class": "form-select"}),
"address": forms.Textarea(
attrs={
"class": "form-control",
"rows": 3,
}
),
"notes": forms.Textarea(
attrs={
"class": "form-control",
"rows": 3,
}
),
}
labels = {
"name": _("Agency Name"),
"contact_person": _("Contact Person"),
"email": _("Email Address"),
"phone": _("Phone Number"),
"website": _("Website"),
"country": _("Country"),
"address": _("Address"),
"notes": _("Internal Notes"),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-md-3"
self.helper.field_class = "col-md-9"
self.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,
}
),
"deadline_date": forms.DateTimeInput(
attrs={"class": "form-control", "type": "datetime-local"}
),
"is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}),
"status": forms.Select(attrs={"class": "form-select"}),
"admin_notes": forms.Textarea(
attrs={
"class": "form-control",
"rows": 3,
}
),
}
labels = {
"agency": _("Agency"),
"job": _("Job Posting"),
"max_candidates": _("Maximum Candidates"),
"deadline_date": _("Deadline Date"),
"is_active": _("Is Active"),
"status": _("Status"),
"admin_notes": _("Admin Notes"),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-md-3"
self.helper.field_class = "col-md-9"
# Filter jobs to only show active jobs
self.fields["job"].queryset = JobPosting.objects.filter(
status="ACTIVE"
).order_by("-created_at")
self.helper.layout = Layout(
Row(
Column("agency", css_class="col-md-6"),
Column("job", css_class="col-md-6"),
css_class="g-3 mb-3",
),
Row(
Column("max_candidates", css_class="col-md-6"),
Column("deadline_date", css_class="col-md-6"),
css_class="g-3 mb-3",
),
Row(
Column("is_active", css_class="col-md-6"),
Column("status", css_class="col-md-6"),
css_class="g-3 mb-3",
),
Field("admin_notes", css_class="form-control"),
Div(
Submit("submit", _("Save Assignment"), css_class="btn btn-main-action"),
css_class="col-12 mt-4",
),
)
def clean_deadline_date(self):
"""Validate deadline date is in the future"""
deadline_date = self.cleaned_data.get("deadline_date")
if deadline_date and deadline_date <= timezone.now():
raise ValidationError("Deadline date must be in the future.")
return deadline_date
def clean_max_candidates(self):
"""Validate maximum candidates is positive"""
max_candidates = self.cleaned_data.get("max_candidates")
if max_candidates and max_candidates <= 0:
raise ValidationError("Maximum candidates must be greater than 0.")
return max_candidates
def clean(self):
"""Check for duplicate assignments"""
cleaned_data = super().clean()
agency = cleaned_data.get("agency")
job = cleaned_data.get("job")
if agency and job:
# Check if this assignment already exists
existing = (
AgencyJobAssignment.objects.filter(agency=agency, job=job)
.exclude(pk=self.instance.pk)
.first()
)
if existing:
raise ValidationError(
f"This job is already assigned to {agency.name}. "
f"Current status: {existing.get_status_display()}"
)
return cleaned_data
class AgencyAccessLinkForm(forms.ModelForm):
"""Form for creating and managing agency access links"""
class Meta:
model = AgencyAccessLink
fields = ["assignment", "expires_at", "is_active"]
widgets = {
"assignment": forms.Select(attrs={"class": "form-select"}),
"expires_at": forms.DateTimeInput(
attrs={"class": "form-control", "type": "datetime-local"}
),
"is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}),
}
labels = {
"assignment": _("Assignment"),
"expires_at": _("Expires At"),
"is_active": _("Is Active"),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-md-3"
self.helper.field_class = "col-md-9"
# Filter assignments to only show active ones without existing links
self.fields["assignment"].queryset = (
AgencyJobAssignment.objects.filter(is_active=True, status="ACTIVE")
.exclude(access_link__isnull=False)
.order_by("-created_at")
)
self.helper.layout = Layout(
Field("assignment", css_class="form-control"),
Field("expires_at", css_class="form-control"),
Field("is_active", css_class="form-check-input"),
Div(
Submit(
"submit", _("Create Access Link"), css_class="btn btn-main-action"
),
css_class="col-12 mt-4",
),
)
def clean_expires_at(self):
"""Validate expiration date is in the future"""
expires_at = self.cleaned_data.get("expires_at")
if expires_at and expires_at <= timezone.now():
raise ValidationError("Expiration date must be in the future.")
return expires_at
# Agency messaging forms removed - AgencyMessage model has been deleted
class AgencyApplicationSubmissionForm(forms.ModelForm):
"""Form for agencies to submit candidates"""
class Meta:
model = Application
fields = ["person", "resume"]
labels = {
"resume": _("Resume"),
}
def __init__(self, assignment, *args, **kwargs):
super().__init__(*args, **kwargs)
self.assignment = assignment
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_class = "g-3"
self.helper.enctype = "multipart/form-data"
self.helper.layout = Layout(
Field("person", css_class="form-control"),
Field("resume", css_class="form-control"),
Div(
Submit("submit", _("Submit Candidate"), css_class="btn btn-main-action"),
css_class="col-12 mt-4",
),
)
def clean_resume(self):
"""Validate resume file"""
resume = self.cleaned_data.get("resume")
if resume:
# Check file size (max 5MB)
if resume.size > 5 * 1024 * 1024:
raise ValidationError("Resume file size must be less than 5MB.")
# Check file extension
allowed_extensions = [".pdf", ".doc", ".docx"]
file_extension = resume.name.lower().split(".")[-1]
if f".{file_extension}" not in allowed_extensions:
raise ValidationError("Resume must be in PDF, DOC, or DOCX format.")
return resume
def save(self, commit=True):
"""Override save to set additional fields"""
instance = super().save(commit=False)
# Set required fields for agency submission
instance.job = self.assignment.job
instance.hiring_agency = self.assignment.agency
instance.stage = Application.Stage.APPLIED
instance.applicant_status = Application.ApplicantType.CANDIDATE
instance.applied = True
if commit:
instance.save()
# Increment the assignment's submitted count
self.assignment.increment_submission_count()
return instance
class AgencyLoginForm(forms.Form):
"""Form for agencies to login with token and password"""
token = forms.CharField(
widget=forms.TextInput(
attrs={"class": "form-control", "placeholder": "Enter your access token"}
),
label=_("Access Token"),
required=True,
)
password = forms.CharField(
widget=forms.PasswordInput(
attrs={"class": "form-control", "placeholder": "Enter your password"}
),
label=_("Password"),
required=True,
)
class PortalLoginForm(forms.Form):
"""Unified login form for agency and candidate"""
USER_TYPE_CHOICES = [
("", _("Select User Type")),
("agency", _("Agency")),
("candidate", _("Candidate")),
]
email = forms.EmailField(
widget=forms.EmailInput(
attrs={"class": "form-control", "placeholder": "Enter your email"}
),
label=_("Email"),
required=True,
)
password = forms.CharField(
widget=forms.PasswordInput(
attrs={"class": "form-control", "placeholder": "Enter your password"}
),
label=_("Password"),
required=True,
)
user_type = forms.ChoiceField(
choices=USER_TYPE_CHOICES,
widget=forms.Select(attrs={"class": "form-control"}),
label=_("User Type"),
required=True,
)
# def __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"""
to = forms.MultipleChoiceField(
widget=forms.CheckboxSelectMultiple(attrs={
'class': 'form-check'
}),
label=_('Select Candidates'), # Use a descriptive label
required=False
)
subject = forms.CharField(
max_length=200,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter email subject',
'required': True
}),
label=_('Subject'),
required=True
)
message = forms.CharField(
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 8,
'placeholder': 'Enter your message here...',
'required': True
}),
label=_('Message'),
required=True
)
def __init__(self, job, candidates, *args, **kwargs):
super().__init__(*args, **kwargs)
self.job = job
self.candidates=candidates
candidate_choices=[]
for candidate in candidates:
candidate_choices.append(
(f'candidate_{candidate.id}', f'{candidate.email}')
)
self.fields['to'].choices =candidate_choices
self.fields['to'].initial = [choice[0] for choice in candidate_choices]
# Set initial message with candidate and meeting info
initial_message = self._get_initial_message()
if initial_message:
self.fields['message'].initial = initial_message
def _get_initial_message(self):
"""Generate initial message with candidate and meeting information"""
candidate=self.candidates.first()
message_parts=[]
if candidate and candidate.stage == 'Applied':
message_parts = [
f"Than you, for your interest in the {self.job.title} role.",
f"We regret to inform you that you were not selected to move forward to the exam round at this time.",
f"We encourage you to check our career page for further updates and future opportunities:",
f"https://kaauh/careers",
f"Wishing you the best in your job search,",
f"The KAAUH Hiring team"
]
elif candidate and candidate.stage == 'Exam':
message_parts = [
f"Than you,for your interest in the {self.job.title} role.",
f"We're pleased to inform you that your initial screening was successful!",
f"The next step is the mandatory online assessment exam.",
f"Please complete the assessment by using the following link:",
f"https://kaauh/hire/exam",
f"We look forward to reviewing your results.",
f"Best regards, The KAAUH Hiring team"
]
elif candidate and candidate.stage == 'Interview':
message_parts = [
f"Than you, for your interest in the {self.job.title} role.",
f"We're pleased to inform you that you have cleared your exam!",
f"The next step is the mandatory interview.",
f"Please complete the assessment by using the following link:",
f"https://kaauh/hire/exam",
f"We look forward to reviewing your results.",
f"Best regards, The KAAUH Hiring team"
]
elif candidate and candidate.stage == 'Offer':
message_parts = [
f"Congratulations, ! We are delighted to inform you that we are extending a formal offer of employment for the {self.job.title} role.",
f"This is an exciting moment, and we look forward to having you join the KAAUH team.",
f"A detailed offer letter and compensation package will be sent to you via email within 24 hours.",
f"In the meantime, please contact our HR department at [HR Contact] if you have immediate questions.",
f"Welcome to the team!",
f"Best regards, The KAAUH Hiring team"
]
elif candidate and candidate.stage == 'Hired':
message_parts = [
f"Welcome aboard,!",
f"We are thrilled to officially confirm your employment as our new {self.job.title}.",
f"You will receive a separate email shortly with details regarding your start date, first-day instructions, and onboarding documents.",
f"We look forward to seeing you at KAAUH.",
f"If you have any questions before your start date, please contact [Onboarding Contact].",
f"Best regards, The KAAUH Hiring team"
]
elif candidate:
message_parts=""
# # 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 get_email_addresses(self):
"""Extract email addresses from selected recipients"""
email_addresses = []
candidates=self.cleaned_data.get('to',[])
if candidates:
for candidate in candidates:
if candidate.startswith('candidate_'):
print("candidadte: {candidate}")
candidate_id = candidate.split('_')[1]
try:
candidate = Application.objects.get(id=candidate_id)
email_addresses.append(candidate.email)
except Application.DoesNotExist:
continue
return list(set(email_addresses)) # Remove duplicates
def get_formatted_message(self):
"""Get the formatted message with optional additional information"""
message = self.cleaned_data.get('message', '')
return message
# class InterviewParticpantsForm(forms.ModelForm):
# participants = forms.ModelMultipleChoiceField(
# queryset=Participants.objects.all(),
# widget=forms.CheckboxSelectMultiple,
# required=False ,
# )
# system_users=forms.ModelMultipleChoiceField(
# queryset=User.objects.filter(user_type='staff'),
# widget=forms.CheckboxSelectMultiple,
# required=False,
# label=_("Select Users"))
# class Meta:
# model = BulkInterviewTemplate
# fields = ['participants','system_users']
# class InterviewEmailForm(forms.Form):
# subject = forms.CharField(
# max_length=200,
# widget=forms.TextInput(attrs={
# 'class': 'form-control',
# 'placeholder': 'Enter email subject',
# 'required': True
# }),
# label=_('Subject'),
# required=True
# )
# message_for_candidate= forms.CharField(
# widget=forms.Textarea(attrs={
# 'class': 'form-control',
# 'rows': 8,
# 'placeholder': 'Enter your message here...',
# 'required': True
# }),
# label=_('Message'),
# required=False
# )
# message_for_agency= forms.CharField(
# widget=forms.Textarea(attrs={
# 'class': 'form-control',
# 'rows': 8,
# 'placeholder': 'Enter your message here...',
# 'required': True
# }),
# label=_('Message'),
# required=False
# )
# message_for_participants= forms.CharField(
# widget=forms.Textarea(attrs={
# 'class': 'form-control',
# 'rows': 8,
# 'placeholder': 'Enter your message here...',
# 'required': True
# }),
# label=_('Message'),
# required=False
# )
# def __init__(self, *args,candidate, external_participants, system_participants,meeting,job,**kwargs):
# super().__init__(*args, **kwargs)
# # --- Data Preparation ---
# # Note: Added error handling for agency name if it's missing (though it shouldn't be based on your check)
# formatted_date = meeting.start_time.strftime('%Y-%m-%d')
# formatted_time = meeting.start_time.strftime('%I:%M %p')
# zoom_link = meeting.join_url
# duration = meeting.duration
# job_title = job.title
# agency_name = candidate.hiring_agency.name if candidate.belong_to_an_agency and candidate.hiring_agency else "Hiring Agency"
# # --- Combined Participants List for Internal Email ---
# external_participants_names = ", ".join([p.name for p in external_participants ])
# system_participants_names = ", ".join([p.first_name for p in system_participants ])
# # Combine and ensure no leading/trailing commas if one list is empty
# participant_names = ", ".join(filter(None, [external_participants_names, system_participants_names]))
# # --- 1. Candidate Message (More concise and structured) ---
# candidate_message = f"""
# Dear {candidate.full_name},
# Thank you for your interest in the **{job_title}** position at KAAUH. We're pleased to invite you to an interview!
# The details of your virtual interview are as follows:
# - **Date:** {formatted_date}
# - **Time:** {formatted_time} (RIYADH TIME)
# - **Duration:** {duration}
# - **Meeting Link:** {zoom_link}
# Please click the link at the scheduled time to join the interview.
# Kindly reply to this email to **confirm your attendance** or to propose an alternative time if necessary.
# We look forward to meeting you.
# Best regards,
# KAAUH Hiring Team
# """
# # --- 2. Agency Message (Professional and clear details) ---
# agency_message = f"""
# Dear {agency_name},
# We have scheduled an interview for your candidate, **{candidate.full_name}**, for the **{job_title}** role.
# Please forward the following details to the candidate and ensure they are fully prepared.
# **Interview Details:**
# - **Candidate:** {candidate.full_name}
# - **Job Title:** {job_title}
# - **Date:** {formatted_date}
# - **Time:** {formatted_time} (RIYADH TIME)
# - **Duration:** {duration}
# - **Meeting Link:** {zoom_link}
# Please let us know if you or the candidate have any questions.
# Best regards,
# KAAUH Hiring Team
# """
# # --- 3. Participants Message (Action-oriented and informative) ---
# participants_message = f"""
# Hi Team,
# This is a reminder of the upcoming interview you are scheduled to participate in for the **{job_title}** position.
# **Interview Summary:**
# - **Candidate:** {candidate.full_name}
# - **Date:** {formatted_date}
# - **Time:** {formatted_time} (RIYADH TIME)
# - **Duration:** {duration}
# - **Your Fellow Interviewers:** {participant_names}
# **Action Items:**
# 1. Please review **{candidate.full_name}'s** resume and notes.
# 2. The official calendar invite contains the meeting link ({zoom_link}) and should be used to join.
# 3. Be ready to start promptly at the scheduled time.
# Thank you for your participation.
# Best regards,
# KAAUH HIRING TEAM
# """
# # Set initial data
# self.initial['subject'] = f"Interview Invitation: {job_title} at KAAUH - {candidate.full_name}"
# # .strip() removes the leading/trailing blank lines caused by the f""" format
# self.initial['message_for_candidate'] = candidate_message.strip()
# self.initial['message_for_agency'] = agency_message.strip()
# self.initial['message_for_participants'] = participants_message.strip()
# class InterviewEmailForm(forms.Form):
# # ... (Field definitions)
# def __init__(self, *args, candidate, external_participants, system_participants, meeting, job, **kwargs):
# super().__init__(*args, **kwargs)
# location = meeting
# # --- Data Preparation ---
# # Safely access details through the related InterviewLocation object
# if location and location.start_time:
# formatted_date = location.start_time.strftime('%Y-%m-%d')
# formatted_time = location.start_time.strftime('%I:%M %p')
# duration = location.duration
# meeting_link = location.details_url if location.details_url else "N/A (See Location Topic)"
# else:
# # Handle case where location or time is missing/None
# formatted_date = "TBD - Awaiting Scheduling"
# formatted_time = "TBD"
# duration = "N/A"
# meeting_link = "Not Available"
# job_title = job.title
# agency_name = candidate.hiring_agency.name if candidate.belong_to_an_agency and candidate.hiring_agency else "Hiring Agency"
# # --- Combined Participants List for Internal Email ---
# external_participants_names = ", ".join([p.name for p in external_participants ])
# system_participants_names = ", ".join([p.first_name for p in system_participants ])
# participant_names = ", ".join(filter(None, [external_participants_names, system_participants_names]))
# # --- 1. Candidate Message (Use meeting_link) ---
# candidate_message = f"""
# Dear {candidate.full_name},
# Thank you for your interest in the **{job_title}** position at KAAUH. We're pleased to invite you to an interview!
# The details of your virtual interview are as follows:
# - **Date:** {formatted_date}
# - **Time:** {formatted_time} (RIYADH TIME)
# - **Duration:** {duration}
# - **Meeting Link:** {meeting_link}
# Please click the link at the scheduled time to join the interview.
# Kindly reply to this email to **confirm your attendance** or to propose an alternative time if necessary.
# We look forward to meeting you.
# Best regards,
# KAAUH Hiring Team
# """
# # ... (Messages for agency and participants remain the same, using the updated safe variables)
# # --- 2. Agency Message (Professional and clear details) ---
# agency_message = f"""
# Dear {agency_name},
# ...
# **Interview Details:**
# ...
# - **Date:** {formatted_date}
# - **Time:** {formatted_time} (RIYADH TIME)
# - **Duration:** {duration}
# - **Meeting Link:** {meeting_link}
# ...
# """
# # --- 3. Participants Message (Action-oriented and informative) ---
# participants_message = f"""
# Hi Team,
# ...
# **Interview Summary:**
# - **Candidate:** {candidate.full_name}
# - **Date:** {formatted_date}
# - **Time:** {formatted_time} (RIYADH TIME)
# - **Duration:** {duration}
# - **Your Fellow Interviewers:** {participant_names}
# **Action Items:**
# 1. Please review **{candidate.full_name}'s** resume and notes.
# 2. The official calendar invite contains the meeting link ({meeting_link}) and should be used to join.
# 3. Be ready to start promptly at the scheduled time.
# ...
# """
# # Set initial data
# self.initial['subject'] = f"Interview Invitation: {job_title} at KAAUH - {candidate.full_name}"
# self.initial['message_for_candidate'] = candidate_message.strip()
# self.initial['message_for_agency'] = agency_message.strip()
# self.initial['message_for_participants'] = participants_message.strip()
# # class OnsiteLocationForm(forms.ModelForm):
# # class Meta:
# # model=
# # fields=['location']
# # widgets={
# # 'location': forms.TextInput(attrs={'placeholder': 'Enter Interview Location'}),
# # }
#during bulk schedule
# class OnsiteLocationForm(forms.ModelForm):
# class Meta:
# model = OnsiteLocationDetails
# # Include 'room_number' and update the field list
# fields = ['topic', 'physical_address', 'room_number']
# widgets = {
# 'topic': forms.TextInput(
# attrs={'placeholder': 'Enter the Meeting Topic', 'class': 'form-control'}
# ),
# 'physical_address': forms.TextInput(
# attrs={'placeholder': 'Physical address (e.g., street address)', 'class': 'form-control'}
# ),
# 'room_number': forms.TextInput(
# attrs={'placeholder': 'Room Number/Name (Optional)', 'class': 'form-control'}
# ),
# }
# class InterviewEmailForm(forms.Form):
# subject = forms.CharField(max_length=255, widget=forms.TextInput(attrs={'class': 'form-control'}))
# message_for_candidate = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 6}))
# message_for_agency = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 6}),required=False)
# message_for_participants = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 6}))
# def __init__(self, *args, candidate, external_participants, system_participants, meeting, job, **kwargs):
# """
# meeting: an InterviewLocation instance (e.g., ZoomMeetingDetails or OnsiteLocationDetails)
# """
# super().__init__(*args, **kwargs)
# # ✅ meeting is already the InterviewLocation — do NOT use .interview_location
# location = meeting
# # --- Determine concrete details (Zoom or Onsite) ---
# if location.location_type == location.LocationType.REMOTE:
# details = getattr(location, 'zoommeetingdetails', None)
# elif location.location_type == location.LocationType.ONSITE:
# details = getattr(location, 'onsitelocationdetails', None)
# else:
# details = None
# # --- Extract meeting info safely ---
# if details and details.start_time:
# formatted_date = details.start_time.strftime('%Y-%m-%d')
# formatted_time = details.start_time.strftime('%I:%M %p')
# duration = details.duration
# meeting_link = location.details_url or "N/A (See Location Topic)"
# else:
# formatted_date = "TBD - Awaiting Scheduling"
# formatted_time = "TBD"
# duration = "N/A"
# meeting_link = "Not Available"
# job_title = job.title
# agency_name = (
# candidate.hiring_agency.name
# if candidate.belong_to_an_agency and candidate.hiring_agency
# else "Hiring Agency"
# )
# # --- Participant names for internal email ---
# external_names = ", ".join([p.name for p in external_participants])
# system_names = ", ".join([u.get_full_name() or u.username for u in system_participants])
# participant_names = ", ".join(filter(None, [external_names, system_names]))
# # --- Candidate Message ---
# candidate_message = f"""
# Dear {candidate.full_name},
# Thank you for your interest in the **{job_title}** position at KAAUH. We're pleased to invite you to an interview!
# The details of your interview are as follows:
# - **Date:** {formatted_date}
# - **Time:** {formatted_time} (RIYADH TIME)
# - **Duration:** {duration} minutes
# - **Meeting Link/Location:** {meeting_link}
# Please be ready at the scheduled time.
# Kindly reply to confirm your attendance or propose an alternative if needed.
# We look forward to meeting you.
# Best regards,
# KAAUH Hiring Team
# """.strip()
# # --- Agency Message ---
# agency_message = f"""
# Dear {agency_name},
# This is to inform you that your candidate, **{candidate.full_name}**, has been scheduled for an interview for the **{job_title}** position.
# **Interview Details:**
# - **Date:** {formatted_date}
# - **Time:** {formatted_time} (RIYADH TIME)
# - **Duration:** {duration} minutes
# - **Meeting Link/Location:** {meeting_link}
# Please ensure the candidate is informed and prepared.
# Best regards,
# KAAUH Hiring Team
# """.strip()
# # --- Participants (Interview Panel) Message ---
# participants_message = f"""
# Hi Team,
# You are scheduled to interview **{candidate.full_name}** for the **{job_title}** role.
# **Interview Summary:**
# - **Candidate:** {candidate.full_name}
# - **Date:** {formatted_date}
# - **Time:** {formatted_time} (RIYADH TIME)
# - **Duration:** {duration} minutes
# - **Location/Link:** {meeting_link}
# - **Fellow Interviewers:** {participant_names}
# **Action Items:**
# 1. Review the candidates resume and application notes.
# 2. Join via the link above (or be at the physical location) on time.
# 3. Coordinate among yourselves for role coverage.
# Thank you!
# """.strip()
# # --- Set initial values ---
# self.initial.update({
# 'subject': f"Interview Invitation: {job_title} - {candidate.full_name}",
# 'message_for_candidate': candidate_message,
# 'message_for_agency': agency_message,
# 'message_for_participants': participants_message,
# })
# class OnsiteReshuduleForm(forms.ModelForm):
# class Meta:
# model = OnsiteLocationDetails
# fields = ['topic', 'physical_address', 'room_number','start_time','duration','status']
# widgets = {
# 'topic': forms.TextInput(
# attrs={'placeholder': 'Enter the Meeting Topic', 'class': 'form-control'}
# ),
# 'physical_address': forms.TextInput(
# attrs={'placeholder': 'Physical address (e.g., street address)', 'class': 'form-control'}
# ),
# 'room_number': forms.TextInput(
# attrs={'placeholder': 'Room Number/Name (Optional)', 'class': 'form-control'}
# ),
# }
# class OnsiteScheduleForm(forms.ModelForm):
# # Add fields for the foreign keys required by ScheduledInterview
# application = forms.ModelChoiceField(
# queryset=Application.objects.all(),
# widget=forms.HiddenInput(), # Hide this in the form, set by the view
# label=_("Candidate Application")
# )
# job = forms.ModelChoiceField(
# queryset=JobPosting.objects.all(),
# widget=forms.HiddenInput(), # Hide this in the form, set by the view
# label=_("Job Posting")
# )
# class Meta:
# model = OnsiteLocationDetails
# # Include all fields from OnsiteLocationDetails plus the new ones
# fields = ['topic', 'physical_address', 'room_number', 'start_time', 'duration', 'status', 'application', 'job']
# widgets = {
# 'topic': forms.TextInput(
# attrs={'placeholder': _('Enter the Meeting Topic'), 'class': 'form-control'}
# ),
# 'physical_address': forms.TextInput(
# attrs={'placeholder': _('Physical address (e.g., street address)'), 'class': 'form-control'}
# ),
# 'room_number': forms.TextInput(
# attrs={'placeholder': _('Room Number/Name (Optional)'), 'class': 'form-control'}
# ),
# # You should explicitly set widgets for start_time, duration, and status here
# # if they need Bootstrap classes, otherwise they will use default HTML inputs.
# # Example:
# 'start_time': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}),
# 'duration': forms.NumberInput(attrs={'class': 'form-control', 'min': 15}),
# 'status': forms.HiddenInput(), # Status should default to SCHEDULED, so hide it.
# }
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","required": True,}
),
"job": forms.Select(
attrs={"class": "form-select", "placeholder": "Select job",
"hx-get": "/en/messages/create/",
"hx-target": "#id_recipient",
"hx-select": "#id_recipient",
"hx-swap": "outerHTML",}
),
"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(
applications__person=self.user.person_profile,
).distinct().order_by("-created_at")
else:
# Staff can see all jobs
self.fields["job"].queryset = JobPosting.objects.filter(
status="ACTIVE"
).order_by("-created_at")
def _filter_recipient_field(self):
"""Filter recipient options based on user type"""
if self.user.user_type == "staff":
# Staff can message anyone
self.fields["recipient"].queryset = User.objects.all().order_by("username")
elif self.user.user_type == "agency":
# Agency can message staff and their candidates
from django.db.models import Q
self.fields["recipient"].queryset = User.objects.filter(
user_type="staff"
).distinct().order_by("username")
elif self.user.user_type == "candidate":
# Candidates can only message staff
self.fields["recipient"].queryset = User.objects.filter(
user_type="staff"
).order_by("username")
def clean(self):
"""Validate message form data"""
cleaned_data = super().clean()
job = cleaned_data.get("job")
recipient = cleaned_data.get("recipient")
# If job is selected but no recipient, auto-assign to job.assigned_to
if job and not recipient:
if job.assigned_to:
cleaned_data["recipient"] = job.assigned_to
# Set message type to job_related
cleaned_data["message_type"] = Message.MessageType.JOB_RELATED
else:
raise forms.ValidationError(
_("Selected job is not assigned to any user. Please assign the job first.")
)
# Validate messaging permissions
if self.user and cleaned_data.get("recipient"):
self._validate_messaging_permissions(cleaned_data)
return cleaned_data
def _validate_messaging_permissions(self, cleaned_data):
"""Validate if user can message the recipient"""
recipient = cleaned_data.get("recipient")
job = cleaned_data.get("job")
# Staff can message anyone
if self.user.user_type == "staff":
return
# Agency users validation
if self.user.user_type == "agency":
if recipient.user_type not in ["staff", "candidate"]:
raise forms.ValidationError(
_("Agencies can only message staff or candidates.")
)
# If messaging a candidate, ensure candidate is from their agency
if recipient.user_type == "candidate" and job:
if not job.hiring_agency.filter(user=self.user).exists():
raise forms.ValidationError(
_("You can only message candidates from your assigned jobs.")
)
# Candidate users validation
if self.user.user_type == "candidate":
if recipient.user_type != "staff":
raise forms.ValidationError(
_("Candidates can only message staff.")
)
# If job-related, ensure candidate applied for the job
if job:
if not Application.objects.filter(job=job, person=self.user.person_profile).exists():
raise forms.ValidationError(
_("You can only message about jobs you have applied for.")
)
class ApplicantSignupForm(forms.ModelForm):
password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'form-control'}))
confirm_password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'form-control'}))
class Meta:
model = Person
fields = ["first_name","middle_name","last_name", "email","phone","gpa","nationality", "date_of_birth","gender","address"]
widgets = {
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
'middle_name': forms.TextInput(attrs={'class': 'form-control'}),
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'phone': forms.TextInput(attrs={'class': 'form-control'}),
# 'gpa': forms.TextInput(attrs={'class': 'form-control'}),
"nationality": forms.Select(attrs={'class': 'form-control select2'}),
'date_of_birth': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'gender': forms.Select(attrs={'class': 'form-control'}),
'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
def clean(self):
cleaned_data = super().clean()
password = cleaned_data.get("password")
confirm_password = cleaned_data.get("confirm_password")
if password and confirm_password and password != confirm_password:
raise forms.ValidationError("Passwords do not match.")
return cleaned_data
class DocumentUploadForm(forms.ModelForm):
"""Form for uploading documents for candidates"""
class Meta:
model = Document
fields = ['document_type', 'description', 'file']
widgets = {
'document_type': forms.Select(
choices=Document.DocumentType.choices,
attrs={'class': 'form-control'}
),
'description': forms.Textarea(
attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Enter document description (optional)'
}
),
'file': forms.FileInput(
attrs={
'class': 'form-control',
'accept': '.pdf,.doc,.docx,.jpg,.jpeg,.png'
}
),
}
labels = {
'document_type': _('Document Type'),
'description': _('Description'),
'file': _('Document File'),
}
def clean_file(self):
"""Validate uploaded file"""
file = self.cleaned_data.get('file')
if file:
# Check file size (max 10MB)
if file.size > 10 * 1024 * 1024: # 10MB
raise forms.ValidationError(
_('File size must be less than 10MB.')
)
# Check file extension
allowed_extensions = ['.pdf', '.doc', '.docx', '.jpg', '.jpeg', '.png']
file_extension = file.name.lower().split('.')[-1]
if f'.{file_extension}' not in allowed_extensions:
raise forms.ValidationError(
_('File type must be one of: PDF, DOC, DOCX, JPG, JPEG, PNG.')
)
return file
def clean(self):
"""Custom validation for document upload"""
cleaned_data = super().clean()
self.clean_file()
return cleaned_data
class PasswordResetForm(forms.Form):
old_password = forms.CharField(
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
label=_('Old Password')
)
new_password1 = forms.CharField(
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
label=_('New Password')
)
new_password2 = forms.CharField(
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
label=_('Confirm New Password')
)
def clean(self):
"""Custom validation for password reset"""
cleaned_data = super().clean()
old_password = cleaned_data.get('old_password')
new_password1 = cleaned_data.get('new_password1')
new_password2 = cleaned_data.get('new_password2')
if old_password:
if not self.data.get('old_password'):
raise forms.ValidationError(_('Old password is incorrect.'))
if new_password1 and new_password2:
if new_password1 != new_password2:
raise forms.ValidationError(_('New passwords do not match.'))
return cleaned_data
class StaffAssignmentForm(forms.ModelForm):
"""Form for assigning staff to a job posting"""
class Meta:
model = JobPosting
fields = ['assigned_to']
widgets = {
'assigned_to': forms.Select(attrs={
'class': 'form-select',
'placeholder': _('Select staff member'),
'required': True
}),
}
labels = {
'assigned_to': _('Assign Staff Member'),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Filter users to only show staff members
self.fields['assigned_to'].queryset = User.objects.filter(
user_type='staff',is_superuser=False
).order_by('first_name', 'last_name')
# Add empty choice for unassigning
self.fields['assigned_to'].required = False
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.form_class = 'g-3'
self.helper.form_id = 'staff-assignment-form'
self.helper.layout = Layout(
Field('assigned_to', css_class='form-control'),
Div(
Submit('submit', _('Assign Staff'), css_class='btn btn-primary'),
css_class='col-12 mt-3'
),
)
def clean_assigned_to(self):
"""Validate that assigned user is a staff member"""
assigned_to = self.cleaned_data.get('assigned_to')
if assigned_to and assigned_to.user_type != 'staff':
raise forms.ValidationError(_('Only staff members can be assigned to jobs.'))
return assigned_to
class RemoteInterviewForm(forms.Form):
"""Form for creating remote interviews"""
# Add Interview model fields to the form
topic = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., Software Interview'
}),
label=_('Meeting Topic')
)
interview_date = forms.DateField(
required=False,
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
}),
label=_('Interview Date')
)
interview_time = forms.TimeField(
required=False,
widget=forms.TimeInput(attrs={
'class': 'form-control',
'type': 'time'
}),
label=_('Interview Time')
)
duration = forms.IntegerField(
min_value=1,
required=False,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'placeholder': 'Duration in minutes'
}),
label=_('Duration (minutes)')
)
# class Meta:
# model = ScheduledInterview
# fields = ['topic','interview_date', 'interview_time']
# widgets = {
# # 'application': forms.Select(attrs={'class': 'form-control', 'required': True}),
# 'interview_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date', 'required': True}),
# 'interview_time': forms.TimeInput(attrs={'class': 'form-control', 'type': 'time', 'required': True}),
# # 'participants': forms.SelectMultiple(attrs={'class': 'form-control select2'}),
# # 'system_users': forms.SelectMultiple(attrs={'class': 'form-control select2'}),
# # 'status': forms.Select(attrs={'class': 'form-control'}),
# }
# labels = {
# # 'application': _('Candidate'),
# 'interview_date': _('Interview Date'),
# 'interview_time': _('Interview Time'),
# 'participants': _('External Participants'),
# 'system_users': _('System Users'),
# 'status': _('Status'),
# }
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
# # Filter applications to only show candidates in Interview stage
# self.fields['application'].queryset = Application.objects.filter(stage='Interview').order_by('-created_at')
# # Filter participants and system users
# self.fields['participants'].queryset = Participants.objects.all().order_by('name')
# self.fields['system_users'].queryset = User.objects.filter(user_type='staff').order_by('first_name', 'last_name')
# self.helper = FormHelper()
# self.helper.form_method = 'post'
# self.helper.form_class = 'g-3'
# self.helper.layout = Layout(
# Field('application', css_class='form-control'),
# Row(
# Column('interview_date', css_class='col-md-6'),
# Column('interview_time', css_class='col-md-6'),
# css_class='g-3 mb-3',
# ),
# Row(
# Column('participants', css_class='col-md-6'),
# Column('system_users', css_class='col-md-6'),
# css_class='g-3 mb-3',
# ),
# Field('status', css_class='form-control'),
# Div(
# Field('topic', css_class='form-control'),
# Field('details_url', css_class='form-control'),
# Field('meeting_id', css_class='form-control'),
# Field('password', css_class='form-control'),
# Field('duration', css_class='form-control'),
# css_class='mb-4'
# ),
# Div(
# Submit('submit', _('Schedule Remote Interview'), css_class='btn btn-primary'),
# css_class='col-12 mt-4',
# ),
# )
# def clean_interview_date(self):
# """Validate interview date is not in the past"""
# interview_date = self.cleaned_data.get('interview_date')
# if interview_date and interview_date < timezone.now().date():
# raise forms.ValidationError(_('Interview date cannot be in the past.'))
# return interview_date
# def clean_meeting_id(self):
# """Validate meeting ID is provided if URL is provided"""
# details_url = self.cleaned_data.get('details_url')
# meeting_id = self.cleaned_data.get('meeting_id')
# # If a URL is provided, require a meeting ID as well
# if details_url and not meeting_id:
# raise forms.ValidationError(_('Meeting ID is required when providing a meeting URL.'))
# return meeting_id
# def clean_details_url(self):
# """Validate URL format"""
# details_url = self.cleaned_data.get('details_url')
# if details_url:
# validator = URLValidator()
# try:
# validator(details_url)
# except ValidationError:
# raise forms.ValidationError(_('Please enter a valid URL (e.g., https://zoom.us/j/...)'))
# return details_url
# def clean_duration(self):
# """Validate duration is positive"""
# duration = self.cleaned_data.get('duration')
# if duration is not None and duration < 1:
# raise forms.ValidationError(_('Duration must be at least 1 minute.'))
# return duration or 60 # Default to 60 if not provided
# def clean(self):
# """Custom validation for remote interview"""
# cleaned_data = super().clean()
# interview_date = cleaned_data.get('interview_date')
# interview_time = cleaned_data.get('interview_time')
# details_url = cleaned_data.get('details_url')
# meeting_id = cleaned_data.get('meeting_id')
# # Validate interview date and time are not in the past
# if interview_date and interview_time:
# interview_datetime = timezone.make_aware(
# timezone.datetime.combine(interview_date, interview_time),
# timezone.get_current_timezone()
# )
# if interview_datetime <= timezone.now():
# raise forms.ValidationError(_('Interview date and time cannot be in the past.'))
# # If both URL and meeting ID are provided, validate they are consistent
# if details_url and meeting_id:
# if meeting_id not in details_url:
# # This is optional - you can remove this validation if you don't want to enforce it
# pass # Just a warning that the two may not match
# # Validate that for remote interviews, at least basic location info is provided
# topic = cleaned_data.get('topic')
# if not topic:
# # Allow empty topic but warn that it's recommended
# pass
# return cleaned_data
# def save(self, commit=True):
# """Override save to handle the related Interview instance"""
# instance = super().save(commit=False)
# if commit:
# # Save the scheduled interview first
# instance.save()
# # Create and save the related Interview instance with remote details
# from .models import Interview
# interview = Interview(
# topic=self.cleaned_data.get('topic', ''),
# details_url=self.cleaned_data.get('details_url', ''),
# meeting_id=self.cleaned_data.get('meeting_id', ''),
# password=self.cleaned_data.get('password', ''),
# duration=self.cleaned_data.get('duration', 60),
# location_type=Interview.LocationType.REMOTE,
# start_time=timezone.make_aware(
# timezone.datetime.combine(
# instance.interview_date,
# instance.interview_time
# )
# )
# )
# interview.full_clean() # Validate the interview model
# interview.save()
# # Link the interview to the scheduled interview
# instance.interview = interview
# instance.save()
# return instance
class OnsiteInterviewForm(forms.Form):
"""Form for creating onsite interviews"""
# Add Interview model fields to the form
topic = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., In-person Interview'
}),
label=_('Interview Topic')
)
physical_address = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Physical address'
}),
label=_('Physical Address')
)
room_number = forms.CharField(
max_length=50,
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Room number'
}),
label=_('Room Number')
)
interview_date = forms.DateField(
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date',
'required': True
}),
label=_('Interview Date')
)
interview_time = forms.TimeField(
widget=forms.TimeInput(attrs={
'class': 'form-control',
'type': 'time',
'required': True
}),
label=_('Interview Time')
)
duration = forms.IntegerField(
min_value=1,
required=False,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'placeholder': 'Duration in minutes'
}),
label=_('Duration (minutes)')
)
# class Meta:
# model = ScheduledInterview
# fields = ['application', 'interview_date', 'interview_time', 'participants', 'system_users', 'status']
# widgets = {
# 'application': forms.Select(attrs={'class': 'form-control', 'required': True}),
# 'interview_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date', 'required': True}),
# 'interview_time': forms.TimeInput(attrs={'class': 'form-control', 'type': 'time', 'required': True}),
# 'participants': forms.SelectMultiple(attrs={'class': 'form-control select2'}),
# 'system_users': forms.SelectMultiple(attrs={'class': 'form-control select2'}),
# 'status': forms.Select(attrs={'class': 'form-control'}),
# }
# labels = {
# 'application': _('Application'),
# 'interview_date': _('Interview Date'),
# 'interview_time': _('Interview Time'),
# 'participants': _('External Participants'),
# 'system_users': _('System Users'),
# 'status': _('Status'),
# }
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
# # Filter applications to only show candidates in Interview stage
# self.fields['application'].queryset = Application.objects.filter(stage='Interview').order_by('-created_at')
# # Filter participants and system users
# self.fields['participants'].queryset = Participants.objects.all().order_by('name')
# self.fields['system_users'].queryset = User.objects.filter(user_type='staff').order_by('first_name', 'last_name')
# self.helper = FormHelper()
# self.helper.form_method = 'post'
# self.helper.form_class = 'g-3'
# self.helper.layout = Layout(
# Field('application', css_class='form-control'),
# Row(
# Column('interview_date', css_class='col-md-6'),
# Column('interview_time', css_class='col-md-6'),
# css_class='g-3 mb-3',
# ),
# Row(
# Column('participants', css_class='col-md-6'),
# Column('system_users', css_class='col-md-6'),
# css_class='g-3 mb-3',
# ),
# Field('status', css_class='form-control'),
# Div(
# Field('topic', css_class='form-control'),
# Field('physical_address', css_class='form-control'),
# Field('room_number', css_class='form-control'),
# Field('duration', css_class='form-control'),
# css_class='mb-4'
# ),
# Div(
# Submit('submit', _('Schedule Onsite Interview'), css_class='btn btn-primary'),
# css_class='col-12 mt-4',
# ),
# )
# def clean_interview_date(self):
# """Validate interview date is not in the past"""
# interview_date = self.cleaned_data.get('interview_date')
# if interview_date and interview_date < timezone.now().date():
# raise forms.ValidationError(_('Interview date cannot be in the past.'))
# return interview_date
# def clean(self):
# """Custom validation for onsite interview"""
# cleaned_data = super().clean()
# interview_date = cleaned_data.get('interview_date')
# interview_time = cleaned_data.get('interview_time')
# if interview_date and interview_time:
# interview_datetime = timezone.make_aware(
# timezone.datetime.combine(interview_date, interview_time),
# timezone.get_current_timezone()
# )
# if interview_datetime <= timezone.now():
# raise forms.ValidationError(_('Interview date and time cannot be in the past.'))
# return cleaned_data
# def save(self, commit=True):
# """Override save to handle the related Interview instance"""
# instance = super().save(commit=False)
# if commit:
# # Save the scheduled interview first
# instance.save()
# # Create and save the related Interview instance with onsite details
# from .models import Interview
# interview = Interview(
# topic=self.cleaned_data.get('topic', ''),
# physical_address=self.cleaned_data.get('physical_address', ''),
# room_number=self.cleaned_data.get('room_number', ''),
# duration=self.cleaned_data.get('duration', 60),
# location_type=Interview.LocationType.ONSITE,
# start_time=timezone.make_aware(
# timezone.datetime.combine(
# instance.interview_date,
# instance.interview_time
# )
# )
# )
# interview.full_clean() # Validate the interview model
# interview.save()
# # Link the interview to the scheduled interview
# instance.interview = interview
# instance.save()
# return instance