first push
This commit is contained in:
commit
45c5d90907
8
.env
Normal file
8
.env
Normal 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
54
Readme.md
Normal 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
0
applicant/__init__.py
Normal file
BIN
applicant/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
applicant/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
applicant/__pycache__/admin.cpython-312.pyc
Normal file
BIN
applicant/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
applicant/__pycache__/apps.cpython-312.pyc
Normal file
BIN
applicant/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
applicant/__pycache__/forms.cpython-312.pyc
Normal file
BIN
applicant/__pycache__/forms.cpython-312.pyc
Normal file
Binary file not shown.
BIN
applicant/__pycache__/forms_builder.cpython-312.pyc
Normal file
BIN
applicant/__pycache__/forms_builder.cpython-312.pyc
Normal file
Binary file not shown.
BIN
applicant/__pycache__/models.cpython-312.pyc
Normal file
BIN
applicant/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
applicant/__pycache__/urls.cpython-312.pyc
Normal file
BIN
applicant/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
applicant/__pycache__/views.cpython-312.pyc
Normal file
BIN
applicant/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
3
applicant/admin.py
Normal file
3
applicant/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
applicant/apps.py
Normal file
6
applicant/apps.py
Normal 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
22
applicant/forms.py
Normal 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'}),
|
||||
}
|
||||
49
applicant/forms_builder.py
Normal file
49
applicant/forms_builder.py
Normal 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)
|
||||
70
applicant/migrations/0001_initial.py
Normal file
70
applicant/migrations/0001_initial.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
applicant/migrations/__init__.py
Normal file
0
applicant/migrations/__init__.py
Normal file
BIN
applicant/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
applicant/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
applicant/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
applicant/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
144
applicant/models.py
Normal file
144
applicant/models.py
Normal 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}"
|
||||
94
applicant/templates/applicant/apply_form.html
Normal file
94
applicant/templates/applicant/apply_form.html
Normal 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">
|
||||
← Review Job Details
|
||||
</a>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
68
applicant/templates/applicant/create_form.html
Normal file
68
applicant/templates/applicant/create_form.html
Normal 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 →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
1018
applicant/templates/applicant/edit_form.html
Normal file
1018
applicant/templates/applicant/edit_form.html
Normal file
File diff suppressed because it is too large
Load Diff
97
applicant/templates/applicant/job_forms_list.html
Normal file
97
applicant/templates/applicant/job_forms_list.html
Normal 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">
|
||||
← Back to Job Details
|
||||
</a>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
129
applicant/templates/applicant/review_job_detail.html
Normal file
129
applicant/templates/applicant/review_job_detail.html
Normal 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 %}
|
||||
35
applicant/templates/applicant/thank_you.html
Normal file
35
applicant/templates/applicant/thank_you.html
Normal 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 %}
|
||||
0
applicant/templatetags/__init__.py
Normal file
0
applicant/templatetags/__init__.py
Normal file
BIN
applicant/templatetags/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
applicant/templatetags/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
applicant/templatetags/__pycache__/mytags.cpython-312.pyc
Normal file
BIN
applicant/templatetags/__pycache__/mytags.cpython-312.pyc
Normal file
Binary file not shown.
24
applicant/templatetags/mytags.py
Normal file
24
applicant/templatetags/mytags.py
Normal 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
3
applicant/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
18
applicant/urls.py
Normal file
18
applicant/urls.py
Normal 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
175
applicant/views.py
Normal 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
BIN
db.sqlite3
Normal file
Binary file not shown.
0
jobs/__init__.py
Normal file
0
jobs/__init__.py
Normal file
BIN
jobs/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
jobs/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
jobs/__pycache__/admin.cpython-312.pyc
Normal file
BIN
jobs/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
jobs/__pycache__/apps.cpython-312.pyc
Normal file
BIN
jobs/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
jobs/__pycache__/forms.cpython-312.pyc
Normal file
BIN
jobs/__pycache__/forms.cpython-312.pyc
Normal file
Binary file not shown.
BIN
jobs/__pycache__/linkedin_service.cpython-312.pyc
Normal file
BIN
jobs/__pycache__/linkedin_service.cpython-312.pyc
Normal file
Binary file not shown.
BIN
jobs/__pycache__/models.cpython-312.pyc
Normal file
BIN
jobs/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
jobs/__pycache__/urls.cpython-312.pyc
Normal file
BIN
jobs/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
jobs/__pycache__/validators.cpython-312.pyc
Normal file
BIN
jobs/__pycache__/validators.cpython-312.pyc
Normal file
Binary file not shown.
BIN
jobs/__pycache__/views.cpython-312.pyc
Normal file
BIN
jobs/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
3
jobs/admin.py
Normal file
3
jobs/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
jobs/apps.py
Normal file
6
jobs/apps.py
Normal 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
215
jobs/forms.py
Normal 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
552
jobs/linkedin_service.py
Normal 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 LinkedIn’s 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
|
||||
# }
|
||||
69
jobs/migrations/0001_initial.py
Normal file
69
jobs/migrations/0001_initial.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
||||
18
jobs/migrations/0002_jobposting_open_positions.py
Normal file
18
jobs/migrations/0002_jobposting_open_positions.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
18
jobs/migrations/0003_jobposting_hash_tags.py
Normal file
18
jobs/migrations/0003_jobposting_hash_tags.py
Normal 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),
|
||||
),
|
||||
]
|
||||
24
jobs/migrations/0004_alter_jobposting_hash_tags_and_more.py
Normal file
24
jobs/migrations/0004_alter_jobposting_hash_tags_and_more.py
Normal 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),
|
||||
),
|
||||
]
|
||||
0
jobs/migrations/__init__.py
Normal file
0
jobs/migrations/__init__.py
Normal file
BIN
jobs/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
jobs/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
jobs/migrations/__pycache__/0003_postfileupload.cpython-312.pyc
Normal file
BIN
jobs/migrations/__pycache__/0003_postfileupload.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
jobs/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
jobs/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
188
jobs/models.py
Normal file
188
jobs/models.py
Normal 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}"
|
||||
103
jobs/templates/jobs/apply_form.html
Normal file
103
jobs/templates/jobs/apply_form.html
Normal 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 %}
|
||||
269
jobs/templates/jobs/create_job.html
Normal file
269
jobs/templates/jobs/create_job.html
Normal 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 %}
|
||||
259
jobs/templates/jobs/edit_job.html
Normal file
259
jobs/templates/jobs/edit_job.html
Normal 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 %}
|
||||
207
jobs/templates/jobs/job_detail.html
Normal file
207
jobs/templates/jobs/job_detail.html
Normal 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 %}
|
||||
113
jobs/templates/jobs/job_list.html
Normal file
113
jobs/templates/jobs/job_list.html
Normal 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 %}
|
||||
21
jobs/templates/jobs/partials/image_upload.html
Normal file
21
jobs/templates/jobs/partials/image_upload.html
Normal 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
3
jobs/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
20
jobs/urls.py
Normal file
20
jobs/urls.py
Normal 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
15
jobs/validators.py
Normal 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
442
jobs/views.py
Normal 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 LinkedIn’s 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
22
manage.py
Executable 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
15
notes
Normal 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
5
requirements.txt
Normal file
@ -0,0 +1,5 @@
|
||||
requests
|
||||
python-dotenv
|
||||
django
|
||||
django-extensions
|
||||
pillow
|
||||
54
templates/base.html
Normal file
54
templates/base.html
Normal 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>
|
||||
0
university_ats/__init__.py
Normal file
0
university_ats/__init__.py
Normal file
BIN
university_ats/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
university_ats/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
university_ats/__pycache__/settings.cpython-312.pyc
Normal file
BIN
university_ats/__pycache__/settings.cpython-312.pyc
Normal file
Binary file not shown.
BIN
university_ats/__pycache__/urls.cpython-312.pyc
Normal file
BIN
university_ats/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
university_ats/__pycache__/wsgi.cpython-312.pyc
Normal file
BIN
university_ats/__pycache__/wsgi.cpython-312.pyc
Normal file
Binary file not shown.
16
university_ats/asgi.py
Normal file
16
university_ats/asgi.py
Normal 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
145
university_ats/settings.py
Normal 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
16
university_ats/urls.py
Normal 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
16
university_ats/wsgi.py
Normal 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
247
venv/bin/Activate.ps1
Normal 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
70
venv/bin/activate
Normal 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
27
venv/bin/activate.csh
Normal 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
69
venv/bin/activate.fish
Normal 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
8
venv/bin/django-admin
Executable 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
8
venv/bin/dotenv
Executable 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
8
venv/bin/normalizer
Executable 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
8
venv/bin/pip
Executable 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
8
venv/bin/pip3
Executable 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
8
venv/bin/pip3.12
Executable 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
1
venv/bin/python
Symbolic link
@ -0,0 +1 @@
|
||||
python3
|
||||
1
venv/bin/python3
Symbolic link
1
venv/bin/python3
Symbolic link
@ -0,0 +1 @@
|
||||
/usr/bin/python3
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user