image upload

This commit is contained in:
Faheed 2025-10-12 13:23:44 +03:00
parent 67a951c45a
commit 7b02120508
28 changed files with 252 additions and 182 deletions

Binary file not shown.

View File

@ -5,7 +5,7 @@ from django.utils import timezone
from .models import (
JobPosting, Candidate, TrainingMaterial, ZoomMeeting,
FormTemplate, FormStage, FormField, FormSubmission, FieldResponse,
SharedFormTemplate, Source, HiringAgency, IntegrationLog,InterviewSchedule,Profile
SharedFormTemplate, Source, HiringAgency, IntegrationLog,InterviewSchedule,Profile,JobPostingImage
)
class FormFieldInline(admin.TabularInline):
@ -263,3 +263,6 @@ admin.site.register(FieldResponse)
admin.site.register(InterviewSchedule)
admin.site.register(Profile)
# admin.site.register(HiringAgency)
admin.site.register(JobPostingImage)

View File

@ -5,7 +5,10 @@ from django.forms.formsets import formset_factory
from django.utils.translation import gettext_lazy as _
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit, Row, Column, Field, Div
from .models import ZoomMeeting, Candidate,TrainingMaterial,JobPosting,FormTemplate,InterviewSchedule,BreakTime
from .models import (
ZoomMeeting, Candidate,TrainingMaterial,JobPosting,
FormTemplate,InterviewSchedule,BreakTime,JobPostingImage
)
# from django_summernote.widgets import SummernoteWidget
from django_ckeditor_5.widgets import CKEditor5Widget
@ -192,8 +195,8 @@ class JobPostingForm(forms.ModelForm):
fields = [
'title', 'department', 'job_type', 'workplace_type',
'location_city', 'location_state', 'location_country',
'description', 'qualifications', 'salary_range', 'benefits',
'application_url', 'application_deadline', 'application_instructions',
'description', 'qualifications', 'salary_range', 'benefits'
,'application_deadline', 'application_instructions',
'position_number', 'reporting_to', 'start_date', 'status',
'created_by','open_positions','hash_tags'
]
@ -239,11 +242,11 @@ class JobPostingForm(forms.ModelForm):
# Application Information
'application_url': forms.URLInput(attrs={
'class': 'form-control',
'placeholder': 'https://university.edu/careers/job123',
'required': True
}),
# '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'
@ -356,6 +359,10 @@ class JobPostingForm(forms.ModelForm):
return cleaned_data
class JobPostingImageForm(forms.ModelForm):
class Meta:
model=JobPostingImage
fields=['post_image']
class FormTemplateForm(forms.ModelForm):
"""Form for creating form templates"""

View File

