2790 lines
101 KiB
Python
2790 lines
101 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 (
|
||
#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 candidate’s 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
|