diff --git a/NorahUniversity/__pycache__/settings.cpython-312.pyc b/NorahUniversity/__pycache__/settings.cpython-312.pyc index a882953..ef70d24 100644 Binary files a/NorahUniversity/__pycache__/settings.cpython-312.pyc and b/NorahUniversity/__pycache__/settings.cpython-312.pyc differ diff --git a/NorahUniversity/__pycache__/urls.cpython-312.pyc b/NorahUniversity/__pycache__/urls.cpython-312.pyc index 3a455bc..0e668c6 100644 Binary files a/NorahUniversity/__pycache__/urls.cpython-312.pyc and b/NorahUniversity/__pycache__/urls.cpython-312.pyc differ diff --git a/NorahUniversity/settings.py b/NorahUniversity/settings.py index 2333ee5..3eb57de 100644 --- a/NorahUniversity/settings.py +++ b/NorahUniversity/settings.py @@ -135,9 +135,9 @@ WSGI_APPLICATION = 'NorahUniversity.wsgi.application' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'norahuniversity', - 'USER': 'norahuniversity', - 'PASSWORD': 'norahuniversity', + 'NAME': 'haikal_db', + 'USER': 'faheed', + 'PASSWORD': 'Faheed@215', 'HOST': '127.0.0.1', 'PORT': '5432', } diff --git a/recruitment/__pycache__/admin.cpython-312.pyc b/recruitment/__pycache__/admin.cpython-312.pyc index 9901ab4..86a871d 100644 Binary files a/recruitment/__pycache__/admin.cpython-312.pyc and b/recruitment/__pycache__/admin.cpython-312.pyc differ diff --git a/recruitment/__pycache__/forms.cpython-312.pyc b/recruitment/__pycache__/forms.cpython-312.pyc index cfa60ce..a61fc29 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..5f13015 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..098b384 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..44189be 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..05d0cff 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..1b785c1 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..a801254 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 109e836..590a0ea 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -11,7 +11,7 @@ from .models import ( ZoomMeeting, Candidate,TrainingMaterial,JobPosting, FormTemplate,InterviewSchedule,BreakTime,JobPostingImage, Profile,MeetingComment,ScheduledInterview,Source,HiringAgency, - AgencyJobAssignment, AgencyAccessLink + AgencyJobAssignment, AgencyAccessLink,Participants ) # from django_summernote.widgets import SummernoteWidget from django_ckeditor_5.widgets import CKEditor5Widget @@ -638,7 +638,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 @@ -1141,3 +1145,57 @@ class AgencyLoginForm(forms.Form): raise ValidationError('Invalid access token.') return cleaned_data + + + + + +#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/0001_initial.py b/recruitment/migrations/0001_initial.py index 4839927..1e9fde3 100644 --- a/recruitment/migrations/0001_initial.py +++ b/recruitment/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.4 on 2025-10-25 14:57 +# Generated by Django 5.2.7 on 2025-10-29 18:04 import django.core.validators import django.db.models.deletion @@ -66,6 +66,22 @@ class Migration(migrations.Migration): 'ordering': ['name'], }, ), + migrations.CreateModel( + name='Participants', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Participant Name')), + ('email', models.EmailField(max_length=254, verbose_name='Email')), + ('phone', models.CharField(blank=True, max_length=12, null=True, verbose_name='Phone Number')), + ('designation', models.CharField(blank=True, max_length=100, null=True, verbose_name='Designation')), + ], + options={ + 'abstract': False, + }, + ), migrations.CreateModel( name='Source', fields=[ @@ -84,6 +100,11 @@ class Migration(migrations.Migration): ('integration_version', models.CharField(blank=True, help_text='Version of the integration protocol', max_length=50, verbose_name='Integration Version')), ('last_sync_at', models.DateTimeField(blank=True, help_text='Timestamp of the last successful synchronization', null=True, verbose_name='Last Sync At')), ('sync_status', models.CharField(blank=True, choices=[('IDLE', 'Idle'), ('SYNCING', 'Syncing'), ('ERROR', 'Error'), ('DISABLED', 'Disabled')], default='IDLE', max_length=20, verbose_name='Sync Status')), + ('sync_endpoint', models.URLField(blank=True, help_text='Endpoint URL for sending candidate data (for outbound sync)', null=True, verbose_name='Sync Endpoint')), + ('sync_method', models.CharField(blank=True, choices=[('POST', 'POST'), ('PUT', 'PUT')], default='POST', help_text='HTTP method for outbound sync requests', max_length=10, verbose_name='Sync Method')), + ('test_method', models.CharField(blank=True, choices=[('GET', 'GET'), ('POST', 'POST')], default='GET', help_text='HTTP method for connection testing', max_length=10, verbose_name='Test Method')), + ('custom_headers', models.TextField(blank=True, help_text='JSON object with custom HTTP headers for sync requests', null=True, verbose_name='Custom Headers')), + ('supports_outbound_sync', models.BooleanField(default=False, help_text='Whether this source supports receiving candidate data from ATS', verbose_name='Supports Outbound Sync')), ], options={ 'verbose_name': 'Source', @@ -201,7 +222,7 @@ class Migration(migrations.Migration): ('is_potential_candidate', models.BooleanField(default=False, verbose_name='Potential Candidate')), ('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')), ('applied', models.BooleanField(default=False, verbose_name='Applied')), - ('stage', models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer')], db_index=True, default='Applied', max_length=100, verbose_name='Stage')), + ('stage', models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer'), ('Hired', 'Hired')], db_index=True, default='Applied', max_length=100, verbose_name='Stage')), ('applicant_status', models.CharField(blank=True, choices=[('Applicant', 'Applicant'), ('Candidate', 'Candidate')], default='Applicant', max_length=100, null=True, verbose_name='Applicant Status')), ('exam_date', models.DateTimeField(blank=True, null=True, verbose_name='Exam Date')), ('exam_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=100, null=True, verbose_name='Exam Status')), @@ -209,9 +230,12 @@ class Migration(migrations.Migration): ('interview_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=100, null=True, verbose_name='Interview Status')), ('offer_date', models.DateField(blank=True, null=True, verbose_name='Offer Date')), ('offer_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected')], max_length=100, null=True, verbose_name='Offer Status')), + ('hired_date', models.DateField(blank=True, null=True, verbose_name='Hired Date')), ('join_date', models.DateField(blank=True, null=True, verbose_name='Join Date')), ('ai_analysis_data', models.JSONField(default=dict, help_text='Full JSON output from the resume scoring model.', verbose_name='AI Analysis Data')), - ('submitted_by_agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='submitted_candidates', to='recruitment.hiringagency', verbose_name='Submitted by Agency')), + ('retry', models.SmallIntegerField(default=3, verbose_name='Resume Parsing Retry')), + ('hiring_source', models.CharField(blank=True, choices=[('Public', 'Public'), ('Internal', 'Internal'), ('Agency', 'Agency')], default='Public', max_length=255, null=True, verbose_name='Hiring Source')), + ('hiring_agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='candidates', to='recruitment.hiringagency', verbose_name='Hiring Agency')), ], options={ 'verbose_name': 'Candidate', @@ -221,6 +245,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='JobPosting', fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), @@ -238,7 +263,7 @@ class Migration(migrations.Migration): ('application_url', models.URLField(blank=True, help_text='URL where candidates apply', null=True, validators=[django.core.validators.URLValidator()])), ('application_deadline', models.DateField(db_index=True)), ('application_instructions', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)), - ('internal_job_id', models.CharField(editable=False, max_length=50, primary_key=True, serialize=False)), + ('internal_job_id', models.CharField(editable=False, max_length=50)), ('created_by', models.CharField(blank=True, help_text='Name of person who created this job', max_length=100)), ('status', models.CharField(choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], db_index=True, default='DRAFT', max_length=20)), ('hash_tags', models.CharField(blank=True, help_text='Comma-separated hashtags for linkedin post like #hiring,#jobopening', max_length=200, validators=[recruitment.validators.validate_hash_tags])), @@ -247,6 +272,7 @@ class Migration(migrations.Migration): ('posted_to_linkedin', models.BooleanField(default=False)), ('linkedin_post_status', models.CharField(blank=True, help_text='Status of LinkedIn posting', max_length=50)), ('linkedin_posted_at', models.DateTimeField(blank=True, null=True)), + ('linkedin_post_formated_data', models.TextField(blank=True, null=True)), ('published_at', models.DateTimeField(blank=True, db_index=True, null=True)), ('position_number', models.CharField(blank=True, help_text='University position number', max_length=50)), ('reporting_to', models.CharField(blank=True, help_text='Who this position reports to', max_length=100)), @@ -256,6 +282,8 @@ class Migration(migrations.Migration): ('cancelled_by', models.CharField(blank=True, help_text='Name of person who cancelled this job', max_length=100, verbose_name='Cancelled By')), ('cancelled_at', models.DateTimeField(blank=True, null=True)), ('hiring_agency', models.ManyToManyField(blank=True, help_text='External agency responsible for sourcing candidates for this role', related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency')), + ('users', 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')), + ('participants', 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')), ('source', models.ForeignKey(blank=True, help_text='The system or channel from which this job posting originated or was first published.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='job_postings', to='recruitment.source')), ], options={ @@ -295,6 +323,31 @@ class Migration(migrations.Migration): name='job', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.jobposting', verbose_name='Job'), ), + migrations.CreateModel( + name='AgencyJobAssignment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('max_candidates', models.PositiveIntegerField(help_text='Maximum candidates agency can submit for this job', verbose_name='Maximum Candidates')), + ('candidates_submitted', models.PositiveIntegerField(default=0, help_text='Number of candidates submitted so far', verbose_name='Candidates Submitted')), + ('assigned_date', models.DateTimeField(auto_now_add=True, verbose_name='Assigned Date')), + ('deadline_date', models.DateTimeField(help_text='Deadline for agency to submit candidates', verbose_name='Deadline Date')), + ('is_active', models.BooleanField(default=True, verbose_name='Is Active')), + ('status', models.CharField(choices=[('ACTIVE', 'Active'), ('COMPLETED', 'Completed'), ('EXPIRED', 'Expired'), ('CANCELLED', 'Cancelled')], default='ACTIVE', max_length=20, verbose_name='Status')), + ('deadline_extended', models.BooleanField(default=False, verbose_name='Deadline Extended')), + ('original_deadline', models.DateTimeField(blank=True, help_text='Original deadline before extensions', null=True, verbose_name='Original Deadline')), + ('admin_notes', models.TextField(blank=True, help_text='Internal notes about this assignment', verbose_name='Admin Notes')), + ('agency', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='job_assignments', to='recruitment.hiringagency', verbose_name='Agency')), + ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agency_assignments', to='recruitment.jobposting', verbose_name='Job')), + ], + options={ + 'verbose_name': 'Agency Job Assignment', + 'verbose_name_plural': 'Agency Job Assignments', + 'ordering': ['-created_at'], + }, + ), migrations.CreateModel( name='JobPostingImage', fields=[ @@ -308,6 +361,8 @@ class Migration(migrations.Migration): fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size])), + ('designation', models.CharField(blank=True, max_length=100, null=True)), + ('phone', models.CharField(blank=True, max_length=12, null=True, verbose_name='Phone Number')), ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), ], ), @@ -336,7 +391,7 @@ class Migration(migrations.Migration): ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), ('action', models.CharField(choices=[('REQUEST', 'Request'), ('RESPONSE', 'Response'), ('ERROR', 'Error'), ('SYNC', 'Sync'), ('CREATE_JOB', 'Create Job'), ('UPDATE_JOB', 'Update Job')], max_length=20, verbose_name='Action')), ('endpoint', models.CharField(blank=True, max_length=255, verbose_name='Endpoint')), - ('method', models.CharField(blank=True, max_length=10, verbose_name='HTTP Method')), + ('method', models.CharField(blank=True, max_length=50, verbose_name='HTTP Method')), ('request_data', models.JSONField(blank=True, null=True, verbose_name='Request Data')), ('response_data', models.JSONField(blank=True, null=True, verbose_name='Response Data')), ('status_code', models.CharField(blank=True, max_length=10, verbose_name='Status Code')), @@ -424,6 +479,28 @@ class Migration(migrations.Migration): 'ordering': ['-created_at'], }, ), + migrations.CreateModel( + name='AgencyAccessLink', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('unique_token', models.CharField(editable=False, max_length=64, unique=True, verbose_name='Unique Token')), + ('access_password', models.CharField(help_text='Password for agency access', max_length=32, verbose_name='Access Password')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('expires_at', models.DateTimeField(help_text='When this access link expires', verbose_name='Expires At')), + ('last_accessed', models.DateTimeField(blank=True, null=True, verbose_name='Last Accessed')), + ('access_count', models.PositiveIntegerField(default=0, verbose_name='Access Count')), + ('is_active', models.BooleanField(default=True, verbose_name='Is Active')), + ('assignment', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='access_link', to='recruitment.agencyjobassignment', verbose_name='Assignment')), + ], + options={ + 'verbose_name': 'Agency Access Link', + 'verbose_name_plural': 'Agency Access Links', + 'ordering': ['-created_at'], + 'indexes': [models.Index(fields=['unique_token'], name='recruitment_unique__f91e76_idx'), models.Index(fields=['expires_at'], name='recruitment_expires_954ed9_idx'), models.Index(fields=['is_active'], name='recruitment_is_acti_4b0804_idx')], + }, + ), migrations.CreateModel( name='FieldResponse', fields=[ @@ -474,6 +551,26 @@ class Migration(migrations.Migration): model_name='candidate', index=models.Index(fields=['created_at'], name='recruitment_created_73590f_idx'), ), + migrations.AddIndex( + model_name='agencyjobassignment', + index=models.Index(fields=['agency', 'status'], name='recruitment_agency__491a54_idx'), + ), + migrations.AddIndex( + model_name='agencyjobassignment', + index=models.Index(fields=['job', 'status'], name='recruitment_job_id_d798a8_idx'), + ), + migrations.AddIndex( + model_name='agencyjobassignment', + index=models.Index(fields=['deadline_date'], name='recruitment_deadlin_57d3b4_idx'), + ), + migrations.AddIndex( + model_name='agencyjobassignment', + index=models.Index(fields=['is_active'], name='recruitment_is_acti_93b919_idx'), + ), + migrations.AlterUniqueTogether( + name='agencyjobassignment', + unique_together={('agency', 'job')}, + ), migrations.AddIndex( model_name='jobposting', index=models.Index(fields=['status', 'created_at', 'title'], name='recruitment_status_8b77aa_idx'), diff --git a/recruitment/migrations/0002_candidate_retry.py b/recruitment/migrations/0002_candidate_retry.py deleted file mode 100644 index 0f405fc..0000000 --- a/recruitment/migrations/0002_candidate_retry.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-26 11:53 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='candidate', - name='retry', - field=models.SmallIntegerField(default=3, verbose_name='Resume Parsing Retry'), - ), - ] diff --git a/recruitment/migrations/0003_candidate_hired_date_source_custom_headers_and_more.py b/recruitment/migrations/0003_candidate_hired_date_source_custom_headers_and_more.py deleted file mode 100644 index 7c999ad..0000000 --- a/recruitment/migrations/0003_candidate_hired_date_source_custom_headers_and_more.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 5.2.4 on 2025-10-26 13:27 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0002_candidate_retry'), - ] - - operations = [ - migrations.AddField( - model_name='candidate', - name='hired_date', - field=models.DateField(blank=True, null=True, verbose_name='Hired Date'), - ), - migrations.AddField( - model_name='source', - name='custom_headers', - field=models.TextField(blank=True, help_text='JSON object with custom HTTP headers for sync requests', null=True, verbose_name='Custom Headers'), - ), - migrations.AddField( - model_name='source', - name='supports_outbound_sync', - field=models.BooleanField(default=False, help_text='Whether this source supports receiving candidate data from ATS', verbose_name='Supports Outbound Sync'), - ), - migrations.AddField( - model_name='source', - name='sync_endpoint', - field=models.URLField(blank=True, help_text='Endpoint URL for sending candidate data (for outbound sync)', null=True, verbose_name='Sync Endpoint'), - ), - migrations.AddField( - model_name='source', - name='sync_method', - field=models.CharField(blank=True, choices=[('POST', 'POST'), ('PUT', 'PUT')], default='POST', help_text='HTTP method for outbound sync requests', max_length=10, verbose_name='Sync Method'), - ), - migrations.AddField( - model_name='source', - name='test_method', - field=models.CharField(blank=True, choices=[('GET', 'GET'), ('POST', 'POST')], default='GET', help_text='HTTP method for connection testing', max_length=10, verbose_name='Test Method'), - ), - migrations.AlterField( - model_name='candidate', - name='stage', - field=models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer'), ('Hired', 'Hired')], db_index=True, default='Applied', max_length=100, verbose_name='Stage'), - ), - ] diff --git a/recruitment/migrations/0004_alter_integrationlog_method.py b/recruitment/migrations/0004_alter_integrationlog_method.py deleted file mode 100644 index e4ab1d0..0000000 --- a/recruitment/migrations/0004_alter_integrationlog_method.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.4 on 2025-10-26 13:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0003_candidate_hired_date_source_custom_headers_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='integrationlog', - name='method', - field=models.CharField(blank=True, max_length=50, verbose_name='HTTP Method'), - ), - ] diff --git a/recruitment/migrations/0005_rename_submitted_by_agency_candidate_hiring_agency.py b/recruitment/migrations/0005_rename_submitted_by_agency_candidate_hiring_agency.py deleted file mode 100644 index 48ea4b0..0000000 --- a/recruitment/migrations/0005_rename_submitted_by_agency_candidate_hiring_agency.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.4 on 2025-10-26 14:37 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0004_alter_integrationlog_method'), - ] - - operations = [ - migrations.RenameField( - model_name='candidate', - old_name='submitted_by_agency', - new_name='hiring_agency', - ), - ] diff --git a/recruitment/migrations/0006_agencyjobassignment_agencyaccesslink_agencymessage_and_more.py b/recruitment/migrations/0006_agencyjobassignment_agencyaccesslink_agencymessage_and_more.py deleted file mode 100644 index 8c1c20c..0000000 --- a/recruitment/migrations/0006_agencyjobassignment_agencyaccesslink_agencymessage_and_more.py +++ /dev/null @@ -1,129 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-26 14:51 - -import django.db.models.deletion -import django_extensions.db.fields -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0005_rename_submitted_by_agency_candidate_hiring_agency'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='AgencyJobAssignment', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), - ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), - ('max_candidates', models.PositiveIntegerField(help_text='Maximum candidates agency can submit for this job', verbose_name='Maximum Candidates')), - ('candidates_submitted', models.PositiveIntegerField(default=0, help_text='Number of candidates submitted so far', verbose_name='Candidates Submitted')), - ('assigned_date', models.DateTimeField(auto_now_add=True, verbose_name='Assigned Date')), - ('deadline_date', models.DateTimeField(help_text='Deadline for agency to submit candidates', verbose_name='Deadline Date')), - ('is_active', models.BooleanField(default=True, verbose_name='Is Active')), - ('status', models.CharField(choices=[('ACTIVE', 'Active'), ('COMPLETED', 'Completed'), ('EXPIRED', 'Expired'), ('CANCELLED', 'Cancelled')], default='ACTIVE', max_length=20, verbose_name='Status')), - ('deadline_extended', models.BooleanField(default=False, verbose_name='Deadline Extended')), - ('original_deadline', models.DateTimeField(blank=True, help_text='Original deadline before extensions', null=True, verbose_name='Original Deadline')), - ('admin_notes', models.TextField(blank=True, help_text='Internal notes about this assignment', verbose_name='Admin Notes')), - ('agency', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='job_assignments', to='recruitment.hiringagency', verbose_name='Agency')), - ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agency_assignments', to='recruitment.jobposting', verbose_name='Job')), - ], - options={ - 'verbose_name': 'Agency Job Assignment', - 'verbose_name_plural': 'Agency Job Assignments', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='AgencyAccessLink', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), - ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), - ('unique_token', models.CharField(editable=False, max_length=64, unique=True, verbose_name='Unique Token')), - ('access_password', models.CharField(help_text='Password for agency access', max_length=32, verbose_name='Access Password')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('expires_at', models.DateTimeField(help_text='When this access link expires', verbose_name='Expires At')), - ('last_accessed', models.DateTimeField(blank=True, null=True, verbose_name='Last Accessed')), - ('access_count', models.PositiveIntegerField(default=0, verbose_name='Access Count')), - ('is_active', models.BooleanField(default=True, verbose_name='Is Active')), - ('assignment', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='access_link', to='recruitment.agencyjobassignment', verbose_name='Assignment')), - ], - options={ - 'verbose_name': 'Agency Access Link', - 'verbose_name_plural': 'Agency Access Links', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='AgencyMessage', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), - ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), - ('subject', models.CharField(max_length=200, verbose_name='Subject')), - ('message', models.TextField(verbose_name='Message')), - ('message_type', models.CharField(choices=[('INFO', 'Information'), ('WARNING', 'Warning'), ('EXTENSION', 'Deadline Extension'), ('GENERAL', 'General')], default='GENERAL', max_length=20, verbose_name='Message Type')), - ('is_read', models.BooleanField(default=False, verbose_name='Is Read')), - ('read_at', models.DateTimeField(blank=True, null=True, verbose_name='Read At')), - ('assignment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='recruitment.agencyjobassignment', verbose_name='Assignment')), - ('recipient_agency', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to='recruitment.hiringagency', verbose_name='Recipient Agency')), - ('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_agency_messages', to=settings.AUTH_USER_MODEL, verbose_name='Sender')), - ], - options={ - 'verbose_name': 'Agency Message', - 'verbose_name_plural': 'Agency Messages', - 'ordering': ['-created_at'], - }, - ), - migrations.AddIndex( - model_name='agencyjobassignment', - index=models.Index(fields=['agency', 'status'], name='recruitment_agency__491a54_idx'), - ), - migrations.AddIndex( - model_name='agencyjobassignment', - index=models.Index(fields=['job', 'status'], name='recruitment_job_id_d798a8_idx'), - ), - migrations.AddIndex( - model_name='agencyjobassignment', - index=models.Index(fields=['deadline_date'], name='recruitment_deadlin_57d3b4_idx'), - ), - migrations.AddIndex( - model_name='agencyjobassignment', - index=models.Index(fields=['is_active'], name='recruitment_is_acti_93b919_idx'), - ), - migrations.AlterUniqueTogether( - name='agencyjobassignment', - unique_together={('agency', 'job')}, - ), - migrations.AddIndex( - model_name='agencyaccesslink', - index=models.Index(fields=['unique_token'], name='recruitment_unique__f91e76_idx'), - ), - migrations.AddIndex( - model_name='agencyaccesslink', - index=models.Index(fields=['expires_at'], name='recruitment_expires_954ed9_idx'), - ), - migrations.AddIndex( - model_name='agencyaccesslink', - index=models.Index(fields=['is_active'], name='recruitment_is_acti_4b0804_idx'), - ), - migrations.AddIndex( - model_name='agencymessage', - index=models.Index(fields=['assignment', 'is_read'], name='recruitment_assignm_4f518d_idx'), - ), - migrations.AddIndex( - model_name='agencymessage', - index=models.Index(fields=['recipient_agency', 'is_read'], name='recruitment_recipie_427b10_idx'), - ), - migrations.AddIndex( - model_name='agencymessage', - index=models.Index(fields=['sender'], name='recruitment_sender__97dd96_idx'), - ), - ] diff --git a/recruitment/migrations/0007_candidate_source_candidate_source_type.py b/recruitment/migrations/0007_candidate_source_candidate_source_type.py deleted file mode 100644 index 5f83b42..0000000 --- a/recruitment/migrations/0007_candidate_source_candidate_source_type.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-27 11:42 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0006_agencyjobassignment_agencyaccesslink_agencymessage_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='candidate', - name='source', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='candidates', to='recruitment.source', verbose_name='Source'), - ), - migrations.AddField( - model_name='candidate', - name='source_type', - field=models.CharField(blank=True, choices=[('Public', 'Public'), ('Internal', 'Internal'), ('Agency', 'Agency')], default='Public', max_length=255, null=True, verbose_name='Source'), - ), - ] diff --git a/recruitment/migrations/0008_remove_candidate_source_remove_candidate_source_type_and_more.py b/recruitment/migrations/0008_remove_candidate_source_remove_candidate_source_type_and_more.py deleted file mode 100644 index 591252c..0000000 --- a/recruitment/migrations/0008_remove_candidate_source_remove_candidate_source_type_and_more.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-27 11:44 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0007_candidate_source_candidate_source_type'), - ] - - operations = [ - migrations.RemoveField( - model_name='candidate', - name='source', - ), - migrations.RemoveField( - model_name='candidate', - name='source_type', - ), - migrations.AddField( - model_name='candidate', - name='hiring_source', - field=models.CharField(blank=True, choices=[('Public', 'Public'), ('Internal', 'Internal'), ('Agency', 'Agency')], default='Public', max_length=255, null=True, verbose_name='Hiring Source'), - ), - migrations.AlterField( - model_name='candidate', - name='hiring_agency', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='candidates', to='recruitment.hiringagency', verbose_name='Hiring Agency'), - ), - ] diff --git a/recruitment/migrations/0009_agencymessage_priority_agencymessage_recipient_user_and_more.py b/recruitment/migrations/0009_agencymessage_priority_agencymessage_recipient_user_and_more.py deleted file mode 100644 index 5b27532..0000000 --- a/recruitment/migrations/0009_agencymessage_priority_agencymessage_recipient_user_and_more.py +++ /dev/null @@ -1,59 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-27 20:26 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0008_remove_candidate_source_remove_candidate_source_type_and_more'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name='agencymessage', - name='priority', - field=models.CharField(choices=[('LOW', 'Low'), ('MEDIUM', 'Medium'), ('HIGH', 'High'), ('URGENT', 'Urgent')], default='MEDIUM', max_length=10, verbose_name='Priority'), - ), - migrations.AddField( - model_name='agencymessage', - name='recipient_user', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='received_agency_messages', to=settings.AUTH_USER_MODEL, verbose_name='Recipient User'), - ), - migrations.AddField( - model_name='agencymessage', - name='send_email', - field=models.BooleanField(default=False, verbose_name='Send Email Notification'), - ), - migrations.AddField( - model_name='agencymessage', - name='send_sms', - field=models.BooleanField(default=False, verbose_name='Send SMS Notification'), - ), - migrations.AddField( - model_name='agencymessage', - name='sender_agency', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to='recruitment.hiringagency', verbose_name='Sender Agency'), - ), - migrations.AddField( - model_name='agencymessage', - name='sender_type', - field=models.CharField(choices=[('ADMIN', 'Admin'), ('AGENCY', 'Agency')], default='ADMIN', max_length=10, verbose_name='Sender Type'), - ), - migrations.AlterField( - model_name='agencymessage', - name='sender', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sent_agency_messages', to=settings.AUTH_USER_MODEL, verbose_name='Sender'), - ), - migrations.AddIndex( - model_name='agencymessage', - index=models.Index(fields=['sender_type', 'created_at'], name='recruitment_sender__14b136_idx'), - ), - migrations.AddIndex( - model_name='agencymessage', - index=models.Index(fields=['priority', 'created_at'], name='recruitment_priorit_80d9f1_idx'), - ), - ] diff --git a/recruitment/migrations/0010_remove_agency_message_model.py b/recruitment/migrations/0010_remove_agency_message_model.py deleted file mode 100644 index f042dcf..0000000 --- a/recruitment/migrations/0010_remove_agency_message_model.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-29 10:59 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0009_agencymessage_priority_agencymessage_recipient_user_and_more'), - ] - - operations = [ - migrations.DeleteModel( - name='AgencyMessage', - ), - ] diff --git a/recruitment/models.py b/recruitment/models.py index a7674b5..5e80ec2 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -28,6 +28,8 @@ class Base(models.Model): class Profile(models.Model): profile_image = models.ImageField(null=True, blank=True, upload_to="profile_pic/",validators=[validate_image_size]) + designation = models.CharField(max_length=100, blank=True,null=True) + phone=models.CharField(blank=True,null=True,verbose_name=_("Phone Number"),max_length=12) user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") def __str__(self): @@ -36,6 +38,7 @@ class Profile(models.Model): class JobPosting(Base): # Basic Job Information + JOB_TYPES = [ ("FULL_TIME", "Full-time"), ("PART_TIME", "Part-time"), @@ -51,6 +54,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) @@ -92,7 +108,7 @@ class JobPosting(Base): ) # Internal Tracking - internal_job_id = models.CharField(max_length=50, primary_key=True, editable=False) + internal_job_id = models.CharField(max_length=50, editable=False) created_by = models.CharField( max_length=100, blank=True, help_text="Name of person who created this job" @@ -130,6 +146,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 @@ -328,23 +345,41 @@ 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 hired_candidates_count(self): + return self.all_candidates.filter(stage="Hired").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): @@ -643,6 +678,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): @@ -711,6 +752,41 @@ class ZoomMeeting(Base): def __str__(self): return self.topic + + @property + def get_job(self): + try: + job=self.interview.job.first() + return job + except: + return None + @property + def get_candidate(self): + try: + candidate=self.interview.candidate.first() + return candidate + except: + return None + + @property + def get_external_participants(self): + try: + interview=self.interview.first() + if interview: + return interview.job.participants.all() + return None + except: + return None + @property + def get_users_participants(self): + try: + interview=self.interview.first() + if interview: + return interview.job.users.all() + return None + except: + return None + class MeetingComment(Base): @@ -1553,7 +1629,8 @@ class InterviewSchedule(Base): models.Index(fields=['end_date']), models.Index(fields=['created_by']), ] - + + class ScheduledInterview(Base): """Stores individual scheduled interviews""" @@ -1564,6 +1641,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 ) @@ -1672,3 +1751,19 @@ class Notification(models.Model): self.last_error = error_message self.attempts += 1 self.save(update_fields=['status', 'last_error', 'attempts']) + + + +class Participants(Base): + """Model to store Participants details""" + name = models.CharField(max_length=255, verbose_name=_("Participant Name"),null=True,blank=True) + email= models.EmailField(verbose_name=_("Email")) + phone = models.CharField(max_length=12,verbose_name=_("Phone Number"),null=True,blank=True) + designation = models.CharField( + max_length=100, blank=True, verbose_name=_("Designation"),null=True + ) + + 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 7f73baf..0f08794 100644 --- a/recruitment/signals.py +++ b/recruitment/signals.py @@ -47,6 +47,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: diff --git a/recruitment/tasks.py b/recruitment/tasks.py index ac2e6ad..c7ec331 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 `