@ -1,7 +1,8 @@
# Generated by Django 5.2.6 on 2025-10-09 10:10
# Generated by Django 5.2.7 on 2025-10-11 11:04
import django.core.validators
import django.db.models.deletion
import django_ckeditor_5.fields
import django_countries.fields
import django_extensions.db.fields
import recruitment.validators
@ -183,7 +184,7 @@ class Migration(migrations.Migration):
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('name', models.CharField(help_text='Name of the form template', max_length=200)),
('description', models.TextField(blank=True, help_text='Description of the form template')),
('is_active', models.BooleanField(default=True, help_text='Whether this template is active')),
('is_active', models.BooleanField(default=False, help_text='Whether this template is active')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='form_templates', to=settings.AUTH_USER_MODEL)),
],
options={
@ -215,6 +216,7 @@ class Migration(migrations.Migration):
('phone', models.CharField(max_length=20, verbose_name='Phone')),
('address', models.TextField(max_length=200, verbose_name='Address')),
('resume', models.FileField(upload_to='resumes/', verbose_name='Resume')),
('is_resume_parsed', models.BooleanField(default=False, verbose_name='Resume Parsed')),
('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')),
('applied', models.BooleanField(default=False, verbose_name='Applied')),
('stage', models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer')], default='Applied', max_length=100, verbose_name='Stage')),
@ -249,16 +251,16 @@ class Migration(migrations.Migration):
('location_city', models.CharField(blank=True, max_length=100)),
('location_state', models.CharField(blank=True, max_length=100)),
('location_country', models.CharField(default='Saudia Arabia', 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')),
('description', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Description')),
('qualifications', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)),
('salary_range', models.CharField(blank=True, help_text='e.g., $60,000 - $80,000', max_length=200)),
('benefits', models.TextField(blank=True, help_text='Benefits offered')),
('benefits', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)),
('application_url', models.URLField(blank=True, help_text='URL where candidates apply', null=True, 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')),
('application_instructions', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)),
('internal_job_id', models.CharField(editable=False, max_length=50, primary_key=True, serialize=False)),
('created_by', models.CharField(blank=True, help_text='Name of person who created this job', max_length=100)),
('status', models.CharField(blank=True, choices=[('DRAFT', 'Draft'), ('PUBLISHED', 'Published'), ('CLOSED', 'Closed'), ('ARCHIVED', 'Archived')], default='DRAFT', max_length=20, null=True)),
('status', models.CharField(blank=True, choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], default='DRAFT', max_length=20, null=True)),
('hash_tags', models.CharField(blank=True, help_text='Comma-separated hashtags for linkedin post like #hiring,#jobopening', max_length=200, validators=[recruitment.validators.validate_hash_tags])),
('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')),
@ -270,6 +272,9 @@ class Migration(migrations.Migration):
('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)),
('open_positions', models.PositiveIntegerField(default=1, help_text='Number of open positions for this job')),
('cancel_reason', models.TextField(blank=True, help_text='Reason for canceling the job posting', verbose_name='Cancel Reason')),
('cancelled_by', models.CharField(blank=True, help_text='Name of person who cancelled this job', max_length=100, verbose_name='Cancelled By')),
('cancelled_at', models.DateTimeField(blank=True, null=True)),
('hiring_agency', models.ManyToManyField(blank=True, help_text='External agency responsible for sourcing candidates for this role', related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency')),
('source', models.ForeignKey(blank=True, help_text='The system or channel from which this job posting originated or was first published.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='job_postings', to='recruitment.source')),
],
@ -312,6 +317,16 @@ class Migration(migrations.Migration):
name='job',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.jobposting', verbose_name='Job'),
),
migrations.CreateModel(
name='JobPostingImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('post_image', models.ImageField(height_field='photo_height', upload_to='post/', width_field='photo_width')),
('post_image_height', models.PositiveIntegerField(blank=True, null=True)),
('post_image_width', models.PositiveIntegerField(blank=True, null=True)),
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='post_images', to='recruitment.jobposting')),
],
),
migrations.CreateModel(
name='Profile',
fields=[
@ -369,7 +384,7 @@ class Migration(migrations.Migration):
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('title', models.CharField(max_length=255, verbose_name='Title')),
('content', models.TextField(blank=True, verbose_name='Content')),
('content', django_ckeditor_5.fields.CKEditor5Field(blank=True, verbose_name='Content')),
('video_link', models.URLField(blank=True, verbose_name='Video Link')),
('file', models.FileField(blank=True, upload_to='training_materials/', verbose_name='File')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Created by')),

View File

@ -1,33 +0,0 @@
# Generated by Django 5.2.6 on 2025-10-09 10:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='jobposting',
name='cancel_reason',
field=models.TextField(blank=True, help_text='Reason for canceling the job posting', verbose_name='Cancel Reason'),
),
migrations.AddField(
model_name='jobposting',
name='cancelled_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='jobposting',
name='cancelled_by',
field=models.CharField(blank=True, help_text='Name of person who cancelled this job', max_length=100, verbose_name='Cancelled By'),
),
migrations.AlterField(
model_name='jobposting',
name='status',
field=models.CharField(blank=True, choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], default='DRAFT', max_length=20, null=True),
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 5.2.7 on 2025-10-11 12:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='jobpostingimage',
name='post_image_height',
),
migrations.RemoveField(
model_name='jobpostingimage',
name='post_image_width',
),
migrations.AlterField(
model_name='jobpostingimage',
name='post_image',
field=models.ImageField(upload_to='post/'),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 5.2.6 on 2025-10-09 12:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0002_jobposting_cancel_reason_jobposting_cancelled_at_and_more'),
]
operations = [
migrations.AddField(
model_name='candidate',
name='is_resume_parsed',
field=models.BooleanField(default=False, verbose_name='Resume Parsed'),
),
migrations.AlterField(
model_name='formtemplate',
name='is_active',
field=models.BooleanField(default=False, help_text='Whether this template is active'),
),
]

View File

@ -1,30 +0,0 @@
# Generated by Django 5.2.7 on 2025-10-10 10:35
import django.db.models.deletion
import django_ckeditor_5.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0003_candidate_is_resume_parsed_and_more'),
]
operations = [
migrations.AlterField(
model_name='jobposting',
name='description',
field=django_ckeditor_5.fields.CKEditor5Field(help_text='Full job description including responsibilities and requirements'),
),
migrations.CreateModel(
name='JobPostingImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('post_image', models.ImageField(height_field='photo_height', upload_to='post/', width_field='photo_width')),
('post_image_height', models.PositiveIntegerField(blank=True, null=True)),
('post_image_width', models.PositiveIntegerField(blank=True, null=True)),
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='post_images', to='recruitment.jobposting')),
],
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 5.2.7 on 2025-10-10 10:56
import django_ckeditor_5.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0004_alter_jobposting_description_jobpostingimage'),
]
operations = [
migrations.AlterField(
model_name='jobposting',
name='description',
field=django_ckeditor_5.fields.CKEditor5Field(verbose_name='Description'),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 5.2.7 on 2025-10-10 11:10
import django_ckeditor_5.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0005_alter_jobposting_description'),
]
operations = [
migrations.AlterField(
model_name='jobposting',
name='qualifications',
field=django_ckeditor_5.fields.CKEditor5Field(blank=True, help_text='Required qualifications and skills'),
),
]

View File

@ -91,7 +91,7 @@ class JobPosting(Base):
)
benefits = CKEditor5Field(blank=True,null=True,config_name='extends')
# Application Information
# Application Information ---job detail apply link for the candidates
application_url = models.URLField(
validators=[URLValidator()],
help_text="URL where candidates apply",
@ -249,11 +249,8 @@ class JobPosting(Base):
class JobPostingImage(models.Model):
job=models.ForeignKey('JobPosting',on_delete=models.CASCADE,related_name='post_images')
post_image = models.ImageField(upload_to='post/',
height_field='photo_height',
width_field='photo_width')
post_image_height = models.PositiveIntegerField(null=True, blank=True)
post_image_width = models.PositiveIntegerField(null=True, blank=True)
post_image = models.ImageField(upload_to='post/')
class Candidate(Base):
class Stage(models.TextChoices):
@ -409,6 +406,7 @@ class Candidate(Base):
return self.STAGE_SEQUENCE.get(old_stage, [])
@property
def submission(self):
return FormSubmission.objects.filter(template__job=self.job).first()
@property

View File

@ -9,6 +9,7 @@ urlpatterns = [
# Job URLs (using JobPosting model)
path('jobs/', views_frontend.JobListView.as_view(), name='job_list'),
path('jobs/create/', views.create_job, name='job_create'),
path('job/<slug:slug>/upload_image_simple/', views.job_image_upload, name='job_image_upload'),
path('jobs/<slug:slug>/update/', views.edit_job, name='job_update'),
# path('jobs/<slug:slug>/delete/', views., name='job_delete'),
path('jobs/<slug:slug>/', views.job_detail, name='job_detail'),
@ -66,7 +67,7 @@ urlpatterns = [
# path('forms/form/<int:template_id>/submit/', views.submit_form, name='submit_form'),
# path('forms/form/<int:template_id>/', views.form_wizard_view, name='form_wizard'),
path('forms/<int:form_id>/submissions/<int:slug>/', views.form_submission_details, name='form_submission_details'),
path('forms/<int:form_id>/submissions/<int:submission_id>/', views.form_submission_details, name='form_submission_details'),
path('forms/template/<slug:slug>/submissions/', views.form_template_submissions_list, name='form_template_submissions_list'),
# path('forms/<int:form_id>/', views.form_preview, name='form_preview'),

View File

@ -17,6 +17,7 @@ from .forms import (
FormTemplateForm,
InterviewScheduleForm,JobPostingStatusForm,
BreakTimeFormSet,
JobPostingImageForm
)
from rest_framework import viewsets
from django.contrib import messages
@ -215,6 +216,11 @@ def create_job(request):
job.created_by = request.POST.get("created_by", "").strip()
if not job.created_by:
job.created_by = "University Administrator"
job.save()
job_apply_url_relative=reverse('job_detail_candidate',kwargs={'slug':job.slug})
job_apply_url_absolute=request.build_absolute_uri(job_apply_url_relative)
job.application_url=job_apply_url_absolute
job.save()
messages.success(request, f'Job "{job.title}" created successfully!')
return redirect("job_list")
@ -279,6 +285,7 @@ def job_detail(request, slug):
offer_count = candidates.filter(stage="Offer").count()
status_form = JobPostingStatusForm(instance=job)
image_upload_form=JobPostingImageForm(instance=job)
# 2. Check for POST request (Status Update Submission)
if request.method == 'POST':
@ -306,10 +313,32 @@ def job_detail(request, slug):
"applied_count": applied_count,
"interview_count": interview_count,
"offer_count": offer_count,
'status_form':status_form
'status_form':status_form,
'image_upload_form':image_upload_form
}
return render(request, "jobs/job_detail.html", context)
def job_image_upload(request, slug):
#only for handling the post request
job=get_object_or_404(JobPosting,slug=slug)
if request.method=='POST':
image_upload_form=JobPostingImageForm(request.POST,request.FILES)
if image_upload_form.is_valid():
image_upload_form = image_upload_form.save(commit=False)
image_upload_form.job = job
image_upload_form.save()
messages.success(request, f"Image uploaded successfully for {job.title}.")
return redirect('job_detail', slug=job.slug)
else:
messages.error(request, "Image upload failed: Please ensure a valid image file was selected.")
return redirect('job_detail', slug=job.slug)
return redirect('job_detail', slug=job.slug)
# job detail facing the candidate:
def job_detail_candidate(request, slug):

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -364,14 +364,14 @@
.add-option {
color: var(--primary);
cursor: pointer;
font-size: 0.9rem;
font-size: 1rem;
display: inline-flex;
align-items: center;
gap: 5px;
margin-top: 5px;
}
.add-option:hover {
text-decoration: underline;
text-decoration: none;
}
/* File Upload Specific Styles */
.file-upload-area {
@ -685,12 +685,10 @@
<div class="container">
<!-- Sidebar with form elements -->
<div class="sidebar">
<div class="sidebar-header">
<a class="" href="{% url 'form_templates_list' %}">
<img src="{% static 'image/kaauh.jpeg' %}" style="height:100px; width:100px;">
<div class="sidebar-header" style="display: flex; flex-direction: column; align-items: center; justify-content: center;">
<a href="{% url 'form_templates_list' %}">
<img src="{% static 'image/kaauh.jpeg' %}" style="height:100px; width:100px; display: block; margin: 0 auto;">
</a>
</div>
<div class="field-categories">
<div class="field-category">

View File

@ -231,10 +231,10 @@
<td>{{ submission.applicant_email|default:"N/A" }}</td>
<td>{{ submission.submitted_at|date:"M d, Y H:i" }}</td>
<td class="text-end">
<a href="{% url 'form_submission_details' template_id=template.id submission_id=submission.id %}" class="btn btn-sm btn-outline-primary">
<a href="{% url 'form_submission_details' template_id=template.id submission_id=submission.id %}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-eye me-1"></i> {% trans "View Details" %}
</a>
</td>
</td>
</tr>
{% endfor %}
</tbody>

View File

@ -505,7 +505,7 @@
<body>
<nav
id="topNavbar"
class="navbar navbar-expand-lg sticky-top"
class="navbar navbar-expand-lg"
style="background-color: white; z-index: 1030"
>
<div class="container-fluid">

View File

@ -193,14 +193,14 @@
{% if form.salary_range.errors %}<div class="text-danger small mt-1">{{ form.salary_range.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
{% comment %} <div class="col-md-6">
<div>
<label for="{{ form.application_url.id_for_label }}" class="form-label">{% trans "Application URL" %} <span class="text-danger">*</span></label>
{{ form.application_url }}
{% if form.application_url.errors %}<div class="text-danger small mt-1">{{ form.application_url.errors }}</div>{% endif %}
<div class="form-text">{% trans "Full URL where candidates will apply" %}</div>
</div>
</div>
</div> {% endcomment %}
<div class="col-12">
<div>

View File

@ -199,14 +199,14 @@
{% if form.salary_range.errors %}<div class="text-danger small mt-1">{{ form.salary_range.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
{% comment %} <div class="col-md-6">
<div>
<label for="{{ form.application_url.id_for_label }}" class="form-label">{% trans "Application URL" %} <span class="text-danger">*</span></label>
{{ form.application_url }}
{% if form.application_url.errors %}<div class="text-danger small mt-1">{{ form.application_url.errors }}</div>{% endif %}
<div class="form-text">{% trans "Full URL where candidates will apply" %}</div>
</div>
</div>
</div> {% endcomment %}
<div class="col-12">
<div>

View File

@ -409,11 +409,11 @@
<i class="fas fa-edit"></i> {% trans "Edit Job" %}
</a>
{% if job.application_url %}
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#myModalForm">
<i class="fas fa-image me-1"></i> {% trans "Upload Image for Post" %}
</button>
{% endif %}
</div>
</div>
</div>
@ -590,7 +590,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="button" class="btn btn-outline-secondary btn-lg" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-main-action">{% trans "Save Changes" %}</button>
</div>
</form>

View File

@ -246,7 +246,7 @@
</div>
</div>
</div>
{{job.form_template}}
<div class="mobile-fixed-apply-bar d-lg-none">
{% if job.form_template %}
<a href="{% url 'form_wizard' job.form_template.pk %}" class="btn btn-main-action btn-lg w-100">

View File

@ -1,17 +1,22 @@
<div class="modal fade" id="myModalForm" tabindex="-1" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal fade mt-4" 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 Image</h5>
<h5 class="modal-title" id="myModalLabel">Add New Image for the Post</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 'job_detail' job.slug %}">
<form method="post" action="{% url 'job_image_upload' job.slug %}" enctype="multipart/form-data" >
{% 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>
{{ image_upload_form.as_p}}
{% if image_upload_form.instance.post_image %}
<p>Current Image:</p>
<img src="{{ image_upload_form.instance.post_image.url }}" alt="Post Image" style="max-width: 200px;">
{% endif %}
<div class="modal-footer mt-2">
<button type="button" class="btn btn-lg btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-main-action ">Save changes</button>
</div>
</form>
</div>