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 ) # from django_summernote.widgets import SummernoteWidget from django_ckeditor_5.widgets import CKEditor5Widget import secrets import string from django.core.exceptions import ValidationError 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): """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', 'resume',] labels = { 'first_name': _('First Name'), 'last_name': _('Last Name'), 'phone': _('Phone'), 'email': _('Email'), 'resume': _('Resume'), } 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'}), } 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('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': 'Assistant Professor of Computer Science', 'required': True }), 'department': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Computer Science, Human Resources, etc.' }), '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 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'}), }