diff --git a/NorahUniversity/__pycache__/settings.cpython-313.pyc b/NorahUniversity/__pycache__/settings.cpython-313.pyc
index b3d3954..b45cc75 100644
Binary files a/NorahUniversity/__pycache__/settings.cpython-313.pyc and b/NorahUniversity/__pycache__/settings.cpython-313.pyc differ
diff --git a/NorahUniversity/settings.py b/NorahUniversity/settings.py
index 705d22e..5c00084 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': 'haikal_db',
- 'USER': 'faheed',
- 'PASSWORD': 'Faheed@215',
+ 'NAME': 'norahuniversity',
+ 'USER': 'norahuniversity',
+ 'PASSWORD': 'norahuniversity',
'HOST': '127.0.0.1',
'PORT': '5432',
}
@@ -148,7 +148,7 @@ DATABASES = {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': BASE_DIR / 'db.sqlite3',
# }
-# }
+# }
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
@@ -171,12 +171,12 @@ AUTH_PASSWORD_VALIDATORS = [
]
-ACCOUNT_LOGIN_METHODS = ['email']
+ACCOUNT_LOGIN_METHODS = ['email']
ACCOUNT_SIGNUP_FIELDS = ['email*', 'password1*', 'password2*']
-
-ACCOUNT_UNIQUE_EMAIL = True
-ACCOUNT_EMAIL_VERIFICATION = 'none'
-ACCOUNT_USER_MODEL_USERNAME_FIELD = None
+
+ACCOUNT_UNIQUE_EMAIL = True
+ACCOUNT_EMAIL_VERIFICATION = 'none'
+ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
@@ -274,7 +274,6 @@ LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw=='
LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/jobs/linkedin/callback/'
-
Q_CLUSTER = {
'name': 'KAAUH_CLUSTER',
'workers': 8,
diff --git a/recruitment/__pycache__/admin.cpython-313.pyc b/recruitment/__pycache__/admin.cpython-313.pyc
index c293cd4..ad3e681 100644
Binary files a/recruitment/__pycache__/admin.cpython-313.pyc and b/recruitment/__pycache__/admin.cpython-313.pyc differ
diff --git a/recruitment/__pycache__/linkedin_service.cpython-313.pyc b/recruitment/__pycache__/linkedin_service.cpython-313.pyc
index 75e7844..e84214f 100644
Binary files a/recruitment/__pycache__/linkedin_service.cpython-313.pyc and b/recruitment/__pycache__/linkedin_service.cpython-313.pyc differ
diff --git a/recruitment/__pycache__/models.cpython-313.pyc b/recruitment/__pycache__/models.cpython-313.pyc
index d45fe48..fbd3902 100644
Binary files a/recruitment/__pycache__/models.cpython-313.pyc and b/recruitment/__pycache__/models.cpython-313.pyc differ
diff --git a/recruitment/__pycache__/signals.cpython-313.pyc b/recruitment/__pycache__/signals.cpython-313.pyc
index fc601be..a1a2263 100644
Binary files a/recruitment/__pycache__/signals.cpython-313.pyc and b/recruitment/__pycache__/signals.cpython-313.pyc differ
diff --git a/recruitment/__pycache__/urls.cpython-313.pyc b/recruitment/__pycache__/urls.cpython-313.pyc
index d758325..c867c4d 100644
Binary files a/recruitment/__pycache__/urls.cpython-313.pyc and b/recruitment/__pycache__/urls.cpython-313.pyc differ
diff --git a/recruitment/__pycache__/views.cpython-313.pyc b/recruitment/__pycache__/views.cpython-313.pyc
index 0ec2a0c..62edf4b 100644
Binary files a/recruitment/__pycache__/views.cpython-313.pyc and b/recruitment/__pycache__/views.cpython-313.pyc differ
diff --git a/recruitment/admin.py b/recruitment/admin.py
index 6b7cb8b..ff1f176 100644
--- a/recruitment/admin.py
+++ b/recruitment/admin.py
@@ -116,7 +116,7 @@ class JobPostingAdmin(admin.ModelAdmin):
'fields': ('internal_job_id', 'created_by', 'created_at', 'updated_at')
}),
('Integration', {
- 'fields': ('source', 'open_positions', 'position_number', 'reporting_to', 'start_date')
+ 'fields': ('source', 'open_positions', 'position_number', 'reporting_to',)
}),
('LinkedIn Integration', {
'fields': ('posted_to_linkedin', 'linkedin_post_id', 'linkedin_post_url', 'linkedin_posted_at')
diff --git a/recruitment/linkedin_service.py b/recruitment/linkedin_service.py
index 63da644..b6702b9 100644
--- a/recruitment/linkedin_service.py
+++ b/recruitment/linkedin_service.py
@@ -8,7 +8,7 @@ import logging
from django.conf import settings
import time
import random
-from django.utils import timezone
+from django.utils import timezone
logger = logging.getLogger(__name__)
@@ -27,7 +27,7 @@ class LinkedInService:
self.ASSET_STATUS_INTERVAL = 2 # Check every 2 seconds
# --- AUTHENTICATION & PROFILE ---
-
+
def get_auth_url(self):
"""Generate LinkedIn OAuth URL"""
params = {
@@ -86,7 +86,7 @@ class LinkedInService:
'X-Restli-Protocol-Version': LINKEDIN_API_VERSION,
'LinkedIn-Version': LINKEDIN_VERSION,
}
-
+
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
@@ -137,7 +137,7 @@ class LinkedInService:
response = requests.post(upload_url, headers=headers, data=image_content, timeout=60)
response.raise_for_status()
-
+
# --- CRITICAL FIX: POLL FOR ASSET STATUS ---
start_time = time.time()
while time.time() - start_time < self.ASSET_STATUS_TIMEOUT:
@@ -149,13 +149,13 @@ class LinkedInService:
return True
if status == "FAILED":
raise Exception(f"LinkedIn image processing failed for asset {asset_urn}")
-
+
logger.info(f"Asset {asset_urn} status: {status}. Waiting...")
time.sleep(self.ASSET_STATUS_INTERVAL)
-
+
except Exception as e:
logger.warning(f"Error during asset status check for {asset_urn}: {e}. Retrying.")
- time.sleep(self.ASSET_STATUS_INTERVAL * 2)
+ time.sleep(self.ASSET_STATUS_INTERVAL * 2)
# If the loop times out, return True to attempt post, but log warning
logger.warning(f"Asset {asset_urn} timed out, but upload succeeded. Forcing post attempt.")
@@ -169,36 +169,36 @@ class LinkedInService:
return ""
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)
# 2. Handle Lists: Convert
tags into a bullet point
- text = re.sub(r'(ul|ol|div)>', '\n', text, flags=re.IGNORECASE)
- text = re.sub(r']*>', '• ', text, flags=re.IGNORECASE)
- text = re.sub(r'', '\n', text, flags=re.IGNORECASE)
-
+ text = re.sub(r'(ul|ol|div)>', '\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)
-
+
# 4. Strip all remaining, unsupported HTML tags
clean_text = re.sub(r'<[^>]+>', '', 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()
-
+
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"]
-
+
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]
@@ -213,10 +213,10 @@ class LinkedInService:
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}*")
-
+ message_parts.append(f"*{job_posting.department}*")
+
message_parts.append("\n" + "=" * 25 + "\n")
# KEY DETAILS SECTION
@@ -229,7 +229,7 @@ class LinkedInService:
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)
@@ -239,7 +239,7 @@ class LinkedInService:
clean_description = self.clean_html_for_social_post(job_posting.description)
if clean_description:
message_parts.append(f"🔎 *About the Role:*\n{clean_description}")
-
+
# CALL TO ACTION
if job_posting.application_url:
message_parts.append(f"\n\n---")
@@ -255,17 +255,17 @@ class LinkedInService:
if len(message_parts)>=3000:
message_parts=message_parts[0:2980]+"........"
-
+
return "\n".join(message_parts)
-
+
def _send_ugc_post(self, person_urn, job_posting, media_category="NONE", media_list=None):
"""
New private method to handle the final UGC post request (text or image).
This eliminates the duplication between create_job_post and create_job_post_with_image.
"""
-
+
message = self._build_post_message(job_posting)
-
+
url = "https://api.linkedin.com/v2/ugcPosts"
headers = {
'Authorization': f'Bearer {self.access_token}',
@@ -280,7 +280,7 @@ class LinkedInService:
"shareMediaCategory": media_category,
}
}
-
+
if media_list:
specific_content["com.linkedin.ugc.ShareContent"]["media"] = media_list
@@ -295,7 +295,7 @@ class LinkedInService:
response = requests.post(url, headers=headers, json=payload, timeout=60)
response.raise_for_status()
-
+
post_id = response.headers.get('x-restli-id', '')
post_url = f"https://www.linkedin.com/feed/update/{quote(post_id)}/" if post_id else ""
@@ -309,13 +309,13 @@ class LinkedInService:
def create_job_post_with_image(self, job_posting, image_file, person_urn, asset_urn):
"""Step 3: Creates the final LinkedIn post payload with the image asset."""
-
+
# Prepare the media list for the _send_ugc_post helper
media_list = [{
"status": "READY",
"media": asset_urn,
"description": {"text": job_posting.title},
- "originalUrl": job_posting.application_url,
+ "originalUrl": job_posting.application_url,
"title": {"text": "Apply Now"}
}]
@@ -371,7 +371,7 @@ class LinkedInService:
except Exception as e:
logger.error(f"Image post failed, falling back to text: {e}")
# Force fallback to text-only if image posting fails
- has_image = False
+ has_image = False
# === FALLBACK TO PURE TEXT POST (shareMediaCategory: NONE) ===
# Use the single helper method here
diff --git a/recruitment/models.py b/recruitment/models.py
index 72d4dca..91a1fb6 100644
--- a/recruitment/models.py
+++ b/recruitment/models.py
@@ -226,6 +226,7 @@ class JobPosting(Base):
parts.append(self.location_country)
return ", ".join(parts) if parts else "Not specified"
+ @property
def is_expired(self):
"""Check if application deadline has passed"""
if self.application_deadline:
@@ -305,7 +306,7 @@ class JobPosting(Base):
@property
def offer_candidates(self):
return self.all_candidates.filter(stage="Offer")
-
+
#counts
@property
diff --git a/recruitment/signals.py b/recruitment/signals.py
index 3795b13..e27c531 100644
--- a/recruitment/signals.py
+++ b/recruitment/signals.py
@@ -7,10 +7,17 @@ from .models import FormField,FormStage,FormTemplate,Candidate,JobPosting
logger = logging.getLogger(__name__)
-# @receiver(post_save, sender=JobPosting)
-# def create_form_for_job(sender, instance, created, **kwargs):
-# if created:
-# FormTemplate.objects.create(job=instance, is_active=True, name=instance.title)
+@receiver(post_save, sender=JobPosting)
+def format_job(sender, instance, created, **kwargs):
+ if created:
+ FormTemplate.objects.create(job=instance, is_active=True, name=instance.title)
+ async_task(
+ 'recruitment.tasks.format_job_description',
+ instance.pk,
+ # hook='myapp.tasks.email_sent_callback' # Optional callback
+ )
+
+
@receiver(post_save, sender=Candidate)
def score_candidate_resume(sender, instance, created, **kwargs):
if not instance.is_resume_parsed:
@@ -75,238 +82,246 @@ def create_default_stages(sender, instance, created, **kwargs):
order=4,
is_predefined=True
)
+ # FormField.objects.create(
+ # stage=contact_stage,
+ # label='National ID / Iqama Number',
+ # field_type='text',
+ # required=False,
+ # order=5,
+ # is_predefined=True
+ # )
FormField.objects.create(
stage=contact_stage,
label='Resume Upload',
field_type='file',
required=True,
- order=5,
+ order=6,
is_predefined=True,
file_types='.pdf,.doc,.docx',
max_file_size=1
)
- # Stage 2: Resume Objective
- objective_stage = FormStage.objects.create(
- template=instance,
- name='Resume Objective',
- order=1,
- is_predefined=True
- )
- FormField.objects.create(
- stage=objective_stage,
- label='Career Objective',
- field_type='textarea',
- required=False,
- order=0,
- is_predefined=True
- )
+ # # Stage 2: Resume Objective
+ # objective_stage = FormStage.objects.create(
+ # template=instance,
+ # name='Resume Objective',
+ # order=1,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=objective_stage,
+ # label='Career Objective',
+ # field_type='textarea',
+ # required=False,
+ # order=0,
+ # is_predefined=True
+ # )
- # Stage 3: Education
- education_stage = FormStage.objects.create(
- template=instance,
- name='Education',
- order=2,
- is_predefined=True
- )
- FormField.objects.create(
- stage=education_stage,
- label='Degree',
- field_type='text',
- required=True,
- order=0,
- is_predefined=True
- )
- FormField.objects.create(
- stage=education_stage,
- label='Institution',
- field_type='text',
- required=True,
- order=1,
- is_predefined=True
- )
- FormField.objects.create(
- stage=education_stage,
- label='Location',
- field_type='text',
- required=False,
- order=2,
- is_predefined=True
- )
- FormField.objects.create(
- stage=education_stage,
- label='Graduation Date',
- field_type='date',
- required=False,
- order=3,
- is_predefined=True
- )
+ # # Stage 3: Education
+ # education_stage = FormStage.objects.create(
+ # template=instance,
+ # name='Education',
+ # order=2,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=education_stage,
+ # label='Degree',
+ # field_type='text',
+ # required=True,
+ # order=0,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=education_stage,
+ # label='Institution',
+ # field_type='text',
+ # required=True,
+ # order=1,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=education_stage,
+ # label='Location',
+ # field_type='text',
+ # required=False,
+ # order=2,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=education_stage,
+ # label='Graduation Date',
+ # field_type='date',
+ # required=False,
+ # order=3,
+ # is_predefined=True
+ # )
- # Stage 4: Experience
- experience_stage = FormStage.objects.create(
- template=instance,
- name='Experience',
- order=3,
- is_predefined=True
- )
- FormField.objects.create(
- stage=experience_stage,
- label='Position Title',
- field_type='text',
- required=True,
- order=0,
- is_predefined=True
- )
- FormField.objects.create(
- stage=experience_stage,
- label='Company Name',
- field_type='text',
- required=True,
- order=1,
- is_predefined=True
- )
- FormField.objects.create(
- stage=experience_stage,
- label='Location',
- field_type='text',
- required=False,
- order=2,
- is_predefined=True
- )
- FormField.objects.create(
- stage=experience_stage,
- label='Start Date',
- field_type='date',
- required=True,
- order=3,
- is_predefined=True
- )
- FormField.objects.create(
- stage=experience_stage,
- label='End Date',
- field_type='date',
- required=True,
- order=4,
- is_predefined=True
- )
- FormField.objects.create(
- stage=experience_stage,
- label='Responsibilities & Achievements',
- field_type='textarea',
- required=False,
- order=5,
- is_predefined=True
- )
+ # # Stage 4: Experience
+ # experience_stage = FormStage.objects.create(
+ # template=instance,
+ # name='Experience',
+ # order=3,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=experience_stage,
+ # label='Position Title',
+ # field_type='text',
+ # required=True,
+ # order=0,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=experience_stage,
+ # label='Company Name',
+ # field_type='text',
+ # required=True,
+ # order=1,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=experience_stage,
+ # label='Location',
+ # field_type='text',
+ # required=False,
+ # order=2,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=experience_stage,
+ # label='Start Date',
+ # field_type='date',
+ # required=True,
+ # order=3,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=experience_stage,
+ # label='End Date',
+ # field_type='date',
+ # required=True,
+ # order=4,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=experience_stage,
+ # label='Responsibilities & Achievements',
+ # field_type='textarea',
+ # required=False,
+ # order=5,
+ # is_predefined=True
+ # )
- # Stage 5: Skills
- skills_stage = FormStage.objects.create(
- template=instance,
- name='Skills',
- order=4,
- is_predefined=True
- )
- FormField.objects.create(
- stage=skills_stage,
- label='Technical Skills',
- field_type='checkbox',
- required=False,
- order=0,
- is_predefined=True,
- options=['Programming Languages', 'Frameworks', 'Tools & Technologies']
- )
+ # # Stage 5: Skills
+ # skills_stage = FormStage.objects.create(
+ # template=instance,
+ # name='Skills',
+ # order=4,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=skills_stage,
+ # label='Technical Skills',
+ # field_type='checkbox',
+ # required=False,
+ # order=0,
+ # is_predefined=True,
+ # options=['Programming Languages', 'Frameworks', 'Tools & Technologies']
+ # )
- # Stage 6: Summary
- summary_stage = FormStage.objects.create(
- template=instance,
- name='Summary',
- order=5,
- is_predefined=True
- )
- FormField.objects.create(
- stage=summary_stage,
- label='Professional Summary',
- field_type='textarea',
- required=False,
- order=0,
- is_predefined=True
- )
+ # # Stage 6: Summary
+ # summary_stage = FormStage.objects.create(
+ # template=instance,
+ # name='Summary',
+ # order=5,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=summary_stage,
+ # label='Professional Summary',
+ # field_type='textarea',
+ # required=False,
+ # order=0,
+ # is_predefined=True
+ # )
- # Stage 7: Certifications
- certifications_stage = FormStage.objects.create(
- template=instance,
- name='Certifications',
- order=6,
- is_predefined=True
- )
- FormField.objects.create(
- stage=certifications_stage,
- label='Certification Name',
- field_type='text',
- required=False,
- order=0,
- is_predefined=True
- )
- FormField.objects.create(
- stage=certifications_stage,
- label='Issuing Organization',
- field_type='text',
- required=False,
- order=1,
- is_predefined=True
- )
- FormField.objects.create(
- stage=certifications_stage,
- label='Issue Date',
- field_type='date',
- required=False,
- order=2,
- is_predefined=True
- )
- FormField.objects.create(
- stage=certifications_stage,
- label='Expiration Date',
- field_type='date',
- required=False,
- order=3,
- is_predefined=True
- )
+ # # Stage 7: Certifications
+ # certifications_stage = FormStage.objects.create(
+ # template=instance,
+ # name='Certifications',
+ # order=6,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=certifications_stage,
+ # label='Certification Name',
+ # field_type='text',
+ # required=False,
+ # order=0,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=certifications_stage,
+ # label='Issuing Organization',
+ # field_type='text',
+ # required=False,
+ # order=1,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=certifications_stage,
+ # label='Issue Date',
+ # field_type='date',
+ # required=False,
+ # order=2,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=certifications_stage,
+ # label='Expiration Date',
+ # field_type='date',
+ # required=False,
+ # order=3,
+ # is_predefined=True
+ # )
- # Stage 8: Awards and Recognitions
- awards_stage = FormStage.objects.create(
- template=instance,
- name='Awards and Recognitions',
- order=7,
- is_predefined=True
- )
- FormField.objects.create(
- stage=awards_stage,
- label='Award Name',
- field_type='text',
- required=False,
- order=0,
- is_predefined=True
- )
- FormField.objects.create(
- stage=awards_stage,
- label='Issuing Organization',
- field_type='text',
- required=False,
- order=1,
- is_predefined=True
- )
- FormField.objects.create(
- stage=awards_stage,
- label='Date Received',
- field_type='date',
- required=False,
- order=2,
- is_predefined=True
- )
- FormField.objects.create(
- stage=awards_stage,
- label='Description',
- field_type='textarea',
- required=False,
- order=3,
- is_predefined=True
- )
\ No newline at end of file
+ # # Stage 8: Awards and Recognitions
+ # awards_stage = FormStage.objects.create(
+ # template=instance,
+ # name='Awards and Recognitions',
+ # order=7,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=awards_stage,
+ # label='Award Name',
+ # field_type='text',
+ # required=False,
+ # order=0,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=awards_stage,
+ # label='Issuing Organization',
+ # field_type='text',
+ # required=False,
+ # order=1,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=awards_stage,
+ # label='Date Received',
+ # field_type='date',
+ # required=False,
+ # order=2,
+ # is_predefined=True
+ # )
+ # FormField.objects.create(
+ # stage=awards_stage,
+ # label='Description',
+ # field_type='textarea',
+ # required=False,
+ # order=3,
+ # is_predefined=True
+ # )
\ No newline at end of file
diff --git a/recruitment/tasks.py b/recruitment/tasks.py
index a972cba..3c3adeb 100644
--- a/recruitment/tasks.py
+++ b/recruitment/tasks.py
@@ -26,11 +26,23 @@ except ImportError:
logger = logging.getLogger(__name__)
OPENROUTER_API_KEY ='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'
-# OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free'
-OPENROUTER_MODEL = 'openai/gpt-oss-20b:free'
+OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free'
+
+# OPENROUTER_MODEL = 'openai/gpt-oss-20b:free'
# OPENROUTER_MODEL = 'openai/gpt-oss-20b'
# OPENROUTER_MODEL = 'mistralai/mistral-small-3.2-24b-instruct:free'
+# from google import genai
+
+# client = genai.Client(api_key="AIzaSyDkwYmvRe5ieTjQi1ClSzD5z5roTwaFsmY")
+
+# def google_ai(text):
+# response = client.models.generate_content(
+# model="gemini-2.5-flash", contents=text
+# )
+# return response
+
+
if not OPENROUTER_API_KEY:
logger.warning("OPENROUTER_API_KEY not set. Resume scoring will be skipped.")
@@ -100,6 +112,43 @@ def extract_text_from_document(file_path):
else:
raise ValueError(f"Unsupported file type: {file_ext}. Only .pdf and .docx files are supported.")
+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}
+
+ **QUALIFICATIONS:**
+ {job_posting.qualifications}
+
+ **STRICT JSON OUTPUT INSTRUCTIONS:**
+ Output a single, valid JSON object with ONLY the following two top-level keys:
+
+ 'job_description': 'A HTML containing the formatted job description',
+ 'job_qualifications': 'A HTML containing the formatted job qualifications',
+
+
+ Do not include any other text except for the JSON output.
+ """
+ result = ai_handler(prompt)
+
+ 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}")
+ return
+ data = result['data']
+ if isinstance(data, str):
+ 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'])
+
+
def ai_handler(prompt):
print("model call")
response = requests.post(
@@ -133,143 +182,6 @@ def ai_handler(prompt):
return {"status": "error", "data": response.json()}
-# def handle_reume_parsing_and_scoring(pk):
-# from django.db import transaction
-
-# logger.info(f"Scoring resume for candidate {pk}")
-# instance = Candidate.objects.get(pk=pk)
-# try:
-# file_path = instance.resume.path
-# with transaction.atomic():
-# if not os.path.exists(file_path):
-# logger.warning(f"Resume file not found: {file_path}")
-# return
-
-# resume_text = extract_text_from_pdf(file_path)
-# job_detail= f"{instance.job.description} {instance.job.qualifications}"
-# resume_parser_prompt = f"""
-# You are an expert resume parser and summarizer. Given a resume in plain text format, extract and organize the following key-value information into a clean, valid JSON object:
-
-# full_name: Full name of the candidate
-# current_title: Most recent or current job title
-# location: City and state (or country if outside the U.S.)
-# contact: Phone number and email (as a single string or separate fields)
-# linkedin: LinkedIn profile URL (if present)
-# github: GitHub or portfolio URL (if present)
-# summary: Brief professional profile or summary (1–2 sentences)
-# education: List of degrees, each with:
-# institution
-# degree
-# year
-# gpa (if provided)
-# relevant_courses (as a list, if mentioned)
-# skills: Grouped by category if possible (e.g., programming, big data, visualization), otherwise as a flat list of technologies/tools
-# experience: List of roles, each with:
-# company
-# job_title
-# location
-# start_date and end_date (or "Present" if applicable)
-# key_achievements (as a list of concise bullet points)
-# projects: List of notable projects (if clearly labeled), each with:
-# name
-# year
-# technologies_used
-# brief_description
-# Instructions:
-
-# Be concise but preserve key details.
-# Normalize formatting (e.g., "Jun. 2014" → "2014-06").
-# Omit redundant or promotional language.
-# If a section is missing, omit the key or set it to null/empty list as appropriate.
-# Output only valid JSON—no markdown, no extra text.
-# Now, process the following resume text:
-# {resume_text}
-# """
-# resume_parser_result = ai_handler(resume_parser_prompt)
-# resume_scoring_prompt = f"""
-# You are an expert technical recruiter. Your task is to score the following candidate for the role based on the provided job criteria.
-
-# **Job Criteria:**
-# {job_detail}
-
-# **Candidate's Extracted Resume Json:**
-# \"\"\"
-# {resume_parser_result}
-# \"\"\"
-
-# **Your Task:**
-# Provide a response in strict JSON format with the following keys:
-# 1. 'match_score': A score from 0 to 100 representing how well the candidate fits the role.
-# 2. 'strengths': A brief summary of why the candidate is a strong fit, referencing specific criteria.
-# 3. 'weaknesses': A brief summary of where the candidate falls short or what criteria are missing.
-# 4. 'years_of_experience': The total number of years of professional experience mentioned in the resume as a numerical value (e.g., 6.5).
-# 5. 'criteria_checklist': An object where you rate the candidate's match for each specific criterion (e.g., {{'Python': 'Met', 'AWS': 'Not Mentioned'}}).
-# 6. 'criteria_checklist': An object where you rate the candidate's match for each specific criterion (e.g., {{'Python': 'Met', 'AWS': 'Not Mentioned'}}).
-# 7. 'category': Based on the content provided, determine the most fitting professional field or category for the individual. (e.g., {{"category" : "Data Science"}}) only output the category name and no other text example ('Software Development', 'correct') , ('Software Development and devops','wrong').
-# 8. 'most_recent_job_title': The candidate's most recent or current professional job title.
-# 9. 'recommendation': Provide a recommendation for the candidate (e.g., {{"recommendation": "
-# Conclusion and Minor Considerations
-# Overall Assessment: Highly Recommended Candidate.
-
-# [Candidate] is an exceptionally strong candidate for this role. His proven track record with the core technology stack (Django, Python, Docker, CI/CD) and relevant experience in large-scale, high-impact enterprise projects (Telecom BPM/MDM) make him an excellent technical fit. His fluency in Arabic and English directly addresses a major non-negotiable requirement.
-
-# The only minor area not explicitly mentioned is the mentoring aspect, but his senior level of experience and technical breadth strongly suggest he possesses the capability to mentor junior engineers.
-
-# The hiring manager should move forward with this candidate with high confidence.
-# ."}}).
-# 10. 'top_3_keywords': A list of the three most dominant and relevant technical skills or technologies from the resume that match the job criteria.
-# 11. 'job_fit_narrative': A single, concise sentence summarizing the core fit.
-# 12. 'language_fluency': A list of languages and their fluency levels mentioned.
-# 13. 'screening_stage_rating': A standardized rating (e.g., "A - Highly Qualified", "B - Qualified").
-# 14. 'min_req_met_bool': Boolean (true/false) indicating if all non-negotiable minimum requirements are met.
-# 15. 'soft_skills_score': A score (0-100) for inferred non-technical skills like leadership and communication.
-# 16. 'experience_industry_match': A score (0-100) for the relevance of the candidate's industry experience.
-
-# Only output valid JSON. Do not include any other text.
-# """
-
-# resume_scoring_result = ai_handler(resume_scoring_prompt)
-
-# print(resume_scoring_result)
-
-# instance.parsed_summary = str(resume_parser_result)
-
-
-# # Core Scores
-# instance.set_field('match_score', resume_scoring_result.get('match_score', 0)) # Set default for int
-# instance.set_field('years_of_experience', resume_scoring_result.get('years_of_experience', 0.0)) # Set default for float
-# instance.set_field('soft_skills_score', resume_scoring_result.get('soft_skills_score', 0))
-# instance.set_field('experience_industry_match', resume_scoring_result.get('experience_industry_match', 0))
-
-# # Screening & Funnel
-# instance.set_field('min_req_met_bool', resume_scoring_result.get('min_req_met_bool', False)) # Set default for bool
-# instance.set_field('screening_stage_rating', resume_scoring_result.get('screening_stage_rating', 'N/A'))
-# instance.set_field('most_recent_job_title', resume_scoring_result.get('most_recent_job_title', 'N/A'))
-# instance.set_field('top_3_keywords', resume_scoring_result.get('top_3_keywords', [])) # Set default for list
-
-# # Summaries & Narrative
-# instance.set_field('strengths', resume_scoring_result.get('strengths', ''))
-# instance.set_field('weaknesses', resume_scoring_result.get('weaknesses', ''))
-# instance.set_field('job_fit_narrative', resume_scoring_result.get('job_fit_narrative', ''))
-# instance.set_field('recommendation', resume_scoring_result.get('recommendation', ''))
-
-# # Structured Data
-# instance.set_field('criteria_checklist', resume_scoring_result.get('criteria_checklist', {})) # Set default for dict
-# instance.set_field('language_fluency', resume_scoring_result.get('language_fluency', [])) # Set default for list
-
-# instance.set_field('category', resume_scoring_result.get('category', 'Uncategorized')) # Use 'category' key
-
-# instance.is_resume_parsed = True
-
-# instance.save(update_fields=['ai_analysis_data', 'is_resume_parsed','parsed_summary'])
-
-# logger.info(f"Successfully scored resume for candidate {instance.id}")
-
-# except Exception as e:
-# instance.is_resume_parsed = False
-# instance.save(update_fields=['is_resume_parsed'])
-# logger.error(f"Failed to score resume for candidate:{instance.pk} {e}")
-
def safe_cast_to_float(value, default=0.0):
"""Safely converts a value (int, float, or string) to a float."""
if isinstance(value, (int, float)):
diff --git a/recruitment/urls.py b/recruitment/urls.py
index a30f8dd..3940ee8 100644
--- a/recruitment/urls.py
+++ b/recruitment/urls.py
@@ -62,7 +62,7 @@ urlpatterns = [
# Form Preview URLs
# path('forms/', views.form_list, name='form_list'),
path('forms/builder/', views.form_builder, name='form_builder'),
- path('forms/builder//', views.form_builder, name='form_builder'),
+ path('forms/builder//', views.form_builder, name='form_builder'),
path('forms/', views.form_templates_list, name='form_templates_list'),
path('forms/create-template/', views.create_form_template, name='create_form_template'),
@@ -82,8 +82,8 @@ urlpatterns = [
path('htmx//candidate_update_status/', views.candidate_update_status, name='candidate_update_status'),
- path('forms/form//submit/', views.submit_form, name='submit_form'),
- path('forms/form//', views.form_wizard_view, name='form_wizard'),
+ path('forms/form//submit/', views.submit_form, name='submit_form'),
+ path('forms/form//', views.form_wizard_view, name='form_wizard'),
path('forms//submissions//', views.form_submission_details, name='form_submission_details'),
path('forms/template//submissions/', views.form_template_submissions_list, name='form_template_submissions_list'),
path('forms/template//all-submissions/', views.form_template_all_submissions, name='form_template_all_submissions'),
@@ -96,6 +96,11 @@ urlpatterns = [
# path('api/forms/save/', views.save_form_builder, name='save_form_builder'),
# path('api/forms//load/', views.load_form, name='load_form'),
# path('api/forms//update/', views.update_form_builder, name='update_form_builder'),
+ path('api/templates/', views.list_form_templates, name='list_form_templates'),
+ path('api/templates/save/', views.save_form_template, name='save_form_template'),
+ path('api/templates//', views.load_form_template, name='load_form_template'),
+ path('api/templates//delete/', views.delete_form_template, name='delete_form_template'),
+
path('jobs//calendar/', views.interview_calendar_view, name='interview_calendar'),
path('jobs//calendar/interview//', views.interview_detail_view, name='interview_detail'),
diff --git a/recruitment/views.py b/recruitment/views.py
index 1d3e901..4d1d2fb 100644
--- a/recruitment/views.py
+++ b/recruitment/views.py
@@ -347,7 +347,6 @@ def edit_job(request, slug):
def job_detail(request, slug):
"""View details of a specific job"""
job = get_object_or_404(JobPosting, slug=slug)
-
# Get all candidates for this job, ordered by most recent
applicants = job.candidates.all().order_by("-created_at")
@@ -633,12 +632,12 @@ def application_success(request,slug):
@ensure_csrf_cookie
@login_required
-def form_builder(request, template_id=None):
+def form_builder(request, template_slug=None):
"""Render the form builder interface"""
context = {}
- if template_id:
+ if template_slug:
template = get_object_or_404(
- FormTemplate, id=template_id, created_by=request.user
+ FormTemplate, slug=template_slug, created_by=request.user
)
context['template']=template
context["template_id"] = template.id
@@ -762,7 +761,7 @@ def load_form_template(request, template_id):
def form_templates_list(request):
"""List all form templates for the current user"""
query = request.GET.get("q", "")
- templates = FormTemplate.objects.filter(created_by=request.user)
+ templates = FormTemplate.objects.filter()
if query:
templates = templates.filter(
@@ -802,7 +801,7 @@ def create_form_template(request):
@require_http_methods(["GET"])
def list_form_templates(request):
"""List all form templates for the current user"""
- templates = FormTemplate.objects.filter(created_by=request.user).values(
+ templates = FormTemplate.objects.filter().values(
"id", "name", "description", "created_at", "updated_at"
)
return JsonResponse({"success": True, "templates": list(templates)})
@@ -812,26 +811,25 @@ def list_form_templates(request):
@require_http_methods(["DELETE"])
def delete_form_template(request, template_id):
"""Delete a form template"""
- template = get_object_or_404(FormTemplate, id=template_id, created_by=request.user)
+ template = get_object_or_404(FormTemplate, id=template_id)
template.delete()
return JsonResponse(
{"success": True, "message": "Form template deleted successfully!"}
)
-def form_wizard_view(request, template_id):
+def form_wizard_view(request, template_slug):
"""Display the form as a step-by-step wizard"""
- template = get_object_or_404(FormTemplate, pk=template_id, is_active=True)
+ template = get_object_or_404(FormTemplate, slug=template_slug, is_active=True)
job_id = template.job.internal_job_id
job=template.job
- is_limit_exceeded=job.is_application_limit_reached
+ is_limit_exceeded = job.is_application_limit_reached
if is_limit_exceeded:
messages.error(
request,
'Application limit reached: This job is no longer accepting new applications. Please explore other available positions.'
)
return redirect('job_detail_candidate',slug=job.slug)
-
if job.is_expired:
messages.error(
request,
@@ -842,14 +840,15 @@ def form_wizard_view(request, template_id):
return render(
request,
"forms/form_wizard.html",
- {"template_id": template_id, "job_id": job_id},
+ {"template_slug": template_slug, "job_id": job_id},
)
@require_POST
-def submit_form(request, template_id):
+def submit_form(request, template_slug):
"""Handle form submission"""
- template = get_object_or_404(FormTemplate, id=template_id)
+ template = get_object_or_404(FormTemplate, slug=template_slug)
+ job = template.job
if request.method == "POST":
try:
with transaction.atomic():
diff --git a/templates/forms/form_list.html b/templates/forms/form_list.html
index e77425c..9421e68 100644
--- a/templates/forms/form_list.html
+++ b/templates/forms/form_list.html
@@ -74,17 +74,17 @@