first push

This commit is contained in:
Faheed 2025-10-02 14:56:59 +03:00
commit 45c5d90907
6306 changed files with 806650 additions and 0 deletions

8
.env Normal file
View File

@ -0,0 +1,8 @@
LINKEDIN_CLIENT_ID = '867jwsiyem1504'
LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw=='
# LINKEDIN_CLIENT_ID = '86jk5afol95aq9'
# LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw=='

54
Readme.md Normal file
View File

@ -0,0 +1,54 @@
Product Requirements Documents.
**workflow**
1.Admin will create a job in erp system.
2.ERP system will send the job data to ATS.
3.ATS receives JOb data and save it to ATS along with Source name, IP addres and timstamp.
4.Admin should verify the data received from the ERP system and apply any changes if required and save the job.
5.Admin can creat a new job even on ATS and then this job is syc to the ERP.*
6.Admin can create a multiple Applicant-form for each job using the dynamic form generator and only one applicant-form per
job is marked active.
7.Admin can publish a job posting from ATS to linkedin.
8.Applicants can apply to job through linkedin post or by visiting the career website.
9.The ATS will save the applicant-form data to the database and a single view for all candidates in one location.
10.The system will use AI to extract and parse the resume of the applicant and match it with the job description.
11.The system should filter the top 100 candidates based on their skills and job description.
12.The admin should verify and can accept or reject the ai top 100 candidates suggestions.
13.The admin can update the status of the accepted candidate and move to next stage.
14.The admin can schedule the interview from with the system using zoom and send invitation links via email to the candidate.
15.The admin can reschedule or cancel the interview meeting and notify the candidate.
16.The admin should update the candidate stage.
**Technical Implementation**
1.CRUD for job
2.CRUD for candidate
3.CRUD for meeting
4.API for integrating ERP
5.MODEL for SOURCE
6.Model for job
7.MOdel for candidate
8.MOdel for User
10.Model for Form
11.Model for FormField
12.Model Meet
13.Model for Schedule
14.crud for schedule
**Features**
1.schedule interview meeting (allow admin to automatically schedule interview meeting for filtered candidate )
2.AI candidate ranking (top 100) for each job.
3.Dashboards for analytics.
4.Agencies
5.Initial permanent field for each candidate form
6.Candidate authentication
**UI and design**
**Integration**

0
applicant/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
applicant/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
applicant/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ApplicantConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'applicant'

22
applicant/forms.py Normal file
View File

@ -0,0 +1,22 @@
from django import forms
from .models import ApplicantForm, FormField
class ApplicantFormCreateForm(forms.ModelForm):
class Meta:
model = ApplicantForm
fields = ['name', 'description']
widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}),
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
class FormFieldForm(forms.ModelForm):
class Meta:
model = FormField
fields = ['label', 'field_type', 'required', 'help_text', 'choices']
widgets = {
'label': forms.TextInput(attrs={'class': 'form-control'}),
'field_type': forms.Select(attrs={'class': 'form-control'}),
'help_text': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
'choices': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Option1, Option2, Option3'}),
}

View File

@ -0,0 +1,49 @@
from django import forms
from .models import FormField
# applicant/forms_builder.py
def create_dynamic_form(form_instance):
fields = {}
for field in form_instance.fields.all():
field_kwargs = {
'label': field.label,
'required': field.required,
'help_text': field.help_text
}
# Use stable field_name instead of database ID
field_key = field.field_name
if field.field_type == 'text':
fields[field_key] = forms.CharField(**field_kwargs)
elif field.field_type == 'email':
fields[field_key] = forms.EmailField(**field_kwargs)
elif field.field_type == 'phone':
fields[field_key] = forms.CharField(**field_kwargs)
elif field.field_type == 'number':
fields[field_key] = forms.IntegerField(**field_kwargs)
elif field.field_type == 'date':
fields[field_key] = forms.DateField(**field_kwargs)
elif field.field_type == 'textarea':
fields[field_key] = forms.CharField(
widget=forms.Textarea,
**field_kwargs
)
elif field.field_type in ['select', 'radio']:
choices = [(c.strip(), c.strip()) for c in field.choices.split(',') if c.strip()]
if not choices:
choices = [('', '---')]
if field.field_type == 'select':
fields[field_key] = forms.ChoiceField(choices=choices, **field_kwargs)
else:
fields[field_key] = forms.ChoiceField(
choices=choices,
widget=forms.RadioSelect,
**field_kwargs
)
elif field.field_type == 'checkbox':
field_kwargs['required'] = False
fields[field_key] = forms.BooleanField(**field_kwargs)
return type('DynamicApplicantForm', (forms.Form,), fields)

View File

@ -0,0 +1,70 @@
# Generated by Django 5.2.6 on 2025-10-01 21:41
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('jobs', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='ApplicantForm',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text="Form version name (e.g., 'Version A', 'Version B' etc)", max_length=200)),
('description', models.TextField(blank=True, help_text='Optional description of this form version')),
('is_active', models.BooleanField(default=False, help_text='Only one form can be active per job')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('job_posting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applicant_forms', to='jobs.jobposting')),
],
options={
'verbose_name': 'Application Form',
'verbose_name_plural': 'Application Forms',
'ordering': ['-created_at'],
'unique_together': {('job_posting', 'name')},
},
),
migrations.CreateModel(
name='ApplicantSubmission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('submitted_at', models.DateTimeField(auto_now_add=True)),
('data', models.JSONField()),
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
('score', models.FloatField(default=0, help_text='Ranking score for the applicant submission')),
('form', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='applicant.applicantform')),
('job_posting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='jobs.jobposting')),
],
options={
'verbose_name': 'Applicant Submission',
'verbose_name_plural': 'Applicant Submissions',
'ordering': ['-submitted_at'],
},
),
migrations.CreateModel(
name='FormField',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('label', models.CharField(max_length=255)),
('field_type', models.CharField(choices=[('text', 'Text'), ('email', 'Email'), ('phone', 'Phone'), ('number', 'Number'), ('date', 'Date'), ('select', 'Dropdown'), ('radio', 'Radio Buttons'), ('checkbox', 'Checkbox'), ('textarea', 'Paragraph Text'), ('file', 'File Upload'), ('image', 'Image Upload')], max_length=20)),
('required', models.BooleanField(default=True)),
('help_text', models.TextField(blank=True)),
('choices', models.TextField(blank=True, help_text='Comma-separated options for select/radio fields')),
('order', models.IntegerField(default=0)),
('field_name', models.CharField(blank=True, max_length=100)),
('form', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='applicant.applicantform')),
],
options={
'verbose_name': 'Form Field',
'verbose_name_plural': 'Form Fields',
'ordering': ['order'],
},
),
]

View File

144
applicant/models.py Normal file
View File

