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'', '\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'', '\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 @@