diff --git a/NorahUniversity/__pycache__/urls.cpython-312.pyc b/NorahUniversity/__pycache__/urls.cpython-312.pyc index 3a455bc..7ad3c1f 100644 Binary files a/NorahUniversity/__pycache__/urls.cpython-312.pyc and b/NorahUniversity/__pycache__/urls.cpython-312.pyc differ diff --git a/recruitment/__pycache__/forms.cpython-312.pyc b/recruitment/__pycache__/forms.cpython-312.pyc index cfa60ce..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__/linkedin_service.cpython-312.pyc b/recruitment/__pycache__/linkedin_service.cpython-312.pyc index 19d53fa..7875609 100644 Binary files a/recruitment/__pycache__/linkedin_service.cpython-312.pyc and b/recruitment/__pycache__/linkedin_service.cpython-312.pyc differ diff --git a/recruitment/__pycache__/models.cpython-312.pyc b/recruitment/__pycache__/models.cpython-312.pyc index 1dc5ea3..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__/signals.cpython-312.pyc b/recruitment/__pycache__/signals.cpython-312.pyc index 56eb7e6..03bb073 100644 Binary files a/recruitment/__pycache__/signals.cpython-312.pyc and b/recruitment/__pycache__/signals.cpython-312.pyc differ diff --git a/recruitment/__pycache__/urls.cpython-312.pyc b/recruitment/__pycache__/urls.cpython-312.pyc index fe68685..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__/utils.cpython-312.pyc b/recruitment/__pycache__/utils.cpython-312.pyc index 7a31e49..3c1ee40 100644 Binary files a/recruitment/__pycache__/utils.cpython-312.pyc and b/recruitment/__pycache__/utils.cpython-312.pyc differ diff --git a/recruitment/__pycache__/views.cpython-312.pyc b/recruitment/__pycache__/views.cpython-312.pyc index 5eb571d..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 85f9dab..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 6f4e28a..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 @@ -629,7 +629,11 @@ class JobPostingStatusForm(forms.ModelForm): widgets = { 'status': forms.Select(attrs={'class': 'form-select'}), } - +class LinkedPostContentForm(forms.ModelForm): + class Meta: + model = JobPosting + fields = ['linkedin_post_formated_data'] + class FormTemplateIsActiveForm(forms.ModelForm): class Meta: model = FormTemplate @@ -645,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/linkedin_service.py b/recruitment/linkedin_service.py index e275f2d..3f645a4 100644 --- a/recruitment/linkedin_service.py +++ b/recruitment/linkedin_service.py @@ -1,19 +1,18 @@ # jobs/linkedin_service.py import uuid -import re -from html import unescape -from urllib.parse import quote, urlencode + import requests import logging import time from django.conf import settings +from urllib.parse import quote, urlencode logger = logging.getLogger(__name__) # Define constants LINKEDIN_API_VERSION = '2.0.0' LINKEDIN_VERSION = '202409' -MAX_POST_CHARS = 3000 # LinkedIn's maximum character limit for shareCommentary + class LinkedInService: def __init__(self): @@ -162,113 +161,114 @@ class LinkedInService: # ---------------- POSTING UTILITIES ---------------- - def clean_html_for_social_post(self, html_content): - """Converts safe HTML to plain text with basic formatting.""" - if not html_content: - return "" + # def clean_html_for_social_post(self, html_content): + # """Converts safe HTML to plain text with basic formatting.""" + # if not html_content: + # return "" - text = html_content + # text = html_content - # 1. Convert Bolding tags to *Markdown* - text = re.sub(r'(.*?)', r'*\1*', text, flags=re.IGNORECASE) - text = re.sub(r'(.*?)', r'*\1*', text, flags=re.IGNORECASE) + # # 1. Convert Bolding tags to *Markdown* + # text = re.sub(r'(.*?)', r'*\1*', text, flags=re.IGNORECASE) + # text = re.sub(r'(.*?)', r'*\1*', text, flags=re.IGNORECASE) - # 2. Handle Lists: Convert
  • tags into a bullet point - text = re.sub(r'', '\n', text, flags=re.IGNORECASE) - text = re.sub(r']*>', 'β€’ ', text, flags=re.IGNORECASE) - text = re.sub(r'
  • ', '\n', text, flags=re.IGNORECASE) + # # 2. Handle Lists: Convert
  • tags into a bullet point + # text = re.sub(r'', '\n', text, flags=re.IGNORECASE) + # text = re.sub(r']*>', 'β€’ ', text, flags=re.IGNORECASE) + # text = re.sub(r'
  • ', '\n', text, flags=re.IGNORECASE) - # 3. Handle Paragraphs and Line Breaks - text = re.sub(r'

    ', '\n\n', text, flags=re.IGNORECASE) - text = re.sub(r'
    ', '\n', text, flags=re.IGNORECASE) + # # 3. Handle Paragraphs and Line Breaks + # text = re.sub(r'

    ', '\n\n', text, flags=re.IGNORECASE) + # text = re.sub(r'
    ', '\n', text, flags=re.IGNORECASE) - # 4. Strip all remaining, unsupported HTML tags - clean_text = re.sub(r'<[^>]+>', '', text) + # # 4. Strip all remaining, unsupported HTML tags + # clean_text = re.sub(r'<[^>]+>', '', text) - # 5. Unescape HTML entities - clean_text = unescape(clean_text) + # # 5. Unescape HTML entities + # clean_text = unescape(clean_text) - # 6. Clean up excessive whitespace/newlines - clean_text = re.sub(r'(\n\s*){3,}', '\n\n', clean_text).strip() + # # 6. Clean up excessive whitespace/newlines + # clean_text = re.sub(r'(\n\s*){3,}', '\n\n', clean_text).strip() - return clean_text + # return clean_text - def hashtags_list(self, hash_tags_str): - """Convert comma-separated hashtags string to list""" - if not hash_tags_str: - return ["#HigherEd", "#Hiring", "#UniversityJobs"] + # def hashtags_list(self, hash_tags_str): + # """Convert comma-separated hashtags string to list""" + # if not hash_tags_str: + # return ["#HigherEd", "#Hiring", "#UniversityJobs"] - tags = [tag.strip() for tag in hash_tags_str.split(',') if tag.strip()] - tags = [tag if tag.startswith('#') else f'#{tag}' for tag in tags] + # tags = [tag.strip() for tag in hash_tags_str.split(',') if tag.strip()] + # tags = [tag if tag.startswith('#') else f'#{tag}' for tag in tags] - if not tags: - return ["#HigherEd", "#Hiring", "#UniversityJobs"] + # if not tags: + # return ["#HigherEd", "#Hiring", "#UniversityJobs"] - return tags + # return tags - def _build_post_message(self, job_posting): - """ - Constructs the final text message. - Includes a unique suffix for duplicate content prevention (422 fix). - """ - message_parts = [ - f"πŸ”₯ *Job Alert!* We’re looking for a talented professional to join our team.", - f"πŸ‘‰ **{job_posting.title}** πŸ‘ˆ", - ] + # def _build_post_message(self, job_posting): + # """ + # Constructs the final text message. + # Includes a unique suffix for duplicate content prevention (422 fix). + # """ + # message_parts = [ + # f"πŸ”₯ *Job Alert!* We’re looking for a talented professional to join our team.", + # f"πŸ‘‰ **{job_posting.title}** πŸ‘ˆ", + # ] - if job_posting.department: - message_parts.append(f"*{job_posting.department}*") + # if job_posting.department: + # message_parts.append(f"*{job_posting.department}*") - message_parts.append("\n" + "=" * 25 + "\n") + # message_parts.append("\n" + "=" * 25 + "\n") - # KEY DETAILS SECTION - details_list = [] - if job_posting.job_type: - details_list.append(f"πŸ’Ό Type: {job_posting.get_job_type_display()}") - if job_posting.get_location_display() != 'Not specified': - details_list.append(f"πŸ“ Location: {job_posting.get_location_display()}") - if job_posting.workplace_type: - details_list.append(f"🏠 Workplace: {job_posting.get_workplace_type_display()}") - if job_posting.salary_range: - details_list.append(f"πŸ’° Salary: {job_posting.salary_range}") + # # KEY DETAILS SECTION + # details_list = [] + # if job_posting.job_type: + # details_list.append(f"πŸ’Ό Type: {job_posting.get_job_type_display()}") + # if job_posting.get_location_display() != 'Not specified': + # details_list.append(f"πŸ“ Location: {job_posting.get_location_display()}") + # if job_posting.workplace_type: + # details_list.append(f"🏠 Workplace: {job_posting.get_workplace_type_display()}") + # if job_posting.salary_range: + # details_list.append(f"πŸ’° Salary: {job_posting.salary_range}") - if details_list: - message_parts.append("*Key Information*:") - message_parts.extend(details_list) - message_parts.append("\n") + # if details_list: + # message_parts.append("*Key Information*:") + # message_parts.extend(details_list) + # message_parts.append("\n") - # DESCRIPTION SECTION - clean_description = self.clean_html_for_social_post(job_posting.description) - if clean_description: - message_parts.append(f"πŸ”Ž *About the Role:*\n{clean_description}") + # # DESCRIPTION SECTION + # clean_description = self.clean_html_for_social_post(job_posting.description) + # if clean_description: + # message_parts.append(f"πŸ”Ž *About the Role:*\n{clean_description}") + # clean_ - # CALL TO ACTION - if job_posting.application_url: - message_parts.append(f"\n\n---") - # CRITICAL: Include the URL explicitly in the text body. - # When media_category is NONE, LinkedIn often makes these URLs clickable. - message_parts.append(f"πŸ”— **APPLY NOW:** {job_posting.application_url}") + # # CALL TO ACTION + # if job_posting.application_url: + # message_parts.append(f"\n\n---") + # # CRITICAL: Include the URL explicitly in the text body. + # # When media_category is NONE, LinkedIn often makes these URLs clickable. + # message_parts.append(f"πŸ”— **APPLY NOW:** {job_posting.application_url}") - # HASHTAGS - hashtags = self.hashtags_list(job_posting.hash_tags) - if job_posting.department: - dept_hashtag = f"#{job_posting.department.replace(' ', '')}" - hashtags.insert(0, dept_hashtag) + # # HASHTAGS + # hashtags = self.hashtags_list(job_posting.hash_tags) + # if job_posting.department: + # dept_hashtag = f"#{job_posting.department.replace(' ', '')}" + # hashtags.insert(0, dept_hashtag) - message_parts.append("\n" + " ".join(hashtags)) + # message_parts.append("\n" + " ".join(hashtags)) - final_message = "\n".join(message_parts) + # final_message = "\n".join(message_parts) - # --- FIX: ADD UNIQUE SUFFIX AND HANDLE LENGTH (422 fix) --- - unique_suffix = f"\n\n| Ref: {int(time.time())}" + # # --- FIX: ADD UNIQUE SUFFIX AND HANDLE LENGTH (422 fix) --- + # unique_suffix = f"\n\n| Ref: {int(time.time())}" - available_length = MAX_POST_CHARS - len(unique_suffix) + # available_length = MAX_POST_CHARS - len(unique_suffix) - if len(final_message) > available_length: - logger.warning("Post message truncated due to character limit.") - final_message = final_message[:available_length - 3] + "..." + # if len(final_message) > available_length: + # logger.warning("Post message truncated due to character limit.") + # final_message = final_message[:available_length - 3] + "..." - return final_message + unique_suffix + # return final_message + unique_suffix # ---------------- MAIN POSTING METHODS ---------------- @@ -279,7 +279,9 @@ class LinkedInService: CRITICAL FIX: Avoids ARTICLE category if not using an image to prevent 402 errors. """ - message = self._build_post_message(job_posting) + message = job_posting.linkedin_post_formated_data + if len(message)>=3000: + message=message[:2900]+"...." # --- FIX FOR 402: Force NONE if no image is present. --- if media_category != "IMAGE": diff --git a/recruitment/migrations/0003_jobposting_linkedin_post_formated_data.py b/recruitment/migrations/0003_jobposting_linkedin_post_formated_data.py new file mode 100644 index 0000000..5e92015 --- /dev/null +++ b/recruitment/migrations/0003_jobposting_linkedin_post_formated_data.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-10-27 10:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0002_candidate_retry'), + ] + + operations = [ + migrations.AddField( + model_name='jobposting', + name='linkedin_post_formated_data', + field=models.JSONField(blank=True, null=True), + ), + ] diff --git a/recruitment/migrations/0004_alter_jobposting_linkedin_post_formated_data.py b/recruitment/migrations/0004_alter_jobposting_linkedin_post_formated_data.py new file mode 100644 index 0000000..d821d60 --- /dev/null +++ b/recruitment/migrations/0004_alter_jobposting_linkedin_post_formated_data.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-10-27 11:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0003_jobposting_linkedin_post_formated_data'), + ] + + operations = [ + migrations.AlterField( + model_name='jobposting', + name='linkedin_post_formated_data', + field=models.CharField(blank=True, null=True), + ), + ] diff --git a/recruitment/migrations/0005_alter_jobposting_linkedin_post_formated_data.py b/recruitment/migrations/0005_alter_jobposting_linkedin_post_formated_data.py new file mode 100644 index 0000000..6bd9476 --- /dev/null +++ b/recruitment/migrations/0005_alter_jobposting_linkedin_post_formated_data.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-10-27 11:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0004_alter_jobposting_linkedin_post_formated_data'), + ] + + operations = [ + migrations.AlterField( + model_name='jobposting', + name='linkedin_post_formated_data', + field=models.TextField(blank=True, null=True), + ), + ] 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 a0776f0..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) @@ -129,6 +143,7 @@ class JobPosting(Base): max_length=50, blank=True, help_text="Status of LinkedIn posting" ) linkedin_posted_at = models.DateTimeField(null=True, blank=True) + linkedin_post_formated_data=models.TextField(null=True,blank=True) published_at = models.DateTimeField(db_index=True, null=True, blank=True) # Added index # University Specific Fields @@ -321,23 +336,37 @@ class JobPosting(Base): def all_candidates_count(self): return self.candidates.annotate( sortable_score=Cast('ai_analysis_data__match_score', output_field=CharField())).order_by( - '-sortable_score').count() + '-sortable_score').count() or 0 @property def screening_candidates_count(self): - return self.all_candidates.filter(stage="Applied").count() + return self.all_candidates.filter(stage="Applied").count() or 0 @property def exam_candidates_count(self): - return self.all_candidates.filter(stage="Exam").count() + return self.all_candidates.filter(stage="Exam").count() or 0 @property def interview_candidates_count(self): - return self.all_candidates.filter(stage="Interview").count() + return self.all_candidates.filter(stage="Interview").count() or 0 @property def offer_candidates_count(self): - return self.all_candidates.filter(stage="Offer").count() + return self.all_candidates.filter(stage="Offer").count() or 0 + + @property + def vacancy_fill_rate(self): + total_positions = self.open_positions + + no_of_positions_filled = self.candidates.filter(stage__in=['HIRED']).count() + + if total_positions > 0: + vacancy_fill_rate = no_of_positions_filled / total_positions + else: + vacancy_fill_rate = 0.0 + + return vacancy_fill_rate + class JobPostingImage(models.Model): @@ -632,6 +661,12 @@ class Candidate(Base): ).exists() return future_meetings or today_future_meetings + + # @property + # def time_to_hire(self): + # time_to_hire=self.hired_date-self.created_at + # return time_to_hire + class TrainingMaterial(Base): @@ -1243,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 ) @@ -1277,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/signals.py b/recruitment/signals.py index 5f6b623..590aed2 100644 --- a/recruitment/signals.py +++ b/recruitment/signals.py @@ -45,6 +45,15 @@ def format_job(sender, instance, created, **kwargs): # If the instance is no longer active, delete the scheduled task existing_schedule.delete() +# @receiver(post_save, sender=JobPosting) +# def update_form_template_status(sender, instance, created, **kwargs): +# if not created: +# if instance.status == "Active": +# instance.form_template.is_active = True +# else: +# instance.form_template.is_active = False +# instance.save() + @receiver(post_save, sender=Candidate) def score_candidate_resume(sender, instance, created, **kwargs): if not instance.is_resume_parsed: @@ -351,4 +360,4 @@ def create_default_stages(sender, instance, created, **kwargs): # required=False, # order=3, # is_predefined=True - # ) \ No newline at end of file + # ) diff --git a/recruitment/tasks.py b/recruitment/tasks.py index d975b10..a6d47cb 100644 --- a/recruitment/tasks.py +++ b/recruitment/tasks.py @@ -116,25 +116,61 @@ def format_job_description(pk): job_posting = JobPosting.objects.get(pk=pk) print(job_posting) prompt = f""" - Can you please organize and format this unformatted job description and qualifications into clear, readable sections using headings and bullet points? - Format the Content: You need to convert the clear, formatted job description and qualifications into a 2 blocks of HTML code. - **JOB DESCRIPTION:** - {job_posting.description} + You are a dual-purpose AI assistant specializing in content formatting and social media copywriting for job announcements. - **QUALIFICATIONS:** - {job_posting.qualifications} + **JOB POSTING DATA (Raw Input):** + --- + **JOB DESCRIPTION:** + {job_posting.description} - **STRICT JSON OUTPUT INSTRUCTIONS:** - Output a single, valid JSON object with ONLY the following two top-level keys: + **QUALIFICATIONS:** + {job_posting.qualifications} - 'job_description': 'A HTML containing the formatted job description', - 'job_qualifications': 'A HTML containing the formatted job qualifications', + **BENEFITS:** + {job_posting.benefits} + **APPLICATION INSTRUCTIONS:** + {job_posting.application_instructions} - Do not include any other text except for the JSON output. + **APPLICATION DEADLINE:** + {job_posting.application_deadline} + + **HASHTAGS: for search and reach:** + {job_posting.hash_tags} + + **APPLICATION URL: for career page only if it is provided** + {job_posting.application_url} + --- + + **TASK 1: HTML Formatting (Two Blocks)** + 1. **Format the Job Description:** Organize and format the raw JOB DESCRIPTION and BENEFITS data into clear, readable sections using `

    ` headings and `