diff --git a/NorahUniversity/__pycache__/settings.cpython-312.pyc b/NorahUniversity/__pycache__/settings.cpython-312.pyc index 741dbde..d8f9217 100644 Binary files a/NorahUniversity/__pycache__/settings.cpython-312.pyc and b/NorahUniversity/__pycache__/settings.cpython-312.pyc differ diff --git a/NorahUniversity/__pycache__/urls.cpython-312.pyc b/NorahUniversity/__pycache__/urls.cpython-312.pyc index f543051..b2d58f6 100644 Binary files a/NorahUniversity/__pycache__/urls.cpython-312.pyc and b/NorahUniversity/__pycache__/urls.cpython-312.pyc differ diff --git a/recruitment/__pycache__/admin.cpython-312.pyc b/recruitment/__pycache__/admin.cpython-312.pyc index 8f7e3a6..6908ef3 100644 Binary files a/recruitment/__pycache__/admin.cpython-312.pyc and b/recruitment/__pycache__/admin.cpython-312.pyc differ diff --git a/recruitment/__pycache__/forms.cpython-312.pyc b/recruitment/__pycache__/forms.cpython-312.pyc index 72385f2..8e6cf04 100644 Binary files a/recruitment/__pycache__/forms.cpython-312.pyc and b/recruitment/__pycache__/forms.cpython-312.pyc differ diff --git a/recruitment/__pycache__/models.cpython-312.pyc b/recruitment/__pycache__/models.cpython-312.pyc index 04323b6..1edec8c 100644 Binary files a/recruitment/__pycache__/models.cpython-312.pyc and b/recruitment/__pycache__/models.cpython-312.pyc differ diff --git a/recruitment/__pycache__/serializers.cpython-312.pyc b/recruitment/__pycache__/serializers.cpython-312.pyc index 8472924..60875c7 100644 Binary files a/recruitment/__pycache__/serializers.cpython-312.pyc and b/recruitment/__pycache__/serializers.cpython-312.pyc differ diff --git a/recruitment/__pycache__/signals.cpython-312.pyc b/recruitment/__pycache__/signals.cpython-312.pyc index 6330397..9fd593d 100644 Binary files a/recruitment/__pycache__/signals.cpython-312.pyc and b/recruitment/__pycache__/signals.cpython-312.pyc differ diff --git a/recruitment/__pycache__/views.cpython-312.pyc b/recruitment/__pycache__/views.cpython-312.pyc index 2f473d0..2b263c2 100644 Binary files a/recruitment/__pycache__/views.cpython-312.pyc and b/recruitment/__pycache__/views.cpython-312.pyc differ diff --git a/recruitment/admin.py b/recruitment/admin.py index 140291a..55ccefa 100644 --- a/recruitment/admin.py +++ b/recruitment/admin.py @@ -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 diff --git a/recruitment/forms.py b/recruitment/forms.py index 491bf2d..90f5aaa 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -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", diff --git a/recruitment/models.py b/recruitment/models.py index 0f7cc7e..e3c33cc 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -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,