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