diff --git a/db.sqlite3 b/db.sqlite3 index 9cc3632..94afcef 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/recruitment/__pycache__/admin.cpython-312.pyc b/recruitment/__pycache__/admin.cpython-312.pyc index ccbec15..5053f08 100644 Binary files a/recruitment/__pycache__/admin.cpython-312.pyc and b/recruitment/__pycache__/admin.cpython-312.pyc differ diff --git a/recruitment/__pycache__/forms.cpython-312.pyc b/recruitment/__pycache__/forms.cpython-312.pyc index a4bf3f6..cc1d2a7 100644 Binary files a/recruitment/__pycache__/forms.cpython-312.pyc and b/recruitment/__pycache__/forms.cpython-312.pyc differ diff --git a/recruitment/__pycache__/models.cpython-312.pyc b/recruitment/__pycache__/models.cpython-312.pyc index 8e5cc9b..dbb414c 100644 Binary files a/recruitment/__pycache__/models.cpython-312.pyc and b/recruitment/__pycache__/models.cpython-312.pyc differ diff --git a/recruitment/__pycache__/urls.cpython-312.pyc b/recruitment/__pycache__/urls.cpython-312.pyc index 4c63ba3..aa24719 100644 Binary files a/recruitment/__pycache__/urls.cpython-312.pyc and b/recruitment/__pycache__/urls.cpython-312.pyc differ diff --git a/recruitment/__pycache__/views.cpython-312.pyc b/recruitment/__pycache__/views.cpython-312.pyc index 382f068..d052836 100644 Binary files a/recruitment/__pycache__/views.cpython-312.pyc and b/recruitment/__pycache__/views.cpython-312.pyc differ diff --git a/recruitment/admin.py b/recruitment/admin.py index 0676c9e..92200c8 100644 --- a/recruitment/admin.py +++ b/recruitment/admin.py @@ -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) \ No newline at end of file diff --git a/recruitment/forms.py b/recruitment/forms.py index 769b3e7..d4d3532 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -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""" diff --git a/recruitment/migrations/0001_initial.py b/recruitment/migrations/0001_initial.py index 27ae747..0e32b64 100644 --- a/recruitment/migrations/0001_initial.py +++ b/recruitment/migrations/0001_initial.py @@ -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')), diff --git a/recruitment/migrations/0002_jobposting_cancel_reason_jobposting_cancelled_at_and_more.py b/recruitment/migrations/0002_jobposting_cancel_reason_jobposting_cancelled_at_and_more.py deleted file mode 100644 index 91b6321..0000000 --- a/recruitment/migrations/0002_jobposting_cancel_reason_jobposting_cancelled_at_and_more.py +++ /dev/null @@ -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), - ), - ] diff --git a/recruitment/migrations/0002_remove_jobpostingimage_post_image_height_and_more.py b/recruitment/migrations/0002_remove_jobpostingimage_post_image_height_and_more.py new file mode 100644 index 0000000..385c411 --- /dev/null +++ b/recruitment/migrations/0002_remove_jobpostingimage_post_image_height_and_more.py @@ -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/'), + ), + ] diff --git a/recruitment/migrations/0003_candidate_is_resume_parsed_and_more.py b/recruitment/migrations/0003_candidate_is_resume_parsed_and_more.py deleted file mode 100644 index 959dd78..0000000 --- a/recruitment/migrations/0003_candidate_is_resume_parsed_and_more.py +++ /dev/null @@ -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'), - ), - ] diff --git a/recruitment/migrations/0004_alter_jobposting_description_jobpostingimage.py b/recruitment/migrations/0004_alter_jobposting_description_jobpostingimage.py deleted file mode 100644 index 2123467..0000000 --- a/recruitment/migrations/0004_alter_jobposting_description_jobpostingimage.py +++ /dev/null @@ -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')), - ], - ), - ] diff --git a/recruitment/migrations/0005_alter_jobposting_description.py b/recruitment/migrations/0005_alter_jobposting_description.py deleted file mode 100644 index 23b2679..0000000 --- a/recruitment/migrations/0005_alter_jobposting_description.py +++ /dev/null @@ -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'), - ), - ] diff --git a/recruitment/migrations/0006_alter_jobposting_qualifications.py b/recruitment/migrations/0006_alter_jobposting_qualifications.py deleted file mode 100644 index beee000..0000000 --- a/recruitment/migrations/0006_alter_jobposting_qualifications.py +++ /dev/null @@ -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'), - ), - ] diff --git a/recruitment/models.py b/recruitment/models.py index 512849d..d470e25 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -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 diff --git a/recruitment/urls.py b/recruitment/urls.py index 9ae1a4e..f3340ca 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -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//upload_image_simple/', views.job_image_upload, name='job_image_upload'), path('jobs//update/', views.edit_job, name='job_update'), # path('jobs//delete/', views., name='job_delete'), path('jobs//', views.job_detail, name='job_detail'), @@ -66,7 +67,7 @@ urlpatterns = [ # path('forms/form//submit/', views.submit_form, name='submit_form'), # path('forms/form//', views.form_wizard_view, name='form_wizard'), - path('forms//submissions//', views.form_submission_details, name='form_submission_details'), + path('forms//submissions//', views.form_submission_details, name='form_submission_details'), path('forms/template//submissions/', views.form_template_submissions_list, name='form_template_submissions_list'), # path('forms//', views.form_preview, name='form_preview'), diff --git a/recruitment/views.py b/recruitment/views.py index fcf6e8d..d83b636 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -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): diff --git a/static/image/vision.svg b/static/image/vision.svg index 43156d8..97124a3 100644 --- a/static/image/vision.svg +++ b/static/image/vision.svg @@ -1,8 +1,120 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/forms/form_builder.html b/templates/forms/form_builder.html index 13e6f08..328e686 100644 --- a/templates/forms/form_builder.html +++ b/templates/forms/form_builder.html @@ -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 @@
diff --git a/templates/jobs/job_detail_candidate.html b/templates/jobs/job_detail_candidate.html index 5d752fc..f74189d 100644 --- a/templates/jobs/job_detail_candidate.html +++ b/templates/jobs/job_detail_candidate.html @@ -246,7 +246,7 @@
- {{job.form_template}} +