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
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'<strong>(.*?)</strong>', r'*\1*', text, flags=re.IGNORECASE)
text = re.sub(r'<b>(.*?)</b>', r'*\1*', text, flags=re.IGNORECASE)
# # 1. Convert Bolding tags to *Markdown*
# text = re.sub(r'<strong>(.*?)</strong>', 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
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>', '\n', text, flags=re.IGNORECASE)
# # 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'<li[^>]*>', '• ', text, flags=re.IGNORECASE)
# text = re.sub(r'</li>', '\n', text, flags=re.IGNORECASE)
# 3. Handle Paragraphs and Line Breaks
text = re.sub(r'</p>', '\n\n', text, flags=re.IGNORECASE)
text = re.sub(r'<br/?>', '\n', text, flags=re.IGNORECASE)
# # 3. Handle Paragraphs and Line Breaks
# text = re.sub(r'</p>', '\n\n', text, flags=re.IGNORECASE)
# text = re.sub(r'<br/?>', '\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!* Were 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!* Were 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":

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"
)
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

View File

@ -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
# )
# )

View File

@ -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 `<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)
print(f"REsults: {result}")
if result['status'] == 'error':
logger.error(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)
print(data)
job_posting.description = data.get('job_description')
job_posting.qualifications = data.get('job_qualifications')
job_posting.save(update_fields=['description', 'qualifications'])
job_posting.description = data.get('html_job_description')
job_posting.qualifications = data.get('html_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):
@ -400,6 +431,8 @@ def handle_reume_parsing_and_scoring(pk):
logger.info(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(
candidate_id,
job_id,

View File

@ -612,4 +612,8 @@ def update_meeting(instance, updated_data):
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')}")
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)
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()
# Add a success message

View File

@ -347,9 +347,14 @@
<i class="fas fa-plus-circle me-1"></i> {% trans "Create New Form Template" %}
</a>
{% else %}
{% if job.form_template.is_active %}
<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" %}
</a>
{% else %}
<p>{% trans "This job status is not active, the form will appear once the job is made active"%}</p>
{% endif %}
{% endif %}
</div>
</div>