1014 lines
33 KiB
Python

from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.db.models import JSONField
from django.contrib.auth.models import User
from django.core.validators import URLValidator
from django_countries.fields import CountryField
from django.core.exceptions import ValidationError
from django_ckeditor_5.fields import CKEditor5Field
from django.utils.translation import gettext_lazy as _
from django_extensions.db.fields import RandomCharField
from .validators import validate_hash_tags, validate_image_size
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
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 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.",
)
max_applications = models.PositiveIntegerField(
default=1000, help_text="Maximum number of applications allowed"
)
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()
@property
def current_applications_count(self):
"""Returns the current number of candidates associated with this job."""
return self.candidates.count()
@property
def is_application_limit_reached(self):
"""Checks if the current application count meets or exceeds the max limit."""
if self.max_applications == 0:
return True
return self.current_applications_count >= self.max_applications
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 save(self, *args, **kwargs):
"""Override save to ensure validation is called"""
self.clean() # Call validation before saving
super().save(*args, **kwargs)
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")
CANCELLED = "cancelled",_("Cancelled")
# 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",null=True,blank=True
)
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", blank=True,null=True)
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"))
break_start_time = models.TimeField(verbose_name=_("Break Start Time"),null=True,blank=True)
break_end_time = models.TimeField(verbose_name=_("Break End Time"),null=True,blank=True)
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)
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}"