before update the meeting
This commit is contained in:
parent
61cd47ccc9
commit
d1a49717ee
@ -2490,12 +2490,11 @@ class StaffAssignmentForm(forms.ModelForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
# Filter users to only show staff members
|
# Filter users to only show staff members
|
||||||
self.fields['assigned_to'].queryset = User.objects.filter(
|
self.fields['assigned_to'].queryset = User.objects.filter(
|
||||||
user_type='staff'
|
user_type='staff',is_superuser=False
|
||||||
).order_by('first_name', 'last_name')
|
).order_by('first_name', 'last_name')
|
||||||
|
|
||||||
# Add empty choice for unassigning
|
# Add empty choice for unassigning
|
||||||
self.fields['assigned_to'].required = False
|
self.fields['assigned_to'].required = False
|
||||||
self.fields['assigned_to'].empty_label = _('-- Unassign Staff --')
|
|
||||||
|
|
||||||
self.helper = FormHelper()
|
self.helper = FormHelper()
|
||||||
self.helper.form_method = 'post'
|
self.helper.form_method = 'post'
|
||||||
@ -2516,3 +2515,4 @@ class StaffAssignmentForm(forms.ModelForm):
|
|||||||
if assigned_to and assigned_to.user_type != 'staff':
|
if assigned_to and assigned_to.user_type != 'staff':
|
||||||
raise forms.ValidationError(_('Only staff members can be assigned to jobs.'))
|
raise forms.ValidationError(_('Only staff members can be assigned to jobs.'))
|
||||||
return assigned_to
|
return assigned_to
|
||||||
|
|
||||||
|
|||||||
@ -129,7 +129,7 @@ class JobPosting(Base):
|
|||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
application_deadline = models.DateField(db_index=True) # Added index
|
application_deadline = models.DateField(db_index=True)
|
||||||
application_instructions = CKEditor5Field(
|
application_instructions = CKEditor5Field(
|
||||||
blank=True, null=True, config_name="extends"
|
blank=True, null=True, config_name="extends"
|
||||||
)
|
)
|
||||||
@ -912,34 +912,34 @@ class Application(Base):
|
|||||||
@property
|
@property
|
||||||
def get_latest_meeting(self):
|
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.
|
of the latest ScheduledInterview for this application, or None.
|
||||||
"""
|
"""
|
||||||
# 1. Get the latest ScheduledInterview
|
# 1. Get the latest ScheduledInterview
|
||||||
schedule = self.scheduled_interviews.order_by("-created_at").first()
|
schedule = self.scheduled_interviews.order_by("-created_at").first()
|
||||||
|
|
||||||
# Check if a schedule exists and if it has an interview location
|
# Check if a schedule exists and if it has an interview location
|
||||||
if not schedule or not schedule.interview_location:
|
if not schedule or not schedule.interview_location:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Get the base location instance
|
# Get the base location instance
|
||||||
interview_location = schedule.interview_location
|
interview_location = schedule.interview_location
|
||||||
|
|
||||||
# 2. Safely retrieve the specific subclass details
|
# 2. Safely retrieve the specific subclass details
|
||||||
|
|
||||||
# Determine the expected subclass accessor name based on the location_type
|
# Determine the expected subclass accessor name based on the location_type
|
||||||
if interview_location.location_type == 'Remote':
|
if interview_location.location_type == 'Remote':
|
||||||
accessor_name = 'zoommeetingdetails'
|
accessor_name = 'zoommeetingdetails'
|
||||||
else: # Assumes 'Onsite' or any other type defaults to Onsite
|
else: # Assumes 'Onsite' or any other type defaults to Onsite
|
||||||
accessor_name = 'onsitelocationdetails'
|
accessor_name = 'onsitelocationdetails'
|
||||||
|
|
||||||
# Use getattr to safely retrieve the specific meeting object (subclass instance).
|
# 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.
|
# 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)
|
meeting_details = getattr(interview_location, accessor_name, None)
|
||||||
|
|
||||||
return meeting_details
|
return meeting_details
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_future_meeting(self):
|
def has_future_meeting(self):
|
||||||
@ -1034,13 +1034,13 @@ class TrainingMaterial(Base):
|
|||||||
|
|
||||||
class InterviewLocation(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.
|
using Multi-Table Inheritance.
|
||||||
"""
|
"""
|
||||||
class LocationType(models.TextChoices):
|
class LocationType(models.TextChoices):
|
||||||
REMOTE = 'Remote', _('Remote (e.g., Zoom, Google Meet)')
|
REMOTE = 'Remote', _('Remote (e.g., Zoom, Google Meet)')
|
||||||
ONSITE = 'Onsite', _('In-Person (Physical Location)')
|
ONSITE = 'Onsite', _('In-Person (Physical Location)')
|
||||||
|
|
||||||
class Status(models.TextChoices):
|
class Status(models.TextChoices):
|
||||||
"""Defines the possible real-time statuses for any interview location/meeting."""
|
"""Defines the possible real-time statuses for any interview location/meeting."""
|
||||||
WAITING = "waiting", _("Waiting")
|
WAITING = "waiting", _("Waiting")
|
||||||
@ -1054,23 +1054,23 @@ class InterviewLocation(Base):
|
|||||||
verbose_name=_("Location Type"),
|
verbose_name=_("Location Type"),
|
||||||
db_index=True
|
db_index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
details_url = models.URLField(
|
details_url = models.URLField(
|
||||||
verbose_name=_("Meeting/Location URL"),
|
verbose_name=_("Meeting/Location URL"),
|
||||||
max_length=2048,
|
max_length=2048,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
topic = models.CharField( # Renamed from 'description' to 'topic' to match your input
|
topic = models.CharField( # Renamed from 'description' to 'topic' to match your input
|
||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_("Location/Meeting Topic"),
|
verbose_name=_("Location/Meeting Topic"),
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_("e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room, 3rd Floor'")
|
help_text=_("e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room, 3rd Floor'")
|
||||||
)
|
)
|
||||||
|
|
||||||
timezone = models.CharField(
|
timezone = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
verbose_name=_("Timezone"),
|
verbose_name=_("Timezone"),
|
||||||
default='UTC'
|
default='UTC'
|
||||||
)
|
)
|
||||||
@ -1086,7 +1086,7 @@ class InterviewLocation(Base):
|
|||||||
|
|
||||||
class ZoomMeetingDetails(InterviewLocation):
|
class ZoomMeetingDetails(InterviewLocation):
|
||||||
"""Concrete model for remote interviews (Zoom specifics)."""
|
"""Concrete model for remote interviews (Zoom specifics)."""
|
||||||
|
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
db_index=True,
|
db_index=True,
|
||||||
max_length=20,
|
max_length=20,
|
||||||
@ -1101,7 +1101,7 @@ class ZoomMeetingDetails(InterviewLocation):
|
|||||||
)
|
)
|
||||||
meeting_id = models.CharField(
|
meeting_id = models.CharField(
|
||||||
db_index=True,
|
db_index=True,
|
||||||
max_length=50,
|
max_length=50,
|
||||||
unique=True,
|
unique=True,
|
||||||
verbose_name=_("External Meeting ID")
|
verbose_name=_("External Meeting ID")
|
||||||
)
|
)
|
||||||
@ -1117,7 +1117,7 @@ class ZoomMeetingDetails(InterviewLocation):
|
|||||||
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)
|
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")
|
||||||
@ -1137,17 +1137,17 @@ class ZoomMeetingDetails(InterviewLocation):
|
|||||||
|
|
||||||
class OnsiteLocationDetails(InterviewLocation):
|
class OnsiteLocationDetails(InterviewLocation):
|
||||||
"""Concrete model for onsite interviews (Room/Address specifics)."""
|
"""Concrete model for onsite interviews (Room/Address specifics)."""
|
||||||
|
|
||||||
physical_address = models.CharField(
|
physical_address = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_("Physical Address"),
|
verbose_name=_("Physical Address"),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
room_number = models.CharField(
|
room_number = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
verbose_name=_("Room Number/Name"),
|
verbose_name=_("Room Number/Name"),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
start_time = models.DateTimeField(
|
start_time = models.DateTimeField(
|
||||||
@ -1181,7 +1181,7 @@ class OnsiteLocationDetails(InterviewLocation):
|
|||||||
|
|
||||||
class InterviewSchedule(Base):
|
class InterviewSchedule(Base):
|
||||||
"""Stores the TEMPLATE criteria for BULK interview generation."""
|
"""Stores the TEMPLATE criteria for BULK interview generation."""
|
||||||
|
|
||||||
# We need a field to store the template location details linked to this bulk schedule.
|
# 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.
|
# This location object contains the generic Zoom/Onsite info to be cloned.
|
||||||
template_location = models.ForeignKey(
|
template_location = models.ForeignKey(
|
||||||
@ -1192,17 +1192,17 @@ class InterviewSchedule(Base):
|
|||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_("Location Template (Zoom/Onsite)")
|
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.
|
# but not on the model itself if we use template_location.
|
||||||
# If you want to keep it:
|
# If you want to keep it:
|
||||||
schedule_interview_type = models.CharField(
|
schedule_interview_type = models.CharField(
|
||||||
max_length=10,
|
max_length=10,
|
||||||
choices=InterviewLocation.LocationType.choices,
|
choices=InterviewLocation.LocationType.choices,
|
||||||
verbose_name=_("Interview Type"),
|
verbose_name=_("Interview Type"),
|
||||||
default=InterviewLocation.LocationType.REMOTE
|
default=InterviewLocation.LocationType.REMOTE
|
||||||
)
|
)
|
||||||
|
|
||||||
job = models.ForeignKey(
|
job = models.ForeignKey(
|
||||||
JobPosting,
|
JobPosting,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@ -1212,14 +1212,14 @@ class InterviewSchedule(Base):
|
|||||||
applications = models.ManyToManyField(
|
applications = models.ManyToManyField(
|
||||||
Application, related_name="interview_schedules", blank=True
|
Application, related_name="interview_schedules", blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
start_date = models.DateField(db_index=True, verbose_name=_("Start Date"))
|
start_date = models.DateField(db_index=True, verbose_name=_("Start Date"))
|
||||||
end_date = models.DateField(db_index=True, verbose_name=_("End Date"))
|
end_date = models.DateField(db_index=True, verbose_name=_("End Date"))
|
||||||
|
|
||||||
working_days = models.JSONField(
|
working_days = models.JSONField(
|
||||||
verbose_name=_("Working Days")
|
verbose_name=_("Working Days")
|
||||||
)
|
)
|
||||||
|
|
||||||
start_time = models.TimeField(verbose_name=_("Start Time"))
|
start_time = models.TimeField(verbose_name=_("Start Time"))
|
||||||
end_time = models.TimeField(verbose_name=_("End Time"))
|
end_time = models.TimeField(verbose_name=_("End Time"))
|
||||||
|
|
||||||
@ -1246,7 +1246,7 @@ class InterviewSchedule(Base):
|
|||||||
|
|
||||||
class ScheduledInterview(Base):
|
class ScheduledInterview(Base):
|
||||||
"""Stores individual scheduled interviews (whether bulk or individually created)."""
|
"""Stores individual scheduled interviews (whether bulk or individually created)."""
|
||||||
|
|
||||||
class InterviewStatus(models.TextChoices):
|
class InterviewStatus(models.TextChoices):
|
||||||
SCHEDULED = "scheduled", _("Scheduled")
|
SCHEDULED = "scheduled", _("Scheduled")
|
||||||
CONFIRMED = "confirmed", _("Confirmed")
|
CONFIRMED = "confirmed", _("Confirmed")
|
||||||
@ -1265,13 +1265,13 @@ class ScheduledInterview(Base):
|
|||||||
related_name="scheduled_interviews",
|
related_name="scheduled_interviews",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Links to the specific, individual location/meeting details for THIS interview
|
# Links to the specific, individual location/meeting details for THIS interview
|
||||||
interview_location = models.OneToOneField(
|
interview_location = models.OneToOneField(
|
||||||
InterviewLocation,
|
InterviewLocation,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name="scheduled_interview",
|
related_name="scheduled_interview",
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
db_index=True,
|
db_index=True,
|
||||||
verbose_name=_("Meeting/Location Details")
|
verbose_name=_("Meeting/Location Details")
|
||||||
@ -1286,13 +1286,13 @@ class ScheduledInterview(Base):
|
|||||||
blank=True,
|
blank=True,
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
participants = models.ManyToManyField('Participants', blank=True)
|
participants = models.ManyToManyField('Participants', blank=True)
|
||||||
system_users = models.ManyToManyField(User, related_name="attended_interviews", 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_date = models.DateField(db_index=True, verbose_name=_("Interview Date"))
|
||||||
interview_time = models.TimeField(verbose_name=_("Interview Time"))
|
interview_time = models.TimeField(verbose_name=_("Interview Time"))
|
||||||
|
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
db_index=True,
|
db_index=True,
|
||||||
max_length=20,
|
max_length=20,
|
||||||
@ -1320,7 +1320,7 @@ class InterviewNote(Base):
|
|||||||
FEEDBACK = 'Feedback', _('Candidate Feedback')
|
FEEDBACK = 'Feedback', _('Candidate Feedback')
|
||||||
LOGISTICS = 'Logistics', _('Logistical Note')
|
LOGISTICS = 'Logistics', _('Logistical Note')
|
||||||
GENERAL = 'General', _('General Comment')
|
GENERAL = 'General', _('General Comment')
|
||||||
|
|
||||||
1
|
1
|
||||||
interview = models.ForeignKey(
|
interview = models.ForeignKey(
|
||||||
ScheduledInterview,
|
ScheduledInterview,
|
||||||
@ -1329,7 +1329,7 @@ class InterviewNote(Base):
|
|||||||
verbose_name=_("Scheduled Interview"),
|
verbose_name=_("Scheduled Interview"),
|
||||||
db_index=True
|
db_index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
author = models.ForeignKey(
|
author = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@ -1337,16 +1337,16 @@ class InterviewNote(Base):
|
|||||||
verbose_name=_("Author"),
|
verbose_name=_("Author"),
|
||||||
db_index=True
|
db_index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
note_type = models.CharField(
|
note_type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=NoteType.choices,
|
choices=NoteType.choices,
|
||||||
default=NoteType.FEEDBACK,
|
default=NoteType.FEEDBACK,
|
||||||
verbose_name=_("Note Type")
|
verbose_name=_("Note Type")
|
||||||
)
|
)
|
||||||
|
|
||||||
content = CKEditor5Field(verbose_name=_("Content/Feedback"), config_name="extends")
|
content = CKEditor5Field(verbose_name=_("Content/Feedback"), config_name="extends")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Interview Note")
|
verbose_name = _("Interview Note")
|
||||||
verbose_name_plural = _("Interview Notes")
|
verbose_name_plural = _("Interview Notes")
|
||||||
@ -1354,7 +1354,7 @@ class InterviewNote(Base):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.get_note_type_display()} by {self.author.get_username()} on {self.interview.id}"
|
return f"{self.get_note_type_display()} by {self.author.get_username()} on {self.interview.id}"
|
||||||
|
|
||||||
|
|
||||||
class FormTemplate(Base):
|
class FormTemplate(Base):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -660,5 +660,5 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Detail View (assuming slug is on ScheduledInterview)
|
# Detail View (assuming slug is on ScheduledInterview)
|
||||||
path("interviews/meetings/<slug:slug>/", views.meeting_details, name="meeting_details"),
|
path("interviews/meetings/<slug:slug>/", views.meeting_details, name="meeting_details"),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|||||||
@ -162,11 +162,11 @@ class PersonListView(StaffRequiredMixin, ListView):
|
|||||||
gender=self.request.GET.get('gender')
|
gender=self.request.GET.get('gender')
|
||||||
if gender:
|
if gender:
|
||||||
queryset=queryset.filter(gender=gender)
|
queryset=queryset.filter(gender=gender)
|
||||||
|
|
||||||
nationality=self.request.GET.get('nationality')
|
nationality=self.request.GET.get('nationality')
|
||||||
if nationality:
|
if nationality:
|
||||||
queryset=queryset.filter(nationality=nationality)
|
queryset=queryset.filter(nationality=nationality)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context=super().get_context_data(**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(
|
nationalities = self.model.objects.values_list('nationality', flat=True).filter(
|
||||||
nationality__isnull=False
|
nationality__isnull=False
|
||||||
).distinct().order_by('nationality')
|
).distinct().order_by('nationality')
|
||||||
|
|
||||||
nationality=self.request.GET.get('nationality')
|
nationality=self.request.GET.get('nationality')
|
||||||
context['nationality']=nationality
|
context['nationality']=nationality
|
||||||
context['nationalities']=nationalities
|
context['nationalities']=nationalities
|
||||||
@ -615,6 +615,7 @@ def job_detail(request, slug):
|
|||||||
"avg_t2i_days": avg_t2i_days,
|
"avg_t2i_days": avg_t2i_days,
|
||||||
"avg_t_in_exam_days": avg_t_in_exam_days,
|
"avg_t_in_exam_days": avg_t_in_exam_days,
|
||||||
"linkedin_content_form": linkedin_content_form,
|
"linkedin_content_form": linkedin_content_form,
|
||||||
|
"staff_form": StaffAssignmentForm(),
|
||||||
}
|
}
|
||||||
return render(request, "jobs/job_detail.html", context)
|
return render(request, "jobs/job_detail.html", context)
|
||||||
|
|
||||||
@ -625,7 +626,7 @@ def job_detail(request, slug):
|
|||||||
# def job_cvs_download(request, slug):
|
# def job_cvs_download(request, slug):
|
||||||
# job = get_object_or_404(JobPosting, slug=slug)
|
# job = get_object_or_404(JobPosting, slug=slug)
|
||||||
# entries = Application.objects.filter(job=job)
|
# entries = Application.objects.filter(job=job)
|
||||||
|
|
||||||
# # 2. Create an in-memory byte stream (BytesIO)
|
# # 2. Create an in-memory byte stream (BytesIO)
|
||||||
# zip_buffer = io.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
|
# Use async_task to run the function in the background
|
||||||
# Pass only simple arguments (like the job ID)
|
# Pass only simple arguments (like the job ID)
|
||||||
async_task('recruitment.tasks.generate_and_save_cv_zip', job.id)
|
async_task('recruitment.tasks.generate_and_save_cv_zip', job.id)
|
||||||
|
|
||||||
# Provide user feedback and redirect
|
# 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.")
|
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
|
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
|
# File is not ready or doesn't exist
|
||||||
messages.warning(request, "The ZIP file is still being generated or an error occurred.")
|
messages.warning(request, "The ZIP file is still being generated or an error occurred.")
|
||||||
return redirect('job_detail', slug=slug)
|
return redirect('job_detail', slug=slug)
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@staff_user_required
|
@staff_user_required
|
||||||
def job_image_upload(request, slug):
|
def job_image_upload(request, slug):
|
||||||
@ -2991,10 +2992,11 @@ def staff_assignment_view(request, slug):
|
|||||||
applications = job.applications.all()
|
applications = job.applications.all()
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = StaffAssignmentForm(request.POST, instance=job)
|
form = StaffAssignmentForm(request.POST, instance=job)
|
||||||
|
|
||||||
if form.is_valid():
|
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!")
|
messages.success(request, f"Staff assigned to job '{job.title}' successfully!")
|
||||||
return redirect("job_detail", slug=job.slug)
|
return redirect("job_detail", slug=job.slug)
|
||||||
else:
|
else:
|
||||||
@ -5268,7 +5270,7 @@ def compose_candidate_email(request, job_slug):
|
|||||||
from_interview=False,
|
from_interview=False,
|
||||||
job=job
|
job=job
|
||||||
)
|
)
|
||||||
|
|
||||||
if email_result["success"]:
|
if email_result["success"]:
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
if hasattr(candidate, 'person') and candidate.person:
|
if hasattr(candidate, 'person') and candidate.person:
|
||||||
@ -5660,7 +5662,7 @@ def send_interview_email(request, slug):
|
|||||||
meeting=meeting,
|
meeting=meeting,
|
||||||
job=job,
|
job=job,
|
||||||
)
|
)
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
# 4. Extract cleaned data
|
# 4. Extract cleaned data
|
||||||
subject = form.cleaned_data["subject"]
|
subject = form.cleaned_data["subject"]
|
||||||
@ -5734,11 +5736,11 @@ def send_interview_email(request, slug):
|
|||||||
)
|
)
|
||||||
return redirect("list_meetings")
|
return redirect("list_meetings")
|
||||||
else:
|
else:
|
||||||
|
|
||||||
error_msg = "Failed to send email. Please check the form for errors."
|
error_msg = "Failed to send email. Please check the form for errors."
|
||||||
print(form.errors)
|
print(form.errors)
|
||||||
messages.error(request, error_msg)
|
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)
|
return redirect("meeting_details", slug=meeting.slug)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load i18n static %}
|
{% load i18n static crispy_forms_tags %}
|
||||||
|
|
||||||
{% block title %}{{ job.title }} - University ATS{% endblock %}
|
{% block title %}{{ job.title }} - University ATS{% endblock %}
|
||||||
|
|
||||||
@ -303,7 +303,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="nav-item flex-fill" role="presentation">
|
<li class="nav-item flex-fill" role="presentation">
|
||||||
<button class="nav-link" id="staff-tab" data-bs-toggle="tab" data-bs-target="#staff-pane" type="button" role="tab" aria-controls="staff-pane" aria-selected="false">
|
<button class="nav-link" id="staff-tab" data-bs-toggle="tab" data-bs-target="#staff-pane" type="button" role="tab" aria-controls="staff-pane" aria-selected="false">
|
||||||
<i class="fas fa-user-tie me-1 text-primary"></i> {% trans "Staff" %}
|
<i class="fas fa-user-tie me-1 text-primary"></i> {% trans "Assigned Staff" %}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item flex-fill" role="presentation">
|
<li class="nav-item flex-fill" role="presentation">
|
||||||
@ -384,42 +384,47 @@
|
|||||||
<div class="tab-pane fade" id="staff-pane" role="tabpanel" aria-labelledby="staff-tab">
|
<div class="tab-pane fade" id="staff-pane" role="tabpanel" aria-labelledby="staff-tab">
|
||||||
<h5 class="mb-3"><i class="fas fa-user-tie me-2 text-primary"></i>{% trans "Staff Assignment" %}</h5>
|
<h5 class="mb-3"><i class="fas fa-user-tie me-2 text-primary"></i>{% trans "Staff Assignment" %}</h5>
|
||||||
<div class="d-grid gap-3">
|
<div class="d-grid gap-3">
|
||||||
<p class="text-muted small mb-3">
|
|
||||||
{% trans "Assign staff members to manage this job posting and track applications." %}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<a href="{% url 'staff_assignment_view' job.slug %}" class="btn btn-main-action">
|
|
||||||
<i class="fas fa-user-plus me-1"></i> {% trans "Assign Staff Member" %}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% if job.assigned_to %}
|
{% if job.assigned_to %}
|
||||||
<div class="mt-3">
|
<p class="text-muted small mb-3">
|
||||||
<h6 class="text-muted">{% trans "Current Assignments" %}</h6>
|
<strong>{% trans "Assigned to:" %}</strong> {{ job.assigned_to }}
|
||||||
{% for assignment in job.staff_assignments.all %}
|
</p>
|
||||||
<div class="card mb-2">
|
{% endif %}
|
||||||
<div class="card-body p-2">
|
{% if not job.assigned_to %}
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<button type="button" class="btn btn-main-action" data-bs-toggle="modal" data-bs-target="#staffAssignmentModal">
|
||||||
<div>
|
<i class="fas fa-user-plus me-1"></i> {% trans "Assign Staff Member" %}
|
||||||
<strong>{{ assignment.staff.get_full_name|default:assignment.staff.username }}</strong>
|
</button>
|
||||||
<br>
|
{% elif job.assigned_to and job.assigned_to == request.user %}
|
||||||
<small class="text-muted">{{ assignment.staff.email }}</small>
|
<button type="button" class="btn btn-main-action" data-bs-toggle="modal" data-bs-target="#staffAssignmentModal">
|
||||||
</div>
|
<i class="fas fa-user-plus me-1"></i> {% trans "Assign Staff Member" %}
|
||||||
<div>
|
</button>
|
||||||
{% if assignment.staff.is_active %}
|
{% endif %}
|
||||||
<span class="badge bg-success">Active</span>
|
|
||||||
{% else %}
|
<!-- Modal for Staff Assignment -->
|
||||||
<span class="badge bg-danger">Inactive</span>
|
<div class="modal fade" id="staffAssignmentModal" tabindex="-1" aria-labelledby="staffAssignmentModalLabel" aria-hidden="true">
|
||||||
{% endif %}
|
<div class="modal-dialog">
|
||||||
</div>
|
<div class="modal-content">
|
||||||
</div>
|
<div class="modal-header">
|
||||||
{% if assignment.notes %}
|
<h5 class="modal-title" id="staffAssignmentModalLabel">
|
||||||
<small class="text-muted d-block mt-1">{{ assignment.notes }}</small>
|
<i class="fas fa-user-plus me-2"></i> {% trans "Assign Staff Member" %}
|
||||||
{% endif %}
|
</h5>
|
||||||
</div>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
<div class="modal-body">
|
||||||
|
<form method="post" action="{% url 'staff_assignment_view' job.slug %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{staff_form|crispy}}
|
||||||
|
<div class="d-flex justify-content-end mt-3">
|
||||||
|
<button type="submit" class="btn btn-main-action">
|
||||||
|
<i class="fas fa-save me-1"></i> {% trans "Save Assignment" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
</div>
|
||||||
|
|
||||||
|
{% if not job.assigned_to %}
|
||||||
<div class="alert alert-info p-2 small mb-0">
|
<div class="alert alert-info p-2 small mb-0">
|
||||||
<i class="fas fa-info-circle me-1"></i> {% trans "No staff members assigned to this job yet." %}
|
<i class="fas fa-info-circle me-1"></i> {% trans "No staff members assigned to this job yet." %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
--kaauh-border: #eaeff3;
|
--kaauh-border: #eaeff3;
|
||||||
--kaauh-primary-text: #343a40;
|
--kaauh-primary-text: #343a40;
|
||||||
--kaauh-gray-light: #f8f9fa;
|
--kaauh-gray-light: #f8f9fa;
|
||||||
--kaauh-warning: #ffc107;
|
--kaauh-warning: #ffc107;
|
||||||
--kaauh-danger: #dc3545;
|
--kaauh-danger: #dc3545;
|
||||||
--kaauh-success: #28a745;
|
--kaauh-success: #28a745;
|
||||||
}
|
}
|
||||||
@ -158,7 +158,7 @@
|
|||||||
<option value="Onsite" {% if type_filter == 'Onsite' %}selected{% endif %}>{% trans "Onsite" %}</option>
|
<option value="Onsite" {% if type_filter == 'Onsite' %}selected{% endif %}>{% trans "Onsite" %}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label for="status" class="form-label small text-muted">{% trans "Filter by Status" %}</label>
|
<label for="status" class="form-label small text-muted">{% trans "Filter by Status" %}</label>
|
||||||
<select name="status" id="status" class="form-select form-select-sm">
|
<select name="status" id="status" class="form-select form-select-sm">
|
||||||
@ -190,7 +190,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if meetings_data %}
|
{% if meetings_data %}
|
||||||
<div id="meetings-list">
|
<div id="meetings-list">
|
||||||
{# View Switcher (not provided, assuming standard include) #}
|
{# View Switcher (not provided, assuming standard include) #}
|
||||||
@ -213,7 +213,7 @@
|
|||||||
<p class="card-text text-muted small mb-3">
|
<p class="card-text text-muted small mb-3">
|
||||||
<i class="fas fa-user"></i> {% trans "Candidate" %}: {{ meeting.interview.application.person.full_name|default:"N/A" }}<br>
|
<i class="fas fa-user"></i> {% trans "Candidate" %}: {{ meeting.interview.application.person.full_name|default:"N/A" }}<br>
|
||||||
<i class="fas fa-briefcase"></i> {% trans "Job" %}: {{ meeting.interview.job.title|default:"N/A" }}<br>
|
<i class="fas fa-briefcase"></i> {% trans "Job" %}: {{ meeting.interview.job.title|default:"N/A" }}<br>
|
||||||
|
|
||||||
{# Dynamic location/type details #}
|
{# Dynamic location/type details #}
|
||||||
{% if meeting.type == 'Remote' %}
|
{% if meeting.type == 'Remote' %}
|
||||||
<i class="fas fa-link"></i> {% trans "Remote ID" %}: {{ meeting.meeting_id|default:meeting.location.id }}<br>
|
<i class="fas fa-link"></i> {% trans "Remote ID" %}: {{ meeting.meeting_id|default:meeting.location.id }}<br>
|
||||||
@ -224,7 +224,7 @@
|
|||||||
<i class="fas fa-clock"></i> {% trans "Start" %}: {{ meeting.start_time|date:"M d, Y H:i" }}<br>
|
<i class="fas fa-clock"></i> {% trans "Start" %}: {{ meeting.start_time|date:"M d, Y H:i" }}<br>
|
||||||
<i class="fas fa-stopwatch"></i> {% trans "Duration" %}: {{ meeting.duration }} minutes
|
<i class="fas fa-stopwatch"></i> {% trans "Duration" %}: {{ meeting.duration }} minutes
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<span class="status-badge bg-{{ meeting.status }}">
|
<span class="status-badge bg-{{ meeting.status }}">
|
||||||
{{ meeting.interview.get_status_display }}
|
{{ meeting.interview.get_status_display }}
|
||||||
</span>
|
</span>
|
||||||
@ -244,7 +244,7 @@
|
|||||||
<i class="fas fa-check"></i> {% trans "Physical Event" %}
|
<i class="fas fa-check"></i> {% trans "Physical Event" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# CORRECTED: Passing the slug to the update URL #}
|
{# CORRECTED: Passing the slug to the update URL #}
|
||||||
<a href="" class="btn btn-sm btn-outline-secondary">
|
<a href="" class="btn btn-sm btn-outline-secondary">
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
|
|||||||
@ -273,7 +273,7 @@ body {
|
|||||||
{% if meeting.join_url %}
|
{% if meeting.join_url %}
|
||||||
<div class="join-url-container pt-3">
|
<div class="join-url-container pt-3">
|
||||||
<div id="copy-message" class="text-white rounded px-2 py-1 small fw-bold mb-2 text-center" style="opacity: 0; transition: opacity 0.3s; position: absolute; right: 0; top: 5px; background-color: var(--kaauh-success); z-index: 10;">{% trans "Copied!" %}</div>
|
<div id="copy-message" class="text-white rounded px-2 py-1 small fw-bold mb-2 text-center" style="opacity: 0; transition: opacity 0.3s; position: absolute; right: 0; top: 5px; background-color: var(--kaauh-success); z-index: 10;">{% trans "Copied!" %}</div>
|
||||||
|
|
||||||
<div class="join-url-display d-flex justify-content-between align-items-center position-relative">
|
<div class="join-url-display d-flex justify-content-between align-items-center position-relative">
|
||||||
<div class="text-truncate me-2">
|
<div class="text-truncate me-2">
|
||||||
<strong>{% trans "Join URL" %}:</strong>
|
<strong>{% trans "Join URL" %}:</strong>
|
||||||
|
|||||||
@ -206,7 +206,7 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
{# Select Input Group - No label needed for this one, so we just flex the select and button #}
|
{# Select Input Group - No label needed for this one, so we just flex the select and button #}
|
||||||
|
|
||||||
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;">
|
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;">
|
||||||
<option selected>
|
<option selected>
|
||||||
----------
|
----------
|
||||||
@ -233,7 +233,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="vr" style="height: 28px;"></div>
|
<div class="vr" style="height: 28px;"></div>
|
||||||
|
|
||||||
|
|
||||||
<button type="button" class="btn btn-outline-info btn-sm"
|
<button type="button" class="btn btn-outline-info btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
@ -248,7 +248,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<form id="candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="get">
|
<form id="candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="get">
|
||||||
@ -291,7 +291,7 @@
|
|||||||
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
|
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
|
||||||
hx-target="#candidateviewModalBody"
|
hx-target="#candidateviewModalBody"
|
||||||
title="View Profile">
|
title="View Profile">
|
||||||
{{ candidate.name }}<i class="fas fa-eye ms-1"></i>
|
{{ candidate.name }} <i class="fas fa-eye ms-1"></i>
|
||||||
</button>
|
</button>
|
||||||
{% comment %} <div class="candidate-name">
|
{% comment %} <div class="candidate-name">
|
||||||
{{ candidate.name }}
|
{{ candidate.name }}
|
||||||
@ -380,7 +380,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
{% if candidate.get_latest_meeting %}
|
{% if candidate.get_latest_meeting %}
|
||||||
{% if candidate.get_latest_meeting.location_type == 'Remote'%}
|
{% if candidate.get_latest_meeting.location_type == 'Remote'%}
|
||||||
|
|
||||||
@ -402,7 +402,7 @@
|
|||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
{% else%}
|
{% else%}
|
||||||
|
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#candidateviewModal"
|
data-bs-target="#candidateviewModal"
|
||||||
@ -422,7 +422,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<button type="button" class="btn btn-main-action btn-sm"
|
<button type="button" class="btn btn-main-action btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
@ -497,7 +497,7 @@
|
|||||||
<div class="text-center py-5 text-muted">
|
<div class="text-center py-5 text-muted">
|
||||||
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
|
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
|
||||||
{% trans "Loading email form..." %}
|
{% trans "Loading email form..." %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user