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.models import User from django.contrib.auth.forms import UserCreationForm 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 ) # 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