small fix for the form template marked active when the job is made active

This commit is contained in:
Faheed 2025-10-27 17:43:46 +03:00
parent e3435e3627
commit d1dda003d6
16 changed files with 220 additions and 103 deletions

View File

@ -1,19 +1,18 @@
# jobs/linkedin_service.py # jobs/linkedin_service.py
import uuid import uuid
import re
from html import unescape
from urllib.parse import quote, urlencode
import requests import requests
import logging import logging
import time import time
from django.conf import settings from django.conf import settings
from urllib.parse import quote, urlencode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Define constants # Define constants
LINKEDIN_API_VERSION = '2.0.0' LINKEDIN_API_VERSION = '2.0.0'
LINKEDIN_VERSION = '202409' LINKEDIN_VERSION = '202409'
MAX_POST_CHARS = 3000 # LinkedIn's maximum character limit for shareCommentary
class LinkedInService: class LinkedInService:
def __init__(self): def __init__(self):
@ -162,113 +161,114 @@ class LinkedInService:
# ---------------- POSTING UTILITIES ---------------- # ---------------- POSTING UTILITIES ----------------
def clean_html_for_social_post(self, html_content): # def clean_html_for_social_post(self, html_content):
"""Converts safe HTML to plain text with basic formatting.""" # """Converts safe HTML to plain text with basic formatting."""
if not html_content: # if not html_content:
return "" # return ""
text = html_content # text = html_content
# 1. Convert Bolding tags to *Markdown* # # 1. Convert Bolding tags to *Markdown*
text = re.sub(r'<strong>(.*?)</strong>', r'*\1*', text, flags=re.IGNORECASE) # text = re.sub(r'<strong>(.*?)</strong>', r'*\1*', text, flags=re.IGNORECASE)
text = re.sub(r'<b>(.*?)</b>', r'*\1*', text, flags=re.IGNORECASE) # text = re.sub(r'<b>(.*?)</b>', r'*\1*', text, flags=re.IGNORECASE)
# 2. Handle Lists: Convert <li> tags into a bullet point # # 2. Handle Lists: Convert <li> tags into a bullet point
text = re.sub(r'</(ul|ol|div)>', '\n', text, flags=re.IGNORECASE) # text = re.sub(r'</(ul|ol|div)>', '\n', text, flags=re.IGNORECASE)
text = re.sub(r'<li[^>]*>', '', text, flags=re.IGNORECASE) # text = re.sub(r'<li[^>]*>', '• ', text, flags=re.IGNORECASE)
text = re.sub(r'</li>', '\n', text, flags=re.IGNORECASE) # text = re.sub(r'</li>', '\n', text, flags=re.IGNORECASE)
# 3. Handle Paragraphs and Line Breaks # # 3. Handle Paragraphs and Line Breaks
text = re.sub(r'</p>', '\n\n', text, flags=re.IGNORECASE) # text = re.sub(r'</p>', '\n\n', text, flags=re.IGNORECASE)
text = re.sub(r'<br/?>', '\n', text, flags=re.IGNORECASE) # text = re.sub(r'<br/?>', '\n', text, flags=re.IGNORECASE)
# 4. Strip all remaining, unsupported HTML tags # # 4. Strip all remaining, unsupported HTML tags
clean_text = re.sub(r'<[^>]+>', '', text) # clean_text = re.sub(r'<[^>]+>', '', text)
# 5. Unescape HTML entities # # 5. Unescape HTML entities
clean_text = unescape(clean_text) # clean_text = unescape(clean_text)
# 6. Clean up excessive whitespace/newlines # # 6. Clean up excessive whitespace/newlines
clean_text = re.sub(r'(\n\s*){3,}', '\n\n', clean_text).strip() # 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): # def hashtags_list(self, hash_tags_str):
"""Convert comma-separated hashtags string to list""" # """Convert comma-separated hashtags string to list"""
if not hash_tags_str: # if not hash_tags_str:
return ["#HigherEd", "#Hiring", "#UniversityJobs"] # return ["#HigherEd", "#Hiring", "#UniversityJobs"]
tags = [tag.strip() for tag in hash_tags_str.split(',') if tag.strip()] # 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 if tag.startswith('#') else f'#{tag}' for tag in tags]
if not tags: # if not tags:
return ["#HigherEd", "#Hiring", "#UniversityJobs"] # return ["#HigherEd", "#Hiring", "#UniversityJobs"]
return tags # return tags
def _build_post_message(self, job_posting): # def _build_post_message(self, job_posting):
""" # """
Constructs the final text message. # Constructs the final text message.
Includes a unique suffix for duplicate content prevention (422 fix). # Includes a unique suffix for duplicate content prevention (422 fix).
""" # """
message_parts = [ # message_parts = [
f"🔥 *Job Alert!* Were looking for a talented professional to join our team.", # f"🔥 *Job Alert!* Were looking for a talented professional to join our team.",
f"👉 **{job_posting.title}** 👈", # f"👉 **{job_posting.title}** 👈",
] # ]
if job_posting.department: # if job_posting.department:
message_parts.append(f"*{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 # # KEY DETAILS SECTION
details_list = [] # details_list = []
if job_posting.job_type: # if job_posting.job_type:
details_list.append(f"💼 Type: {job_posting.get_job_type_display()}") # details_list.append(f"💼 Type: {job_posting.get_job_type_display()}")
if job_posting.get_location_display() != 'Not specified': # if job_posting.get_location_display() != 'Not specified':
details_list.append(f"📍 Location: {job_posting.get_location_display()}") # details_list.append(f"📍 Location: {job_posting.get_location_display()}")
if job_posting.workplace_type: # if job_posting.workplace_type:
details_list.append(f"🏠 Workplace: {job_posting.get_workplace_type_display()}") # details_list.append(f"🏠 Workplace: {job_posting.get_workplace_type_display()}")
if job_posting.salary_range: # if job_posting.salary_range:
details_list.append(f"💰 Salary: {job_posting.salary_range}") # details_list.append(f"💰 Salary: {job_posting.salary_range}")
if details_list: # if details_list:
message_parts.append("*Key Information*:") # message_parts.append("*Key Information*:")
message_parts.extend(details_list) # message_parts.extend(details_list)
message_parts.append("\n") # message_parts.append("\n")
# DESCRIPTION SECTION # # DESCRIPTION SECTION
clean_description = self.clean_html_for_social_post(job_posting.description) # clean_description = self.clean_html_for_social_post(job_posting.description)
if clean_description: # if clean_description:
message_parts.append(f"🔎 *About the Role:*\n{clean_description}") # message_parts.append(f"🔎 *About the Role:*\n{clean_description}")
# clean_
# CALL TO ACTION # # CALL TO ACTION
if job_posting.application_url: # if job_posting.application_url:
message_parts.append(f"\n\n---") # message_parts.append(f"\n\n---")
# CRITICAL: Include the URL explicitly in the text body. # # CRITICAL: Include the URL explicitly in the text body.
# When media_category is NONE, LinkedIn often makes these URLs clickable. # # When media_category is NONE, LinkedIn often makes these URLs clickable.
message_parts.append(f"🔗 **APPLY NOW:** {job_posting.application_url}") # message_parts.append(f"🔗 **APPLY NOW:** {job_posting.application_url}")
# HASHTAGS # # HASHTAGS
hashtags = self.hashtags_list(job_posting.hash_tags) # hashtags = self.hashtags_list(job_posting.hash_tags)
if job_posting.department: # if job_posting.department:
dept_hashtag = f"#{job_posting.department.replace(' ', '')}" # dept_hashtag = f"#{job_posting.department.replace(' ', '')}"
hashtags.insert(0, dept_hashtag) # 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) --- # # --- FIX: ADD UNIQUE SUFFIX AND HANDLE LENGTH (422 fix) ---
unique_suffix = f"\n\n| Ref: {int(time.time())}" # 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: # if len(final_message) > available_length:
logger.warning("Post message truncated due to character limit.") # logger.warning("Post message truncated due to character limit.")
final_message = final_message[:available_length - 3] + "..." # final_message = final_message[:available_length - 3] + "..."
return final_message + unique_suffix # return final_message + unique_suffix
# ---------------- MAIN POSTING METHODS ---------------- # ---------------- MAIN POSTING METHODS ----------------
@ -279,7 +279,9 @@ class LinkedInService:
CRITICAL FIX: Avoids ARTICLE category if not using an image to prevent 402 errors. 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. --- # --- FIX FOR 402: Force NONE if no image is present. ---
if media_category != "IMAGE": if media_category != "IMAGE":

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -129,6 +129,7 @@ class JobPosting(Base):
max_length=50, blank=True, help_text="Status of LinkedIn posting" max_length=50, blank=True, help_text="Status of LinkedIn posting"
) )
linkedin_posted_at = models.DateTimeField(null=True, blank=True) 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 published_at = models.DateTimeField(db_index=True, null=True, blank=True) # Added index
# University Specific Fields # University Specific Fields