@ -0,0 +1,144 @@
# models.py
from django.db import models
from django.core.exceptions import ValidationError
from jobs.models import JobPosting
from django.urls import reverse
class ApplicantForm(models.Model):
"""Multiple dynamic forms per job posting, only one active at a time"""
job_posting = models.ForeignKey(
JobPosting,
on_delete=models.CASCADE,
related_name='applicant_forms'
)
name = models.CharField(
max_length=200,
help_text="Form version name (e.g., 'Version A', 'Version B' etc)"
)
description = models.TextField(
blank=True,
help_text="Optional description of this form version"
)
is_active = models.BooleanField(
default=False,
help_text="Only one form can be active per job"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ('job_posting', 'name')
ordering = ['-created_at']
verbose_name = "Application Form"
verbose_name_plural = "Application Forms"
def __str__(self):
status = "(Active)" if self.is_active else "(Inactive)"
return f"{self.name} for {self.job_posting.title} {status}"
def clean(self):
"""Ensure only one active form per job"""
if self.is_active:
existing_active = self.job_posting.applicant_forms.filter(
is_active=True
).exclude(pk=self.pk)
if existing_active.exists():
raise ValidationError(
"Only one active application form is allowed per job posting."
)
super().clean()
def activate(self):
"""Set this form as active and deactivate others"""
self.is_active = True
self.save()
# Deactivate other forms
self.job_posting.applicant_forms.exclude(pk=self.pk).update(
is_active=False
)
def get_public_url(self):
"""Returns the public application URL for this job's active form"""
return reverse('applicant:apply_form', args=[self.job_posting.internal_job_id])
class FormField(models.Model):
FIELD_TYPES = [
('text', 'Text'),
('email', 'Email'),
('phone', 'Phone'),
('number', 'Number'),
('date', 'Date'),
('select', 'Dropdown'),
('radio', 'Radio Buttons'),
('checkbox', 'Checkbox'),
('textarea', 'Paragraph Text'),
('file', 'File Upload'),
('image', 'Image Upload'),
]
form = models.ForeignKey(
ApplicantForm,
related_name='fields',
on_delete=models.CASCADE
)
label = models.CharField(max_length=255)
field_type = models.CharField(max_length=20, choices=FIELD_TYPES)
required = models.BooleanField(default=True)
help_text = models.TextField(blank=True)
choices = models.TextField(
blank=True,
help_text="Comma-separated options for select/radio fields"
)
order = models.IntegerField(default=0)
field_name = models.CharField(max_length=100, blank=True)
class Meta:
ordering = ['order']
verbose_name = "Form Field"
verbose_name_plural = "Form Fields"
def __str__(self):
return f"{self.label} ({self.field_type}) in {self.form.name}"
def save(self, *args, **kwargs):
if not self.field_name:
# Create a stable field name from label (e.g., "Full Name" → "full_name")
import re
# Use Unicode word characters, including Arabic, for field_name
self.field_name = re.sub(
r'[^\w]+',
'_',
self.label.lower(),
flags=re.UNICODE
).strip('_')
# Ensure uniqueness within the form
base_name = self.field_name
counter = 1
while FormField.objects.filter(
form=self.form,
field_name=self.field_name
).exists():
self.field_name = f"{base_name}_{counter}"
counter += 1
super().save(*args, **kwargs)
class ApplicantSubmission(models.Model):
job_posting = models.ForeignKey(JobPosting, on_delete=models.CASCADE)
form = models.ForeignKey(ApplicantForm, on_delete=models.CASCADE)
submitted_at = models.DateTimeField(auto_now_add=True)
data = models.JSONField()
ip_address = models.GenericIPAddressField(null=True, blank=True)
score = models.FloatField(
default=0,
help_text="Ranking score for the applicant submission"
)
class Meta:
ordering = ['-submitted_at']
verbose_name = "Applicant Submission"
verbose_name_plural = "Applicant Submissions"
def __str__(self):
return f"Submission for {self.job_posting.title} at {self.submitted_at}"

View File

@ -0,0 +1,94 @@
{% extends 'base.html' %}
{% block title %}
Apply: {{ job.title }}
{% endblock %}
{% block content %}
<div class="container my-5">
<div class="row justify-content-center">
<div class="col-lg-8">
{# --- 1. Job Header and Overview (Fixed/Static Info) --- #}
<div class="card bg-light-subtle mb-4 p-4 border-0 rounded-3 shadow-sm">
<h1 class="h2 fw-bold text-primary mb-1">{{ job.title }}</h1>
<p class="mb-3 text-muted">
Your final step to apply for this position.
</p>
<div class="d-flex gap-4 small text-secondary">
<div>
<i class="fas fa-building me-1"></i>
<strong>Department:</strong> {{ job.department|default:"Not specified" }}
</div>
<div>
<i class="fas fa-map-marker-alt me-1"></i>
<strong>Location:</strong> {{ job.get_location_display }}
</div>
<div>
<i class="fas fa-briefcase me-1"></i>
<strong>Type:</strong> {{ job.get_job_type_display }} • {{ job.get_workplace_type_display }}
</div>
</div>
</div>
{# --- 2. Application Form Section --- #}
<div class="card p-5 border-0 rounded-3 shadow">
<h2 class="h3 fw-semibold mb-3">Application Details</h2>
{% if applicant_form.description %}
<p class="text-muted mb-4">{{ applicant_form.description }}</p>
{% endif %}
<form method="post" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="form-group mb-4">
{# Label Tag #}
<label for="{{ field.id_for_label }}" class="form-label">
{{ field.label }}
{% if field.field.required %}<span class="text-danger">*</span>{% endif %}
</label>
{# The Field Widget (Assumes form-control is applied in backend) #}
{{ field }}
{# Field Errors #}
{% if field.errors %}
<div class="invalid-feedback d-block">{{ field.errors }}</div>
{% endif %}
{# Help Text #}
{% if field.help_text %}
<div class="form-text">{{ field.help_text }}</div>
{% endif %}
</div>
{% endfor %}
{# General Form Errors (Non-field errors) #}
{% if form.non_field_errors %}
<div class="alert alert-danger mb-4">
{{ form.non_field_errors }}
</div>
{% endif %}
<button type="submit" class="btn btn-primary btn-lg mt-3 w-100">
<i class="fas fa-paper-plane me-2"></i> Submit Application
</button>
</form>
</div>
<footer class="mt-4 text-center">
<a href="{% url 'applicant:review_job_detail' job.internal_job_id %}"
class="btn btn-link text-secondary">
&larr; Review Job Details
</a>
</footer>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,68 @@
{% extends 'base.html' %}
{% block title %}
Define Form for {{ job.title }}
{% endblock %}
{% block content %}
<div class="container my-5">
<div class="row justify-content-center">
<div class="col-lg-8 col-md-10">
<div class="card shadow-lg border-0 p-4 p-md-5">
<h2 class="card-title text-center mb-4 text-primary">
🛠️ New Application Form Configuration
</h2>
<p class="text-center text-muted mb-4 border-bottom pb-3">
You are creating a new form structure for job: <strong>{{ job.title }}</strong>
</p>
<form method="post" novalidate>
{% csrf_token %}
<fieldset class="mb-5">
<legend class="h5 mb-3 text-secondary">Form Metadata</legend>
<div class="form-group mb-4">
<label for="{{ form.name.id_for_label }}" class="form-label required">
Form Name
</label>
{# The field should already have form-control applied from the backend #}
{{ form.name }}
{% if form.name.errors %}
<div class="text-danger small mt-1">{{ form.name.errors }}</div>
{% endif %}
</div>
<div class="form-group mb-4">
<label for="{{ form.description.id_for_label }}" class="form-label">
Description
</label>
{# The field should already have form-control applied from the backend #}
{{ form.description}}
{% if form.description.errors %}
<div class="text-danger small mt-1">{{ form.description.errors }}</div>
{% endif %}
</div>
</fieldset>
<div class="d-flex justify-content-end gap-3 pt-3">
<a href="{% url 'applicant:job_forms_list' job.internal_job_id %}"
class="btn btn-outline-secondary">
Cancel
</a>
<button type="submit" class="btn btn-primary btn-lg">
Create Form & Continue &rarr;
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,97 @@
{% extends 'base.html' %}
{% block title %}
Manage Forms | {{ job.title }}
{% endblock %}
{% block content %}
<div class="container my-5">
<div class="row justify-content-center">
<div class="col-lg-10">
<header class="mb-4 pb-3 border-bottom d-flex justify-content-between align-items-center">
<div>
<h2 class="h3 mb-1">
Application Forms for <span class="text-primary">"{{ job.title }}"</span>
</h2>
<p class="text-muted small">
Job ID: {{ job.internal_job_id }}
</p>
</div>
{# Primary Action Button is placed prominently in the header #}
<a href="{% url 'applicant:create_form' job_id=job.internal_job_id %}"
class="btn btn-success btn-lg shadow-sm">
+ Create New Form
</a>
</header>
{% if forms %}
<div class="list-group">
{% for form in forms %}
{# Use a list-group-item for modern list styling #}
<div class="list-group-item d-flex justify-content-between align-items-center p-3 mb-3 border rounded shadow-sm
{% if form.is_active %}list-group-item-primary border-primary{% endif %}">
{# Left Section: Form Details #}
<div class="flex-grow-1 me-4">
<h4 class="mb-1 d-inline-block">
{{ form.name }}
</h4>
{# Status Badge #}
{% if form.is_active %}
<span class="badge bg-success ms-2">Active Form</span>
{% else %}
<span class="badge bg-secondary ms-2">Inactive</span>
{% endif %}
<p class="text-muted mt-1 mb-1 small">
{{ form.description|default:"— No description provided. —" }}
</p>
</div>
{# Right Section: Actions #}
<div class="d-flex gap-2 align-items-center">
{# Always allow editing #}
<a href="{% url 'applicant:edit_form' form.id %}"
class="btn btn-sm btn-outline-info">
Edit Structure
</a>
{# Conditional Activation Button #}
{% if not form.is_active %}
<a href="{% url 'applicant:activate_form' form.id %}"
class="btn btn-sm btn-success">
Activate Form
</a>
{% else %}
{# Placeholder or Deactivate button if needed #}
<button class="btn btn-sm btn-outline-dark" disabled>Currently Active</button>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info text-center mt-5 p-4">
<p class="lead mb-0">No application forms have been created yet for this job.</p>
<p class="mt-2 mb-0">Click the button above to define a new form structure.</p>
</div>
{% endif %}
<footer class="text-end mt-5">
<a href="{% url 'jobs:job_detail' job.internal_job_id %}"
class="btn btn-outline-secondary">
&larr; Back to Job Details
</a>
</footer>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,129 @@
{% extends "base.html" %}
{% block title %}{{ job.title }} - University ATS{% endblock %}
{% block content %}
<div class="row mb-5">
<div class="col-lg-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2>{{ job.title }}</h2>
<span class="badge bg-{{ job.status|lower }} status-badge">
{{ job.get_status_display }}
</span>
</div>
<div class="card-body">
<!-- Job Details -->
<div class="row mb-3">
<div class="col-md-6">
<strong>Department:</strong> {{ job.department|default:"Not specified" }}
</div>
<div class="col-md-6">
<strong>Position Number:</strong> {{ job.position_number|default:"Not specified" }}
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<strong>Job Type:</strong> {{ job.get_job_type_display }}
</div>
<div class="col-md-6">
<strong>Workplace:</strong> {{ job.get_workplace_type_display }}
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<strong>Location:</strong> {{ job.get_location_display }}
</div>
<div class="col-md-6">
<strong>Created By:</strong> {{ job.created_by|default:"Not specified" }}
</div>
</div>
{% if job.salary_range %}
<div class="row mb-3">
<div class="col-12">
<strong>Salary Range:</strong> {{ job.salary_range }}
</div>
</div>
{% endif %}
{% if job.start_date %}
<div class="row mb-3">
<div class="col-12">
<strong>Start Date:</strong> {{ job.start_date }}
</div>
</div>
{% endif %}
{% if job.application_deadline %}
<div class="row mb-3">
<div class="col-12">
<strong>Application Deadline:</strong> {{ job.application_deadline }}
{% if job.is_expired %}
<span class="badge bg-danger">EXPIRED</span>
{% endif %}
</div>
</div>
{% endif %}
<!-- Description -->
{% if job.description %}
<div class="mb-3">
<h5>Description</h5>
<div>{{ job.description|linebreaks }}</div>
</div>
{% endif %}
{% if job.qualifications %}
<div class="mb-3">
<h5>Qualifications</h5>
<div>{{ job.qualifications|linebreaks }}</div>
</div>
{% endif %}
{% if job.benefits %}
<div class="mb-3">
<h5>Benefits</h5>
<div>{{ job.benefits|linebreaks }}</div>
</div>
{% endif %}
{% if job.application_instructions %}
<div class="mb-3">
<h5>Application Instructions</h5>
<div>{{ job.application_instructions|linebreaks }}</div>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-lg-4">
<!-- Add this section below your existing job details -->
<div class="card mt-4">
<div class="card-header bg-success text-white">
<h5><i class="fas fa-file-signature"></i> Ready to Apply?</h5>
</div>
<div class="card-body">
<p>Review the job details on the left, then click the button below to submit your application.</p>
<a href="{% url 'applicant:apply_form' job.internal_job_id %}" class="btn btn-success btn-lg w-100">
<i class="fas fa-paper-plane"></i> Apply for this Position
</a>
<p class="text-muted mt-2">
<small>You'll be redirected to our secure application form where you can upload your resume and provide additional details.</small>
</p>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,35 @@
{% extends 'base.html' %}
{% block title %}Application Submitted - {{ job.title }}{% endblock %}
{% block content %}
<div class="card">
<div style="text-align: center; padding: 30px 0;">
<div style="width: 80px; height: 80px; background: #d4edda; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 20px;">
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="#28a745" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
</svg>
</div>
<h1 style="color: #28a745; margin-bottom: 15px;">Thank You!</h1>
<h2>Your application has been submitted successfully</h2>
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 25px 0; text-align: left;">
<p><strong>Position:</strong> {{ job.title }}</p>
<p><strong>Job ID:</strong> {{ job.internal_job_id }}</p>
<p><strong>Department:</strong> {{ job.department|default:"Not specified" }}</p>
{% if job.application_deadline %}
<p><strong>Application Deadline:</strong> {{ job.application_deadline|date:"F j, Y" }}</p>
{% endif %}
</div>
<p style="font-size: 18px; line-height: 1.6;">
We appreciate your interest in joining our team. Our hiring team will review your application
and contact you if there's a potential match for this position.
</p>
{% comment %} <div style="margin-top: 30px;">
<a href="/" class="btn btn-primary" style="margin-right: 10px;">Apply to Another Position</a>
<a href="{% url 'jobs:job_detail' job.internal_job_id %}" class="btn btn-outline">View Job Details</a>
</div> {% endcomment %}
</div>
</div>
{% endblock %}

View File

View File

@ -0,0 +1,24 @@
import json
from django import template
register = template.Library()
@register.filter(name='from_json')
def from_json(json_string):
"""
Safely loads a JSON string into a Python object (list or dict).
"""
try:
# The JSON string comes from the context and needs to be parsed
return json.loads(json_string)
except (TypeError, json.JSONDecodeError):
# Handle cases where the string is invalid or None/empty
return []
@register.filter(name='split')
def split_string(value, key=None):
"""Splits a string by the given key (default is space)."""
if key is None:
return value.split()
return value.split(key)

3
applicant/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

18
applicant/urls.py Normal file
View File

@ -0,0 +1,18 @@
from django.urls import path
from . import views
app_name = 'applicant'
urlpatterns = [
# Form Management
path('job/<str:job_id>/forms/', views.job_forms_list, name='job_forms_list'),
path('job/<str:job_id>/forms/create/', views.create_form_for_job, name='create_form'),
path('form/<int:form_id>/edit/', views.edit_form, name='edit_form'),
path('field/<int:field_id>/delete/', views.delete_field, name='delete_field'),
path('form/<int:form_id>/activate/', views.activate_form, name='activate_form'),
# Public Application
path('apply/<str:job_id>/', views.apply_form_view, name='apply_form'),
path('review/job/detail/<str:job_id>/',views.review_job_detail, name="review_job_detail"),
path('apply/<str:job_id>/thank-you/', views.thank_you_view, name='thank_you'),
]

175
applicant/views.py Normal file
View File

@ -0,0 +1,175 @@
# applicant/views.py (Updated edit_form function)
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib import messages
from django.http import Http404, JsonResponse # <-- Import JsonResponse
from django.views.decorators.csrf import csrf_exempt # <-- Needed for JSON POST if not using FormData
import json # <-- Import json
from django.db import transaction # <-- Import transaction
# (Keep all your existing imports)
from .models import ApplicantForm, FormField, ApplicantSubmission
from .forms import ApplicantFormCreateForm, FormFieldForm
from jobs.models import JobPosting
from .forms_builder import create_dynamic_form
# ... (Keep all other functions like job_forms_list, create_form_for_job, etc.)
# ...
# === FORM MANAGEMENT VIEWS ===
def job_forms_list(request, job_id):
"""List all forms for a specific job"""
job = get_object_or_404(JobPosting, internal_job_id=job_id)
forms = job.applicant_forms.all()
return render(request, 'applicant/job_forms_list.html', {
'job': job,
'forms': forms
})
def create_form_for_job(request, job_id):
"""Create a new form for a job"""
job = get_object_or_404(JobPosting, internal_job_id=job_id)
if request.method == 'POST':
form = ApplicantFormCreateForm(request.POST)
if form.is_valid():
applicant_form = form.save(commit=False)
applicant_form.job_posting = job
applicant_form.save()
messages.success(request, 'Form created successfully!')
return redirect('applicant:job_forms_list', job_id=job_id)
else:
form = ApplicantFormCreateForm()
return render(request, 'applicant/create_form.html', {
'job': job,
'form': form
})
@transaction.atomic # Ensures all fields are saved or none are
def edit_form(request, form_id):
"""Edit form details and manage fields, including dynamic builder save."""
applicant_form = get_object_or_404(ApplicantForm, id=form_id)
job = applicant_form.job_posting
if request.method == 'POST':
# --- 1. Handle JSON data from the Form Builder (JavaScript) ---
if request.content_type == 'application/json':
try:
field_data = json.loads(request.body)
# Clear existing fields for this form
applicant_form.fields.all().delete()
# Create new fields from the JSON data
for field_config in field_data:
# Sanitize/ensure required fields are present
FormField.objects.create(
form=applicant_form,
label=field_config.get('label', 'New Field'),
field_type=field_config.get('field_type', 'text'),
required=field_config.get('required', True),
help_text=field_config.get('help_text', ''),
choices=field_config.get('choices', ''),
order=field_config.get('order', 0),
# field_name will be auto-generated/re-generated on save() if needed
)
return JsonResponse({'status': 'success', 'message': 'Form structure saved successfully!'})
except json.JSONDecodeError:
return JsonResponse({'status': 'error', 'message': 'Invalid JSON data.'}, status=400)
except Exception as e:
return JsonResponse({'status': 'error', 'message': f'Server error: {str(e)}'}, status=500)
# --- 2. Handle standard POST requests (e.g., saving form details) ---
elif 'save_form_details' in request.POST: # Changed the button name for clarity
form_details = ApplicantFormCreateForm(request.POST, instance=applicant_form)
if form_details.is_valid():
form_details.save()
messages.success(request, 'Form details updated successfully!')
return redirect('applicant:edit_form', form_id=form_id)
# Note: The 'add_field' branch is now redundant since we use the builder,
# but you can keep it if you want the old manual way too.
# --- GET Request (or unsuccessful POST) ---
form_details = ApplicantFormCreateForm(instance=applicant_form)
# Get initial fields to load into the JS builder
initial_fields_json = list(applicant_form.fields.values(
'label', 'field_type', 'required', 'help_text', 'choices', 'order', 'field_name'
))
return render(request, 'applicant/edit_form.html', {
'applicant_form': applicant_form,
'job': job,
'form_details': form_details,
'initial_fields_json': json.dumps(initial_fields_json)
})
def delete_field(request, field_id):
"""Delete a form field"""
field = get_object_or_404(FormField, id=field_id)
form_id = field.form.id
field.delete()
messages.success(request, 'Field deleted successfully!')
return redirect('applicant:edit_form', form_id=form_id)
def activate_form(request, form_id):
"""Activate a form (deactivates others automatically)"""
applicant_form = get_object_or_404(ApplicantForm, id=form_id)
applicant_form.activate()
messages.success(request, f'Form "{applicant_form.name}" is now active!')
return redirect('applicant:job_forms_list', job_id=applicant_form.job_posting.internal_job_id)
# === PUBLIC VIEWS (for applicants) ===
def apply_form_view(request, job_id):
"""Public application form - serves active form"""
job = get_object_or_404(JobPosting, internal_job_id=job_id, status='ACTIVE')
if job.is_expired():
raise Http404("Application deadline has passed")
try:
applicant_form = job.applicant_forms.get(is_active=True)
except ApplicantForm.DoesNotExist:
raise Http404("No active application form configured for this job")
DynamicForm = create_dynamic_form(applicant_form)
if request.method == 'POST':
form = DynamicForm(request.POST)
if form.is_valid():
ApplicantSubmission.objects.create(
job_posting=job,
form=applicant_form,
data=form.cleaned_data,
ip_address=request.META.get('REMOTE_ADDR')
)
return redirect('applicant:thank_you', job_id=job_id)
else:
form = DynamicForm()
return render(request, 'applicant/apply_form.html', {
'form': form,
'job': job,
'applicant_form': applicant_form
})
def review_job_detail(request,job_id):
"""Public job detail view for applicants"""
job = get_object_or_404(JobPosting, internal_job_id=job_id, status='ACTIVE')
if job.is_expired():
raise Http404("This job posting has expired.")
return render(request,'applicant/review_job_detail.html',{'job':job})
def thank_you_view(request, job_id):
job = get_object_or_404(JobPosting, internal_job_id=job_id)
return render(request, 'applicant/thank_you.html', {'job': job})

BIN
db.sqlite3 Normal file

Binary file not shown.

0
jobs/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
jobs/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
jobs/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class JobsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'jobs'

215
jobs/forms.py Normal file
View File

@ -0,0 +1,215 @@
from django import forms
from . import models
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError # Correct import for the exception
from .validators import validate_hash_tags
class JobPostingForm(forms.ModelForm):
"""Form for creating and editing job postings"""
class Meta:
model = models.JobPosting
fields = [
'title', 'department', 'job_type', 'workplace_type',
'location_city', 'location_state', 'location_country',
'description', 'qualifications', 'salary_range', 'benefits',
'application_url', 'application_deadline', 'application_instructions',
'position_number', 'reporting_to', 'start_date', 'status',
'created_by','open_positions','hash_tags'
]
widgets = {
# Basic Information
'title': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Assistant Professor of Computer Science',
'required': True
}),
'department': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Computer Science, Human Resources, etc.'
}),
'job_type': forms.Select(attrs={
'class': 'form-select',
'required': True
}),
'workplace_type': forms.Select(attrs={
'class': 'form-select',
'required': True
}),
# Location
'location_city': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Boston'
}),
'location_state': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'MA'
}),
'location_country': forms.TextInput(attrs={
'class': 'form-control',
'value': 'United States'
}),
# Job Details
'description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 6,
'placeholder': 'Provide a comprehensive description of the role, responsibilities, and expectations...',
'required': True
}),
'qualifications': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'List required qualifications, skills, education, and experience...'
}),
'salary_range': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '$60,000 - $80,000'
}),
'benefits': forms.Textarea(attrs={
'class': 'form-control',
'rows': 2,
'placeholder': 'Health insurance, retirement plans, tuition reimbursement, etc.'
}),
# Application Information
'application_url': forms.URLInput(attrs={
'class': 'form-control',
'placeholder': 'https://university.edu/careers/job123',
'required': True
}),
'application_deadline': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
}),
'application_instructions': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Special instructions for applicants (e.g., required documents, reference requirements, etc.)'
}),
'open_positions': forms.NumberInput(attrs={
'class': 'form-control',
'min': 1,
'placeholder': 'Number of open positions'
}),
'hash_tags': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '#hiring,#jobopening',
'validators':validate_hash_tags,
}),
# Internal Information
'position_number': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'UNIV-2025-001'
}),
'reporting_to': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Department Chair, Director, etc.'
}),
'start_date': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
}),
'created_by': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'University Administrator'
}),
}
def __init__(self,*args,**kwargs):
# Extract your custom argument BEFORE calling super()
self.is_anonymous_user = kwargs.pop('is_anonymous_user', False)
# Now call the parent __init__ with remaining args
super().__init__(*args, **kwargs)
if not self.instance.pk:# Creating new job posting
if not self.is_anonymous_user:
self.fields['created_by'].initial = 'University Administrator'
self.fields['status'].initial = 'Draft'
self.fields['location_city'].initial='Riyadh'
self.fields['location_state'].initial='Riyadh Province'
self.fields['location_country'].initial='Saudi Arabia'
def clean_hash_tags(self):
hash_tags=self.cleaned_data.get('hash_tags')
if hash_tags:
tags=[tag.strip() for tag in hash_tags.split(',') if tag.strip()]
for tag in tags:
if not tag.startswith('#'):
raise forms.ValidationError("Each hashtag must start with '#' symbol and must be comma(,) sepearted.")
return ','.join(tags)
return hash_tags # Allow blank
def clean_title(self):
title=self.cleaned_data.get('title')
if not title or len(title.strip())<3:
raise forms.ValidationError("Job title must be at least 3 characters long.")
if len(title)>200:
raise forms.ValidationError("Job title cannot exceed 200 characters.")
return title.strip()
def clean_description(self):
description=self.cleaned_data.get('description')
if not description or len(description.strip())<20:
raise forms.ValidationError("Job description must be at least 20 characters long.")
return description.strip() # to remove leading/trailing whitespace
def clean_application_url(self):
url=self.cleaned_data.get('application_url')
if url:
validator=URLValidator()
try:
validator(url)
except forms.ValidationError:
raise forms.ValidationError('Please enter a valid URL (e.g., https://example.com)')
return url
def clean(self):
"""Cross-field validation"""
cleaned_data = super().clean()
# Validate dates
start_date = cleaned_data.get('start_date')
application_deadline = cleaned_data.get('application_deadline')
# Perform cross-field validation only if both fields have values
if start_date and application_deadline:
if application_deadline > start_date:
self.add_error('application_deadline',
'The application deadline must be set BEFORE the job start date.')
# # Validate that if status is ACTIVE, we have required fields
# status = cleaned_data.get('status')
# if status == 'ACTIVE':
# if not cleaned_data.get('application_url'):
# self.add_error('application_url',
# 'Application URL is required for active jobs.')
# if not cleaned_data.get('description'):
# self.add_error('description',
# 'Job description is required for active jobs.')
return cleaned_data
class PostImageUploadForm(forms.ModelForm):
class Meta:
model=models.PostImageUpload
fields=['linkedinpost_image']
widgets={
'linkedinpost_image':forms.ClearableFileInput(attrs={'class':'form-control','accept':'.png,.jpg,.jpeg'})
}
def clean_linkedinpost_image(self):
linkedinpost_image=self.cleaned_data.get('image')
if linkedinpost_image:
if linkedinpost_image.size>2*1024*1024:#2MB limit
raise forms.ValidationError("Image size should not exceed 2MB.")
return linkedinpost_image

