kaauh_ats/ATS_PROJECT_LLD.md
2025-10-19 17:23:06 +03:00

39 KiB

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

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

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

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

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

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

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

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

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

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

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

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

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

// 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

# 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

# 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

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

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

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

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

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

# 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

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

# 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

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

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

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

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

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

# 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
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"]
# 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

# 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

# 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