2025-10-14 14:03:01 +03:00

1050 lines
34 KiB
Python

from django.db import models
from django.utils import timezone
from .validators import validate_hash_tags, validate_image_size
from django.contrib.auth.models import User
from django.core.validators import URLValidator
from django.utils.translation import gettext_lazy as _
from django_extensions.db.fields import RandomCharField
from django.core.exceptions import ValidationError
from django_countries.fields import CountryField
from django.urls import reverse
# from ckeditor.fields import RichTextField
from django_ckeditor_5.fields import CKEditor5Field
class Profile(models.Model):
profile_image = models.ImageField(null=True, blank=True, upload_to="profile_pic/")
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
class Base(models.Model):
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated at"))
slug = RandomCharField(
length=8, unique=True, editable=False, verbose_name=_("Slug")
)
class Meta:
abstract = True
# # Create your models here.
# class Job(Base):
# title = models.CharField(max_length=255, verbose_name=_('Title'))
# description_en = models.TextField(verbose_name=_('Description English'))
# description_ar = models.TextField(verbose_name=_('Description Arabic'))
# is_published = models.BooleanField(default=False, verbose_name=_('Published'))
# posted_to_linkedin = models.BooleanField(default=False, verbose_name=_('Posted to LinkedIn'))
# created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created at'))
# updated_at = models.DateTimeField(auto_now=True, verbose_name=_('Updated at'))
# class Meta:
# verbose_name = _('Job')
# verbose_name_plural = _('Jobs')
# def __str__(self):
# return self.title
class JobPosting(Base):
# Basic Job Information
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"),
]
# Core Fields
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
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")
# Job Details
description = CKEditor5Field(
'Description',
config_name='extends' # Matches the config name you defined in settings.py
)
qualifications = CKEditor5Field(blank=True,null=True,
config_name='extends'
)
salary_range = models.CharField(
max_length=200, blank=True, help_text="e.g., $60,000 - $80,000"
)
benefits = CKEditor5Field(blank=True,null=True,config_name='extends')
# Application Information ---job detail apply link for the candidates
application_url = models.URLField(
validators=[URLValidator()],
help_text="URL where candidates apply",
null=True,
blank=True,
)
application_start_date=models.DateField(null=True, blank=True)
application_deadline = models.DateField(null=True, blank=True)
application_instructions =CKEditor5Field(
blank=True, null=True,config_name='extends'
)
# Internal Tracking
internal_job_id = models.CharField(max_length=50, primary_key=True, editable=False)
created_by = models.CharField(
max_length=100, blank=True, help_text="Name of person who created this job"
)
# Status Fields
STATUS_CHOICES = [
("DRAFT", "Draft"),
("ACTIVE", "Active"),
("CLOSED", "Closed"),
("CANCELLED", "Cancelled"),
("ARCHIVED", "Archived"),
]
status = models.CharField(
max_length=20, choices=STATUS_CHOICES, default="DRAFT"
)
# hashtags for social media
hash_tags = models.CharField(
max_length=200,
blank=True,
help_text="Comma-separated hashtags for linkedin post like #hiring,#jobopening",
validators=[validate_hash_tags],
)
# LinkedIn Integration Fields
linkedin_post_id = models.CharField(
max_length=200, blank=True, help_text="LinkedIn post ID after posting"
)
linkedin_post_url = models.URLField(
blank=True, help_text="Direct URL to LinkedIn post"
)
posted_to_linkedin = models.BooleanField(default=False)
linkedin_post_status = models.CharField(
max_length=50, blank=True, help_text="Status of LinkedIn posting"
)
linkedin_posted_at = models.DateTimeField(null=True, blank=True)
published_at = models.DateTimeField(null=True, blank=True)
# University Specific Fields
position_number = models.CharField(
max_length=50, blank=True, help_text="University position number"
)
reporting_to = models.CharField(
max_length=100, blank=True, help_text="Who this position reports to"
)
joining_date = models.DateField(null=True, blank=True, help_text="Desired start date")
open_positions = models.PositiveIntegerField(
default=1, help_text="Number of open positions for this job"
)
source = models.ForeignKey(
"Source",
on_delete=models.SET_NULL, # Recommended: If a source is deleted, job's source is set to NULL
related_name="job_postings",
null=True,
blank=True,
help_text="The system or channel from which this job posting originated or was first published.",
)
hiring_agency = models.ManyToManyField(
"HiringAgency",
blank=True,
related_name="jobs",
verbose_name=_("Hiring Agency"),
help_text=_(
"External agency responsible for sourcing candidates for this role"
),
)
cancel_reason = models.TextField(
blank=True,
help_text=_("Reason for canceling the job posting"),
verbose_name=_("Cancel Reason"),
)
cancelled_by = models.CharField(
max_length=100,
blank=True,
help_text=_("Name of person who cancelled this job"),
verbose_name=_("Cancelled By"),
)
cancelled_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ["-created_at"]
verbose_name = "Job Posting"
verbose_name_plural = "Job Postings"
def __str__(self):
return f"{self.title} - {self.internal_job_id}-{self.get_status_display()}"
def get_source(self):
return self.source.name if self.source else "System"
def save(self, *args, **kwargs):
# Generate unique internal job ID if not exists
if not self.internal_job_id:
prefix = "KAAUH"
year = timezone.now().year
# Get next sequential number
last_job = (
JobPosting.objects.filter(
internal_job_id__startswith=f"{prefix}-{year}-"
)
.order_by("internal_job_id")
.last()
)
if last_job:
last_num = int(last_job.internal_job_id.split("-")[-1])
next_num = last_num + 1
else:
next_num = 1
self.internal_job_id = f"{prefix}-{year}-{next_num:06d}"
super().save(*args, **kwargs)
def get_location_display(self):
"""Return formatted location string"""
parts = []
if self.location_city:
parts.append(self.location_city)
if self.location_state:
parts.append(self.location_state)
if self.location_country:
parts.append(self.location_country)
return ", ".join(parts) if parts else "Not specified"
def is_expired(self):
"""Check if application deadline has passed"""
if self.application_deadline:
return self.application_deadline < timezone.now().date()
return False
def publish(self):
self.status = "PUBLISHED"
self.published_at = timezone.now()
self.application_url = reverse(
"form_wizard", kwargs={"slug": self.form_template.slug}
)
self.save()
class JobPostingImage(models.Model):
job=models.OneToOneField('JobPosting',on_delete=models.CASCADE,related_name='post_images')
post_image = models.ImageField(upload_to='post/',validators=[validate_image_size])
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 transition validation constants
STAGE_SEQUENCE = {
"Applied": ["Exam", "Interview", "Offer"],
"Exam": ["Interview", "Offer"],
"Interview": ["Offer"],
"Offer": [], # Final stage - no further transitions
}
job = models.ForeignKey(
JobPosting,
on_delete=models.CASCADE,
related_name="candidates",
verbose_name=_("Job"),
)
first_name = models.CharField(max_length=255, verbose_name=_("First Name"))
last_name = models.CharField(max_length=255, verbose_name=_("Last Name"))
email = models.EmailField(verbose_name=_("Email"))
phone = models.CharField(max_length=20, verbose_name=_("Phone"))
address = models.TextField(max_length=200, verbose_name=_("Address"))
resume = models.FileField(upload_to="resumes/", verbose_name=_("Resume"))
is_resume_parsed = models.BooleanField(
default=False, verbose_name=_("Resume Parsed")
)
is_potential_candidate = models.BooleanField(
default=False, verbose_name=_("Potential Candidate")
)
parsed_summary = models.TextField(blank=True, verbose_name=_("Parsed Summary"))
applied = models.BooleanField(default=False, verbose_name=_("Applied"))
stage = models.CharField(
max_length=100,
default="Applied",
choices=Stage.choices,
verbose_name=_("Stage"),
)
applicant_status = models.CharField(
choices=ApplicantType.choices,
default="Applicant",
max_length=100,
null=True,
blank=True,
verbose_name=_("Applicant Status"),
)
exam_date = models.DateTimeField(null=True, blank=True, verbose_name=_("Exam Date"))
exam_status = models.CharField(
choices=ExamStatus.choices,
max_length=100,
null=True,
blank=True,
verbose_name=_("Exam Status"),
)
interview_date = models.DateTimeField(
null=True, blank=True, verbose_name=_("Interview Date")
)
interview_status = models.CharField(
choices=Status.choices,
max_length=100,
null=True,
blank=True,
verbose_name=_("Interview Status"),
)
offer_date = models.DateField(null=True, blank=True, verbose_name=_("Offer Date"))
offer_status = models.CharField(
choices=Status.choices,
max_length=100,
null=True,
blank=True,
verbose_name=_("Offer Status"),
)
join_date = models.DateField(null=True, blank=True, verbose_name=_("Join Date"))
# Scoring fields (populated by signal)
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)
submitted_by_agency = models.ForeignKey(
"HiringAgency",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="submitted_candidates",
verbose_name=_("Submitted by Agency"),
)
class Meta:
verbose_name = _("Candidate")
verbose_name_plural = _("Candidates")
@property
def name(self):
return f"{self.first_name} {self.last_name}"
@property
def full_name(self):
return self.name
@property
def get_file_size(self):
if self.resume:
return self.resume.size
return 0
def clean(self):
"""Validate stage transitions"""
# Only validate if this is an existing record (not being created)
if self.pk and self.stage != self.__class__.objects.get(pk=self.pk).stage:
old_stage = self.__class__.objects.get(pk=self.pk).stage
allowed_next_stages = self.STAGE_SEQUENCE.get(old_stage, [])
if self.stage not in allowed_next_stages:
raise ValidationError(
{
"stage": f'Cannot transition from "{old_stage}" to "{self.stage}". '
f"Allowed transitions: {', '.join(allowed_next_stages) or 'None (final stage)'}"
}
)
# Validate that the stage is a valid choice
if self.stage not in [choice[0] for choice in self.Stage.choices]:
raise ValidationError(
{
"stage": f"Invalid stage. Must be one of: {', '.join(choice[0] for choice in self.Stage.choices)}"
}
)
def save(self, *args, **kwargs):
"""Override save to ensure validation is called"""
self.clean() # Call validation before saving
super().save(*args, **kwargs)
def can_transition_to(self, new_stage):
"""Check if a stage transition is allowed"""
if not self.pk: # New record - can be in Applied stage
return new_stage == "Applied"
old_stage = self.__class__.objects.get(pk=self.pk).stage
allowed_next_stages = self.STAGE_SEQUENCE.get(old_stage, [])
return new_stage in allowed_next_stages
def get_available_stages(self):
"""Get list of stages this candidate can transition to"""
if not self.pk: # New record
return ["Applied"]
old_stage = self.__class__.objects.get(pk=self.pk).stage
return self.STAGE_SEQUENCE.get(old_stage, [])
@property
def submission(self):
return FormSubmission.objects.filter(template__job=self.job).first()
@property
def responses(self):
if self.submission:
return self.submission.responses.all()
return []
def __str__(self):
return self.full_name
@property
def get_meetings(self):
return self.scheduled_interviews.all()
@property
def get_latest_meeting(self):
schedule = self.scheduled_interviews.order_by('-created_at').first()
if schedule:
return schedule.zoom_meeting
return None
@property
def has_future_meeting(self):
"""
Checks if the candidate has any scheduled interviews for a future date/time.
"""
# Ensure timezone.now() is used for comparison
now = timezone.now()
# Check if any related ScheduledInterview has a future interview_date and interview_time
# We need to combine date and time for a proper datetime comparison if they are separate fields
future_meetings = self.scheduled_interviews.filter(
interview_date__gt=now.date()
).filter(
interview_time__gte=now.time()
).exists()
# Also check for interviews happening later today
today_future_meetings = self.scheduled_interviews.filter(
interview_date=now.date(),
interview_time__gte=now.time()
).exists()
return future_meetings or today_future_meetings
class TrainingMaterial(Base):
title = models.CharField(max_length=255, verbose_name=_("Title"))
content = CKEditor5Field(blank=True, verbose_name=_("Content"),config_name='extends')
video_link = models.URLField(blank=True, verbose_name=_("Video Link"))
file = models.FileField(
upload_to="training_materials/", blank=True, verbose_name=_("File")
)
created_by = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, verbose_name=_("Created by")
)
class Meta:
verbose_name = _("Training Material")
verbose_name_plural = _("Training Materials")
def __str__(self):
return self.title
class ZoomMeeting(Base):
class MeetingStatus(models.TextChoices):
SCHEDULED = "scheduled", _("Scheduled")
STARTED = "started", _("Started")
ENDED = "ended", _("Ended")
# Basic meeting details
topic = models.CharField(max_length=255, verbose_name=_("Topic"))
meeting_id = models.CharField(
max_length=20, unique=True, verbose_name=_("Meeting ID")
) # Unique identifier for the meeting
start_time = models.DateTimeField(verbose_name=_("Start Time"))
duration = models.PositiveIntegerField(
verbose_name=_("Duration")
) # Duration in minutes
timezone = models.CharField(max_length=50, verbose_name=_("Timezone"))
join_url = models.URLField(
verbose_name=_("Join URL")
) # URL for participants to join
participant_video = models.BooleanField(
default=True, verbose_name=_("Participant Video")
)
password = models.CharField(
max_length=20, blank=True, null=True, verbose_name=_("Password")
)
join_before_host = models.BooleanField(
default=False, verbose_name=_("Join Before Host")
)
mute_upon_entry = models.BooleanField(
default=False, verbose_name=_("Mute Upon Entry")
)
waiting_room = models.BooleanField(default=False, verbose_name=_("Waiting Room"))
zoom_gateway_response = models.JSONField(
blank=True, null=True, verbose_name=_("Zoom Gateway Response")
)
status = models.CharField(
max_length=20,
null=True,
blank=True,
verbose_name=_("Status"),
)
# Timestamps
def __str__(self):
return self.topic
class FormTemplate(Base):
"""
Represents a complete form template with multiple stages
"""
job = models.OneToOneField(
JobPosting, on_delete=models.CASCADE, related_name="form_template"
)
name = models.CharField(max_length=200, help_text="Name of the form template")
description = models.TextField(
blank=True, help_text="Description of the form template"
)
created_by = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="form_templates"
)
is_active = models.BooleanField(
default=False, help_text="Whether this template is active"
)
class Meta:
ordering = ["-created_at"]
verbose_name = "Form Template"
verbose_name_plural = "Form Templates"
def __str__(self):
return self.name
def get_stage_count(self):
return self.stages.count()
def get_field_count(self):
return sum(stage.fields.count() for stage in self.stages.all())
class FormStage(Base):
"""
Represents a stage/section within a form template
"""
template = models.ForeignKey(
FormTemplate, on_delete=models.CASCADE, related_name="stages"
)
name = models.CharField(max_length=200, help_text="Name of the stage")
order = models.PositiveIntegerField(
default=0, help_text="Order of the stage in the form"
)
is_predefined = models.BooleanField(
default=False, help_text="Whether this is a default resume stage"
)
class Meta:
ordering = ["order"]
verbose_name = "Form Stage"
verbose_name_plural = "Form Stages"
def __str__(self):
return f"{self.template.name} - {self.name}"
def clean(self):
if self.order < 0:
raise ValidationError("Order must be a positive integer")
class FormField(Base):
"""
Represents a single field within a form stage
"""
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, help_text="Label for the field")
field_type = models.CharField(
max_length=20, choices=FIELD_TYPES, help_text="Type of the field"
)
placeholder = models.CharField(
max_length=200, blank=True, help_text="Placeholder text"
)
required = models.BooleanField(
default=False, help_text="Whether the field is required"
)
order = models.PositiveIntegerField(
default=0, help_text="Order of the field in the stage"
)
is_predefined = models.BooleanField(
default=False, help_text="Whether this is a default field"
)
# For selection fields (select, radio, checkbox)
options = models.JSONField(
default=list,
blank=True,
help_text="Options for selection fields (stored as JSON array)",
)
# For file upload fields
file_types = models.CharField(
max_length=200,
blank=True,
help_text="Allowed file types (comma-separated, e.g., '.pdf,.doc,.docx')",
)
max_file_size = models.PositiveIntegerField(
default=5, help_text="Maximum file size in MB (default: 5MB)"
)
multiple_files = models.BooleanField(
default=False, help_text="Allow multiple files to be uploaded"
)
max_files = models.PositiveIntegerField(
default=1,
help_text="Maximum number of files allowed (when multiple_files is True)",
)
class Meta:
ordering = ["order"]
verbose_name = "Form Field"
verbose_name_plural = "Form Fields"
def clean(self):
# Validate options for selection fields
if self.field_type in ["select", "radio", "checkbox"]:
if not isinstance(self.options, list):
raise ValidationError("Options must be a list for selection fields")
else:
# Clear options for non-selection fields
if self.options:
self.options = []
# Validate file settings for file fields
if self.field_type == "file":
if not self.file_types:
self.file_types = ".pdf,.doc,.docx"
if self.max_file_size <= 0:
raise ValidationError("Max file size must be greater than 0")
if self.multiple_files and self.max_files <= 0:
raise ValidationError(
"Max files must be greater than 0 when multiple files are allowed"
)
if not self.multiple_files:
self.max_files = 1
else:
# Clear file settings for non-file fields
self.file_types = ""
self.max_file_size = 0
self.multiple_files = False
self.max_files = 1
# Validate order
if self.order < 0:
raise ValidationError("Order must be a positive integer")
def __str__(self):
return f"{self.stage.template.name} - {self.stage.name} - {self.label}"
class FormSubmission(Base):
"""
Represents a completed form submission by an applicant
"""
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, help_text="Name of the applicant"
)
applicant_email = models.EmailField(blank=True, help_text="Email of the applicant")
class Meta:
ordering = ["-submitted_at"]
verbose_name = "Form Submission"
verbose_name_plural = "Form Submissions"
def __str__(self):
return f"Submission for {self.template.name} - {self.submitted_at.strftime('%Y-%m-%d %H:%M')}"
class FieldResponse(Base):
"""
Represents a response to a specific field in a form submission
"""
submission = models.ForeignKey(
FormSubmission, on_delete=models.CASCADE, related_name="responses"
)
field = models.ForeignKey(
FormField, on_delete=models.CASCADE, related_name="responses"
)
# Store the response value as JSON to handle different data types
value = models.JSONField(
null=True, blank=True, help_text="Response value (stored as JSON)"
)
# For file uploads, store the file path
uploaded_file = models.FileField(upload_to="form_uploads/", null=True, blank=True)
class Meta:
verbose_name = "Field Response"
verbose_name_plural = "Field Responses"
def __str__(self):
return f"Response to {self.field.label} in {self.submission}"
@property
def is_file(self):
if self.uploaded_file:
return True
return False
@property
def get_file(self):
if self.is_file:
return self.uploaded_file
return None
@property
def get_file_size(self):
if self.is_file:
return self.uploaded_file.size
return 0
@property
def display_value(self):
"""Return a human-readable representation of the response value"""
if self.is_file:
return f"File: {self.uploaded_file.name}"
elif self.value is None:
return ""
elif isinstance(self.value, list):
return ", ".join(str(v) for v in self.value)
else:
return str(self.value)
# Optional: Create a model for form templates that can be shared across organizations
class SharedFormTemplate(Base):
"""
Represents a form template that can be shared across different organizations/users
"""
template = models.OneToOneField(FormTemplate, on_delete=models.CASCADE)
is_public = models.BooleanField(
default=False, help_text="Whether this template is publicly available"
)
shared_with = models.ManyToManyField(
User, blank=True, related_name="shared_templates"
)
class Meta:
verbose_name = "Shared Form Template"
verbose_name_plural = "Shared Form Templates"
def __str__(self):
return f"Shared: {self.template.name}"
class Source(Base):
name = models.CharField(
max_length=100,
unique=True,
verbose_name=_("Source Name"),
help_text=_("e.g., ATS, ERP "),
)
source_type = models.CharField(
max_length=100, verbose_name=_("Source Type"), help_text=_("e.g., ATS, ERP ")
)
description = models.TextField(
blank=True,
verbose_name=_("Description"),
help_text=_("A description of the source"),
)
ip_address = models.GenericIPAddressField(
blank=True,
null=True,
verbose_name=_("IP Address"),
help_text=_("The IP address of the source"),
)
created_at = models.DateTimeField(auto_now_add=True)
# Integration specific fields
api_key = models.CharField(
max_length=255,
blank=True,
null=True,
verbose_name=_("API Key"),
help_text=_("API key for authentication (will be encrypted)"),
)
api_secret = models.CharField(
max_length=255,
blank=True,
null=True,
verbose_name=_("API Secret"),
help_text=_("API secret for authentication (will be encrypted)"),
)
trusted_ips = models.TextField(
blank=True,
null=True,
verbose_name=_("Trusted IP Addresses"),
help_text=_("Comma-separated list of trusted IP addresses"),
)
is_active = models.BooleanField(
default=True,
verbose_name=_("Active"),
help_text=_("Whether this source is active for integration"),
)
integration_version = models.CharField(
max_length=50,
blank=True,
verbose_name=_("Integration Version"),
help_text=_("Version of the integration protocol"),
)
last_sync_at = models.DateTimeField(
null=True,
blank=True,
verbose_name=_("Last Sync At"),
help_text=_("Timestamp of the last successful synchronization"),
)
sync_status = models.CharField(
max_length=20,
blank=True,
choices=[
("IDLE", "Idle"),
("SYNCING", "Syncing"),
("ERROR", "Error"),
("DISABLED", "Disabled"),
],
default="IDLE",
verbose_name=_("Sync Status"),
)
def __str__(self):
return self.name
class Meta:
verbose_name = _("Source")
verbose_name_plural = _("Sources")
ordering = ["name"]
class IntegrationLog(Base):
"""
Log all integration requests and responses for audit and debugging purposes
"""
class ActionChoices(models.TextChoices):
REQUEST = "REQUEST", _("Request")
RESPONSE = "RESPONSE", _("Response")
ERROR = "ERROR", _("Error")
SYNC = "SYNC", _("Sync")
CREATE_JOB = "CREATE_JOB", _("Create Job")
UPDATE_JOB = "UPDATE_JOB", _("Update Job")
source = models.ForeignKey(
Source,
on_delete=models.CASCADE,
related_name="integration_logs",
verbose_name=_("Source"),
)
action = models.CharField(
max_length=20, choices=ActionChoices.choices, verbose_name=_("Action")
)
endpoint = models.CharField(max_length=255, blank=True, verbose_name=_("Endpoint"))
method = models.CharField(max_length=10, blank=True, verbose_name=_("HTTP Method"))
request_data = models.JSONField(
blank=True, null=True, verbose_name=_("Request Data")
)
response_data = models.JSONField(
blank=True, null=True, verbose_name=_("Response Data")
)
status_code = models.CharField(
max_length=10, blank=True, verbose_name=_("Status Code")
)
error_message = models.TextField(blank=True, verbose_name=_("Error Message"))
ip_address = models.GenericIPAddressField(verbose_name=_("IP Address"))
user_agent = models.CharField(
max_length=255, blank=True, verbose_name=_("User Agent")
)
processing_time = models.FloatField(
null=True, blank=True, verbose_name=_("Processing Time (seconds)")
)
def __str__(self):
return f"{self.source.name} - {self.action} - {self.created_at}"
class Meta:
ordering = ["-created_at"]
verbose_name = _("Integration Log")
verbose_name_plural = _("Integration Logs")
@property
def is_successful(self):
"""Check if the integration action was successful"""
if self.action == self.ActionChoices.ERROR:
return False
if self.action == self.ActionChoices.REQUEST:
return True # Requests are always logged, success depends on response
if self.status_code and self.status_code.startswith("2"):
return True
return False
class HiringAgency(Base):
name = models.CharField(max_length=200, unique=True, verbose_name=_("Agency Name"))
contact_person = models.CharField(
max_length=150, blank=True, verbose_name=_("Contact Person")
)
email = models.EmailField(blank=True)
phone = models.CharField(max_length=20, blank=True)
website = models.URLField(blank=True)
notes = models.TextField(blank=True, help_text=_("Internal notes about the agency"))
country = CountryField(blank=True, null=True, blank_label=_("Select country"))
address = models.TextField(blank=True, null=True)
def __str__(self):
return self.name
class Meta:
verbose_name = _("Hiring Agency")
verbose_name_plural = _("Hiring Agencies")
ordering = ["name"]
class BreakTime(models.Model):
"""Model to store break times for a schedule"""
start_time = models.TimeField(verbose_name=_("Start Time"))
end_time = models.TimeField(verbose_name=_("End Time"))
def __str__(self):
return f"{self.start_time} - {self.end_time}"
class InterviewSchedule(Base):
"""Stores the scheduling criteria for interviews"""
job = models.ForeignKey(
JobPosting, on_delete=models.CASCADE, related_name="interview_schedules"
)
candidates = models.ManyToManyField(Candidate, related_name="interview_schedules")
start_date = models.DateField(verbose_name=_("Start Date"))
end_date = models.DateField(verbose_name=_("End Date"))
working_days = models.JSONField(
verbose_name=_("Working Days")
) # Store days of week as [0,1,2,3,4] for Mon-Fri
start_time = models.TimeField(verbose_name=_("Start Time"))
end_time = models.TimeField(verbose_name=_("End Time"))
breaks = models.JSONField(default=list, blank=True, verbose_name=_('Break Times'))
interview_duration = models.PositiveIntegerField(
verbose_name=_("Interview Duration (minutes)")
)
buffer_time = models.PositiveIntegerField(
verbose_name=_("Buffer Time (minutes)"), default=0
)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Interview Schedule for {self.job.title}"
class ScheduledInterview(Base):
"""Stores individual scheduled interviews"""
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(verbose_name=_("Interview Date"))
interview_time = models.TimeField(verbose_name=_("Interview Time"))
status = models.CharField(
max_length=20,
choices=[
("scheduled", _("Scheduled")),
("confirmed", _("Confirmed")),
("cancelled", _("Cancelled")),
("completed", _("Completed")),
],
default="scheduled",
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"Interview with {self.candidate.name} for {self.job.title}"