import re 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 from .models import ( Application, JobPosting, FormTemplate, BulkInterviewTemplate, JobPostingImage, Note, ScheduledInterview, Source, HiringAgency, AgencyJobAssignment, AgencyAccessLink, Message, Person, Document, CustomUser, Settings, Interview ) from django_ckeditor_5.widgets import CKEditor5Widget import secrets import string from django.core.exceptions import ValidationError from django.utils import timezone User = get_user_model() 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", "required": True, } ), "source_type": forms.TextInput( attrs={ "class": "form-control", "required": True, } ), "description": forms.Textarea( attrs={ "class": "form-control", "rows": 3, } ), "ip_address": forms.TextInput( attrs={"class": "form-control", "required":True}, ), "trusted_ips":forms.TextInput( attrs={"class": "form-control", "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 def clean_ip_address(self): ip_address=self.cleaned_data.get('ip_address') if not ip_address: raise ValidationError(_("Ip address should not be empty")) return ip_address 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","gpa","national_id","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}), "gpa": forms.TextInput(attrs={'class': 'custom-decimal-input'}), "national_id":forms.NumberInput(attrs={'min': 0, 'step': 1}), } def clean_email(self): email = self.cleaned_data.get('email') if not email: return email if email: instance = self.instance qs = CustomUser.objects.filter(email=email) | CustomUser.objects.filter(username=email) if not instance.pk: # Creating new instance if qs.exists(): raise ValidationError(_("A user account with this email address already exists. Please use a different email.")) else: # Editing existing instance # if ( # qs # .exclude(pk=instance.user.pk) # .exists() # ): # raise ValidationError(_("An user with this email already exists.")) pass return email.strip() 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 else: self.fields['job'].queryset = self.fields['job'].queryset.filter( status="ACTIVE" ) # 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_job(self): job = self.cleaned_data.get("job") if job.max_applications <= Application.objects.filter(job=job).count(): raise forms.ValidationError( "The maximum number of applicants for this job has been reached." ) return job # 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 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 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', 'topic', 'physical_address', "applications", "start_date", "end_date", "working_days", "start_time", "end_time", "interview_duration", "buffer_time", "break_start_time", "break_end_time", ] widgets = { "topic": forms.TextInput(attrs={"class": "form-control"}), "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(), "physical_address": forms.Textarea( attrs={"class": "form-control", "rows": 3, "placeholder": "Enter physical address if 'In-Person' is selected"} ), } def __init__(self, slug, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["applications"].queryset = Application.objects.filter( job__slug=slug, stage="Interview" ) self.fields["topic"].initial = "Interview for " + str( self.fields["applications"].queryset.first().job.title ) self.fields["start_date"].initial = timezone.now().date() working_days_initial = [0, 1, 2, 3, 6] self.fields["working_days"].initial = working_days_initial self.fields["start_time"].initial = "08:00" self.fields["end_time"].initial = "14:00" self.fields["interview_duration"].initial = 30 self.fields["buffer_time"].initial = 10 self.fields["break_start_time"].initial = "11:30" self.fields["break_end_time"].initial = "12:30" self.fields["physical_address"].initial = "Airport Road, King Khalid International Airport, Riyadh 11564, Saudi Arabia" def clean_working_days(self): working_days = self.cleaned_data.get("working_days") return [int(day) for day in working_days] def clean_start_date(self): start_date = self.cleaned_data.get("start_date") if start_date and start_date < timezone.now().date(): raise forms.ValidationError(_("Start date must be in the future")) return start_date def clean_end_date(self): start_date = self.cleaned_data.get("start_date") end_date = self.cleaned_data.get("end_date") if end_date and start_date and end_date < start_date: raise forms.ValidationError(_("End date must be after start date")) return end_date def clean_end_time(self): start_time = self.cleaned_data.get("start_time") end_time = self.cleaned_data.get("end_time") if end_time and start_time and end_time < start_time: raise forms.ValidationError(_("End time must be after start time")) return end_time class InterviewCancelForm(forms.ModelForm): class Meta: model = ScheduledInterview fields = ["cancelled_reason","cancelled_at"] widgets = { "cancelled_reason": forms.Textarea( attrs={"class": "form-control", "rows": 3} ), "cancelled_at": forms.DateTimeInput( attrs={"class": "form-control", "type": "datetime-local"} ), } class NoteForm(forms.ModelForm): """Form for creating and editing meeting comments""" class Meta: model = Note fields = "__all__" widgets = { "content": CKEditor5Widget( attrs={ "class": "form-control", "placeholder": _("Enter your comment or note"), }, config_name="extends", ), } labels = { "content": _("Note"), } 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.password1=self.cleaned_data["password1"] user.password2=self.cleaned_data["password2"] 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"}), } def clean_status(self): status = self.cleaned_data.get("status") if status == "ACTIVE": if self.instance and self.instance.pk: print(self.instance.assigned_to) if not self.instance.assigned_to: raise ValidationError("Please assign the job posting before setting it to Active.") return status 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","required": True} ), "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.fields['email'].required=True 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") instance=self.instance 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 email = email.lower().strip() if not instance.pk: # Creating new instance if HiringAgency.objects.filter(email=email).exists() or User.objects.filter(email=email): 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.") # 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 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 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"Thank you for your interest in the {self.job.title} position at KAAUH and for taking the time to submit your application.", f"We have carefully reviewed your qualifications; however, we regret to inform you that your application was not selected to proceed to the examination round at this time.", f"The selection process was highly competitive, and we had a large number of highly qualified applicants.", f"We encourage you to review other opportunities and apply for roles that align with your skills on our career portal:", f"[settings.CAREER_PAGE_URL]", # Use a Django setting for the URL for flexibility f"We wish you the best of luck in your current job search and future career endeavors.", f"Sincerely,", f"The KAAUH Recruitment Team", ] elif candidate and candidate.stage == 'Exam': message_parts = [ f"Dear Candidate,", f"Thank you once again for your continued interest in the **{self.job.title}** position.", f"We are pleased to inform you that, following a careful review of your application, you have been **selected to proceed to the next phase** of our recruitment process.", f"The next mandatory step is the **Online Assessment Examination** designed to evaluate essential skills for this role.", f"\n**Action Required:**", f"Please click on the link below to access and complete the assessment:", f"[settings.EXAM_LINK_URL]", # Using a settings variable is a professional best practice f"\n**Important Details:**", f"* **Deadline:** The exam must be completed within **72 hours** of receiving this notification.", f"* **Duration:** The assessment is timed and will take approximately [Insert Time e.g., 60 minutes] to complete.", f"* **Technical Note:** Please ensure you have a stable internet connection before beginning.", f"We appreciate your dedication to this process and look forward to reviewing your results.", f"Sincerely,", f"The KAAUH Recruitment Team", ] elif candidate and candidate.stage == 'Interview': message_parts = [ f"Dear Candidate,", f"Thank you for your performance in the recent assessment for the **{self.job.title}** role.", f"We are pleased to inform you that you have **successfully cleared the examination phase** and have been selected to proceed to an interview.", f"The interview is a mandatory step that allows us to learn more about your experience and fit for the role.", f"\n**Next Steps:**", f"Our recruitment coordinator will contact you directly within the next 1-2 business days to schedule your interview time and provide the necessary details (such as the interview panel, format, and location/virtual meeting link).", f"\n**Please ensure your phone number and email address are current.**", f"We look forward to speaking with you and discussing this exciting opportunity further.", f"Sincerely,", f"The KAAUH Recruitment Team", ] elif candidate and candidate.stage == 'Offer': message_parts = [ f"Dear Candidate,", f"We are delighted to extend to you a **formal offer of employment** for the position of **{self.job.title}** at KAAUH.", f"Congratulations! This is an exciting moment, and we are very enthusiastic about the prospect of you joining our team.", f"\n**Next Steps & Documentation:**", f"A comprehensive offer package, detailing your compensation, benefits, and the full terms of employment, will be transmitted to your email address within the next **24 hours**.", f"Please review this document carefully.", f"\n**Questions and Support:**", f"Should you have any immediate questions regarding the offer or the next steps, please do not hesitate to contact our Human Resources department directly at [HR Contact Email/Phone].", f"\nWe eagerly anticipate your favorable response and officially welcoming you to the KAAUH team!", f"Sincerely,", f"The KAAUH Recruitment Team", ] elif candidate and candidate.stage == 'Document Review': message_parts = [ f"Congratulations on progressing to the final stage for the {self.job.title} role!", f"The next critical step is to complete your application by uploading the required employment verification documents.", f"**Please log into the Candidate Portal immediately** to access the 'Document Upload' section.", f"Required documents typically include: National ID/Iqama, Academic Transcripts, and Professional Certifications.", f"You have **7 days** to upload all documents. Failure to do so may delay or invalidate your candidacy.", f"If you encounter any technical issues, please contact our support team at [Support Email/Phone] immediately.", f"We appreciate your cooperation as we finalize your employment process.", ] 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].", ] elif candidate: message_parts="" return '\n'.join(message_parts) def get_email_addresses(self): """Extract email addresses from selected recipients""" email_addresses = [] candidates=self.cleaned_data.get('to',[]) print(f"candidates are {candidates}") 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 MessageForm(forms.ModelForm): """Form for creating and editing messages between users""" class Meta: model = Message fields = ["job","recipient", "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"} ), "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, 'spellcheck': '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" self._filter_recipient_field() self._filter_job_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": print("jhjkshfjksd") job_assignments =AgencyJobAssignment.objects.filter( agency__user=self.user, job__status="ACTIVE" ) job_ids = job_assignments.values_list('job__id', flat=True) self.fields["job"].queryset = JobPosting.objects.filter( id__in=job_ids ).order_by("-created_at") print("Agency user job queryset:", self.fields["job"].queryset) elif self.user.user_type == "candidate": print("sjhdakjhsdkjashkdjhskd") # Candidates can only see jobs they applied for person=self.user.person_profile print(person) applications=person.applications.all() print(applications) self.fields["job"].queryset = JobPosting.objects.filter( applications__in=applications, ).distinct().order_by("-created_at") else: print("shhadjkhkd") # 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) if self.cleaned_data.get('recipient')==self.user: raise forms.ValidationError(_("You cannot message yourself")) 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","national_id", "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'}), "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}), 'national_id': forms.TextInput(attrs={'class': 'form-control'}), } 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 def email_clean(self): email = self.cleaned_data.get('email') if CustomUser.objects.filter(email=email).exists(): raise forms.ValidationError("Email is already in use.") return email 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 PersonPasswordResetForm(forms.Form): 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() new_password1 = cleaned_data.get('new_password1') new_password2 = cleaned_data.get('new_password2') 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=True, widget=forms.NumberInput(attrs={ 'class': 'form-control', 'placeholder': 'Duration in minutes' }), label=_('Duration (minutes)'), error_messages={ 'required': _('Please enter how long the interview will last.'), 'min_value': _('Duration must be at least 1 minute.') } ) 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=True, widget=forms.NumberInput(attrs={ 'class': 'form-control', 'placeholder': 'Duration in minutes' }), label=_('Duration (minutes)'), error_messages={ 'required': _('Please enter how long the interview will last.'), 'min_value': _('Duration must be at least 1 minute.') } ) class ScheduledInterviewForm(forms.Form): topic = forms.CharField( max_length=255, required=False, widget=forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'e.g., Interview Topic' }), label=_('Interview Topic') ) start_time = forms.DateTimeField( widget=forms.DateTimeInput(attrs={ 'class': 'form-control', 'type': 'datetime-local', 'required': True }), label=_('Start Time') ) duration = forms.IntegerField( min_value=1, required=True, widget=forms.NumberInput(attrs={ 'class': 'form-control', 'placeholder': 'Duration in minutes' }), label=_('Duration (minutes)'), error_messages={ 'required': _('Please enter how long the interview will last.'), 'min_value': _('Duration must be at least 1 minute.') } ) def clean_start_time(self): """Validate start time is not in the past""" start_time = self.cleaned_data.get('start_time') if start_time and start_time < timezone.now(): raise forms.ValidationError(_('Start time cannot be in the past.')) return start_time class OnsiteScheduleInterviewUpdateForm(forms.Form): topic = forms.CharField( max_length=255, required=False, widget=forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'e.g., Interview Topic' }), label=_('Interview Topic') ) start_time = forms.DateTimeField( widget=forms.DateTimeInput(attrs={ 'class': 'form-control', 'type': 'datetime-local', 'required': True }), label=_('Start Time') ) duration = forms.IntegerField( min_value=1, required=True, widget=forms.NumberInput(attrs={ 'class': 'form-control', 'placeholder': 'Duration in minutes' }), label=_('Duration (minutes)'), error_messages={ 'required': _('Please enter how long the interview will last.'), 'min_value': _('Duration must be at least 1 minute.') } ) 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') ) class ScheduledInterviewUpdateStatusForm(forms.Form): status = forms.ChoiceField( choices=ScheduledInterview.InterviewStatus.choices, widget=forms.Select(attrs={ 'class': 'form-control', 'required': True }), label=_('Interview Status') ) class Meta: model = ScheduledInterview fields = ['status'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Filter the choices here EXCLUDED_STATUS = ScheduledInterview.InterviewStatus.CANCELLED filtered_choices = [ choice for choice in ScheduledInterview.InterviewStatus.choices if choice[0]!= EXCLUDED_STATUS ] # Apply the filtered list back to the field self.fields['status'].choices = filtered_choices class SettingsForm(forms.ModelForm): """Form for creating and editing settings""" class Meta: model = Settings fields = ['name','key', 'value'] widgets = { 'name': forms.TextInput(attrs={ 'class': 'form-control mb-3', 'placeholder': 'e.g., Zoom', 'required': True }), 'key': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Enter setting key', 'required': True }), 'value': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Enter setting value', 'required': True }), } labels = { 'key': _('Setting Key'), 'value': _('Setting Value'), } 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('key', css_class='form-control'), Field('value', css_class='form-control'), Div( Submit('submit', _('Save Setting'), css_class='btn btn-main-action'), css_class='col-12 mt-4', ), ) def clean_key(self): """Ensure key is unique and properly formatted""" key = self.cleaned_data.get('key') if key: # Convert to uppercase for consistency key = key.upper().strip() # Check for duplicates excluding current instance if editing instance = self.instance if not instance.pk: # Creating new instance if Settings.objects.filter(key=key).exists(): raise forms.ValidationError("A setting with this key already exists.") else: # Editing existing instance if Settings.objects.filter(key=key).exclude(pk=instance.pk).exists(): raise forms.ValidationError("A setting with this key already exists.") # Validate key format (alphanumeric and underscores only) import re if not re.match(r'^[A-Z][A-Z0-9_]*$', key): raise forms.ValidationError( "Setting key must start with a letter and contain only uppercase letters, numbers, and underscores." ) return key def clean_value(self): """Validate setting value""" value = self.cleaned_data.get('value') if value: value = value.strip() if not value: raise forms.ValidationError("Setting value cannot be empty.") return value class InterviewEmailForm(forms.Form): """Form for composing emails to participants about a candidate""" to = forms.CharField( label=_('To'), # Use a descriptive label required=True, ) 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, application,schedule, *args, **kwargs): applicant=application.person.user interview=schedule.interview super().__init__(*args, **kwargs) if application.hiring_agency: self.fields['to'].initial=application.hiring_agency.email self.fields['to'].disabled= True else: self.fields['to'].initial=application.person.email self.fields['to'].disabled= True # Set initial message with candidate and meeting info initial_message = f""" Dear {applicant.first_name} {applicant.last_name}, Your interview details are as follows: Date: {interview.start_time.strftime("%d-%m-%Y")} Time: {interview.start_time.strftime("%I:%M %p")} Interview Duration: {interview.duration} minutes Job: {job.title} """ if interview.location_type == 'Remote': initial_message += f"Pease join using meeting link {interview.join_url} \n\n" else: initial_message += f""" Location: {interview.physical_address} Room No: {interview.room_number} This is an onsite schedule. Please arrive 10 minutes early.\n\n""" self.fields['message'].initial = initial_message # class InterviewResultForm(forms.ModelForm): # class Meta: # model = Interview # fields = ['interview_result', 'result_comments'] # widgets = { # 'interview_result': forms.Select(attrs={ # 'class': 'form-select', # Standard Bootstrap class # 'required': True # }), # 'result_comments': forms.Textarea(attrs={ # 'class': 'form-control', # 'rows': 3, # 'placeholder': 'Enter setting value', # 'required': True # }), # } RESULT_CHOICES = ( ('passed', 'Passed'), ('failed', 'Failed'), ('on_hold', 'On Hold'), ) class InterviewResultForm(forms.Form): interview_result = forms.ChoiceField( choices=RESULT_CHOICES, widget=forms.Select(attrs={ 'class': 'form-select', }) ) result_comments = forms.CharField( widget=forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Enter result comment', }) )