From d1a49717ee4dde1d75600b018219490a57050955 Mon Sep 17 00:00:00 2001 From: ismail Date: Wed, 19 Nov 2025 14:50:20 +0300 Subject: [PATCH 1/7] before update the meeting --- recruitment/forms.py | 4 +- recruitment/models.py | 92 +++++++++---------- recruitment/urls.py | 2 +- recruitment/views.py | 28 +++--- templates/jobs/job_detail.html | 75 ++++++++------- templates/meetings/list_meetings.html | 12 +-- templates/meetings/meeting_details.html | 2 +- .../recruitment/candidate_interview_view.html | 16 ++-- 8 files changed, 119 insertions(+), 112 deletions(-) diff --git a/recruitment/forms.py b/recruitment/forms.py index 74971fd..dd34115 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -2490,12 +2490,11 @@ class StaffAssignmentForm(forms.ModelForm): super().__init__(*args, **kwargs) # Filter users to only show staff members self.fields['assigned_to'].queryset = User.objects.filter( - user_type='staff' + user_type='staff',is_superuser=False ).order_by('first_name', 'last_name') # Add empty choice for unassigning self.fields['assigned_to'].required = False - self.fields['assigned_to'].empty_label = _('-- Unassign Staff --') self.helper = FormHelper() self.helper.form_method = 'post' @@ -2516,3 +2515,4 @@ class StaffAssignmentForm(forms.ModelForm): if assigned_to and assigned_to.user_type != 'staff': raise forms.ValidationError(_('Only staff members can be assigned to jobs.')) return assigned_to + diff --git a/recruitment/models.py b/recruitment/models.py index 826b858..fc0f411 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -129,7 +129,7 @@ class JobPosting(Base): blank=True, ) - application_deadline = models.DateField(db_index=True) # Added index + application_deadline = models.DateField(db_index=True) application_instructions = CKEditor5Field( blank=True, null=True, config_name="extends" ) @@ -912,34 +912,34 @@ class Application(Base): @property def get_latest_meeting(self): """ - Retrieves the most specific location details (subclass instance) + Retrieves the most specific location details (subclass instance) of the latest ScheduledInterview for this application, or None. """ # 1. Get the latest ScheduledInterview schedule = self.scheduled_interviews.order_by("-created_at").first() - + # Check if a schedule exists and if it has an interview location if not schedule or not schedule.interview_location: return None - + # Get the base location instance interview_location = schedule.interview_location - + # 2. Safely retrieve the specific subclass details - + # Determine the expected subclass accessor name based on the location_type if interview_location.location_type == 'Remote': accessor_name = 'zoommeetingdetails' else: # Assumes 'Onsite' or any other type defaults to Onsite accessor_name = 'onsitelocationdetails' - + # Use getattr to safely retrieve the specific meeting object (subclass instance). - # If the accessor exists but points to None (because the subclass record was deleted), + # If the accessor exists but points to None (because the subclass record was deleted), # or if the accessor name is wrong for the object's true type, it will return None. meeting_details = getattr(interview_location, accessor_name, None) - + return meeting_details - + @property def has_future_meeting(self): @@ -1034,13 +1034,13 @@ class TrainingMaterial(Base): class InterviewLocation(Base): """ - Base model for all interview location/meeting details (remote or onsite) + 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") @@ -1054,23 +1054,23 @@ class InterviewLocation(Base): verbose_name=_("Location Type"), db_index=True ) - + details_url = models.URLField( verbose_name=_("Meeting/Location URL"), max_length=2048, blank=True, 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, + max_length=50, verbose_name=_("Timezone"), default='UTC' ) @@ -1086,7 +1086,7 @@ class InterviewLocation(Base): class ZoomMeetingDetails(InterviewLocation): """Concrete model for remote interviews (Zoom specifics).""" - + status = models.CharField( db_index=True, max_length=20, @@ -1101,7 +1101,7 @@ class ZoomMeetingDetails(InterviewLocation): ) meeting_id = models.CharField( db_index=True, - max_length=50, + max_length=50, unique=True, verbose_name=_("External Meeting ID") ) @@ -1117,7 +1117,7 @@ class ZoomMeetingDetails(InterviewLocation): 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") @@ -1137,17 +1137,17 @@ class ZoomMeetingDetails(InterviewLocation): class OnsiteLocationDetails(InterviewLocation): """Concrete model for onsite interviews (Room/Address specifics).""" - + physical_address = models.CharField( - max_length=255, + max_length=255, verbose_name=_("Physical Address"), - blank=True, + blank=True, null=True ) room_number = models.CharField( - max_length=50, + max_length=50, verbose_name=_("Room Number/Name"), - blank=True, + blank=True, null=True ) start_time = models.DateTimeField( @@ -1181,7 +1181,7 @@ class OnsiteLocationDetails(InterviewLocation): 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( @@ -1192,17 +1192,17 @@ class InterviewSchedule(Base): blank=True, verbose_name=_("Location Template (Zoom/Onsite)") ) - - # NOTE: schedule_interview_type field is needed in the form, + + # 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, + max_length=10, choices=InterviewLocation.LocationType.choices, verbose_name=_("Interview Type"), default=InterviewLocation.LocationType.REMOTE ) - + job = models.ForeignKey( JobPosting, on_delete=models.CASCADE, @@ -1212,14 +1212,14 @@ class InterviewSchedule(Base): 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")) @@ -1246,7 +1246,7 @@ class InterviewSchedule(Base): class ScheduledInterview(Base): """Stores individual scheduled interviews (whether bulk or individually created).""" - + class InterviewStatus(models.TextChoices): SCHEDULED = "scheduled", _("Scheduled") CONFIRMED = "confirmed", _("Confirmed") @@ -1265,13 +1265,13 @@ class ScheduledInterview(Base): 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.CASCADE, - related_name="scheduled_interview", - null=True, + InterviewLocation, + on_delete=models.CASCADE, + related_name="scheduled_interview", + null=True, blank=True, db_index=True, verbose_name=_("Meeting/Location Details") @@ -1286,13 +1286,13 @@ class ScheduledInterview(Base): 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, @@ -1320,7 +1320,7 @@ class InterviewNote(Base): FEEDBACK = 'Feedback', _('Candidate Feedback') LOGISTICS = 'Logistics', _('Logistical Note') GENERAL = 'General', _('General Comment') - + 1 interview = models.ForeignKey( ScheduledInterview, @@ -1329,7 +1329,7 @@ class InterviewNote(Base): verbose_name=_("Scheduled Interview"), db_index=True ) - + author = models.ForeignKey( User, on_delete=models.CASCADE, @@ -1337,16 +1337,16 @@ class InterviewNote(Base): verbose_name=_("Author"), db_index=True ) - + 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 = _("Interview Note") verbose_name_plural = _("Interview Notes") @@ -1354,7 +1354,7 @@ class InterviewNote(Base): def __str__(self): return f"{self.get_note_type_display()} by {self.author.get_username()} on {self.interview.id}" - + class FormTemplate(Base): """ diff --git a/recruitment/urls.py b/recruitment/urls.py index 2fbcb4d..592717d 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -660,5 +660,5 @@ urlpatterns = [ # Detail View (assuming slug is on ScheduledInterview) path("interviews/meetings//", views.meeting_details, name="meeting_details"), - + ] diff --git a/recruitment/views.py b/recruitment/views.py index 1692ce7..494f326 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -162,11 +162,11 @@ class PersonListView(StaffRequiredMixin, ListView): gender=self.request.GET.get('gender') if gender: queryset=queryset.filter(gender=gender) - + nationality=self.request.GET.get('nationality') if nationality: queryset=queryset.filter(nationality=nationality) - + return queryset def get_context_data(self, **kwargs): context=super().get_context_data(**kwargs) @@ -174,7 +174,7 @@ class PersonListView(StaffRequiredMixin, ListView): nationalities = self.model.objects.values_list('nationality', flat=True).filter( nationality__isnull=False ).distinct().order_by('nationality') - + nationality=self.request.GET.get('nationality') context['nationality']=nationality context['nationalities']=nationalities @@ -615,6 +615,7 @@ def job_detail(request, slug): "avg_t2i_days": avg_t2i_days, "avg_t_in_exam_days": avg_t_in_exam_days, "linkedin_content_form": linkedin_content_form, + "staff_form": StaffAssignmentForm(), } return render(request, "jobs/job_detail.html", context) @@ -625,7 +626,7 @@ def job_detail(request, slug): # def job_cvs_download(request, slug): # job = get_object_or_404(JobPosting, slug=slug) # entries = Application.objects.filter(job=job) - + # # 2. Create an in-memory byte stream (BytesIO) # zip_buffer = io.BytesIO() @@ -681,7 +682,7 @@ def request_cvs_download(request, slug): # Use async_task to run the function in the background # Pass only simple arguments (like the job ID) async_task('recruitment.tasks.generate_and_save_cv_zip', job.id) - + # Provide user feedback and redirect messages.info(request, "The CV compilation has started in the background. It may take a few moments. Refresh this page to check status.") return redirect('job_detail', slug=slug) # Redirect back to the job detail page @@ -701,7 +702,7 @@ def download_ready_cvs(request, slug): # File is not ready or doesn't exist messages.warning(request, "The ZIP file is still being generated or an error occurred.") return redirect('job_detail', slug=slug) - + @login_required @staff_user_required def job_image_upload(request, slug): @@ -2991,10 +2992,11 @@ def staff_assignment_view(request, slug): applications = job.applications.all() if request.method == "POST": - form = StaffAssignmentForm(request.POST, instance=job) - + form = StaffAssignmentForm(request.POST, instance=job) + if form.is_valid(): - assignment = form.save() + job.assigned_to = form.cleaned_data["assigned_to"] + job.save(update_fields=["assigned_to"]) messages.success(request, f"Staff assigned to job '{job.title}' successfully!") return redirect("job_detail", slug=job.slug) else: @@ -5268,7 +5270,7 @@ def compose_candidate_email(request, job_slug): from_interview=False, job=job ) - + if email_result["success"]: for candidate in candidates: if hasattr(candidate, 'person') and candidate.person: @@ -5660,7 +5662,7 @@ def send_interview_email(request, slug): meeting=meeting, job=job, ) - + if form.is_valid(): # 4. Extract cleaned data subject = form.cleaned_data["subject"] @@ -5734,11 +5736,11 @@ def send_interview_email(request, slug): ) return redirect("list_meetings") else: - + error_msg = "Failed to send email. Please check the form for errors." print(form.errors) messages.error(request, error_msg) - return redirect("meeting_details", slug=meeting.slug) + return redirect("meeting_details", slug=meeting.slug) return redirect("meeting_details", slug=meeting.slug) diff --git a/templates/jobs/job_detail.html b/templates/jobs/job_detail.html index d480b04..9b559c4 100644 --- a/templates/jobs/job_detail.html +++ b/templates/jobs/job_detail.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% load i18n static %} +{% load i18n static crispy_forms_tags %} {% block title %}{{ job.title }} - University ATS{% endblock %} @@ -303,7 +303,7 @@ {% endif %} {% endcomment %} - {% endcomment %} + + + +