View File

@ -45,6 +45,15 @@ def format_job(sender, instance, created, **kwargs):
# If the instance is no longer active, delete the scheduled task # If the instance is no longer active, delete the scheduled task
existing_schedule.delete() 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) @receiver(post_save, sender=Candidate)
def score_candidate_resume(sender, instance, created, **kwargs): def score_candidate_resume(sender, instance, created, **kwargs):
if not instance.is_resume_parsed: if not instance.is_resume_parsed:
@ -351,4 +360,4 @@ def create_default_stages(sender, instance, created, **kwargs):
# required=False, # required=False,
# order=3, # order=3,
# is_predefined=True # is_predefined=True
# ) # )

View File

@ -116,25 +116,55 @@ def format_job_description(pk):
job_posting = JobPosting.objects.get(pk=pk) job_posting = JobPosting.objects.get(pk=pk)
print(job_posting) print(job_posting)
prompt = f""" prompt = f"""
Can you please organize and format this unformatted job description and qualifications into clear, readable sections using headings and bullet points? You are a dual-purpose AI assistant specializing in content formatting and social media copywriting for job announcements.
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}
**QUALIFICATIONS:** **JOB POSTING DATA (Raw Input):**
{job_posting.qualifications} ---
**JOB DESCRIPTION:**
{job_posting.description}
**STRICT JSON OUTPUT INSTRUCTIONS:** **QUALIFICATIONS:**
Output a single, valid JSON object with ONLY the following two top-level keys: {job_posting.qualifications}
'job_description': 'A HTML containing the formatted job description', **BENEFITS:**
'job_qualifications': 'A HTML containing the formatted job qualifications', {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 `<h2>` headings and `<ul>`/`<li>` bullet points. Encapsulate the entire formatted block within a single `<div>`.
2. **Format the Qualifications:** Organize and format the raw QUALIFICATIONS data into clear, readable sections using `<h2>` headings and `<ul>`/`<li>` bullet points. Encapsulate the entire formatted block within a single `<div>`.
2. **Format the Requirements:** Organize and format the raw Requirements data into clear, readable sections using `<h2>` headings and `<ul>`/`<li>` bullet points. Encapsulate the entire formatted block within a single `<div>`.
**TASK 2: LinkedIn Post Creation**
1. **Write the Post:** Create an engaging, professional, and concise LinkedIn post (maximum 1300 characters) summarizing the opportunity.
2. **Encourage Action:** The post must have a strong call-to-action (CTA) encouraging applications.
3. **Use Hashtags:** Integrate relevant industry, role, and company hashtags (including any provided in the raw input) naturally at the end of the post.
**STRICT JSON OUTPUT INSTRUCTIONS:**
Output a **single, valid JSON object** with **ONLY** the following three top-level key-value pairs.
* The values for `html_job_description` and `html_qualifications` MUST be the complete, formatted HTML strings (including all tags).
* The value for `linkedin_post` MUST be the complete, final LinkedIn post as a single string not greater than 3000 characters.
**Output Keys:**
1. `html_job_description`
2. `html_qualifications`
3 `html_job_requirements`
4. `linkedin_post_data`
**Do not include any other text, explanation, or markdown outside of the final JSON object.**
""" """
result = ai_handler(prompt) result = ai_handler(prompt)
print(f"REsults: {result}")
if result['status'] == 'error': if result['status'] == 'error':
logger.error(f"AI handler returned error for candidate {job_posting.pk}") logger.error(f"AI handler returned error for candidate {job_posting.pk}")
print(f"AI handler returned error for candidate {job_posting.pk}") print(f"AI handler returned error for candidate {job_posting.pk}")
@ -144,9 +174,10 @@ def format_job_description(pk):
data = json.loads(data) data = json.loads(data)
print(data) print(data)
job_posting.description = data.get('job_description') job_posting.description = data.get('html_job_description')
job_posting.qualifications = data.get('job_qualifications') job_posting.qualifications = data.get('html_qualifications')
job_posting.save(update_fields=['description', 'qualifications']) job_posting.linkedin_post_formated_data=data.get('linkedin_post_data')
job_posting.save(update_fields=['description', 'qualifications','linkedin_post_formated_data'])
def ai_handler(prompt): def ai_handler(prompt):
@ -400,6 +431,8 @@ def handle_reume_parsing_and_scoring(pk):
logger.info(f"Successfully scored and saved analysis for candidate {instance.id}") logger.info(f"Successfully scored and saved analysis for candidate {instance.id}")
print(f"Successfully scored and saved analysis for candidate {instance.id}") print(f"Successfully scored and saved analysis for candidate {instance.id}")
def create_interview_and_meeting( def create_interview_and_meeting(
candidate_id, candidate_id,
job_id, job_id,

View File

@ -612,4 +612,8 @@ def update_meeting(instance, updated_data):
return {"status": "success", "message": "Zoom meeting updated successfully."} return {"status": "success", "message": "Zoom meeting updated successfully."}
logger.warning(f"Failed to update Zoom meeting {instance.meeting_id}. Error: {result.get('message', 'Unknown error')}") logger.warning(f"Failed to update Zoom meeting {instance.meeting_id}. Error: {result.get('message', 'Unknown error')}")
return {"status": "error", "message": result.get("message", "Zoom meeting update failed.")} return {"status": "error", "message": result.get("message", "Zoom meeting update failed.")}

View File

@ -357,6 +357,15 @@ def job_detail(request, slug):
status_form = JobPostingStatusForm(request.POST, instance=job) status_form = JobPostingStatusForm(request.POST, instance=job)
if status_form.is_valid(): if status_form.is_valid():
job_status=status_form.cleaned_data['status']
form_template=job.form_template
if job_status=='ACTIVE':
form_template.is_active=True
form_template.save(update_fields=['is_active'])
else:
form_template.is_active=False
form_template.save(update_fields=['is_active'])
status_form.save() status_form.save()
# Add a success message # Add a success message

View File

@ -347,9 +347,14 @@
<i class="fas fa-plus-circle me-1"></i> {% trans "Create New Form Template" %} <i class="fas fa-plus-circle me-1"></i> {% trans "Create New Form Template" %}
</a> </a>
{% else %} {% else %}
{% if job.form_template.is_active %}
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-outline-secondary w-100"> <a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-outline-secondary w-100">
<i class="fas fa-list-alt me-1"></i> {% trans "View Form Template" %} <i class="fas fa-list-alt me-1"></i> {% trans "View Form Template" %}
</a> </a>
{% else %}
<p>{% trans "This job status is not active, the form will appear once the job is made active"%}</p>
{% endif %}
{% endif %} {% endif %}
</div> </div>
</div> </div>