552
jobs/linkedin_service.py Normal file
View File

@ -0,0 +1,552 @@
# jobs/linkedin_service.py
import uuid
from urllib.parse import quote
import requests
import logging
from django.conf import settings
from urllib.parse import urlencode, quote
logger = logging.getLogger(__name__)
class LinkedInService:
def __init__(self):
self.client_id = settings.LINKEDIN_CLIENT_ID
self.client_secret = settings.LINKEDIN_CLIENT_SECRET
self.redirect_uri = settings.LINKEDIN_REDIRECT_URI
self.access_token = None
def get_auth_url(self):
"""Generate LinkedIn OAuth URL"""
params = {
'response_type': 'code',
'client_id': self.client_id,
'redirect_uri': self.redirect_uri,
'scope': 'w_member_social openid profile',
'state': 'university_ats_linkedin'
}
return f"https://www.linkedin.com/oauth/v2/authorization?{urlencode(params)}"
def get_access_token(self, code):
"""Exchange authorization code for access token"""
# This function exchanges LinkedIns temporary authorization code for a usable access token.
url = "https://www.linkedin.com/oauth/v2/accessToken"
data = {
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': self.redirect_uri,
'client_id': self.client_id,
'client_secret': self.client_secret
}
try:
response = requests.post(url, data=data, timeout=60)
response.raise_for_status()
token_data = response.json()
"""
Example response:{
"access_token": "AQXq8HJkLmNpQrStUvWxYz...",
"expires_in": 5184000
}
"""
self.access_token = token_data.get('access_token')
return self.access_token
except Exception as e:
logger.error(f"Error getting access token: {e}")
raise
def get_user_profile(self):
"""Get user profile information"""
if not self.access_token:
raise Exception("No access token available")
url = "https://api.linkedin.com/v2/userinfo"
headers = {'Authorization': f'Bearer {self.access_token}'}
try:
response = requests.get(url, headers=headers, timeout=60)
response.raise_for_status() # Ensure we raise an error for bad responses(4xx, 5xx) and does nothing for 2xx(success)
return response.json() # returns a dict from json response (deserialize)
except Exception as e:
logger.error(f"Error getting user profile: {e}")
raise
def register_image_upload(self, person_urn):
"""Step 1: Register image upload with LinkedIn"""
url = "https://api.linkedin.com/v2/assets?action=registerUpload"
headers = {
'Authorization': f'Bearer {self.access_token}',
'Content-Type': 'application/json',
'X-Restli-Protocol-Version': '2.0.0'
}
payload = {
"registerUploadRequest": {
"recipes": ["urn:li:digitalmediaRecipe:feedshare-image"],
"owner": f"urn:li:person:{person_urn}",
"serviceRelationships": [{
"relationshipType": "OWNER",
"identifier": "urn:li:userGeneratedContent"
}]
}
}
response = requests.post(url, headers=headers, json=payload, timeout=30)
response.raise_for_status()
data = response.json()
return {
'upload_url': data['value']['uploadMechanism']['com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest']['uploadUrl'],
'asset': data['value']['asset']
}
def upload_image_to_linkedin(self, upload_url, image_file):
"""Step 2: Upload actual image file to LinkedIn"""
# Open and read the Django ImageField
image_file.open()
image_content = image_file.read()
image_file.close()
headers = {
'Authorization': f'Bearer {self.access_token}',
}
response = requests.post(upload_url, headers=headers, data=image_content, timeout=60)
response.raise_for_status()
return True
def create_job_post(self, job_posting):
"""Create a job announcement post on LinkedIn (with image support)"""
if not self.access_token:
raise Exception("Not authenticated with LinkedIn")
try:
# Get user profile for person URN
profile = self.get_user_profile()
person_urn = profile.get('sub')
if not person_urn:
raise Exception("Could not retrieve LinkedIn user ID")
# Check if job has an image
try:
image_upload = job_posting.files.first()
has_image = image_upload and image_upload.linkedinpost_image
except Exception:
has_image = False
if has_image:
# === POST WITH IMAGE ===
try:
# Step 1: Register image upload
upload_info = self.register_image_upload(person_urn)
# Step 2: Upload image
self.upload_image_to_linkedin(
upload_info['upload_url'],
image_upload.linkedinpost_image
)
# Step 3: Create post with image
return self.create_job_post_with_image(
job_posting,
image_upload.linkedinpost_image,
person_urn,
upload_info['asset']
)
except Exception as e:
logger.error(f"Image upload failed: {e}")
# Fall back to text-only post if image upload fails
has_image = False
# === FALLBACK TO URL/ARTICLE POST ===
# Add unique timestamp to prevent duplicates
from django.utils import timezone
import random
unique_suffix = f"\n\nPosted: {timezone.now().strftime('%b %d, %Y at %I:%M %p')} (ID: {random.randint(1000, 9999)})"
message_parts = [f"🚀 **We're Hiring: {job_posting.title}**"]
if job_posting.department:
message_parts.append(f"**Department:** {job_posting.department}")
if job_posting.description:
message_parts.append(f"\n{job_posting.description}")
details = []
if job_posting.job_type:
details.append(f"💼 {job_posting.get_job_type_display()}")
if job_posting.get_location_display() != 'Not specified':
details.append(f"📍 {job_posting.get_location_display()}")
if job_posting.workplace_type:
details.append(f"🏠 {job_posting.get_workplace_type_display()}")
if job_posting.salary_range:
details.append(f"💰 {job_posting.salary_range}")
if details:
message_parts.append("\n" + " | ".join(details))
if job_posting.application_url:
message_parts.append(f"\n🔗 **Apply now:** {job_posting.application_url}")
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\n" + " ".join(hashtags))
message_parts.append(unique_suffix)
message = "\n".join(message_parts)
# 🔥 FIX URL - REMOVE TRAILING SPACES 🔥
url = "https://api.linkedin.com/v2/ugcPosts"
headers = {
'Authorization': f'Bearer {self.access_token}',
'Content-Type': 'application/json',
'X-Restli-Protocol-Version': '2.0.0'
}
payload = {
"author": f"urn:li:person:{person_urn}",
"lifecycleState": "PUBLISHED",
"specificContent": {
"com.linkedin.ugc.ShareContent": {
"shareCommentary": {"text": message},
"shareMediaCategory": "ARTICLE",
"media": [{
"status": "READY",
"description": {"text": f"Apply for {job_posting.title} at our university!"},
"originalUrl": job_posting.application_url,
"title": {"text": job_posting.title}
}]
}
},
"visibility": {
"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
}
}
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 ""
return {
'success': True,
'post_id': post_id,
'post_url': post_url,
'status_code': response.status_code
}
except Exception as e:
logger.error(f"Error creating LinkedIn post: {e}")
return {
'success': False,
'error': str(e),
'status_code': getattr(e.response, 'status_code', 500) if hasattr(e, 'response') else 500
}
# def create_job_post_with_image(self, job_posting, image_file, person_urn, asset_urn):
# """Step 3: Create post with uploaded image"""
# url = "https://api.linkedin.com/v2/ugcPosts"
# headers = {
# 'Authorization': f'Bearer {self.access_token}',
# 'Content-Type': 'application/json',
# 'X-Restli-Protocol-Version': '2.0.0'
# }
# # Build the same message as before
# message_parts = [f"🚀 **We're Hiring: {job_posting.title}**"]
# if job_posting.department:
# message_parts.append(f"**Department:** {job_posting.department}")
# if job_posting.description:
# message_parts.append(f"\n{job_posting.description}")
# details = []
# if job_posting.job_type:
# details.append(f"💼 {job_posting.get_job_type_display()}")
# if job_posting.get_location_display() != 'Not specified':
# details.append(f"📍 {job_posting.get_location_display()}")
# if job_posting.workplace_type:
# details.append(f"🏠 {job_posting.get_workplace_type_display()}")
# if job_posting.salary_range:
# details.append(f"💰 {job_posting.salary_range}")
# if details:
# message_parts.append("\n" + " | ".join(details))
# if job_posting.application_url:
# message_parts.append(f"\n🔗 **Apply now:** {job_posting.application_url}")
# 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\n" + " ".join(hashtags))
# message = "\n".join(message_parts)
# # Create image post payload
# payload = {
# "author": f"urn:li:person:{person_urn}",
# "lifecycleState": "PUBLISHED",
# "specificContent": {
# "com.linkedin.ugc.ShareContent": {
# "shareCommentary": {"text": message},
# "shareMediaCategory": "IMAGE",
# "media": [{
# "status": "READY",
# "media": asset_urn
# }]
# }
# },
# "visibility": {
# "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
# }
# }
# response = requests.post(url, headers=headers, json=payload, timeout=30)
# 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 ""
# return {
# 'success': True,
# 'post_id': post_id,
# 'post_url': post_url,
# 'status_code': response.status_code
# }
def hashtags_list(self,hash_tags_str):
"""Convert comma-separated hashtags string to list"""
if not hash_tags_str:
return [""]
tags = [tag.strip() for tag in hash_tags_str.split(',') if tag.strip()]
if not tags:
return ["#HigherEd", "#Hiring", "#FacultyJobs", "#UniversityJobs"]
return tags
# def create_job_post(self, job_posting):
# """Create a job announcement post on LinkedIn (with image support)"""
# if not self.access_token:
# raise Exception("Not authenticated with LinkedIn")
# try:
# # Get user profile for person URN
# profile = self.get_user_profile()
# person_urn = profile.get('sub')
# if not person_urn:
# raise Exception("Could not retrieve LinkedIn user ID")
# # Check if job has an image
# try:
# image_upload = job_posting.files.first()
# has_image = image_upload and image_upload.linkedinpost_image
# except Exception:
# has_image = False
# if has_image:
# # === POST WITH IMAGE ===
# upload_info = self.register_image_upload(person_urn)
# self.upload_image_to_linkedin(
# upload_info['upload_url'],
# image_upload.linkedinpost_image
# )
# return self.create_job_post_with_image(
# job_posting,
# image_upload.linkedinpost_image,
# person_urn,
# upload_info['asset']
# )
# else:
# # === FALLBACK TO URL/ARTICLE POST ===
# # 🔥 ADD UNIQUE TIMESTAMP TO PREVENT DUPLICATES 🔥
# from django.utils import timezone
# import random
# unique_suffix = f"\n\nPosted: {timezone.now().strftime('%b %d, %Y at %I:%M %p')} (ID: {random.randint(1000, 9999)})"
# message_parts = [f"🚀 **We're Hiring: {job_posting.title}**"]
# if job_posting.department:
# message_parts.append(f"**Department:** {job_posting.department}")
# if job_posting.description:
# message_parts.append(f"\n{job_posting.description}")
# details = []
# if job_posting.job_type:
# details.append(f"💼 {job_posting.get_job_type_display()}")
# if job_posting.get_location_display() != 'Not specified':
# details.append(f"📍 {job_posting.get_location_display()}")
# if job_posting.workplace_type:
# details.append(f"🏠 {job_posting.get_workplace_type_display()}")
# if job_posting.salary_range:
# details.append(f"💰 {job_posting.salary_range}")
# if details:
# message_parts.append("\n" + " | ".join(details))
# if job_posting.application_url:
# message_parts.append(f"\n🔗 **Apply now:** {job_posting.application_url}")
# 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\n" + " ".join(hashtags))
# message_parts.append(unique_suffix) # 🔥 Add unique suffix
# message = "\n".join(message_parts)
# # 🔥 FIX URL - REMOVE TRAILING SPACES 🔥
# url = "https://api.linkedin.com/v2/ugcPosts"
# headers = {
# 'Authorization': f'Bearer {self.access_token}',
# 'Content-Type': 'application/json',
# 'X-Restli-Protocol-Version': '2.0.0'
# }
# payload = {
# "author": f"urn:li:person:{person_urn}",
# "lifecycleState": "PUBLISHED",
# "specificContent": {
# "com.linkedin.ugc.ShareContent": {
# "shareCommentary": {"text": message},
# "shareMediaCategory": "ARTICLE",
# "media": [{
# "status": "READY",
# "description": {"text": f"Apply for {job_posting.title} at our university!"},
# "originalUrl": job_posting.application_url,
# "title": {"text": job_posting.title}
# }]
# }
# },
# "visibility": {
# "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
# }
# }
# response = requests.post(url, headers=headers, json=payload, timeout=60)
# response.raise_for_status()
# post_id = response.headers.get('x-restli-id', '')
# # 🔥 FIX POST URL - REMOVE TRAILING SPACES 🔥
# post_url = f"https://www.linkedin.com/feed/update/{quote(post_id)}/" if post_id else ""
# return {
# 'success': True,
# 'post_id': post_id,
# 'post_url': post_url,
# 'status_code': response.status_code
# }
# except Exception as e:
# logger.error(f"Error creating LinkedIn post: {e}")
# return {
# 'success': False,
# 'error': str(e),
# 'status_code': getattr(e.response, 'status_code', 500) if hasattr(e, 'response') else 500
# }
# def create_job_post(self, job_posting):
# """Create a job announcement post on LinkedIn"""
# if not self.access_token:
# raise Exception("Not authenticated with LinkedIn")
# try:
# # Get user profile for person URN
# profile = self.get_user_profile()
# person_urn = profile.get('sub')
# if not person_urn: # uniform resource name used to uniquely identify linked-id for internal systems and apis
# raise Exception("Could not retrieve LinkedIn user ID")
# # Build professional job post message
# message_parts = [f"🚀 **We're Hiring: {job_posting.title}**"]
# if job_posting.department:
# message_parts.append(f"**Department:** {job_posting.department}")
# if job_posting.description:
# message_parts.append(f"\n{job_posting.description}")
# # Add job details
# details = []
# if job_posting.job_type:
# details.append(f"💼 {job_posting.get_job_type_display()}")
# if job_posting.get_location_display() != 'Not specified':
# details.append(f"📍 {job_posting.get_location_display()}")
# if job_posting.workplace_type:
# details.append(f"🏠 {job_posting.get_workplace_type_display()}")
# if job_posting.salary_range:
# details.append(f"💰 {job_posting.salary_range}")
# if details:
# message_parts.append("\n" + " | ".join(details))
# # Add application link
# if job_posting.application_url:
# message_parts.append(f"\n🔗 **Apply now:** {job_posting.application_url}")
# # Add hashtags
# hashtags = ["#HigherEd", "#Hiring", "#FacultyJobs", "#UniversityJobs"]
# if job_posting.department:
# dept_hashtag = f"#{job_posting.department.replace(' ', '')}"
# hashtags.insert(0, dept_hashtag)
# message_parts.append("\n\n" + " ".join(hashtags))
# message = "\n".join(message_parts)
# # Create LinkedIn post
# url = "https://api.linkedin.com/v2/ugcPosts"
# headers = {
# 'Authorization': f'Bearer {self.access_token}',
# 'Content-Type': 'application/json',
# 'X-Restli-Protocol-Version': '2.0.0'
# }
# payload = {
# "author": f"urn:li:person:{person_urn}",
# "lifecycleState": "PUBLISHED",
# "specificContent": {
# "com.linkedin.ugc.ShareContent": {
# "shareCommentary": {"text": message},
# "shareMediaCategory": "ARTICLE",
# "media": [{
# "status": "READY",
# "description": {"text": f"Apply for {job_posting.title} at our university!"},
# "originalUrl": job_posting.application_url,
# "title": {"text": job_posting.title}
# }]
# }
# },
# "visibility": {
# "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
# }
# }
# response = requests.post(url, headers=headers, json=payload, timeout=60)
# response.raise_for_status()
# # Extract post ID from response
# post_id = response.headers.get('x-restli-id', '')
# post_url = f"https://www.linkedin.com/feed/update/{quote(post_id)}/" if post_id else ""
# return {
# 'success': True,
# 'post_id': post_id,
# 'post_url': post_url,
# 'status_code': response.status_code
# }
# except Exception as e:
# logger.error(f"Error creating LinkedIn post: {e}")
# return {
# 'success': False,
# 'error': str(e),
# 'status_code': getattr(e.response, 'status_code', 500) if hasattr(e, 'response') else 500
# }

