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__/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 5f50140..c1360f1 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__/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..f6c5db4 100644 Binary files a/recruitment/__pycache__/views.cpython-312.pyc and b/recruitment/__pycache__/views.cpython-312.pyc differ 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/models.py b/recruitment/models.py index 4d545ec..f651e57 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -129,6 +129,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 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..7edffd7 100644 --- a/recruitment/tasks.py +++ b/recruitment/tasks.py @@ -116,25 +116,55 @@ 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} + --- + + **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 `