small fix for the form template marked active when the job is made active
This commit is contained in:
parent
e3435e3627
commit
d1dda003d6
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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!* 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":
|
||||
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
# )
|
||||
# )
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.")}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user