# KAAUH Applicant Tracking System (ATS) - Low Level Design Document ## 1. Introduction This document provides the Low-Level Design (LLD) for the KAAUH Applicant Tracking System (ATS). It details the technical specifications, database schema, API endpoints, and implementation details for the system. ## 2. Database Design ### 2.1 Entity-Relationship Diagram (ERD) ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ JobPosting │ │ Candidate │ │ ZoomMeeting │ ├─────────────────┤ ├─────────────────┤ ├─────────────────┤ │ id (PK) │ │ id (PK) │ │ id (PK) │ │ slug │ │ slug │ │ slug │ │ title │ │ first_name │ │ topic │ │ department │ │ last_name │ │ meeting_id │ │ job_type │ │ email │ │ start_time │ │ workplace_type │ │ phone │ │ duration │ │ location_city │ │ address │ │ timezone │ │ location_state │ │ resume │ │ join_url │ │ location_country│ │ is_resume_parsed│ │ password │ │ description │ │ stage │ │ status │ │ qualifications │ │ exam_status │ │ created_at │ │ salary_range │ │ interview_status│ │ updated_at │ │ benefits │ │ offer_status │ │ │ │ application_url │ │ match_score │ │ │ │ status │ │ strengths │ │ │ │ created_by │ │ weaknesses │ │ │ │ created_at │ │ job (FK) │ │ │ │ updated_at │ │ │ │ │ │ │ │ │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ └───────────────────────┼───────────────────────┘ │ ┌─────────────────┐ │ ScheduledInterview │ ├─────────────────┤ │ id (PK) │ │ candidate (FK) │ │ job (FK) │ │ zoom_meeting (FK)│ │ interview_date │ │ interview_time │ │ status │ │ created_at │ │ updated_at │ └─────────────────┘ │ ┌─────────────────┐ │ FormTemplate │ ├─────────────────┤ │ id (PK) │ │ slug │ │ name │ │ description │ │ job (FK) │ │ is_active │ │ created_by (FK) │ │ created_at │ │ updated_at │ └─────────────────┘ │ ┌─────────────────┐ │ FormStage │ ├─────────────────┤ │ id (PK) │ │ name │ │ order │ │ is_predefined │ │ template (FK) │ │ created_at │ │ updated_at │ └─────────────────┘ │ ┌─────────────────┐ │ FormField │ ├─────────────────┤ │ id (PK) │ │ label │ │ field_type │ │ placeholder │ │ required │ │ order │ │ options │ │ file_types │ │ max_file_size │ │ stage (FK) │ │ created_at │ │ updated_at │ └─────────────────┘ │ ┌─────────────────┐ │ FormSubmission │ ├─────────────────┤ │ id (PK) │ │ slug │ │ template (FK) │ │ submitted_by (FK)│ │ submitted_at │ │ applicant_name │ │ applicant_email │ └─────────────────┘ │ ┌─────────────────┐ │ FieldResponse │ ├─────────────────┤ │ id (PK) │ │ submission (FK) │ │ field (FK) │ │ value │ │ uploaded_file │ │ created_at │ │ updated_at │ └─────────────────┘ │ ┌─────────────────┐ │ MeetingComment │ ├─────────────────┤ │ id (PK) │ │ meeting (FK) │ │ author (FK) │ │ content │ │ created_at │ │ updated_at │ └─────────────────┘ ``` ### 2.2 Detailed Schema Definitions #### 2.2.1 Base Model ```python class Base(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) slug = RandomCharField(length=8, unique=True, editable=False) ``` #### 2.2.2 JobPosting Model ```python class JobPosting(Base): JOB_TYPES = [ ("FULL_TIME", "Full-time"), ("PART_TIME", "Part-time"), ("CONTRACT", "Contract"), ("INTERNSHIP", "Internship"), ("FACULTY", "Faculty"), ("TEMPORARY", "Temporary"), ] WORKPLACE_TYPES = [ ("ON_SITE", "On-site"), ("REMOTE", "Remote"), ("HYBRID", "Hybrid"), ] STATUS_CHOICES = [ ("DRAFT", "Draft"), ("ACTIVE", "Active"), ("CLOSED", "Closed"), ("CANCELLED", "Cancelled"), ("ARCHIVED", "Archived"), ] title = models.CharField(max_length=200) department = models.CharField(max_length=100, blank=True) job_type = models.CharField(max_length=20, choices=JOB_TYPES, default="FULL_TIME") workplace_type = models.CharField(max_length=20, choices=WORKPLACE_TYPES, default="ON_SITE") location_city = models.CharField(max_length=100, blank=True) location_state = models.CharField(max_length=100, blank=True) location_country = models.CharField(max_length=100, default="Saudia Arabia") description = CKEditor5Field(config_name='extends') qualifications = CKEditor5Field(blank=True, config_name='extends') salary_range = models.CharField(max_length=200, blank=True) benefits = CKEditor5Field(blank=True, config_name='extends') application_url = models.URLField(blank=True) application_start_date = models.DateField(null=True, blank=True) application_deadline = models.DateField(null=True, blank=True) application_instructions = CKEditor5Field(blank=True, config_name='extends') internal_job_id = models.CharField(max_length=50, primary_key=True, editable=False) created_by = models.CharField(max_length=100, blank=True) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="DRAFT") hash_tags = models.CharField(max_length=200, blank=True, validators=[validate_hash_tags]) linkedin_post_id = models.CharField(max_length=200, blank=True) linkedin_post_url = models.URLField(blank=True) posted_to_linkedin = models.BooleanField(default=False) linkedin_post_status = models.CharField(max_length=50, blank=True) linkedin_posted_at = models.DateTimeField(null=True, blank=True) published_at = models.DateTimeField(null=True, blank=True) position_number = models.CharField(max_length=50, blank=True) reporting_to = models.CharField(max_length=100, blank=True) joining_date = models.DateField(null=True, blank=True) open_positions = models.PositiveIntegerField(default=1) source = models.ForeignKey("Source", on_delete=models.SET_NULL, null=True, blank=True) max_applications = models.PositiveIntegerField(default=1000) hiring_agency = models.ManyToManyField("HiringAgency", blank=True) cancel_reason = models.TextField(blank=True) cancelled_by = models.CharField(max_length=100, blank=True) cancelled_at = models.DateTimeField(null=True, blank=True) ``` #### 2.2.3 Candidate Model ```python class Candidate(Base): class Stage(models.TextChoices): APPLIED = "Applied", _("Applied") EXAM = "Exam", _("Exam") INTERVIEW = "Interview", _("Interview") OFFER = "Offer", _("Offer") class ExamStatus(models.TextChoices): PASSED = "Passed", _("Passed") FAILED = "Failed", _("Failed") class Status(models.TextChoices): ACCEPTED = "Accepted", _("Accepted") REJECTED = "Rejected", _("Rejected") class ApplicantType(models.TextChoices): APPLICANT = "Applicant", _("Applicant") CANDIDATE = "Candidate", _("Candidate") STAGE_SEQUENCE = { "Applied": ["Exam", "Interview", "Offer"], "Exam": ["Interview", "Offer"], "Interview": ["Offer"], "Offer": [], } job = models.ForeignKey(JobPosting, on_delete=models.CASCADE, related_name="candidates") first_name = models.CharField(max_length=255) last_name = models.CharField(max_length=255) email = models.EmailField() phone = models.CharField(max_length=20) address = models.TextField(max_length=200) resume = models.FileField(upload_to="resumes/") is_resume_parsed = models.BooleanField(default=False) is_potential_candidate = models.BooleanField(default=False) parsed_summary = models.TextField(blank=True) applied = models.BooleanField(default=False) stage = models.CharField(max_length=100, default="Applied", choices=Stage.choices) applicant_status = models.CharField(max_length=100, default="Applicant", choices=ApplicantType.choices) exam_date = models.DateTimeField(null=True, blank=True) exam_status = models.CharField(max_length=100, null=True, blank=True, choices=ExamStatus.choices) interview_date = models.DateTimeField(null=True, blank=True) interview_status = models.CharField(max_length=100, null=True, blank=True, choices=Status.choices) offer_date = models.DateField(null=True, blank=True) offer_status = models.CharField(max_length=100, null=True, blank=True, choices=Status.choices) join_date = models.DateField(null=True, blank=True) match_score = models.IntegerField(null=True, blank=True) strengths = models.TextField(blank=True) weaknesses = models.TextField(blank=True) criteria_checklist = models.JSONField(default=dict, blank=True) resume_parsed_category = models.TextField(blank=True) submitted_by_agency = models.ForeignKey("HiringAgency", on_delete=models.SET_NULL, null=True, blank=True) ``` #### 2.2.4 ZoomMeeting Model ```python class ZoomMeeting(Base): class MeetingStatus(models.TextChoices): SCHEDULED = "waiting", _("Waiting") STARTED = "started", _("Started") ENDED = "ended", _("Ended") CANCELLED = "cancelled", _("Cancelled") topic = models.CharField(max_length=255) meeting_id = models.CharField(max_length=20, unique=True) start_time = models.DateTimeField() duration = models.PositiveIntegerField() timezone = models.CharField(max_length=50) join_url = models.URLField() participant_video = models.BooleanField(default=True) password = models.CharField(max_length=20, blank=True, null=True) join_before_host = models.BooleanField(default=False) mute_upon_entry = models.BooleanField(default=False) waiting_room = models.BooleanField(default=False) zoom_gateway_response = models.JSONField(blank=True, null=True) status = models.CharField(max_length=20, null=True, blank=True) ``` #### 2.2.5 FormTemplate Model ```python class FormTemplate(Base): job = models.OneToOneField(JobPosting, on_delete=models.CASCADE, related_name="form_template") name = models.CharField(max_length=200) description = models.TextField(blank=True) created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name="form_templates", null=True, blank=True) is_active = models.BooleanField(default=False) ``` #### 2.2.6 FormStage Model ```python class FormStage(Base): template = models.ForeignKey(FormTemplate, on_delete=models.CASCADE, related_name="stages") name = models.CharField(max_length=200) order = models.PositiveIntegerField(default=0) is_predefined = models.BooleanField(default=False) ``` #### 2.2.7 FormField Model ```python class FormField(Base): FIELD_TYPES = [ ("text", "Text Input"), ("email", "Email"), ("phone", "Phone"), ("textarea", "Text Area"), ("file", "File Upload"), ("date", "Date Picker"), ("select", "Dropdown"), ("radio", "Radio Buttons"), ("checkbox", "Checkboxes"), ] stage = models.ForeignKey(FormStage, on_delete=models.CASCADE, related_name="fields") label = models.CharField(max_length=200) field_type = models.CharField(max_length=20, choices=FIELD_TYPES) placeholder = models.CharField(max_length=200, blank=True) required = models.BooleanField(default=False) order = models.PositiveIntegerField(default=0) is_predefined = models.BooleanField(default=False) options = models.JSONField(default=list, blank=True) file_types = models.CharField(max_length=200, blank=True) max_file_size = models.PositiveIntegerField(default=5) multiple_files = models.BooleanField(default=False) max_files = models.PositiveIntegerField(default=1) ``` #### 2.2.8 FormSubmission Model ```python class FormSubmission(Base): template = models.ForeignKey(FormTemplate, on_delete=models.CASCADE, related_name="submissions") submitted_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="form_submissions") submitted_at = models.DateTimeField(auto_now_add=True) applicant_name = models.CharField(max_length=200, blank=True) applicant_email = models.EmailField(blank=True) ``` #### 2.2.9 FieldResponse Model ```python class FieldResponse(Base): submission = models.ForeignKey(FormSubmission, on_delete=models.CASCADE, related_name="responses") field = models.ForeignKey(FormField, on_delete=models.CASCADE, related_name="responses") value = models.JSONField(null=True, blank=True) uploaded_file = models.FileField(upload_to="form_uploads/", null=True, blank=True) ``` #### 2.2.10 ScheduledInterview Model ```python class ScheduledInterview(Base): candidate = models.ForeignKey(Candidate, on_delete=models.CASCADE, related_name="scheduled_interviews") job = models.ForeignKey(JobPosting, on_delete=models.CASCADE, related_name="scheduled_interviews") zoom_meeting = models.OneToOneField(ZoomMeeting, on_delete=models.CASCADE, related_name="interview") schedule = models.ForeignKey(InterviewSchedule, on_delete=models.CASCADE, related_name="interviews", null=True, blank=True) interview_date = models.DateField() interview_time = models.TimeField() status = models.CharField(max_length=20, choices=[ ("scheduled", "Scheduled"), ("confirmed", "Confirmed"), ("cancelled", "Cancelled"), ("completed", "Completed"), ], default="scheduled") ``` #### 2.2.11 InterviewSchedule Model ```python class InterviewSchedule(Base): job = models.ForeignKey(JobPosting, on_delete=models.CASCADE, related_name="interview_schedules") candidates = models.ManyToManyField(Candidate, related_name="interview_schedules", blank=True, null=True) start_date = models.DateField() end_date = models.DateField() working_days = models.JSONField() start_time = models.TimeField() end_time = models.TimeField() break_start_time = models.TimeField(null=True, blank=True) break_end_time = models.TimeField(null=True, blank=True) interview_duration = models.PositiveIntegerField() buffer_time = models.PositiveIntegerField(default=0) created_by = models.ForeignKey(User, on_delete=models.CASCADE) ``` #### 2.2.12 MeetingComment Model ```python class MeetingComment(Base): meeting = models.ForeignKey(ZoomMeeting, on_delete=models.CASCADE, related_name="comments") author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="meeting_comments") content = CKEditor5Field(config_name='extends') ``` ## 3. API Design ### 3.1 REST API Endpoints #### 3.1.1 Job Posting Endpoints ``` GET /api/jobs/ # List all job postings POST /api/jobs/ # Create new job posting GET /api/jobs/{id}/ # Get specific job posting PUT /api/jobs/{id}/ # Update job posting DELETE /api/jobs/{id}/ # Delete job posting ``` #### 3.1.2 Candidate Endpoints ``` GET /api/candidates/ # List all candidates POST /api/candidates/ # Create new candidate GET /api/candidates/{id}/ # Get specific candidate PUT /api/candidates/{id}/ # Update candidate DELETE /api/candidates/{id}/ # Delete candidate GET /api/candidates/job/{job_id}/ # Get candidates for specific job ``` #### 3.1.3 Meeting Endpoints ``` GET /api/meetings/ # List all meetings POST /api/meetings/ # Create new meeting GET /api/meetings/{id}/ # Get specific meeting PUT /api/meetings/{id}/ # Update meeting DELETE /api/meetings/{id}/ # Delete meeting POST /api/meetings/{id}/join/ # Join meeting ``` #### 3.1.4 Form Template Endpoints ``` GET /api/templates/ # List form templates POST /api/templates/ # Create form template GET /api/templates/{id}/ # Get specific template PUT /api/templates/{id}/ # Update template DELETE /api/templates/{id}/ # Delete template POST /api/templates/{id}/submit/ # Submit form ``` ### 3.2 WebSocket Events (HTMX) #### 3.2.1 Real-time Updates ```javascript // Meeting status updates event: 'meeting:status_update' data: { meeting_id: '123', status: 'started' } // Candidate status updates event: 'candidate:stage_update' data: { candidate_id: '456', stage: 'Interview' } // Interview schedule updates event: 'interview:schedule_update' data: { interview_id: '789', date: '2025-10-20' } ``` ## 4. Authentication & Authorization ### 4.1 Authentication Flow ```python # Authentication Middleware class CustomAuthenticationMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): # Check for session authentication if 'user_id' in request.session: request.user = get_user_from_session(request.session['user_id']) # Check for JWT token elif 'Authorization' in request.headers: request.user = authenticate_jwt(request.headers['Authorization']) else: request.user = AnonymousUser() return self.get_response(request) ``` ### 4.2 Permission Classes ```python # Custom Permission Classes class IsHiringManager(permissions.BasePermission): def has_permission(self, request, view): return request.user.groups.filter(name='Hiring Managers').exists() class IsRecruiter(permissions.BasePermission): def has_permission(self, request, view): return request.user.groups.filter(name='Recruiters').exists() class IsInterviewer(permissions.BasePermission): def has_permission(self, request, view): return request.user.groups.filter(name='Interviewers').exists() class IsCandidate(permissions.BasePermission): def has_permission(self, request, view): return request.user.groups.filter(name='Candidates').exists() ``` ## 5. Business Logic Implementation ### 5.1 Candidate Stage Transitions ```python class CandidateService: @staticmethod def advance_candidate_stage(candidate, new_stage): """Advance candidate to new stage with validation""" if new_stage not in candidate.get_available_stages(): raise ValidationError(f"Cannot advance from {candidate.stage} to {new_stage}") old_stage = candidate.stage candidate.stage = new_stage # Auto-set exam date when moving to Exam stage if new_stage == "Exam" and not candidate.exam_date: candidate.exam_date = timezone.now() + timedelta(days=7) # Auto-set interview date when moving to Interview stage if new_stage == "Interview" and not candidate.interview_date: candidate.interview_date = timezone.now() + timedelta(days=14) # Auto-set offer date when moving to Offer stage if new_stage == "Offer" and not candidate.offer_date: candidate.offer_date = timezone.now() + timedelta(days=3) candidate.save() # Log stage transition StageTransition.objects.create( candidate=candidate, old_stage=old_stage, new_stage=new_stage, changed_by=candidate.changed_by ) return candidate ``` ### 5.2 Interview Scheduling Logic ```python class InterviewScheduler: @staticmethod def get_available_slots(schedule, date): """Get available interview slots for a specific date""" # Get working hours start_time = datetime.combine(date, schedule.start_time) end_time = datetime.combine(date, schedule.end_time) # Apply break times if schedule.break_start_time and schedule.break_end_time: break_start = datetime.combine(date, schedule.break_start_time) break_end = datetime.combine(date, schedule.break_end_time) end_time = break_start # Remove break time from available slots # Calculate available slots slots = [] current_time = start_time while current_time < end_time: slot_end = current_time + timedelta(minutes=schedule.interview_duration + schedule.buffer_time) if slot_end <= end_time: slots.append({ 'start': current_time.time(), 'end': slot_end.time(), 'available': True }) current_time = slot_end # Filter out booked slots booked_slots = ScheduledInterview.objects.filter( interview_date=date, job=schedule.job ).values_list('interview_time', 'interview_time') for slot in slots: if slot['start'] in booked_slots: slot['available'] = False return slots @staticmethod def schedule_interview(candidate, job, schedule_data): """Schedule an interview with candidate""" # Create Zoom meeting zoom_meeting = create_zoom_meeting( topic=f"Interview: {job.title} with {candidate.name}", start_time=schedule_data['start_time'], duration=schedule_data['duration'] ) # Create scheduled interview interview = ScheduledInterview.objects.create( candidate=candidate, job=job, zoom_meeting=zoom_meeting, interview_date=schedule_data['start_date'], interview_time=schedule_data['start_time'].time(), status='scheduled' ) return interview ``` ### 5.3 Form Submission Processing ```python class FormSubmissionService: @staticmethod def process_submission(template, submission_data, files=None): """Process form submission and create candidate""" with transaction.atomic(): # Create submission record submission = FormSubmission.objects.create( template=template, applicant_name=submission_data.get('applicant_name', ''), applicant_email=submission_data.get('applicant_email', '') ) # Process field responses for field_id, value in submission_data.items(): if field_id.startswith('field_'): field_id = field_id.replace('field_', '') try: field = FormField.objects.get(id=field_id, stage__template=template) response = FieldResponse.objects.create( submission=submission, field=field, value=value if value else None ) # Handle file uploads if field.field_type == 'file' and files: file_key = f'field_{field_id}' if file_key in files: response.uploaded_file = files[file_key] response.save() except FormField.DoesNotExist: continue # Create candidate if required fields are present try: first_name = submission.responses.get(field__label='First Name') last_name = submission.responses.get(field__label='Last Name') email = submission.responses.get(field__label='Email Address') phone = submission.responses.get(field__label='Phone Number') address = submission.responses.get(field__label='Address') resume = submission.responses.get(field__label='Resume Upload') candidate = Candidate.objects.create( first_name=first_name.display_value, last_name=last_name.display_value, email=email.display_value, phone=phone.display_value, address=address.display_value, resume=resume.get_file if resume.is_file else None, job=template.job ) return submission, candidate except Exception as e: logger.error(f"Candidate creation failed: {e}") return submission, None ``` ## 6. Error Handling Strategy ### 6.1 Custom Exception Classes ```python class ATSException(Exception): """Base exception for ATS system""" pass class CandidateStageTransitionError(ATSException): """Raised when candidate stage transition fails""" pass class InterviewSchedulingError(ATSException): """Raised when interview scheduling fails""" pass class FormSubmissionError(ATSException): """Raised when form submission processing fails""" pass class ZoomAPIError(ATSException): """Raised when Zoom API calls fail""" pass class LinkedInAPIError(ATSException): """Raised when LinkedIn API calls fail""" pass ``` ### 6.2 Error Response Format ```python class ErrorResponse: def __init__(self, error_code, message, details=None): self.error_code = error_code self.message = message self.details = details self.timestamp = timezone.now() def to_dict(self): return { 'error': { 'code': self.error_code, 'message': self.message, 'details': self.details, 'timestamp': self.timestamp.isoformat() } } ``` ## 7. Caching Strategy ### 7.1 Cache Configuration ```python # settings.py CACHES = { 'default': { 'BACKEND': 'django_redis.cache.RedisCache', 'LOCATION': 'redis://127.0.0.1:6379/1', 'OPTIONS': { 'CLIENT_CLASS': 'django_redis.client.DefaultClient', } } } # Cache timeouts CACHE_TIMEOUTS = { 'job_listings': 60 * 15, # 15 minutes 'candidate_profiles': 60 * 30, # 30 minutes 'meeting_details': 60 * 5, # 5 minutes 'form_templates': 60 * 60, # 1 hour } ``` ### 7.2 Cache Implementation ```python class CacheService: @staticmethod def get_or_set(key, func, timeout=None): """Get from cache or set if not exists""" cached_value = cache.get(key) if cached_value is None: cached_value = func() cache.set(key, cached_value, timeout or CACHE_TIMEOUTS.get(key, 300)) return cached_value @staticmethod def invalidate_pattern(pattern): """Invalidate all keys matching pattern""" keys = cache.keys(pattern) if keys: cache.delete_many(keys) ``` ## 8. Logging Strategy ### 8.1 Logging Configuration ```python # settings.py LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'verbose': { 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', 'style': '{', }, 'simple': { 'format': '{levelname} {message}', 'style': '{', }, }, 'handlers': { 'file': { 'level': 'INFO', 'class': 'logging.FileHandler', 'filename': 'logs/ats.log', 'formatter': 'verbose', }, 'console': { 'level': 'DEBUG', 'class': 'logging.StreamHandler', 'formatter': 'simple', }, }, 'loggers': { 'ats': { 'handlers': ['file', 'console'], 'level': 'DEBUG', 'propagate': False, }, }, } ``` ### 8.2 Audit Logging ```python class AuditLogger: @staticmethod def log_action(user, action, model, instance_id, details=None): """Log user actions for audit trail""" AuditLog.objects.create( user=user, action=action, model=model, instance_id=instance_id, details=details ) @staticmethod def log_candidate_stage_transition(candidate, old_stage, new_stage, user): """Log candidate stage transitions""" CandidateStageTransition.objects.create( candidate=candidate, old_stage=old_stage, new_stage=new_stage, changed_by=user ) ``` ## 9. Security Implementation ### 9.1 Data Encryption ```python from cryptography.fernet import Fernet class EncryptionService: def __init__(self): self.key = Fernet.generate_key() self.cipher = Fernet(self.key) def encrypt(self, data): """Encrypt sensitive data""" return self.cipher.encrypt(data.encode()).decode() def decrypt(self, encrypted_data): """Decrypt sensitive data""" return self.cipher.decrypt(encrypted_data.encode()).decode() ``` ### 9.2 File Upload Security ```python class SecureFileUpload: @staticmethod def validate_file(file): """Validate uploaded file for security""" # Check file size if file.size > 10 * 1024 * 1024: # 10MB raise ValidationError("File size exceeds 10MB limit") # Check file type allowed_types = [ 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ] if file.content_type not in allowed_types: raise ValidationError("Invalid file type") # Scan for malware (placeholder) if not SecureFileUpload.scan_malware(file): raise ValidationError("File contains malicious content") return True @staticmethod def scan_malware(file): """Placeholder for malware scanning""" # Implement actual malware scanning logic return True ``` ## 10. Testing Strategy ### 10.1 Unit Test Structure ```python class CandidateTestCase(TestCase): def setUp(self): self.job = JobPosting.objects.create( title="Software Engineer", department="IT", status="ACTIVE" ) self.candidate = Candidate.objects.create( first_name="John", last_name="Doe", email="john@example.com", phone="1234567890", job=self.job ) def test_candidate_stage_transition(self): """Test candidate stage transitions""" # Test valid transition self.candidate.stage = "Exam" self.candidate.save() self.assertEqual(self.candidate.stage, "Exam") # Test invalid transition with self.assertRaises(ValidationError): self.candidate.stage = "Offer" self.candidate.save() ``` ### 10.2 Integration Tests ```python class InterviewSchedulingTestCase(TestCase): def setUp(self): self.job = JobPosting.objects.create( title="Product Manager", department="Product", status="ACTIVE" ) self.candidate = Candidate.objects.create( first_name="Jane", last_name="Smith", email="jane@example.com", phone="9876543210", job=self.job ) self.schedule = InterviewSchedule.objects.create( job=self.job, start_date=timezone.now().date(), end_date=timezone.now().date() + timedelta(days=7), working_days=[0, 1, 2, 3, 4], start_time=time(9, 0), end_time=time(17, 0), interview_duration=60, buffer_time=15, created_by=self.user ) def test_interview_scheduling(self): """Test interview scheduling process""" # Test slot availability available_slots = InterviewScheduler.get_available_slots( self.schedule, timezone.now().date() ) self.assertGreater(len(available_slots), 0) # Test interview scheduling schedule_data = { 'start_date': timezone.now().date(), 'start_time': timezone.now().time(), 'duration': 60 } interview = InterviewScheduler.schedule_interview( self.candidate, self.job, schedule_data ) self.assertIsNotNone(interview) ``` ## 11. Deployment Configuration ### 11.1 Production Settings ```python # settings/production.py DEBUG = False ALLOWED_HOSTS = ['your-domain.com'] DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'ats_db', 'USER': 'ats_user', 'PASSWORD': 'secure_password', 'HOST': 'localhost', 'PORT': '5432', } } STATIC_ROOT = '/var/www/ats/static/' MEDIA_ROOT = '/var/www/ats/media/' SECURE_SSL_REDIRECT = True SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True ``` ### 11.2 Docker Configuration ```dockerfile # Dockerfile FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . EXPOSE 8000 CMD ["gunicorn", "--bind", "0.0.0.0:8000", "kaauh_ats.wsgi:application"] ``` ```yaml # docker-compose.yml version: '3.8' services: web: build: . ports: - "8000:8000" depends_on: - db - redis environment: - DEBUG=0 - DATABASE_URL=postgresql://ats_user:secure_password@db:5432/ats_db - REDIS_URL=redis://redis:6379/0 db: image: postgres:13 environment: - POSTGRES_DB=ats_db - POSTGRES_USER=ats_user - POSTGRES_PASSWORD=secure_password volumes: - postgres_data:/var/lib/postgresql/data redis: image: redis:6-alpine volumes: - redis_data:/data volumes: postgres_data: redis_data: ``` ## 12. Monitoring & Analytics ### 12.1 Performance Monitoring ```python # monitoring.py class PerformanceMonitor: @staticmethod def track_performance(func): """Decorator to track function performance""" def wrapper(*args, **kwargs): start_time = time.time() result = func(*args, **kwargs) end_time = time.time() # Log performance metrics PerformanceMetric.objects.create( function_name=func.__name__, execution_time=end_time - start_time, timestamp=timezone.now() ) return result return wrapper ``` ### 12.2 Analytics Dashboard ```python # analytics.py class AnalyticsService: @staticmethod def get_recruitment_metrics(): """Get recruitment performance metrics""" return { 'total_jobs': JobPosting.objects.count(), 'active_jobs': JobPosting.objects.filter(status='ACTIVE').count(), 'total_candidates': Candidate.objects.count(), 'conversion_rate': AnalyticsService.calculate_conversion_rate(), 'time_to_hire': AnalyticsService.calculate_average_time_to_hire(), 'source_effectiveness': AnalyticsService.get_source_effectiveness() } @staticmethod def calculate_conversion_rate(): """Calculate candidate conversion rate""" total_candidates = Candidate.objects.count() hired_candidates = Candidate.objects.filter( stage='Offer', offer_status='Accepted' ).count() return (hired_candidates / total_candidates * 100) if total_candidates > 0 else 0 ``` --- *Document Version: 1.0* *Last Updated: October 17, 2025*