changes
This commit is contained in:
parent
a28bfc11f3
commit
d0235bfefe
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -5,7 +5,7 @@ from django.utils import timezone
|
||||
from .models import (
|
||||
JobPosting, Application, TrainingMaterial, ZoomMeetingDetails,
|
||||
FormTemplate, FormStage, FormField, FormSubmission, FieldResponse,
|
||||
SharedFormTemplate, Source, HiringAgency, IntegrationLog,InterviewSchedule,JobPostingImage,MeetingComment,
|
||||
SharedFormTemplate, Source, HiringAgency, IntegrationLog,InterviewSchedule,JobPostingImage,InterviewNote,
|
||||
AgencyAccessLink, AgencyJobAssignment
|
||||
)
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
@ -18,7 +18,7 @@ from .models import (
|
||||
InterviewSchedule,
|
||||
BreakTime,
|
||||
JobPostingImage,
|
||||
MeetingComment,
|
||||
InterviewNote,
|
||||
ScheduledInterview,
|
||||
Source,
|
||||
HiringAgency,
|
||||
@ -26,7 +26,7 @@ from .models import (
|
||||
AgencyAccessLink,
|
||||
Participants,
|
||||
Message,
|
||||
Person,OnsiteMeeting,
|
||||
Person,OnsiteLocationDetails,
|
||||
Document
|
||||
)
|
||||
|
||||
@ -725,7 +725,7 @@ class InterviewScheduleForm(forms.ModelForm):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterviewSchedule
|
||||
model = InterviewSchedule
|
||||
fields = [
|
||||
'schedule_interview_type',
|
||||
"applications",
|
||||
|
||||
@ -983,137 +983,326 @@ class TrainingMaterial(Base):
|
||||
return self.title
|
||||
|
||||
|
||||
class OnsiteMeeting(Base):
|
||||
class MeetingStatus(models.TextChoices):
|
||||
class InterviewLocation(Base):
|
||||
"""
|
||||
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")
|
||||
STARTED = "started", _("Started")
|
||||
ENDED = "ended", _("Ended")
|
||||
CANCELLED = "cancelled", _("Cancelled")
|
||||
|
||||
# Basic meeting details
|
||||
topic = models.CharField(max_length=255, verbose_name=_("Topic"))
|
||||
start_time = models.DateTimeField(
|
||||
db_index=True, verbose_name=_("Start Time")
|
||||
) # Added index
|
||||
duration = models.PositiveIntegerField(
|
||||
verbose_name=_("Duration")
|
||||
) # Duration in minutes
|
||||
timezone = models.CharField(max_length=50, verbose_name=_("Timezone"))
|
||||
location = models.CharField(null=True, blank=True)
|
||||
status = models.CharField(
|
||||
db_index=True,
|
||||
max_length=20, # Added index
|
||||
null=True,
|
||||
location_type = models.CharField(
|
||||
max_length=10,
|
||||
choices=LocationType.choices,
|
||||
verbose_name=_("Location Type"),
|
||||
db_index=True
|
||||
)
|
||||
|
||||
details_url = models.URLField(
|
||||
verbose_name=_("Meeting/Location URL"),
|
||||
max_length=2048,
|
||||
blank=True,
|
||||
verbose_name=_("Status"),
|
||||
default=MeetingStatus.WAITING,
|
||||
null=True
|
||||
)
|
||||
|
||||
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 MeetingStatus(models.TextChoices):
|
||||
WAITING = "waiting", _("Waiting")
|
||||
STARTED = "started", _("Started")
|
||||
ENDED = "ended", _("Ended")
|
||||
CANCELLED = "cancelled", _("Cancelled")
|
||||
class Meta:
|
||||
verbose_name = _("Interview Location")
|
||||
verbose_name_plural = _("Interview Locations")
|
||||
|
||||
# Basic meeting details
|
||||
topic = models.CharField(max_length=255, verbose_name=_("Topic"))
|
||||
meeting_id = models.CharField(
|
||||
|
||||
class ZoomMeetingDetails(InterviewLocation):
|
||||
"""Concrete model for remote interviews (Zoom specifics)."""
|
||||
|
||||
status = models.CharField(
|
||||
db_index=True,
|
||||
max_length=20,
|
||||
unique=True,
|
||||
verbose_name=_("Meeting ID"), # Added index
|
||||
) # Unique identifier for the meeting
|
||||
choices=InterviewLocation.Status.choices,
|
||||
default=InterviewLocation.Status.WAITING,
|
||||
)
|
||||
start_time = models.DateTimeField(
|
||||
db_index=True, verbose_name=_("Start Time")
|
||||
) # Added index
|
||||
)
|
||||
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")
|
||||
verbose_name=_("Duration (minutes)")
|
||||
)
|
||||
meeting_id = models.CharField(
|
||||
db_index=True,
|
||||
max_length=50,
|
||||
unique=True,
|
||||
verbose_name=_("External Meeting ID")
|
||||
)
|
||||
password = models.CharField(
|
||||
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(
|
||||
default=False, verbose_name=_("Join Before Host")
|
||||
)
|
||||
|
||||
host_email=models.CharField(null=True,blank=True)
|
||||
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")
|
||||
# *** REVERTED TO @classmethod (Factory Method) for cleaner instantiation ***
|
||||
# @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(
|
||||
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,
|
||||
blank=True,
|
||||
verbose_name=_("Status"),
|
||||
default=MeetingStatus.WAITING,
|
||||
verbose_name=_("Location Template (Zoom/Onsite)")
|
||||
)
|
||||
|
||||
# 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):
|
||||
return self.topic
|
||||
|
||||
@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()
|
||||
return f"Schedule for {self.job.title}"
|
||||
|
||||
|
||||
class MeetingComment(Base):
|
||||
"""
|
||||
Model for storing meeting comments/notes
|
||||
"""
|
||||
class ScheduledInterview(Base):
|
||||
"""Stores individual scheduled interviews (whether bulk or individually created)."""
|
||||
|
||||
class InterviewStatus(models.TextChoices):
|
||||
SCHEDULED = "scheduled", _("Scheduled")
|
||||
CONFIRMED = "confirmed", _("Confirmed")
|
||||
CANCELLED = "cancelled", _("Cancelled")
|
||||
COMPLETED = "completed", _("Completed")
|
||||
|
||||
meeting = models.ForeignKey(
|
||||
ZoomMeeting,
|
||||
application = models.ForeignKey(
|
||||
Application,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="comments",
|
||||
verbose_name=_("Meeting"),
|
||||
related_name="scheduled_interviews",
|
||||
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(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="meeting_comments",
|
||||
related_name="interview_notes",
|
||||
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:
|
||||
verbose_name = _("Meeting Comment")
|
||||
verbose_name_plural = _("Meeting Comments")
|
||||
ordering = ["-created_at"]
|
||||
verbose_name = _("Interview Note")
|
||||
verbose_name_plural = _("Interview Notes")
|
||||
ordering = ["created_at"]
|
||||
|
||||
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):
|
||||
"""
|
||||
@ -1926,143 +2115,6 @@ class BreakTime(models.Model):
|
||||
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):
|
||||
@ -2101,7 +2153,7 @@ class Notification(models.Model):
|
||||
verbose_name=_("Status"),
|
||||
)
|
||||
related_meeting = models.ForeignKey(
|
||||
ZoomMeeting,
|
||||
ZoomMeetingDetails,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="notifications",
|
||||
null=True,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user