This commit is contained in:
Faheed 2025-11-17 12:48:32 +03:00
parent a28bfc11f3
commit d0235bfefe
11 changed files with 277 additions and 225 deletions

View File

@ -5,7 +5,7 @@ from django.utils import timezone
from .models import ( from .models import (
JobPosting, Application, TrainingMaterial, ZoomMeetingDetails, JobPosting, Application, TrainingMaterial, ZoomMeetingDetails,
FormTemplate, FormStage, FormField, FormSubmission, FieldResponse, FormTemplate, FormStage, FormField, FormSubmission, FieldResponse,
SharedFormTemplate, Source, HiringAgency, IntegrationLog,InterviewSchedule,JobPostingImage,MeetingComment, SharedFormTemplate, Source, HiringAgency, IntegrationLog,InterviewSchedule,JobPostingImage,InterviewNote,
AgencyAccessLink, AgencyJobAssignment AgencyAccessLink, AgencyJobAssignment
) )
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model

View File

@ -18,7 +18,7 @@ from .models import (
InterviewSchedule, InterviewSchedule,
BreakTime, BreakTime,
JobPostingImage, JobPostingImage,
MeetingComment, InterviewNote,
ScheduledInterview, ScheduledInterview,
Source, Source,
HiringAgency, HiringAgency,
@ -26,7 +26,7 @@ from .models import (
AgencyAccessLink, AgencyAccessLink,
Participants, Participants,
Message, Message,
Person,OnsiteMeeting, Person,OnsiteLocationDetails,
Document Document
) )
@ -725,7 +725,7 @@ class InterviewScheduleForm(forms.ModelForm):
) )
class Meta: class Meta:
model = InterviewSchedule model = InterviewSchedule
fields = [ fields = [
'schedule_interview_type', 'schedule_interview_type',
"applications", "applications",

View File

@ -983,137 +983,326 @@ class TrainingMaterial(Base):
return self.title return self.title
class OnsiteMeeting(Base): class InterviewLocation(Base):
class MeetingStatus(models.TextChoices): """
Base model for all interview location/meeting details (remote or onsite)
using Multi-Table Inheritance.
"""
class LocationType(models.TextChoices):
REMOTE = 'Remote', _('Remote (e.g., Zoom, Google Meet)')
ONSITE = 'Onsite', _('In-Person (Physical Location)')
class Status(models.TextChoices):
"""Defines the possible real-time statuses for any interview location/meeting."""
WAITING = "waiting", _("Waiting") WAITING = "waiting", _("Waiting")
STARTED = "started", _("Started") STARTED = "started", _("Started")
ENDED = "ended", _("Ended") ENDED = "ended", _("Ended")
CANCELLED = "cancelled", _("Cancelled") CANCELLED = "cancelled", _("Cancelled")
# Basic meeting details location_type = models.CharField(
topic = models.CharField(max_length=255, verbose_name=_("Topic")) max_length=10,
start_time = models.DateTimeField( choices=LocationType.choices,
db_index=True, verbose_name=_("Start Time") verbose_name=_("Location Type"),
) # Added index db_index=True
duration = models.PositiveIntegerField( )
verbose_name=_("Duration")
) # Duration in minutes details_url = models.URLField(
timezone = models.CharField(max_length=50, verbose_name=_("Timezone")) verbose_name=_("Meeting/Location URL"),
location = models.CharField(null=True, blank=True) max_length=2048,
status = models.CharField(
db_index=True,
max_length=20, # Added index
null=True,
blank=True, blank=True,
verbose_name=_("Status"), null=True
default=MeetingStatus.WAITING, )
topic = models.CharField( # Renamed from 'description' to 'topic' to match your input
max_length=255,
verbose_name=_("Location/Meeting Topic"),
blank=True,
help_text=_("e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room, 3rd Floor'")
)
timezone = models.CharField(
max_length=50,
verbose_name=_("Timezone"),
default='UTC'
) )
def __str__(self):
# Use 'topic' instead of 'description'
return f"{self.get_location_type_display()} - {self.topic[:50]}"
class ZoomMeeting(Base): class Meta:
class MeetingStatus(models.TextChoices): verbose_name = _("Interview Location")
WAITING = "waiting", _("Waiting") verbose_name_plural = _("Interview Locations")
STARTED = "started", _("Started")
ENDED = "ended", _("Ended")
CANCELLED = "cancelled", _("Cancelled")
# Basic meeting details
topic = models.CharField(max_length=255, verbose_name=_("Topic")) class ZoomMeetingDetails(InterviewLocation):
meeting_id = models.CharField( """Concrete model for remote interviews (Zoom specifics)."""
status = models.CharField(
db_index=True, db_index=True,
max_length=20, max_length=20,
unique=True, choices=InterviewLocation.Status.choices,
verbose_name=_("Meeting ID"), # Added index default=InterviewLocation.Status.WAITING,
) # Unique identifier for the meeting )
start_time = models.DateTimeField( start_time = models.DateTimeField(
db_index=True, verbose_name=_("Start Time") db_index=True, verbose_name=_("Start Time")
) # Added index )
duration = models.PositiveIntegerField( duration = models.PositiveIntegerField(
verbose_name=_("Duration") verbose_name=_("Duration (minutes)")
) # Duration in minutes )
timezone = models.CharField(max_length=50, verbose_name=_("Timezone")) meeting_id = models.CharField(
join_url = models.URLField( db_index=True,
verbose_name=_("Join URL") max_length=50,
) # URL for participants to join unique=True,
participant_video = models.BooleanField( verbose_name=_("External Meeting ID")
default=True, verbose_name=_("Participant Video")
) )
password = models.CharField( password = models.CharField(
max_length=20, blank=True, null=True, verbose_name=_("Password") max_length=20, blank=True, null=True, verbose_name=_("Password")
) )
zoom_gateway_response = models.JSONField(
blank=True, null=True, verbose_name=_("Zoom Gateway Response")
)
participant_video = models.BooleanField(
default=True, verbose_name=_("Participant Video")
)
join_before_host = models.BooleanField( join_before_host = models.BooleanField(
default=False, verbose_name=_("Join Before Host") default=False, verbose_name=_("Join Before Host")
) )
host_email=models.CharField(null=True,blank=True)
mute_upon_entry = models.BooleanField( mute_upon_entry = models.BooleanField(
default=False, verbose_name=_("Mute Upon Entry") default=False, verbose_name=_("Mute Upon Entry")
) )
waiting_room = models.BooleanField(default=False, verbose_name=_("Waiting Room")) waiting_room = models.BooleanField(default=False, verbose_name=_("Waiting Room"))
zoom_gateway_response = models.JSONField( # *** REVERTED TO @classmethod (Factory Method) for cleaner instantiation ***
blank=True, null=True, verbose_name=_("Zoom Gateway Response") # @classmethod
# def create(cls, **kwargs):
# """Factory method to ensure location_type is set to REMOTE."""
# return cls(location_type=InterviewLocation.LocationType.REMOTE, **kwargs)
class Meta:
verbose_name = _("Zoom Meeting Details")
verbose_name_plural = _("Zoom Meeting Details")
class OnsiteLocationDetails(InterviewLocation):
"""Concrete model for onsite interviews (Room/Address specifics)."""
physical_address = models.CharField(
max_length=255,
verbose_name=_("Physical Address"),
blank=True,
null=True
)
room_number = models.CharField(
max_length=50,
verbose_name=_("Room Number/Name"),
blank=True,
null=True
)
start_time = models.DateTimeField(
db_index=True, verbose_name=_("Start Time")
)
duration = models.PositiveIntegerField(
verbose_name=_("Duration (minutes)")
) )
status = models.CharField( status = models.CharField(
db_index=True, db_index=True,
max_length=20, # Added index max_length=20,
choices=InterviewLocation.Status.choices,
default=InterviewLocation.Status.WAITING,
)
# *** REVERTED TO @classmethod (Factory Method) for cleaner instantiation ***
# @classmethod
# def create(cls, **kwargs):
# """Factory method to ensure location_type is set to ONSITE."""
# return cls(location_type=InterviewLocation.LocationType.ONSITE, **kwargs)
class Meta:
verbose_name = _("Onsite Location Details")
verbose_name_plural = _("Onsite Location Details")
# --- 2. Scheduling Models ---
class InterviewSchedule(Base):
"""Stores the TEMPLATE criteria for BULK interview generation."""
# We need a field to store the template location details linked to this bulk schedule.
# This location object contains the generic Zoom/Onsite info to be cloned.
template_location = models.ForeignKey(
InterviewLocation,
on_delete=models.SET_NULL,
related_name="schedule_templates",
null=True, null=True,
blank=True, blank=True,
verbose_name=_("Status"), verbose_name=_("Location Template (Zoom/Onsite)")
default=MeetingStatus.WAITING, )
# NOTE: schedule_interview_type field is needed in the form,
# but not on the model itself if we use template_location.
# If you want to keep it:
schedule_interview_type = models.CharField(
max_length=10,
choices=InterviewLocation.LocationType.choices,
verbose_name=_("Interview Type"),
default=InterviewLocation.LocationType.REMOTE
)
job = models.ForeignKey(
JobPosting,
on_delete=models.CASCADE,
related_name="interview_schedules",
db_index=True,
)
applications = models.ManyToManyField(
Application, related_name="interview_schedules", blank=True
)
start_date = models.DateField(db_index=True, verbose_name=_("Start Date"))
end_date = models.DateField(db_index=True, verbose_name=_("End Date"))
working_days = models.JSONField(
verbose_name=_("Working Days")
)
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, db_index=True
) )
# Timestamps
def __str__(self): def __str__(self):
return self.topic return f"Schedule for {self.job.title}"
@property
def get_job(self):
return self.interview.job
@property
def get_candidate(self):
return self.interview.application.person
@property
def candidate_full_name(self):
return self.interview.application.person.full_name
@property
def get_participants(self):
return self.interview.job.participants.all()
@property
def get_users(self):
return self.interview.job.users.all()
class MeetingComment(Base): class ScheduledInterview(Base):
""" """Stores individual scheduled interviews (whether bulk or individually created)."""
Model for storing meeting comments/notes
""" class InterviewStatus(models.TextChoices):
SCHEDULED = "scheduled", _("Scheduled")
CONFIRMED = "confirmed", _("Confirmed")
CANCELLED = "cancelled", _("Cancelled")
COMPLETED = "completed", _("Completed")
meeting = models.ForeignKey( application = models.ForeignKey(
ZoomMeeting, Application,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="comments", related_name="scheduled_interviews",
verbose_name=_("Meeting"), db_index=True,
) )
job = models.ForeignKey(
JobPosting,
on_delete=models.CASCADE,
related_name="scheduled_interviews",
db_index=True,
)
# Links to the specific, individual location/meeting details for THIS interview
interview_location = models.OneToOneField(
InterviewLocation,
on_delete=models.SET_NULL,
related_name="scheduled_interview",
null=True,
blank=True,
db_index=True,
verbose_name=_("Meeting/Location Details")
)
# Link back to the bulk schedule template (optional if individually created)
schedule = models.ForeignKey(
InterviewSchedule,
on_delete=models.SET_NULL,
related_name="interviews",
null=True,
blank=True,
db_index=True,
)
participants = models.ManyToManyField('Participants', blank=True)
system_users = models.ManyToManyField(User, related_name="attended_interviews", blank=True)
interview_date = models.DateField(db_index=True, verbose_name=_("Interview Date"))
interview_time = models.TimeField(verbose_name=_("Interview Time"))
status = models.CharField(
db_index=True,
max_length=20,
choices=InterviewStatus.choices,
default=InterviewStatus.SCHEDULED,
)
def __str__(self):
return f"Interview with {self.application.person.full_name} for {self.job.title}"
class Meta:
indexes = [
models.Index(fields=["job", "status"]),
models.Index(fields=["interview_date", "interview_time"]),
models.Index(fields=["application", "job"]),
]
# --- 3. Interview Notes Model (Fixed) ---
class InterviewNote(Base):
"""Model for storing notes, feedback, or comments related to a specific ScheduledInterview."""
class NoteType(models.TextChoices):
FEEDBACK = 'Feedback', _('Candidate Feedback')
LOGISTICS = 'Logistics', _('Logistical Note')
GENERAL = 'General', _('General Comment')
1
interview = models.ForeignKey(
ScheduledInterview,
on_delete=models.CASCADE,
related_name="notes",
verbose_name=_("Scheduled Interview"),
db_index=True
)
author = models.ForeignKey( author = models.ForeignKey(
User, User,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="meeting_comments", related_name="interview_notes",
verbose_name=_("Author"), verbose_name=_("Author"),
db_index=True
) )
content = CKEditor5Field(verbose_name=_("Content"), config_name="extends")
# Inherited from Base: created_at, updated_at, slug note_type = models.CharField(
max_length=50,
choices=NoteType.choices,
default=NoteType.FEEDBACK,
verbose_name=_("Note Type")
)
content = CKEditor5Field(verbose_name=_("Content/Feedback"), config_name="extends")
class Meta: class Meta:
verbose_name = _("Meeting Comment") verbose_name = _("Interview Note")
verbose_name_plural = _("Meeting Comments") verbose_name_plural = _("Interview Notes")
ordering = ["-created_at"] ordering = ["created_at"]
def __str__(self): def __str__(self):
return f"Comment by {self.author.get_username()} on {self.meeting.topic}" return f"{self.get_note_type_display()} by {self.author.get_username()} on {self.interview.id}"
class FormTemplate(Base): class FormTemplate(Base):
""" """
@ -1926,143 +2115,6 @@ class BreakTime(models.Model):
return f"{self.start_time} - {self.end_time}" return f"{self.start_time} - {self.end_time}"
class InterviewSchedule(Base):
"""Stores the scheduling criteria for interviews"""
class InterviewType(models.TextChoices):
REMOTE = "Remote", "Remote Interview"
ONSITE = "Onsite", "In-Person Interview"
interview_type = models.CharField(
max_length=10,
choices=InterviewType.choices,
default=InterviewType.REMOTE,
verbose_name="Interview Meeting Type",
)
job = models.ForeignKey(
JobPosting,
on_delete=models.CASCADE,
related_name="interview_schedules",
db_index=True,
)
applications = models.ManyToManyField(
Application, related_name="interview_schedules", blank=True, null=True
)
start_date = models.DateField(
db_index=True, verbose_name=_("Start Date")
) # Added index
end_date = models.DateField(
db_index=True, verbose_name=_("End Date")
) # Added index
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, db_index=True
) # Added index
def __str__(self):
return f"Interview Schedule for {self.job.title}"
class Meta:
indexes = [
models.Index(fields=["start_date"]),
models.Index(fields=["end_date"]),
models.Index(fields=["created_by"]),
]
class ScheduledInterview(Base):
"""Stores individual scheduled interviews"""
application = models.ForeignKey(
Application,
on_delete=models.CASCADE,
related_name="scheduled_interviews",
db_index=True,
)
participants = models.ManyToManyField("Participants", blank=True)
system_users = models.ManyToManyField(User, blank=True)
job = models.ForeignKey(
"JobPosting",
on_delete=models.CASCADE,
related_name="scheduled_interviews",
db_index=True,
)
zoom_meeting = models.OneToOneField(
ZoomMeeting,
on_delete=models.CASCADE,
related_name="interview",
db_index=True,
null=True,
blank=True,
)
onsite_meeting = models.OneToOneField(
OnsiteMeeting,
on_delete=models.CASCADE,
related_name="onsite_interview",
db_index=True,
null=True,
blank=True,
)
schedule = models.ForeignKey(
InterviewSchedule,
on_delete=models.CASCADE,
related_name="interviews",
null=True,
blank=True,
db_index=True,
)
interview_date = models.DateField(
db_index=True, verbose_name=_("Interview Date")
) # Added index
interview_time = models.TimeField(verbose_name=_("Interview Time"))
status = models.CharField(
db_index=True,
max_length=20, # Added index
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.application.person.full_name} for {self.job.title}"
)
class Meta:
indexes = [
models.Index(fields=["job", "status"]),
models.Index(fields=["interview_date", "interview_time"]),
models.Index(fields=["application", "job"]),
]
class Notification(models.Model): class Notification(models.Model):
@ -2101,7 +2153,7 @@ class Notification(models.Model):
verbose_name=_("Status"), verbose_name=_("Status"),
) )
related_meeting = models.ForeignKey( related_meeting = models.ForeignKey(
ZoomMeeting, ZoomMeetingDetails,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="notifications", related_name="notifications",
null=True, null=True,