View File

@ -0,0 +1,69 @@
# Generated by Django 5.2.6 on 2025-10-01 21:41
import django.core.validators
import django.db.models.deletion
import jobs.models
import jobs.validators
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='JobPosting',
fields=[
('title', models.CharField(max_length=200)),
('department', models.CharField(blank=True, max_length=100)),
('job_type', models.CharField(choices=[('FULL_TIME', 'Full-time'), ('PART_TIME', 'Part-time'), ('CONTRACT', 'Contract'), ('INTERNSHIP', 'Internship'), ('FACULTY', 'Faculty'), ('TEMPORARY', 'Temporary')], default='FULL_TIME', max_length=20)),
('workplace_type', models.CharField(choices=[('ON_SITE', 'On-site'), ('REMOTE', 'Remote'), ('HYBRID', 'Hybrid')], default='ON_SITE', max_length=20)),
('location_city', models.CharField(blank=True, max_length=100)),
('location_state', models.CharField(blank=True, max_length=100)),
('location_country', models.CharField(default='United States', max_length=100)),
('description', models.TextField(help_text='Full job description including responsibilities and requirements')),
('qualifications', models.TextField(blank=True, help_text='Required qualifications and skills')),
('salary_range', models.CharField(blank=True, help_text='e.g., $60,000 - $80,000', max_length=100)),
('benefits', models.TextField(blank=True, help_text='Benefits offered')),
('application_url', models.URLField(help_text='URL where candidates apply', validators=[django.core.validators.URLValidator()])),
('application_deadline', models.DateField(blank=True, null=True)),
('application_instructions', models.TextField(blank=True, help_text='Special instructions for applicants')),
('internal_job_id', models.CharField(editable=False, max_length=50, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_by', models.CharField(blank=True, help_text='Name of person who created this job', max_length=100)),
('status', models.CharField(choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('ARCHIVED', 'Archived')], default='DRAFT', max_length=20)),
('linkedin_post_id', models.CharField(blank=True, help_text='LinkedIn post ID after posting', max_length=200)),
('linkedin_post_url', models.URLField(blank=True, help_text='Direct URL to LinkedIn post')),
('posted_to_linkedin', models.BooleanField(default=False)),
('linkedin_post_status', models.CharField(blank=True, help_text='Status of LinkedIn posting', max_length=50)),
('linkedin_posted_at', models.DateTimeField(blank=True, null=True)),
('position_number', models.CharField(blank=True, help_text='University position number', max_length=50)),
('reporting_to', models.CharField(blank=True, help_text='Who this position reports to', max_length=100)),
('start_date', models.DateField(blank=True, help_text='Desired start date', null=True)),
],
options={
'verbose_name': 'Job Posting',
'verbose_name_plural': 'Job Postings',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='PostImageUpload',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('linkedinpost_image', models.ImageField(blank=True, help_text='Image file (Max size: 2MB). Accepted formats: .png, .jpg, .jpeg', null=True, upload_to=jobs.models.image_file_name, validators=[jobs.validators.validate_image_size])),
('uploaded_at', models.DateTimeField(auto_now_add=True)),
('job_posting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='jobs.jobposting')),
],
options={
'verbose_name': 'Job File',
'verbose_name_plural': 'Job Files',
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.6 on 2025-10-01 21:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('jobs', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='jobposting',
name='open_positions',
field=models.PositiveIntegerField(default=1, help_text='Number of open positions for this job'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.6 on 2025-10-01 23:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('jobs', '0002_jobposting_open_positions'),
]
operations = [
migrations.AddField(
model_name='jobposting',
name='hash_tags',
field=models.CharField(blank=True, help_text='Comma-separated hashtags for linkedin post like #hiring,#jobopening', max_length=200),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 5.2.6 on 2025-10-02 10:07
import jobs.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('jobs', '0003_jobposting_hash_tags'),
]
operations = [
migrations.AlterField(
model_name='jobposting',
name='hash_tags',
field=models.CharField(blank=True, help_text='Comma-separated hashtags for linkedin post like #hiring,#jobopening', max_length=200, validators=[jobs.validators.validate_hash_tags]),
),
migrations.AlterField(
model_name='jobposting',
name='salary_range',
field=models.CharField(blank=True, help_text='e.g., $60,000 - $80,000', max_length=200),
),
]

View File

Binary file not shown.

188
jobs/models.py Normal file
View File

@ -0,0 +1,188 @@
# jobs/models.py
from django.db import models
from django.utils import timezone
from django.core.validators import URLValidator
from .validators import validate_image_size, validate_hash_tags
import uuid
import os
class JobPosting(models.Model):
# Basic Job Information
JOB_TYPES = [
('FULL_TIME', 'Full-time'),
('PART_TIME', 'Part-time'),
('CONTRACT', 'Contract'),
('INTERNSHIP', 'Internship'),
('FACULTY', 'Faculty'),
('TEMPORARY', 'Temporary'),
]
WORKPLACE_TYPES = [
('ON_SITE', 'On-site'),
('REMOTE', 'Remote'),
('HYBRID', 'Hybrid'),
]
# Core Fields
title = models.CharField(max_length=200)
department = models.CharField(max_length=100, blank=True)
job_type = models.CharField(max_length=20, choices=JOB_TYPES, default='FULL_TIME')
workplace_type = models.CharField(max_length=20, choices=WORKPLACE_TYPES, default='ON_SITE')
# Location
location_city = models.CharField(max_length=100, blank=True)
location_state = models.CharField(max_length=100, blank=True)
location_country = models.CharField(max_length=100, default='United States')
# Job Details
description = models.TextField(help_text="Full job description including responsibilities and requirements")
qualifications = models.TextField(blank=True, help_text="Required qualifications and skills")
salary_range = models.CharField(max_length=200, blank=True, help_text="e.g., $60,000 - $80,000")
benefits = models.TextField(blank=True, help_text="Benefits offered")
# Application Information
application_url = models.URLField(validators=[URLValidator()], help_text="URL where candidates apply")
application_deadline = models.DateField(null=True, blank=True)
application_instructions = models.TextField(blank=True, help_text="Special instructions for applicants")
# Internal Tracking
internal_job_id = models.CharField(max_length=50, primary_key=True, editable=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.CharField(max_length=100, blank=True, help_text="Name of person who created this job")
# Status Fields
STATUS_CHOICES = [
('DRAFT', 'Draft'),
('ACTIVE', 'Active'),
('CLOSED', 'Closed'),
('ARCHIVED', 'Archived'),
]
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='DRAFT')
#hashtags for social media
hash_tags = models.CharField(max_length=200, blank=True, help_text="Comma-separated hashtags for linkedin post like #hiring,#jobopening",validators=[validate_hash_tags])
# LinkedIn Integration Fields
linkedin_post_id = models.CharField(max_length=200, blank=True, help_text="LinkedIn post ID after posting")
linkedin_post_url = models.URLField(blank=True, help_text="Direct URL to LinkedIn post")
posted_to_linkedin = models.BooleanField(default=False)
linkedin_post_status = models.CharField(max_length=50, blank=True, help_text="Status of LinkedIn posting")
linkedin_posted_at = models.DateTimeField(null=True, blank=True)
# University Specific Fields
position_number = models.CharField(max_length=50, blank=True, help_text="University position number")
reporting_to = models.CharField(max_length=100, blank=True, help_text="Who this position reports to")
start_date = models.DateField(null=True, blank=True, help_text="Desired start date")
open_positions = models.PositiveIntegerField(default=1, help_text="Number of open positions for this job")
class Meta:
ordering = ['-created_at']
verbose_name = "Job Posting"
verbose_name_plural = "Job Postings"
def __str__(self):
return f"{self.title} - {self.get_status_display()}"
def save(self, *args, **kwargs):
# Generate unique internal job ID if not exists
if not self.internal_job_id:
prefix = "UNIV"
year = timezone.now().year
# Get next sequential number
last_job = JobPosting.objects.filter(
internal_job_id__startswith=f"{prefix}-{year}-"
).order_by('internal_job_id').last()
if last_job:
last_num = int(last_job.internal_job_id.split('-')[-1])
next_num = last_num + 1
else:
next_num = 1
self.internal_job_id = f"{prefix}-{year}-{next_num:04d}"
super().save(*args, **kwargs)
def get_location_display(self):
"""Return formatted location string"""
parts = []
if self.location_city:
parts.append(self.location_city)
if self.location_state:
parts.append(self.location_state)
if self.location_country and self.location_country != 'United States':
parts.append(self.location_country)
return ', '.join(parts) if parts else 'Not specified'
def is_expired(self):
"""Check if application deadline has passed"""
if self.application_deadline:
return self.application_deadline < timezone.now().date()
return False
def image_file_name(instance, filename):
ext = os.path.splitext(filename)[-1]
new_filename = f'{instance.id}{ext}'
return f'uploads/linkedinpost/images/{new_filename}'
class PostImageUpload(models.Model):
id = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False,
unique=True
)
job_posting = models.ForeignKey(JobPosting, on_delete=models.CASCADE, related_name='files')
linkedinpost_image=models.ImageField(upload_to=image_file_name, blank=True, null=True,validators=[validate_image_size], help_text="Image file (Max size: 2MB). Accepted formats: .png, .jpg, .jpeg")
uploaded_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Job File"
verbose_name_plural = "Job Files"
def __str__(self):
return f"File for {self.job_posting.title}-{self.job_posting.internal_job_id} - {self.file.name}"
# class JobApplication(models.Model):
# # Job reference
# job_posting = models.ForeignKey(JobPosting, on_delete=models.CASCADE,related_name='applications')
# # Personal Information
# first_name = models.CharField(max_length=100)
# last_name = models.CharField(max_length=100)
# email = models.EmailField()
# phone = models.CharField(max_length=20, blank=True)
# # Resume and Documents
# resume = models.FileField(upload_to='resumes/', help_text="PDF or Word document")
# cover_letter = models.FileField(upload_to='cover_letters/', blank=True)
# # Additional Information
# linkedin_profile = models.URLField(blank=True, help_text="Your LinkedIn profile URL")
# portfolio_url = models.URLField(blank=True, help_text="Portfolio or personal website")
# salary_expectations = models.CharField(max_length=100, blank=True)
# availability = models.CharField(max_length=100, blank=True)
# # Application Details
# applied_at = models.DateTimeField(auto_now_add=True)
# status = models.CharField(max_length=20, default='RECEIVED', choices=[
# ('RECEIVED', 'Received'),
# ('UNDER_REVIEW', 'Under Review'),
# ('INTERVIEW', 'Interview Scheduled'),
# ('OFFER', 'Offer Extended'),
# ('REJECTED', 'Rejected'),
# ('HIRED', 'Hired'),
# ])
# class Meta:
# ordering = ['-applied_at']
# verbose_name = "Job Application"
# verbose_name_plural = "Job Applications"
# def __str__(self):
# return f"{self.first_name} {self.last_name} - {self.job_posting.title}"

View File

@ -0,0 +1,103 @@
{% extends "base.html" %}
{% block title %}Apply for {{ job.title }} - University Careers{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h2><i class="fas fa-file-signature"></i> Apply for {{ job.title }}</h2>
<p class="text-muted">{{ job.department }} • {{ job.get_location_display }}</p>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<h4>Personal Information</h4>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">First Name *</label>
{{ form.first_name }}
{% if form.first_name.errors %}<div class="text-danger">{{ form.first_name.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Last Name *</label>
{{ form.last_name }}
{% if form.last_name.errors %}<div class="text-danger">{{ form.last_name.errors }}</div>{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Email *</label>
{{ form.email }}
{% if form.email.errors %}<div class="text-danger">{{ form.email.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Phone</label>
{{ form.phone }}
{% if form.phone.errors %}<div class="text-danger">{{ form.phone.errors }}</div>{% endif %}
</div>
</div>
</div>
<h4 class="mt-4">Documents</h4>
<div class="mb-3">
<label class="form-label">Resume/CV * (PDF or Word)</label>
{{ form.resume }}
{% if form.resume.errors %}<div class="text-danger">{{ form.resume.errors }}</div>{% endif %}
</div>
<div class="mb-3">
<label class="form-label">Cover Letter (Optional)</label>
{{ form.cover_letter }}
{% if form.cover_letter.errors %}<div class="text-danger">{{ form.cover_letter.errors }}</div>{% endif %}
</div>
<h4 class="mt-4">Additional Information</h4>
<div class="mb-3">
<label class="form-label">LinkedIn Profile</label>
{{ form.linkedin_profile }}
{% if form.linkedin_profile.errors %}<div class="text-danger">{{ form.linkedin_profile.errors }}</div>{% endif %}
</div>
<div class="mb-3">
<label class="form-label">Portfolio/Website</label>
{{ form.portfolio_url }}
{% if form.portfolio_url.errors %}<div class="text-danger">{{ form.portfolio_url.errors }}</div>{% endif %}
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Salary Expectations</label>
{{ form.salary_expectations }}
{% if form.salary_expectations.errors %}<div class="text-danger">{{ form.salary_expectations.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Availability</label>
{{ form.availability }}
{% if form.availability.errors %}<div class="text-danger">{{ form.availability.errors }}</div>{% endif %}
</div>
</div>
</div>
<button type="submit" class="btn btn-primary w-100 mt-3">
<i class="fas fa-paper-plane"></i> Submit Application
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,269 @@
{% extends "base.html" %}
{% block content %}
<form method="post" id="jobForm" class="mb-5">
{% csrf_token %}
<!-- Basic Information Section -->
<div class="card mb-4">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-info-circle"></i> Basic Information</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label for="{{ form.title.id_for_label }}" class="form-label">Job Title <span class="text-danger">*</span></label>
{{ form.title }}
{% if form.title.errors %}
<div class="text-danger mt-1">{{ form.title.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="{{ form.job_type.id_for_label }}" class="form-label">Job Type <span class="text-danger">*</span></label>
{{ form.job_type }}
{% if form.job_type.errors %}
<div class="text-danger mt-1">{{ form.job_type.errors }}</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.department.id_for_label }}" class="form-label">Department</label>
{{ form.department }}
{% if form.department.errors %}
<div class="text-danger mt-1">{{ form.department.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.position_number.id_for_label }}" class="form-label">Position Number</label>
{{ form.position_number }}
{% if form.position_number.errors %}
<div class="text-danger mt-1">{{ form.position_number.errors }}</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.workplace_type.id_for_label }}" class="form-label">Workplace Type <span class="text-danger">*</span></label>
{{ form.workplace_type }}
{% if form.workplace_type.errors %}
<div class="text-danger mt-1">{{ form.workplace_type.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.created_by.id_for_label }}" class="form-label">Created By</label>
{{ form.created_by }}
{% if form.created_by.errors %}
<div class="text-danger mt-1">{{ form.created_by.errors }}</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Location Section -->
<div class="card mb-4">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-map-marker-alt"></i> Location</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="mb-3">
<label for="{{ form.location_city.id_for_label }}" class="form-label">City</label>
{{ form.location_city }}
{% if form.location_city.errors %}
<div class="text-danger mt-1">{{ form.location_city.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="{{ form.location_state.id_for_label }}" class="form-label">State/Province</label>
{{ form.location_state }}
{% if form.location_state.errors %}
<div class="text-danger mt-1">{{ form.location_state.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="{{ form.location_country.id_for_label }}" class="form-label">Country</label>
{{ form.location_country }}
{% if form.location_country.errors %}
<div class="text-danger mt-1">{{ form.location_country.errors }}</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Job Details Section -->
<div class="card mb-4">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-file-alt"></i> Job Details</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label for="{{ form.description.id_for_label }}" class="form-label">Job Description <span class="text-danger">*</span></label>
{{ form.description }}
{% if form.description.errors %}
<div class="text-danger mt-1">{{ form.description.errors }}</div>
{% endif %}
</div>
<div class="mb-3">
<label for="{{ form.qualifications.id_for_label }}" class="form-label">Qualifications and Requirements</label>
{{ form.qualifications }}
{% if form.qualifications.errors %}
<div class="text-danger mt-1">{{ form.qualifications.errors }}</div>
{% endif %}
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.salary_range.id_for_label }}" class="form-label">Salary Range</label>
{{ form.salary_range }}
{% if form.salary_range.errors %}
<div class="text-danger mt-1">{{ form.salary_range.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.benefits.id_for_label }}" class="form-label">Benefits</label>
{{ form.benefits }}
{% if form.benefits.errors %}
<div class="text-danger mt-1">{{ form.benefits.errors }}</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Application Information Section -->
<div class="card mb-4">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-file-signature"></i> Application Information</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label for="{{ form.application_url.id_for_label }}" class="form-label">Application URL <span class="text-danger">*</span></label>
{{ form.application_url }}
{% if form.application_url.errors %}
<div class="text-danger mt-1">{{ form.application_url.errors }}</div>
{% endif %}
<div class="form-text">Full URL where candidates will apply</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.application_deadline.id_for_label }}" class="form-label">Application Deadline</label>
{{ form.application_deadline }}
{% if form.application_deadline.errors %}
<div class="text-danger mt-1">{{ form.application_deadline.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.start_date.id_for_label }}" class="form-label">Desired Start Date</label>
{{ form.start_date }}
{% if form.start_date.errors %}
<div class="text-danger mt-1">{{ form.start_date.errors }}</div>
{% endif %}
</div>
</div>
</div>
<div class="mb-3">
<label for="{{ form.application_instructions.id_for_label }}" class="form-label">Application Instructions</label>
{{ form.application_instructions }}
{% if form.application_instructions.errors %}
<div class="text-danger mt-1">{{ form.application_instructions.errors }}</div>
{% endif %}
</div>
</div>
</div>
<!-- Post Reach -->
<div class="card mb-4">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-star"></i>Post Reach Field</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-12">
<div class="mb-3">
<label for="{{ form.reporting_to.id_for_label }}" class="form-label">Hashtags</label>
{{ form.hash_tags }}
{% if form.hash_tags.errors %}
<div class="text-danger mt-1">{{ form.hash_tags.errors }}</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Internal Information Section -->
<div class="card mb-4">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-building"></i> Internal Information</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.reporting_to.id_for_label }}" class="form-label">Reports To</label>
{{ form.reporting_to }}
{% if form.reporting_to.errors %}
<div class="text-danger mt-1">{{ form.reporting_to.errors }}</div>
{% endif %}
</div>
</div>
{% comment %} <div class="col-md-6">
<div class="mb-3">
<label for="{{ form.open_positions.id_for_label }}" class="form-label">Open Positions</label>
{{ form.open_positions }}
{% if form.open_positions.errors %}
<div class="text-danger mt-1">{{ form.open_positions.errors }}</div>
{% endif %}
</div>
</div> {% endcomment %}
</div>
</div>
</div>
<!-- Form Actions -->
<div class="d-flex justify-content-between">
<a href="{% url 'jobs:job_list' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Cancel
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Create Job
</button>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,259 @@
{% extends "base.html" %}
{% block title %}Edit {{ job.title }} - University ATS{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="card">
<div class="card-header">
<h2><i class="fas fa-edit"></i> Edit Job Posting</h2>
<small class="text-muted">Internal ID: {{ job.internal_job_id }}</small>
</div>
<div class="card-body">
<form method="post" id="jobForm">
{% csrf_token %}
<!-- Basic Information Section -->
<div class="card mb-4">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-info-circle"></i> Basic Information</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label for="{{ form.title.id_for_label }}" class="form-label">Job Title <span class="text-danger">*</span></label>
{{ form.title }}
{% if form.title.errors %}
<div class="text-danger mt-1">{{ form.title.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="{{ form.job_type.id_for_label }}" class="form-label">Job Type <span class="text-danger">*</span></label>
{{ form.job_type }}
{% if form.job_type.errors %}
<div class="text-danger mt-1">{{ form.job_type.errors }}</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.department.id_for_label }}" class="form-label">Department</label>
{{ form.department }}
{% if form.department.errors %}
<div class="text-danger mt-1">{{ form.department.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.position_number.id_for_label }}" class="form-label">Position Number</label>
{{ form.position_number }}
{% if form.position_number.errors %}
<div class="text-danger mt-1">{{ form.position_number.errors }}</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.workplace_type.id_for_label }}" class="form-label">Workplace Type <span class="text-danger">*</span></label>
{{ form.workplace_type }}
{% if form.workplace_type.errors %}
<div class="text-danger mt-1">{{ form.workplace_type.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.created_by.id_for_label }}" class="form-label">Created By</label>
{{ form.created_by }}
{% if form.created_by.errors %}
<div class="text-danger mt-1">{{ form.created_by.errors }}</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Location Section -->
<div class="card mb-4">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-map-marker-alt"></i> Location</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="mb-3">
<label for="{{ form.location_city.id_for_label }}" class="form-label">City</label>
{{ form.location_city }}
{% if form.location_city.errors %}
<div class="text-danger mt-1">{{ form.location_city.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="{{ form.location_state.id_for_label }}" class="form-label">State/Province</label>
{{ form.location_state }}
{% if form.location_state.errors %}
<div class="text-danger mt-1">{{ form.location_state.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="{{ form.location_country.id_for_label }}" class="form-label">Country</label>
{{ form.location_country }}
{% if form.location_country.errors %}
<div class="text-danger mt-1">{{ form.location_country.errors }}</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Job Details Section -->
<div class="card mb-4">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-file-alt"></i> Job Details</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label for="{{ form.description.id_for_label }}" class="form-label">Job Description <span class="text-danger">*</span></label>
{{ form.description }}
{% if form.description.errors %}
<div class="text-danger mt-1">{{ form.description.errors }}</div>
{% endif %}
</div>
<div class="mb-3">
<label for="{{ form.qualifications.id_for_label }}" class="form-label">Qualifications and Requirements</label>
{{ form.qualifications }}
{% if form.qualifications.errors %}
<div class="text-danger mt-1">{{ form.qualifications.errors }}</div>
{% endif %}
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.salary_range.id_for_label }}" class="form-label">Salary Range</label>
{{ form.salary_range }}
{% if form.salary_range.errors %}
<div class="text-danger mt-1">{{ form.salary_range.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.benefits.id_for_label }}" class="form-label">Benefits</label>
{{ form.benefits }}
{% if form.benefits.errors %}
<div class="text-danger mt-1">{{ form.benefits.errors }}</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Application Information Section -->
<div class="card mb-4">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-file-signature"></i> Application Information</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label for="{{ form.application_url.id_for_label }}" class="form-label">Application URL <span class="text-danger">*</span></label>
{{ form.application_url }}
{% if form.application_url.errors %}
<div class="text-danger mt-1">{{ form.application_url.errors }}</div>
{% endif %}
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.application_deadline.id_for_label }}" class="form-label">Application Deadline</label>
{{ form.application_deadline }}
{% if form.application_deadline.errors %}
<div class="text-danger mt-1">{{ form.application_deadline.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.start_date.id_for_label }}" class="form-label">Desired Start Date</label>
{{ form.start_date }}
{% if form.start_date.errors %}
<div class="text-danger mt-1">{{ form.start_date.errors }}</div>
{% endif %}
</div>
</div>
</div>
<div class="mb-3">
<label for="{{ form.application_instructions.id_for_label }}" class="form-label">Application Instructions</label>
{{ form.application_instructions }}
{% if form.application_instructions.errors %}
<div class="text-danger mt-1">{{ form.application_instructions.errors }}</div>
{% endif %}
</div>
</div>
</div>
<!-- Internal Information Section -->
<div class="card mb-4">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-building"></i> Internal Information</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.reporting_to.id_for_label }}" class="form-label">Reports To</label>
{{ form.reporting_to }}
{% if form.reporting_to.errors %}
<div class="text-danger mt-1">{{ form.reporting_to.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.open_positions.id_for_label }}" class="form-label">Open positions</label>
{{ form.open_positions }}
{% if form.open_positions.errors %}
<div class="text-danger mt-1">{{ form.open_positions.errors }}</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="d-flex justify-content-between">
<a href="{% url 'jobs:job_detail' job_id%}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Cancel
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Update Job
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,207 @@
{% extends "base.html" %}
{% block title %}{{ job.title }} - University ATS{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2>{{ job.title }}</h2>
<span class="badge bg-{{ job.status|lower }} status-badge">
{{ job.get_status_display }}
</span>
</div>
<div class="card-body">
<!-- Job Details -->
<div class="row mb-3">
<div class="col-md-6">
<strong>Department:</strong> {{ job.department|default:"Not specified" }}
</div>
<div class="col-md-6">
<strong>Position Number:</strong> {{ job.position_number|default:"Not specified" }}
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<strong>Job Type:</strong> {{ job.get_job_type_display }}
</div>
<div class="col-md-6">
<strong>Workplace:</strong> {{ job.get_workplace_type_display }}
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<strong>Location:</strong> {{ job.get_location_display }}
</div>
<div class="col-md-6">
<strong>Created By:</strong> {{ job.created_by|default:"Not specified" }}
</div>
</div>
{% if job.salary_range %}
<div class="row mb-3">
<div class="col-12">
<strong>Salary Range:</strong> {{ job.salary_range }}
</div>
</div>
{% endif %}
{% if job.start_date %}
<div class="row mb-3">
<div class="col-12">
<strong>Start Date:</strong> {{ job.start_date }}
</div>
</div>
{% endif %}
{% if job.application_deadline %}
<div class="row mb-3">
<div class="col-12">
<strong>Application Deadline:</strong> {{ job.application_deadline }}
{% if job.is_expired %}
<span class="badge bg-danger">EXPIRED</span>
{% endif %}
</div>
</div>
{% endif %}
<!-- Description -->
{% if job.description %}
<div class="mb-3">
<h5>Description</h5>
<div>{{ job.description|linebreaks }}</div>
</div>
{% endif %}
{% if job.qualifications %}
<div class="mb-3">
<h5>Qualifications</h5>
<div>{{ job.qualifications|linebreaks }}</div>
</div>
{% endif %}
{% if job.benefits %}
<div class="mb-3">
<h5>Benefits</h5>
<div>{{ job.benefits|linebreaks }}</div>
</div>
{% endif %}
{% if job.application_instructions %}
<div class="mb-3">
<h5>Application Instructions</h5>
<div>{{ job.application_instructions|linebreaks }}</div>
</div>
{% endif %}
<!-- Application URL -->
{% if job.application_url %}
<div class="mb-3">
<button type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#myModalForm">
Upload Image for Post
</button>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-lg-4">
<!-- LinkedIn Integration Card -->
<div class="card mb-4">
<div class="card-header">
<h5><i class="fab fa-linkedin"></i> LinkedIn Integration</h5>
</div>
<div class="card-body">
{% if job.posted_to_linkedin %}
<div class="alert alert-success">
<i class="fas fa-check-circle"></i> Posted to LinkedIn successfully!
</div>
{% if job.linkedin_post_url %}
<a href="{{ job.linkedin_post_url }}" target="_blank" class="btn btn-primary w-100 mb-2">
<i class="fab fa-linkedin"></i> View on LinkedIn
</a>
{% endif %}
<small class="text-muted">Posted on: {{ job.linkedin_posted_at|date:"M d, Y" }}</small>
{% else %}
<p class="text-muted">This job has not been posted to LinkedIn yet.</p>
{% endif %}
<form method="post" action="{% url 'jobs:post_to_linkedin' job.pk %}" class="mt-3">
{% csrf_token %}
<button type="submit" class="btn btn-primary w-100"
{% if not request.session.linkedin_authenticated %}disabled{% endif %}>
<i class="fab fa-linkedin"></i>
{% if job.posted_to_linkedin %}Re-post to LinkedIn{% else %}Post to LinkedIn{% endif %}
</button>
</form>
{% if not request.session.linkedin_authenticated %}
<small class="text-muted">You need to <a href="{% url 'jobs:linkedin_login' %}">authenticate with LinkedIn</a> first.</small>
{% endif %}
{% if job.linkedin_post_status and 'ERROR' in job.linkedin_post_status %}
<div class="alert alert-danger mt-2">
<small>{{ job.linkedin_post_status }}</small>
</div>
{% endif %}
</div>
</div>
<div class="card border-secondary shadow-sm mb-4">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0">Applicant Form Management</h5>
</div>
<div class="card-body d-grid gap-2">
<p class="text-muted mb-3">
Manage the custom application forms associated with this job posting.
</p>
{# Primary Action: Highlight the creation of a NEW form #}
<a href="{% url 'applicant:create_form' job_id=job.internal_job_id %}"
class="btn btn-lg btn-success">
<i class="fas fa-plus-circle me-2"></i> Create New Form
</a>
{# Secondary Action: Make the list button less prominent #}
<a href="{% url 'applicant:job_forms_list' job.internal_job_id %}"
class="btn btn-sm btn-outline-primary">
View All Existing Forms
</a>
</div>
</div>
<!-- Internal Info Card -->
<div class="card">
<div class="card-header">
<h5>Internal Information</h5>
</div>
<div class="card-body">
<p><strong>Internal Job ID:</strong> {{ job.internal_job_id }}</p>
<p><strong>Created:</strong> {{ job.created_at|date:"M d, Y" }}</p>
<p><strong>Last Updated:</strong> {{ job.updated_at|date:"M d, Y" }}</p>
{% if job.reporting_to %}
<p><strong>Reports To:</strong> {{ job.reporting_to }}</p>
{% endif %}
</div>
</div>
</div>
</div>
<div class="mt-3 mb-5">
<a href="{% url 'jobs:edit_job' job.pk %}" class="btn btn-outline-primary me-2">
<i class="fas fa-edit"></i> Edit Job
</a>
<a href="{% url 'jobs:job_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Jobs
</a>
</div>
<!--modal class for upload file-->
{% include "jobs/partials/image_upload.html" %}
{% endblock %}

View File

@ -0,0 +1,113 @@
{% extends "base.html" %}
{% block title %}Job Postings - University ATS{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="fas fa-briefcase"></i> Job Postings</h1>
<a href="{% url 'jobs:create_job' %}" class="btn btn-primary">
<i class="fas fa-plus"></i> Create New Job
</a>
</div>
<!-- Filters -->
<div class="card mb-4">
<div class="card-body">
<form method="get" class="row g-3">
<div class="col-md-3">
<label for="status" class="form-label">Status</label>
<select name="status" id="status" class="form-select">
<option value="">All Statuses</option>
<option value="DRAFT" {% if status_filter == 'DRAFT' %}selected{% endif %}>Draft</option>
<option value="ACTIVE" {% if status_filter == 'ACTIVE' %}selected{% endif %}>Active</option>
<option value="CLOSED" {% if status_filter == 'CLOSED' %}selected{% endif %}>Closed</option>
<option value="ARCHIVED" {% if status_filter == 'ARCHIVED' %}selected{% endif %}>Archived</option>
</select>
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="submit" class="btn btn-outline-primary">Filter</button>
</div>
</form>
</div>
</div>
<!-- Job List -->
{% if page_obj %}
<div class="row">
{% for job in page_obj %}
<div class="col-md-6 col-lg-4 mb-4">
<div class="card job-card h-100 {% if job.posted_to_linkedin %}linkedin-posted{% endif %}">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<h5 class="card-title mb-1">{{ job.title }}</h5>
<span class="badge bg-{{ job.status|lower }} status-badge">
{{ job.get_status_display }}
</span>
</div>
<p class="card-text text-muted small">
<i class="fas fa-building"></i> {{ job.department|default:"No Department" }}<br>
<i class="fas fa-map-marker-alt"></i> {{ job.get_location_display }}<br>
<i class="fas fa-clock"></i> {{ job.get_job_type_display }}
</p>
<div class="mt-3">
{% if job.posted_to_linkedin %}
<span class="badge bg-info">
<i class="fab fa-linkedin"></i> Posted to LinkedIn
</span>
{% endif %}
<div class="mt-2">
<a href="{% url 'jobs:job_detail' job.pk %}" class="btn btn-sm btn-outline-primary me-2">
View
</a>
<a href="{% url 'jobs:edit_job' job.pk %}" class="btn btn-sm btn-outline-secondary">
Edit
</a>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Pagination -->
{% if page_obj.has_other_pages %}
<nav aria-label="Job pagination">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1{% if status_filter %}&status={{ status_filter }}{% endif %}">First</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if status_filter %}&status={{ status_filter }}{% endif %}">Previous</a>
</li>
{% endif %}
<li class="page-item active">
<span class="page-link">{{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
</li>
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if status_filter %}&status={{ status_filter }}{% endif %}">Next</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if status_filter %}&status={{ status_filter }}{% endif %}">Last</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="fas fa-briefcase fa-3x text-muted mb-3"></i>
<h3>No job postings found</h3>
<p class="text-muted">Create your first job posting to get started.</p>
<a href="{% url 'jobs:create_job' %}" class="btn btn-primary">Create Job</a>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,21 @@
<div class="modal fade" id="myModalForm" tabindex="-1" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="myModalLabel">Add New Comment</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="post" action="{% url 'jobs:create_jobpost_image' job.pk %}">
{% csrf_token %}
{{ image_form }}
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save changes</button>
</div>
</form>
</div>
</div>
</div>
</div>

3
jobs/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

20
jobs/urls.py Normal file
View File

@ -0,0 +1,20 @@
# jobs/urls.py - Clean URL version
from django.urls import path
from . import views
app_name = 'jobs'
urlpatterns = [
# Job Management URLs
path('', views.job_list, name='job_list'),
path('create/', views.create_job, name='create_job'),
path('jobpost/image/upload/<str:job_id>/',views.create_jobpost_image,name='create_jobpost_image'),
path('<str:job_id>/', views.job_detail, name='job_detail'),
path('<str:job_id>/edit/', views.edit_job, name='edit_job'),
# LinkedIn Integration URLs
path('<str:job_id>/post-to-linkedin/', views.post_to_linkedin, name='post_to_linkedin'),
path('linkedin/login/', views.linkedin_login, name='linkedin_login'),
path('linkedin/callback/', views.linkedin_callback, name='linkedin_callback'),
]

15
jobs/validators.py Normal file
View File

@ -0,0 +1,15 @@
from django.core.exceptions import ValidationError
def validate_image_size(image):
max_size_mb = 2
if image.size > max_size_mb * 1024 * 1024:
raise ValidationError(f"Image size should not exceed {max_size_mb}MB.")
def validate_hash_tags(value):
if value:
tags = [tag.strip() for tag in value.split(',')]
for tag in tags:
if ' ' in tag:
raise ValidationError("Hash tags should not contain spaces.")
if not tag.startswith('#'):
raise ValidationError("Each hash tag should start with '#' symbol.")

442
jobs/views.py Normal file
View File

@ -0,0 +1,442 @@
# def job_list(request):
# """Display list of all job postings"""
# jobs = JobPosting.objects.all().order_by('-created_at')
# # Filter by status if provided
# status = request.GET.get('status')
# if status:
# jobs = jobs.filter(status=status)
# paginator = Paginator(jobs, 10) # Show 10 jobs per page
# page_number = request.GET.get('page')
# page_obj = paginator.get_page(page_number)
# return render(request, 'jobs/job_list.html', {
# 'page_obj': page_obj,
# 'status_filter': status
# })
# def create_job(request):
# """Create a new job posting"""
# if request.method == 'POST':
# try:
# job = JobPosting()
# job.title = request.POST.get('title', '').strip()
# job.department = request.POST.get('department', '').strip()
# job.job_type = request.POST.get('job_type', 'FULL_TIME')
# job.workplace_type = request.POST.get('workplace_type', 'ON_SITE')
# job.location_city = request.POST.get('location_city', '').strip()
# job.location_state = request.POST.get('location_state', '').strip()
# job.location_country = request.POST.get('location_country', 'United States').strip()
# job.description = request.POST.get('description', '').strip()
# job.qualifications = request.POST.get('qualifications', '').strip()
# job.salary_range = request.POST.get('salary_range', '').strip()
# job.benefits = request.POST.get('benefits', '').strip()
# job.application_url = request.POST.get('application_url', '').strip()
# job.application_deadline = request.POST.get('application_deadline') or None
# job.application_instructions = request.POST.get('application_instructions', '').strip()
# # job.created_by = request.POST.get('created_by', request.user.get_full_name() or request.user.username)
# # Handle created_by without user authentication
# job.created_by = request.POST.get('created_by', '').strip()
# if not job.created_by:
# job.created_by = "University Administrator"
# job.position_number = request.POST.get('position_number', '').strip()
# job.reporting_to = request.POST.get('reporting_to', '').strip()
# job.start_date = request.POST.get('start_date') or None
# # Set initial status
# status = request.POST.get('status', 'DRAFT')
# if status in ['ACTIVE', 'CLOSED', 'ARCHIVED']:
# job.status = status
# job.save()
# messages.success(request, f'Job "{job.title}" created successfully!')
# return redirect('job_detail', job_id=job.id)
# except Exception as e:
# logger.error(f"Error creating job: {e}")
# messages.error(request, f'Error creating job: {e}')
# return render(request, 'jobs/create_job.html')
# def job_detail(request, job_id):
# """View details of a specific job"""
# job = get_object_or_404(JobPosting, id=job_id)
# return render(request, 'jobs/job_detail.html', {'job': job})
# def edit_job(request, job_id):
# """Edit an existing job posting"""
# job = get_object_or_404(JobPosting, id=job_id)
# if request.method == 'POST':
# try:
# job.title = request.POST.get('title', job.title).strip()
# job.department = request.POST.get('department', job.department).strip()
# job.job_type = request.POST.get('job_type', job.job_type)
# job.workplace_type = request.POST.get('workplace_type', job.workplace_type)
# job.location_city = request.POST.get('location_city', job.location_city).strip()
# job.location_state = request.POST.get('location_state', job.location_state).strip()
# job.location_country = request.POST.get('location_country', job.location_country).strip()
# job.description = request.POST.get('description', job.description).strip()
# job.qualifications = request.POST.get('qualifications', job.qualifications).strip()
# job.salary_range = request.POST.get('salary_range', job.salary_range).strip()
# job.benefits = request.POST.get('benefits', job.benefits).strip()
# job.application_url = request.POST.get('application_url', job.application_url).strip()
# job.application_deadline = request.POST.get('application_deadline') or job.application_deadline
# job.application_instructions = request.POST.get('application_instructions', job.application_instructions).strip()
# job.created_by = request.POST.get('created_by', job.created_by).strip()
# job.position_number = request.POST.get('position_number', job.position_number).strip()
# job.reporting_to = request.POST.get('reporting_to', job.reporting_to).strip()
# job.start_date = request.POST.get('start_date') or job.start_date
# # Update status
# status = request.POST.get('status')
# if status and status in dict(JobPosting.STATUS_CHOICES):
# job.status = status
# job.save()
# messages.success(request, f'Job "{job.title}" updated successfully!')
# return redirect('job_detail', job_id=job.id)
# except Exception as e:
# logger.error(f"Error updating job: {e}")
# messages.error(request, f'Error updating job: {e}')
# return render(request, 'jobs/edit_job.html', {'job': job})
# def post_to_linkedin(request, job_id):
# """Post a job to LinkedIn"""
# job = get_object_or_404(JobPosting, id=job_id)
# if request.method == 'POST':
# try:
# # Check if user is authenticated with LinkedIn
# if 'linkedin_access_token' not in request.session:
# messages.error(request, 'Please authenticate with LinkedIn first.')
# return redirect('linkedin_login')
# # Initialize LinkedIn service
# service = LinkedInService()
# service.access_token = request.session['linkedin_access_token']
# # Post to LinkedIn
# result = service.create_job_post(job)
# if result['success']:
# # Update job with LinkedIn info
# job.posted_to_linkedin = True
# job.linkedin_post_id = result['post_id']
# job.linkedin_post_url = result['post_url']
# job.linkedin_post_status = 'SUCCESS'
# job.linkedin_posted_at = timezone.now()
# job.save()
# messages.success(request, 'Job posted to LinkedIn successfully!')
# else:
# error_msg = result.get('error', 'Unknown error')
# job.linkedin_post_status = f'ERROR: {error_msg}'
# job.save()
# messages.error(request, f'Error posting to LinkedIn: {error_msg}')
# except Exception as e:
# logger.error(f"Error in post_to_linkedin: {e}")
# job.linkedin_post_status = f'ERROR: {str(e)}'
# job.save()
# messages.error(request, f'Error posting to LinkedIn: {e}')
# return redirect('job_detail', job_id=job.id)
# # LinkedIn Authentication Views
# def linkedin_login(request):
# """Redirect to LinkedIn OAuth"""
# service = LinkedInService()
# auth_url = service.get_auth_url()
# return redirect(auth_url)
# def linkedin_callback(request):
# """Handle LinkedIn OAuth callback"""
# code = request.GET.get('code')
# if not code:
# messages.error(request, 'No authorization code received from LinkedIn.')
# return redirect('job_list')
# try:
# service = LinkedInService()
# access_token = service.get_access_token(code)
# request.session['linkedin_access_token'] = access_token
# request.session['linkedin_authenticated'] = True
# messages.success(request, 'Successfully authenticated with LinkedIn!')
# except Exception as e:
# logger.error(f"LinkedIn authentication error: {e}")
# messages.error(request, f'LinkedIn authentication failed: {e}')
# return redirect('job_list')
# @csrf_exempt
# def api_post_job_to_linkedin(request, job_id):
# """API endpoint to post job to LinkedIn (for automated systems)"""
# if request.method != 'POST':
# return JsonResponse({'error': 'POST method required'}, status=405)
# try:
# job = JobPosting.objects.get(id=job_id)
# # Check if LinkedIn is authenticated
# if 'linkedin_access_token' not in request.session:
# return JsonResponse({'error': 'Not authenticated with LinkedIn'}, status=401)
# service = LinkedInService()
# service.access_token = request.session['linkedin_access_token']
# result = service.create_job_post(job)
# if result['success']:
# job.posted_to_linkedin = True
# job.linkedin_post_id = result['post_id']
# job.linkedin_post_url = result['post_url']
# job.linkedin_post_status = 'SUCCESS'
# job.linkedin_posted_at = timezone.now()
# job.save()
# return JsonResponse({
# 'success': True,
# 'linkedin_post_id': result['post_id'],
# 'linkedin_post_url': result['post_url']
# })
# else:
# return JsonResponse({
# 'error': result.get('error', 'Unknown error'),
# 'status_code': result.get('status_code', 500)
# }, status=400)
# except JobPosting.DoesNotExist:
# return JsonResponse({'error': 'Job not found'}, status=404)
# except Exception as e:
# logger.error(f"API error: {e}")
# return JsonResponse({'error': str(e)}, status=500)
# from .forms import JobPostingForm
# def form_check(request):
# form=JobPostingForm()
# return render(request,'jobs/new_form.html',{'form':form})
from .import forms
from .import models
from django.shortcuts import render, get_object_or_404, redirect
from django.views.decorators.http import require_POST
from django.core.paginator import Paginator
from django.utils import timezone
from django.contrib import messages
from .linkedin_service import LinkedInService
import json
import logging
logger=logging.getLogger(__name__)
def job_list(request):
"""Display the list of job postings order by creation date descending"""
jobs=models.JobPosting.objects.all().order_by('-created_at')
# Filter by status if provided
status=request.GET.get('status')
if status:
jobs=jobs.filter(status=status)
#pagination
paginator=Paginator(jobs,10) # Show 10 jobs per page
page_number=request.GET.get('page')
page_obj=paginator.get_page(page_number)
return render(request, 'jobs/job_list.html', {
'page_obj': page_obj,
'status_filter': status
})
def create_job(request):
"""Create a new job posting"""
if request.method=='POST':
form=forms.JobPostingForm(request.POST,is_anonymous_user=not request.user.is_authenticated) # pass the boolean value
#to check user is authenticated or not
if form.is_valid():
try:
job=form.save(commit=False)
if request.user.is_authenticated:
job.created_by=request.user.get_full_name() or request.user.username
else:
job.created_by=request.POST.get('created_by','').strip()
if not job.created_by:
job.created_by="University Administrator"
job.save()
messages.success(request,f'Job "{job.title}" created successfully!')
return redirect('jobs:job_list')
except Exception as e:
logger.error(f"Error creating job: {e}")
messages.error(request,f"Error creating job: {e}")
else:
messages.error(request, 'Please correct the errors below.')
else:
form=forms.JobPostingForm(is_anonymous_user=not request.user.is_authenticated)
return render(request,'jobs/create_job.html',{'form':form})
def edit_job(request,job_id):
"""Edit an existing job posting"""
if request.method=='POST':
job=get_object_or_404(models.JobPosting,pk=job_id)
form=forms.JobPostingForm(request.POST,instance=job,is_anonymous_user=not request.user.is_authenticated)
if form.is_valid():
try:
job=form.save(commit=False)
if request.user.is_authenticated:
job.created_by=request.user.get_full_name() or request.user.username
else:
job.created_by=request.POST.get('created_by','').strip()
if not job.created_by:
job.created_by="University Administrator"
job.save()
messages.success(request,f'Job "{job.title}" updated successfully!')
return redirect('jobs:job_list')
except Exception as e:
logger.error(f"Error updating job: {e}")
messages.error(request,f"Error updating job: {e}")
else:
messages.error(request, 'Please correct the errors below.')
else:
job=get_object_or_404(models.JobPosting,pk=job_id)
form=forms.JobPostingForm(instance=job,is_anonymous_user=not request.user.is_authenticated)
return render(request,'jobs/edit_job.html',{'form':form,'job_id':job_id})
def job_detail(request, job_id):
"""View details of a specific job"""
job = get_object_or_404(models.JobPosting, pk=job_id)
image_form=forms.PostImageUploadForm()
return render(request, 'jobs/job_detail.html', {'job': job,'image_form':image_form})
@require_POST
def create_jobpost_image(request,job_id):
"""Handle image upload for a job posting"""
job=get_object_or_404(models.JobPosting,pk=job_id)
form=forms.PostImageUploadForm(request.POST,request.FILES)
if form.is_valid():
try:
post_image=form.save(commit=False)
post_image.job_posting=job
post_image.save()
messages.success(request,'Image uploaded successfully!')
except Exception as e:
logger.error(f"Error uploading image: {e}")
messages.error(request,f'Error uploading image: {e}')
else:
messages.error(request,'Please correct the errors below.')
return redirect('jobs:job_detail',job_id=job.pk)
def post_to_linkedin(request,job_id):
"""Post a job to LinkedIn"""
job=get_object_or_404(models.JobPosting,pk=job_id)
if job.status!='ACTIVE':
messages.info(request,'Only active jobs can be posted to LinkedIn.')
return redirect('jobs:job_list')
if request.method=='POST':
try:
# Check if user is authenticated with LinkedIn
if 'linkedin_access_token' not in request.session:
messages.error(request,'Please authenticate with LinkedIn first.')
return redirect('linkedin_login')
# Clear previous LinkedIn data for re-posting
job.posted_to_linkedin=False
job.linkedin_post_id=''
job.linkedin_post_url=''
job.linkedin_post_status=''
job.linkedin_posted_at=None
job.save()
# Initialize LinkedIn service
service=LinkedInService()
service.access_token=request.session['linkedin_access_token']
# Post to LinkedIn
result=service.create_job_post(job)
if result['success']:
# Update job with LinkedIn info
job.posted_to_linkedin=True
job.linkedin_post_id=result['post_id']
job.linkedin_post_url=result['post_url']
job.linkedin_post_status='SUCCESS'
job.linkedin_posted_at=timezone.now()
job.save()
messages.success(request,'Job posted to LinkedIn successfully!')
else:
error_msg=result.get('error','Unknown error')
job.linkedin_post_status=f'ERROR: {error_msg}'
job.save()
messages.error(request,f'Error posting to LinkedIn: {error_msg}')
except Exception as e:
logger.error(f"Error in post_to_linkedin: {e}")
job.linkedin_post_status = f'ERROR: {str(e)}'
job.save()
messages.error(request, f'Error posting to LinkedIn: {e}')
return redirect('jobs:job_detail', job_id=job.pk)
def linkedin_login(request):
"""Redirect to LinkedIn OAuth"""
service=LinkedInService()
auth_url=service.get_auth_url()
"""
It creates a special URL that:
Sends the user to LinkedIn to log in
Asks the user to grant your app permission to post on their behalf
Tells LinkedIn where to send the user back after they approve (your redirect_uri)
http://yoursite.com/linkedin/callback/?code=TEMPORARY_CODE_HERE
"""
return redirect(auth_url)
def linkedin_callback(request):
"""Handle LinkedIn OAuth callback"""
code=request.GET.get('code')
if not code:
messages.error(request,'No authorization code received from LinkedIn.')
return redirect('jobs:job_list')
try:
service=LinkedInService()
#get_access_token(code)->It makes a POST request to LinkedIns token endpoint with parameters
access_token=service.get_access_token(code)
request.session['linkedin_access_token']=access_token
request.session['linkedin_authenticated']=True
messages.success(request,'Successfully authenticated with LinkedIn!')
except Exception as e:
logger.error(f"LinkedIn authentication error: {e}")
messages.error(request,f'LinkedIn authentication failed: {e}')
return redirect('jobs:job_list')
#applicant views
def applicant_job_detail(request,job_id):
"""View job details for applicants"""
job=get_object_or_404(models.JobPosting,pk=job_id,status='ACTIVE')
return render(request,'jobs/applicant_job_detail.html',{'job':job})

22
manage.py Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'university_ats.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

15
notes Normal file
View File

@ -0,0 +1,15 @@
User clicks "Login with LinkedIn"
Your app redirects to LinkedIn OAuth URL (with client_id)
User logs in + grants permission
LinkedIn redirects back with ?code=TEMP_CODE
Your app calls get_access_token(TEMP_CODE)
LinkedIn returns access_token
Your app stores token in session
Now you can post to LinkedIn API!

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
requests
python-dotenv
django
django-extensions
pillow

54
templates/base.html Normal file
View File

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}University ATS{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
.navbar-brand { font-weight: bold; }
.status-badge { font-size: 0.8em; }
.linkedin-posted { background-color: #d1ecf1; border-left: 4px solid #0dcaf0; }
.job-card { transition: transform 0.2s; }
.job-card:hover { transform: translateY(-2px); }
</style>
{% block customCSS %}
{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-success">
<div class="container">
<a class="navbar-brand" href="{% url 'jobs:job_list' %}">
<i class="fas fa-university"></i> Princess Nourah Bint Abdul Rahman University
</a>
<div class="navbar-nav ms-auto">
{% if request.session.linkedin_authenticated %}
<span class="navbar-text text-white me-3">
<i class="fab fa-linkedin"></i> LinkedIn Connected
</span>
{% endif %}
<a class="nav-link" href="{% url 'jobs:create_job' %}">Create Job</a>
<a class="nav-link" href="{% url 'jobs:linkedin_login' %}">LinkedIn Login</a>
<a></a>
</div>
</div>
</nav>
<div class="container mt-4">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
{% block customJS %}{% endblock %}
</body>
</html>

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

16
university_ats/asgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
ASGI config for university_ats project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'university_ats.settings')
application = get_asgi_application()

145
university_ats/settings.py Normal file
View File

@ -0,0 +1,145 @@
"""
Django settings for university_ats project.
Generated by 'django-admin startproject' using Django 5.2.6.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-%#3rt-5n!9v$2l7gyyi8*z19*xqhxjq)2k0fgr4)z2da$+2m-e'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_extensions',
'jobs',
'applicant',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'university_ats.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'university_ats.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'Asia/Riyadh'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# LinkedIn Settings
import os
from dotenv import load_dotenv
load_dotenv()
LINKEDIN_CLIENT_ID = os.getenv('LINKEDIN_CLIENT_ID')
LINKEDIN_CLIENT_SECRET = os.getenv('LINKEDIN_CLIENT_SECRET')
LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/linkedin/callback/'
# Session settings
SESSION_COOKIE_AGE = 200 # 1 week
SESSION_SAVE_EVERY_REQUEST = True
#Media files (uploads)
MEDIA_URL='/media/'
MEDIA_ROOT=BASE_DIR/'media' # platform independent path joining for windows/linux/mac since django 3.1 (from pathlib)
#it automatically uses correct slash based on OS like / or \
#MEDIA_ROOT=os.path.join(BASE_DIR,'media') --- IGNORE ---

16
university_ats/urls.py Normal file
View File

@ -0,0 +1,16 @@
# university_ats/urls.py - Clean URL version
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('jobs.urls')), # Jobs app handles root URL
path('applicant/', include('applicant.urls')), # Applicant app URLs
]
# ONLY SERVE MEDIA FILES IN DEVELOPMENT
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

16
university_ats/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for university_ats project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'university_ats.settings')
application = get_wsgi_application()

247
venv/bin/Activate.ps1 Normal file
View File

@ -0,0 +1,247 @@
<#
.Synopsis
Activate a Python virtual environment for the current PowerShell session.
.Description
Pushes the python executable for a virtual environment to the front of the
$Env:PATH environment variable and sets the prompt to signify that you are
in a Python virtual environment. Makes use of the command line switches as
well as the `pyvenv.cfg` file values present in the virtual environment.
.Parameter VenvDir
Path to the directory that contains the virtual environment to activate. The
default value for this is the parent of the directory that the Activate.ps1
script is located within.
.Parameter Prompt
The prompt prefix to display when this virtual environment is activated. By
default, this prompt is the name of the virtual environment folder (VenvDir)
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
.Example
Activate.ps1
Activates the Python virtual environment that contains the Activate.ps1 script.
.Example
Activate.ps1 -Verbose
Activates the Python virtual environment that contains the Activate.ps1 script,
and shows extra information about the activation as it executes.
.Example
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
Activates the Python virtual environment located in the specified location.
.Example
Activate.ps1 -Prompt "MyPython"
Activates the Python virtual environment that contains the Activate.ps1 script,
and prefixes the current prompt with the specified string (surrounded in
parentheses) while the virtual environment is active.
.Notes
On Windows, it may be required to enable this Activate.ps1 script by setting the
execution policy for the user. You can do this by issuing the following PowerShell
command:
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
For more information on Execution Policies:
https://go.microsoft.com/fwlink/?LinkID=135170
#>
Param(
[Parameter(Mandatory = $false)]
[String]
$VenvDir,
[Parameter(Mandatory = $false)]
[String]
$Prompt
)
<# Function declarations --------------------------------------------------- #>
<#
.Synopsis
Remove all shell session elements added by the Activate script, including the
addition of the virtual environment's Python executable from the beginning of
the PATH variable.
.Parameter NonDestructive
If present, do not remove this function from the global namespace for the
session.
#>
function global:deactivate ([switch]$NonDestructive) {
# Revert to original values
# The prior prompt:
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
}
# The prior PYTHONHOME:
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
}
# The prior PATH:
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
}
# Just remove the VIRTUAL_ENV altogether:
if (Test-Path -Path Env:VIRTUAL_ENV) {
Remove-Item -Path env:VIRTUAL_ENV
}
# Just remove VIRTUAL_ENV_PROMPT altogether.
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
}
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
}
# Leave deactivate function in the global namespace if requested:
if (-not $NonDestructive) {
Remove-Item -Path function:deactivate
}
}
<#
.Description
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
given folder, and returns them in a map.
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
two strings separated by `=` (with any amount of whitespace surrounding the =)
then it is considered a `key = value` line. The left hand string is the key,
the right hand is the value.
If the value starts with a `'` or a `"` then the first and last character is
stripped from the value before being captured.
.Parameter ConfigDir
Path to the directory that contains the `pyvenv.cfg` file.
#>
function Get-PyVenvConfig(
[String]
$ConfigDir
) {
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
# An empty map will be returned if no config file is found.
$pyvenvConfig = @{ }
if ($pyvenvConfigPath) {
Write-Verbose "File exists, parse `key = value` lines"
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
$pyvenvConfigContent | ForEach-Object {
$keyval = $PSItem -split "\s*=\s*", 2
if ($keyval[0] -and $keyval[1]) {
$val = $keyval[1]
# Remove extraneous quotations around a string value.
if ("'""".Contains($val.Substring(0, 1))) {
$val = $val.Substring(1, $val.Length - 2)
}
$pyvenvConfig[$keyval[0]] = $val
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
}
}
}
return $pyvenvConfig
}
<# Begin Activate script --------------------------------------------------- #>
# Determine the containing directory of this script
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$VenvExecDir = Get-Item -Path $VenvExecPath
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
# Set values required in priority: CmdLine, ConfigFile, Default
# First, get the location of the virtual environment, it might not be
# VenvExecDir if specified on the command line.
if ($VenvDir) {
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
}
else {
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
Write-Verbose "VenvDir=$VenvDir"
}
# Next, read the `pyvenv.cfg` file to determine any required value such
# as `prompt`.
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
# Next, set the prompt from the command line, or the config file, or
# just use the name of the virtual environment folder.
if ($Prompt) {
Write-Verbose "Prompt specified as argument, using '$Prompt'"
}
else {
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
$Prompt = $pyvenvCfg['prompt'];
}
else {
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
$Prompt = Split-Path -Path $venvDir -Leaf
}
}
Write-Verbose "Prompt = '$Prompt'"
Write-Verbose "VenvDir='$VenvDir'"
# Deactivate any currently active virtual environment, but leave the
# deactivate function in place.
deactivate -nondestructive
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
# that there is an activated venv.
$env:VIRTUAL_ENV = $VenvDir
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
Write-Verbose "Setting prompt to '$Prompt'"
# Set the prompt to include the env name
# Make sure _OLD_VIRTUAL_PROMPT is global
function global:_OLD_VIRTUAL_PROMPT { "" }
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
function global:prompt {
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
_OLD_VIRTUAL_PROMPT
}
$env:VIRTUAL_ENV_PROMPT = $Prompt
}
# Clear PYTHONHOME
if (Test-Path -Path Env:PYTHONHOME) {
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
Remove-Item -Path Env:PYTHONHOME
}
# Add the venv to the PATH
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"

70
venv/bin/activate Normal file
View File

@ -0,0 +1,70 @@
# This file must be used with "source bin/activate" *from bash*
# You cannot run it directly
deactivate () {
# reset old environment variables
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
PATH="${_OLD_VIRTUAL_PATH:-}"
export PATH
unset _OLD_VIRTUAL_PATH
fi
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
export PYTHONHOME
unset _OLD_VIRTUAL_PYTHONHOME
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
PS1="${_OLD_VIRTUAL_PS1:-}"
export PS1
unset _OLD_VIRTUAL_PS1
fi
unset VIRTUAL_ENV
unset VIRTUAL_ENV_PROMPT
if [ ! "${1:-}" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}
# unset irrelevant variables
deactivate nondestructive
# on Windows, a path can contain colons and backslashes and has to be converted:
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
# transform D:\path\to\venv to /d/path/to/venv on MSYS
# and to /cygdrive/d/path/to/venv on Cygwin
export VIRTUAL_ENV=$(cygpath /home/faheed/Learning/LinkedinPosts/venv)
else
# use the path as-is
export VIRTUAL_ENV=/home/faheed/Learning/LinkedinPosts/venv
fi
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/"bin":$PATH"
export PATH
# unset PYTHONHOME if set
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
# could use `if (set -u; : $PYTHONHOME) ;` in bash
if [ -n "${PYTHONHOME:-}" ] ; then
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
unset PYTHONHOME
fi
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
_OLD_VIRTUAL_PS1="${PS1:-}"
PS1='(venv) '"${PS1:-}"
export PS1
VIRTUAL_ENV_PROMPT='(venv) '
export VIRTUAL_ENV_PROMPT
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null

27
venv/bin/activate.csh Normal file
View File

@ -0,0 +1,27 @@
# This file must be used with "source bin/activate.csh" *from csh*.
# You cannot run it directly.
# Created by Davide Di Blasi <davidedb@gmail.com>.
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
# Unset irrelevant variables.
deactivate nondestructive
setenv VIRTUAL_ENV /home/faheed/Learning/LinkedinPosts/venv
set _OLD_VIRTUAL_PATH="$PATH"
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
set _OLD_VIRTUAL_PROMPT="$prompt"
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
set prompt = '(venv) '"$prompt"
setenv VIRTUAL_ENV_PROMPT '(venv) '
endif
alias pydoc python -m pydoc
rehash

69
venv/bin/activate.fish Normal file
View File

@ -0,0 +1,69 @@
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
# (https://fishshell.com/). You cannot run it directly.
function deactivate -d "Exit virtual environment and return to normal shell environment"
# reset old environment variables
if test -n "$_OLD_VIRTUAL_PATH"
set -gx PATH $_OLD_VIRTUAL_PATH
set -e _OLD_VIRTUAL_PATH
end
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
set -e _OLD_VIRTUAL_PYTHONHOME
end
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
set -e _OLD_FISH_PROMPT_OVERRIDE
# prevents error when using nested fish instances (Issue #93858)
if functions -q _old_fish_prompt
functions -e fish_prompt
functions -c _old_fish_prompt fish_prompt
functions -e _old_fish_prompt
end
end
set -e VIRTUAL_ENV
set -e VIRTUAL_ENV_PROMPT
if test "$argv[1]" != "nondestructive"
# Self-destruct!
functions -e deactivate
end
end
# Unset irrelevant variables.
deactivate nondestructive
set -gx VIRTUAL_ENV /home/faheed/Learning/LinkedinPosts/venv
set -gx _OLD_VIRTUAL_PATH $PATH
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
# Unset PYTHONHOME if set.
if set -q PYTHONHOME
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
set -e PYTHONHOME
end
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
# fish uses a function instead of an env var to generate the prompt.
# Save the current fish_prompt function as the function _old_fish_prompt.
functions -c fish_prompt _old_fish_prompt
# With the original prompt function renamed, we can override with our own.
function fish_prompt
# Save the return status of the last command.
set -l old_status $status
# Output the venv prompt; color taken from the blue of the Python logo.
printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal)
# Restore the return status of the previous command.
echo "exit $old_status" | .
# Output the original/"old" prompt.
_old_fish_prompt
end
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
set -gx VIRTUAL_ENV_PROMPT '(venv) '
end

8
venv/bin/django-admin Executable file
View File

@ -0,0 +1,8 @@
#!/home/faheed/Learning/LinkedinPosts/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from django.core.management import execute_from_command_line
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(execute_from_command_line())

8
venv/bin/dotenv Executable file
View File

@ -0,0 +1,8 @@
#!/home/faheed/Learning/LinkedinPosts/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from dotenv.__main__ import cli
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(cli())

8
venv/bin/normalizer Executable file
View File

@ -0,0 +1,8 @@
#!/home/faheed/Learning/LinkedinPosts/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from charset_normalizer.cli import cli_detect
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(cli_detect())

8
venv/bin/pip Executable file
View File

@ -0,0 +1,8 @@
#!/home/faheed/Learning/LinkedinPosts/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
venv/bin/pip3 Executable file
View File

@ -0,0 +1,8 @@
#!/home/faheed/Learning/LinkedinPosts/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
venv/bin/pip3.12 Executable file
View File

@ -0,0 +1,8 @@
#!/home/faheed/Learning/LinkedinPosts/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

1
venv/bin/python Symbolic link
View File

@ -0,0 +1 @@
python3

1
venv/bin/python3 Symbolic link
View File

@ -0,0 +1 @@
/usr/bin/python3

Some files were not shown because too many files have changed in this diff Show More