diff --git a/recruitment/__pycache__/forms.cpython-312.pyc b/recruitment/__pycache__/forms.cpython-312.pyc index 4c46da2..8e0f6cc 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 c1360f1..b4e2d8a 100644 Binary files a/recruitment/__pycache__/models.cpython-312.pyc and b/recruitment/__pycache__/models.cpython-312.pyc differ diff --git a/recruitment/__pycache__/urls.cpython-312.pyc b/recruitment/__pycache__/urls.cpython-312.pyc index 5e3fefc..9a77a66 100644 Binary files a/recruitment/__pycache__/urls.cpython-312.pyc and b/recruitment/__pycache__/urls.cpython-312.pyc differ diff --git a/recruitment/__pycache__/views.cpython-312.pyc b/recruitment/__pycache__/views.cpython-312.pyc index 2e15eb7..317da31 100644 Binary files a/recruitment/__pycache__/views.cpython-312.pyc and b/recruitment/__pycache__/views.cpython-312.pyc differ diff --git a/recruitment/__pycache__/views_frontend.cpython-312.pyc b/recruitment/__pycache__/views_frontend.cpython-312.pyc index cfe62c9..8f09ef6 100644 Binary files a/recruitment/__pycache__/views_frontend.cpython-312.pyc and b/recruitment/__pycache__/views_frontend.cpython-312.pyc differ diff --git a/recruitment/forms.py b/recruitment/forms.py index d9989d1..81e2e3f 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -10,7 +10,7 @@ import re from .models import ( ZoomMeeting, Candidate,TrainingMaterial,JobPosting, FormTemplate,InterviewSchedule,BreakTime,JobPostingImage, - Profile,MeetingComment,ScheduledInterview,Source + Profile,MeetingComment,ScheduledInterview,Source,Participants ) # from django_summernote.widgets import SummernoteWidget from django_ckeditor_5.widgets import CKEditor5Widget @@ -649,3 +649,52 @@ class CandidateExamDateForm(forms.ModelForm): +#participants form +class ParticipantsForm(forms.ModelForm): + """Form for creating and editing Participants""" + + class Meta: + model = Participants + fields = ['name', 'email', 'phone', 'designation'] + widgets = { + 'name': forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Enter participant name', + 'required': True + }), + 'email': forms.EmailInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Enter email address', + 'required': True + }), + 'phone': forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Enter phone number' + }), + 'designation': forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Enter designation' + }), + # 'jobs': forms.CheckboxSelectMultiple(), + } + + +class ParticipantsSelectForm(forms.ModelForm): + """Form for selecting Participants""" + + participants=forms.ModelMultipleChoiceField( + queryset=Participants.objects.all(), + widget=forms.CheckboxSelectMultiple, + required=False, + label=_("Select Participants")) + + users=forms.ModelMultipleChoiceField( + queryset=User.objects.all(), + widget=forms.CheckboxSelectMultiple, + required=False, + label=_("Select Users")) + + class Meta: + model = JobPosting + fields = ['participants','users'] # No direct fields from Participants model + \ No newline at end of file diff --git a/recruitment/migrations/0006_participants.py b/recruitment/migrations/0006_participants.py new file mode 100644 index 0000000..db585d3 --- /dev/null +++ b/recruitment/migrations/0006_participants.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.7 on 2025-10-28 12:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0005_alter_jobposting_linkedin_post_formated_data'), + ] + + operations = [ + migrations.CreateModel( + name='Participants', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Participant Name')), + ('email', models.EmailField(max_length=254, verbose_name='Email')), + ('phone', models.CharField(blank=True, max_length=20, verbose_name='Phone')), + ('designation', models.CharField(blank=True, max_length=100, verbose_name='Designation')), + ('job', models.ManyToManyField(blank=True, related_name='participants', to='recruitment.jobposting')), + ], + ), + ] diff --git a/recruitment/migrations/0007_participants_created_at_participants_slug_and_more.py b/recruitment/migrations/0007_participants_created_at_participants_slug_and_more.py new file mode 100644 index 0000000..fc89f39 --- /dev/null +++ b/recruitment/migrations/0007_participants_created_at_participants_slug_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 5.2.7 on 2025-10-28 12:14 + +import django_extensions.db.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0006_participants'), + ] + + operations = [ + migrations.AddField( + model_name='participants', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=None, verbose_name='Created at'), + preserve_default=False, + ), + migrations.AddField( + model_name='participants', + name='slug', + field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'), + ), + migrations.AddField( + model_name='participants', + name='updated_at', + field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), + ), + ] diff --git a/recruitment/migrations/0008_rename_job_participants_jobs.py b/recruitment/migrations/0008_rename_job_participants_jobs.py new file mode 100644 index 0000000..4bea33f --- /dev/null +++ b/recruitment/migrations/0008_rename_job_participants_jobs.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-10-28 13:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0007_participants_created_at_participants_slug_and_more'), + ] + + operations = [ + migrations.RenameField( + model_name='participants', + old_name='job', + new_name='jobs', + ), + ] diff --git a/recruitment/migrations/0009_jobposting_assigned_users.py b/recruitment/migrations/0009_jobposting_assigned_users.py new file mode 100644 index 0000000..1ec5f0b --- /dev/null +++ b/recruitment/migrations/0009_jobposting_assigned_users.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.7 on 2025-10-28 16:41 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0008_rename_job_participants_jobs'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='jobposting', + name='assigned_users', + field=models.ManyToManyField(blank=True, related_name='assigned_jobs', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/recruitment/migrations/0010_remove_jobposting_assigned_users.py b/recruitment/migrations/0010_remove_jobposting_assigned_users.py new file mode 100644 index 0000000..3b7548d --- /dev/null +++ b/recruitment/migrations/0010_remove_jobposting_assigned_users.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.7 on 2025-10-28 17:12 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0009_jobposting_assigned_users'), + ] + + operations = [ + migrations.RemoveField( + model_name='jobposting', + name='assigned_users', + ), + ] diff --git a/recruitment/migrations/0011_jobposting_internal_participant.py b/recruitment/migrations/0011_jobposting_internal_participant.py new file mode 100644 index 0000000..e051508 --- /dev/null +++ b/recruitment/migrations/0011_jobposting_internal_participant.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.7 on 2025-10-28 20:42 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0010_remove_jobposting_assigned_users'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='jobposting', + name='internal_participant', + field=models.ManyToManyField(blank=True, help_text='Internal staff involved in the recruitment process for this job', related_name='internal_participant_jobs', to=settings.AUTH_USER_MODEL, verbose_name='Internal Participant'), + ), + ] diff --git a/recruitment/migrations/0012_remove_participants_jobs_and_more.py b/recruitment/migrations/0012_remove_participants_jobs_and_more.py new file mode 100644 index 0000000..799ee52 --- /dev/null +++ b/recruitment/migrations/0012_remove_participants_jobs_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 5.2.7 on 2025-10-28 21:30 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0011_jobposting_internal_participant'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RemoveField( + model_name='participants', + name='jobs', + ), + migrations.AddField( + model_name='jobposting', + name='external_participant', + field=models.ManyToManyField(blank=True, help_text='External participants involved in the recruitment process for this job', related_name='jobs', to='recruitment.participants', verbose_name='External Participant'), + ), + migrations.AlterField( + model_name='jobposting', + name='internal_participant', + field=models.ManyToManyField(blank=True, help_text='Internal staff involved in the recruitment process for this job', related_name='jobs', to=settings.AUTH_USER_MODEL, verbose_name='Internal Participant'), + ), + ] diff --git a/recruitment/migrations/0013_remove_jobposting_external_participant_and_more.py b/recruitment/migrations/0013_remove_jobposting_external_participant_and_more.py new file mode 100644 index 0000000..e830392 --- /dev/null +++ b/recruitment/migrations/0013_remove_jobposting_external_participant_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.7 on 2025-10-28 22:20 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0012_remove_participants_jobs_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RemoveField( + model_name='jobposting', + name='external_participant', + ), + migrations.RemoveField( + model_name='jobposting', + name='internal_participant', + ), + migrations.AddField( + model_name='jobposting', + name='participants', + field=models.ManyToManyField(blank=True, help_text='External participants involved in the recruitment process for this job', related_name='jobs_participating', to='recruitment.participants', verbose_name='External Participant'), + ), + migrations.AddField( + model_name='jobposting', + name='users', + field=models.ManyToManyField(blank=True, help_text='Internal staff involved in the recruitment process for this job', related_name='jobs_assigned', to=settings.AUTH_USER_MODEL, verbose_name='Internal Participant'), + ), + ] diff --git a/recruitment/models.py b/recruitment/models.py index f651e57..a258a42 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -36,6 +36,7 @@ class Profile(models.Model): class JobPosting(Base): # Basic Job Information + JOB_TYPES = [ ("FULL_TIME", "Full-time"), ("PART_TIME", "Part-time"), @@ -51,6 +52,19 @@ class JobPosting(Base): ("HYBRID", "Hybrid"), ] + users=models.ManyToManyField( + User, + blank=True,related_name="jobs_assigned", + verbose_name=_("Internal Participant"), + help_text=_("Internal staff involved in the recruitment process for this job"), + ) + + participants=models.ManyToManyField('Participants', + blank=True,related_name="jobs_participating", + verbose_name=_("External Participant"), + help_text=_("External participants involved in the recruitment process for this job"), + ) + # Core Fields title = models.CharField(max_length=200) department = models.CharField(max_length=100, blank=True) @@ -1264,6 +1278,8 @@ class ScheduledInterview(Base): related_name="scheduled_interviews", db_index=True ) + + job = models.ForeignKey( "JobPosting", on_delete=models.CASCADE, related_name="scheduled_interviews", db_index=True ) @@ -1298,3 +1314,19 @@ class ScheduledInterview(Base): models.Index(fields=['interview_date', 'interview_time']), models.Index(fields=['candidate', 'job']), ] + + + +class Participants(Base): + """Model to store Participants details""" + name = models.CharField(max_length=255, verbose_name=_("Participant Name")) + email= models.EmailField(verbose_name=_("Email")) + phone = models.CharField(max_length=20, blank=True, verbose_name=_("Phone")) + designation = models.CharField( + max_length=100, blank=True, verbose_name=_("Designation") + ) + + def __str__(self): + return f"{self.name} - {self.email}" + + \ No newline at end of file diff --git a/recruitment/urls.py b/recruitment/urls.py index 1c63b71..805da73 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -144,4 +144,12 @@ urlpatterns = [ path('meetings//comments//delete/', views.delete_meeting_comment, name='delete_meeting_comment'), path('meetings//set_meeting_candidate/', views.set_meeting_candidate, name='set_meeting_candidate'), + + + #participants urls + path('participants/', views_frontend.ParticipantsListView.as_view(), name='participants_list'), + path('participants/create/', views_frontend.ParticipantsCreateView.as_view(), name='participants_create'), + path('participants//', views_frontend.ParticipantsDetailView.as_view(), name='participants_detail'), + path('participants//update/', views_frontend.ParticipantsUpdateView.as_view(), name='participants_update'), + path('participants//delete/', views_frontend.ParticipantsDeleteView.as_view(), name='participants_delete'), ] diff --git a/recruitment/views.py b/recruitment/views.py index c2d9331..029d172 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -33,7 +33,8 @@ from .forms import ( StaffUserCreationForm, MeetingCommentForm, ToggleAccountForm, - LinkedPostContentForm + LinkedPostContentForm, + ParticipantsSelectForm ) from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent @@ -1421,7 +1422,42 @@ def candidate_update_status(request, slug): @login_required def candidate_interview_view(request,slug): job = get_object_or_404(JobPosting,slug=slug) - context = {"job":job,"candidates":job.interview_candidates,'current_stage':'Interview'} + + if request.method == "POST": + form = ParticipantsSelectForm(request.POST, instance=job) + print(form.errors) + + if form.is_valid(): + + # Save the main instance (JobPosting) + job_instance = form.save(commit=False) + job_instance.save() + + # MANUALLY set the M2M relationships based on submitted data + job_instance.participants.set(form.cleaned_data['participants']) + job_instance.users.set(form.cleaned_data['users']) + + messages.success(request, "Interview participants updated successfully.") + return redirect("candidate_interview_view", slug=job.slug) + + else: + # 🛑 FIX: Explicitly pass the initial data for M2M fields + initial_data = { + 'participants': job.participants.all(), + 'users': job.users.all(), + } + form = ParticipantsSelectForm(instance=job, initial=initial_data) + + else: + form = ParticipantsSelectForm(instance=job) + + + context = { + "job":job, + "candidates":job.interview_candidates, + 'current_stage':'Interview', + 'form':form + } return render(request,"recruitment/candidate_interview_view.html",context) @login_required diff --git a/recruitment/views_frontend.py b/recruitment/views_frontend.py index 4e97d0e..a7b0b8c 100644 --- a/recruitment/views_frontend.py +++ b/recruitment/views_frontend.py @@ -522,3 +522,71 @@ def update_candidate_status(request, job_slug, candidate_slug, stage_type, statu # Removed incorrect JobDetailView class. # The job_detail view is handled by function-based view in recruitment.views + + +#participants views +class ParticipantsListView(LoginRequiredMixin, ListView): + model = models.Participants + template_name = 'participants/participants_list.html' + context_object_name = 'participants' + paginate_by = 10 + + def get_queryset(self): + queryset = super().get_queryset() + + # Handle search + search_query = self.request.GET.get('search', '') + if search_query: + queryset = queryset.filter( + Q(name__icontains=search_query) | + Q(email__icontains=search_query) | + Q(phone__icontains=search_query) | + Q(designation__icontains=search_query) + ) + + # Filter for non-staff users + if not self.request.user.is_staff: + return models.Participants.objects.none() # Restrict for non-staff + + return queryset.order_by('-created_at') + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['search_query'] = self.request.GET.get('search', '') + return context +class ParticipantsDetailView(LoginRequiredMixin, DetailView): + model = models.Participants + template_name = 'participants/participants_detail.html' + context_object_name = 'participant' + slug_url_kwarg = 'slug' + +class ParticipantsCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): + model = models.Participants + form_class = forms.ParticipantsForm + template_name = 'participants/participants_create.html' + success_url = reverse_lazy('job_list') + success_message = 'Participant created successfully.' + + # def get_initial(self): + # initial = super().get_initial() + # if 'slug' in self.kwargs: + # job = get_object_or_404(models.JobPosting, slug=self.kwargs['slug']) + # initial['jobs'] = [job] + # return initial + + + +class ParticipantsUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): + model = models.Participants + form_class = forms.ParticipantsForm + template_name = 'participants/participants_create.html' + success_url = reverse_lazy('job_list') + success_message = 'Participant updated successfully.' + slug_url_kwarg = 'slug' + +class ParticipantsDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView): + model = models.Participants + + success_url = reverse_lazy('participants_list') # Redirect to the participants list after success + success_message = 'Participant deleted successfully.' + slug_url_kwarg = 'slug' \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 333d46d..41da997 100644 --- a/templates/base.html +++ b/templates/base.html @@ -223,16 +223,18 @@ - {% comment %} {% endcomment %} + {% comment %}