diff --git a/NorahUniversity/__pycache__/settings.cpython-312.pyc b/NorahUniversity/__pycache__/settings.cpython-312.pyc index c5c0a1a..6946313 100644 Binary files a/NorahUniversity/__pycache__/settings.cpython-312.pyc and b/NorahUniversity/__pycache__/settings.cpython-312.pyc differ diff --git a/NorahUniversity/settings.py b/NorahUniversity/settings.py index 780b1eb..5e75a2d 100644 --- a/NorahUniversity/settings.py +++ b/NorahUniversity/settings.py @@ -191,15 +191,6 @@ SOCIALACCOUNT_PROVIDERS = { } } -UNFOLD = { - "DASHBOARD_CALLBACK": "recruitment.utils.dashboard_callback", - "STYLES": [ - lambda request: static("unfold/css/styles.css"), - ], - "SCRIPTS": [ - lambda request: static("unfold/js/app.js"), - ], -} ZOOM_ACCOUNT_ID = 'HoGikHXsQB2GNDC5Rvyw9A' ZOOM_CLIENT_ID = 'brC39920R8C8azfudUaQgA' diff --git a/db.sqlite3 b/db.sqlite3 index c37b46d..736bcc0 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 72b3fe5..7e73a6f 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 3f5b7af..a421df6 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 56ce80a..f67832d 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 f9d611e..8fd07cf 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 094f95f..cb165ec 100644 Binary files a/recruitment/__pycache__/views.cpython-312.pyc and b/recruitment/__pycache__/views.cpython-312.pyc differ diff --git a/recruitment/forms.py b/recruitment/forms.py index 4c6ad9b..9faadaa 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -4,7 +4,7 @@ from crispy_forms.helper import FormHelper from django.core.validators import URLValidator from django.utils.translation import gettext_lazy as _ from crispy_forms.layout import Layout, Submit, HTML, Div, Field -from .models import ZoomMeeting, Candidate,Job,TrainingMaterial,JobPosting +from .models import ZoomMeeting, Candidate,TrainingMaterial,JobPosting class CandidateForm(forms.ModelForm): class Meta: diff --git a/recruitment/migrations/0019_merge_20251006_1224.py b/recruitment/migrations/0019_merge_20251006_1224.py new file mode 100644 index 0000000..a706fa2 --- /dev/null +++ b/recruitment/migrations/0019_merge_20251006_1224.py @@ -0,0 +1,14 @@ +# Generated by Django 5.2.7 on 2025-10-06 12:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0013_formfield_formstage_remove_formsubmission_form_and_more'), + ('recruitment', '0018_alter_jobposting_hiring_agency'), + ] + + operations = [ + ] diff --git a/recruitment/migrations/__pycache__/0013_formfield_formstage_remove_formsubmission_form_and_more.cpython-312.pyc b/recruitment/migrations/__pycache__/0013_formfield_formstage_remove_formsubmission_form_and_more.cpython-312.pyc new file mode 100644 index 0000000..4c63586 Binary files /dev/null and b/recruitment/migrations/__pycache__/0013_formfield_formstage_remove_formsubmission_form_and_more.cpython-312.pyc differ diff --git a/recruitment/migrations/__pycache__/0019_merge_20251006_1224.cpython-312.pyc b/recruitment/migrations/__pycache__/0019_merge_20251006_1224.cpython-312.pyc new file mode 100644 index 0000000..7591704 Binary files /dev/null and b/recruitment/migrations/__pycache__/0019_merge_20251006_1224.cpython-312.pyc differ diff --git a/recruitment/models.py b/recruitment/models.py index e5a8d7b..e1a6159 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -16,22 +16,22 @@ class Base(models.Model): class Meta: abstract = True -# Create your models here. -class Job(Base): - title = models.CharField(max_length=255, verbose_name=_('Title')) - description_en = models.TextField(verbose_name=_('Description English')) - description_ar = models.TextField(verbose_name=_('Description Arabic')) - is_published = models.BooleanField(default=False, verbose_name=_('Published')) - posted_to_linkedin = models.BooleanField(default=False, verbose_name=_('Posted to LinkedIn')) - created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created at')) - updated_at = models.DateTimeField(auto_now=True, verbose_name=_('Updated at')) +# # Create your models here. +# class Job(Base): +# title = models.CharField(max_length=255, verbose_name=_('Title')) +# description_en = models.TextField(verbose_name=_('Description English')) +# description_ar = models.TextField(verbose_name=_('Description Arabic')) +# is_published = models.BooleanField(default=False, verbose_name=_('Published')) +# posted_to_linkedin = models.BooleanField(default=False, verbose_name=_('Posted to LinkedIn')) +# created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created at')) +# updated_at = models.DateTimeField(auto_now=True, verbose_name=_('Updated at')) - class Meta: - verbose_name = _('Job') - verbose_name_plural = _('Jobs') +# class Meta: +# verbose_name = _('Job') +# verbose_name_plural = _('Jobs') - def __str__(self): - return self.title +# def __str__(self): +# return self.title class JobPosting(Base): # Basic Job Information @@ -506,4 +506,43 @@ class SharedFormTemplate(models.Model): verbose_name_plural = 'Shared Form Templates' def __str__(self): - return f"Shared: {self.template.name}" \ No newline at end of file + return f"Shared: {self.template.name}" + + +class Source(models.Model): + name = models.CharField( + max_length=100, + unique=True, + verbose_name=_('Source Name'), + help_text=_("e.g., ATS, ERP ") + ) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.name + + class Meta: + verbose_name = _('Source') + verbose_name_plural = _('Sources') + ordering = ['name'] + +class HiringAgency(Base): + name = models.CharField(max_length=200, unique=True, verbose_name=_('Agency Name')) + contact_person = models.CharField(max_length=150, blank=True, verbose_name=_('Contact Person')) + email = models.EmailField(blank=True) + phone = models.CharField(max_length=20, blank=True) + website = models.URLField(blank=True) + notes = models.TextField(blank=True, help_text=_("Internal notes about the agency")) + country=CountryField(blank=True, null=True,blank_label=_('Select country')) + address=models.TextField(blank=True,null=True) + + def __str__(self): + return self.name + + class Meta: + verbose_name = _('Hiring Agency') + verbose_name_plural = _('Hiring Agencies') + ordering = ['name'] + + + \ No newline at end of file diff --git a/recruitment/templatetags/__pycache__/__init__.cpython-312.pyc b/recruitment/templatetags/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..364ecb7 Binary files /dev/null and b/recruitment/templatetags/__pycache__/__init__.cpython-312.pyc differ diff --git a/recruitment/templatetags/__pycache__/form_filters.cpython-312.pyc b/recruitment/templatetags/__pycache__/form_filters.cpython-312.pyc new file mode 100644 index 0000000..dc240d2 Binary files /dev/null and b/recruitment/templatetags/__pycache__/form_filters.cpython-312.pyc differ diff --git a/recruitment/views.py b/recruitment/views.py index fe0e444..2f26c1b 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -14,7 +14,7 @@ from django.contrib import messages from django.core.paginator import Paginator from .linkedin_service import LinkedInService from .models import FormTemplate, FormStage, FormField,FieldResponse,FormSubmission -from .models import ZoomMeeting, Job, Candidate, JobPosting +from .models import ZoomMeeting, Candidate, JobPosting from .serializers import JobPostingSerializer, CandidateSerializer from django.shortcuts import get_object_or_404, render, redirect from django.views.generic import CreateView,UpdateView,DetailView,ListView diff --git a/static/image/applicant/__init__.py b/static/image/applicant/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/static/image/applicant/__pycache__/__init__.cpython-312.pyc b/static/image/applicant/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..3e50e87 Binary files /dev/null and b/static/image/applicant/__pycache__/__init__.cpython-312.pyc differ diff --git a/static/image/applicant/__pycache__/admin.cpython-312.pyc b/static/image/applicant/__pycache__/admin.cpython-312.pyc new file mode 100644 index 0000000..c57cd4a Binary files /dev/null and b/static/image/applicant/__pycache__/admin.cpython-312.pyc differ diff --git a/static/image/applicant/__pycache__/apps.cpython-312.pyc b/static/image/applicant/__pycache__/apps.cpython-312.pyc new file mode 100644 index 0000000..c134cdd Binary files /dev/null and b/static/image/applicant/__pycache__/apps.cpython-312.pyc differ diff --git a/static/image/applicant/__pycache__/forms.cpython-312.pyc b/static/image/applicant/__pycache__/forms.cpython-312.pyc new file mode 100644 index 0000000..cbee2e9 Binary files /dev/null and b/static/image/applicant/__pycache__/forms.cpython-312.pyc differ diff --git a/static/image/applicant/__pycache__/forms_builder.cpython-312.pyc b/static/image/applicant/__pycache__/forms_builder.cpython-312.pyc new file mode 100644 index 0000000..dfa9f76 Binary files /dev/null and b/static/image/applicant/__pycache__/forms_builder.cpython-312.pyc differ diff --git a/static/image/applicant/__pycache__/models.cpython-312.pyc b/static/image/applicant/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..bb29aff Binary files /dev/null and b/static/image/applicant/__pycache__/models.cpython-312.pyc differ diff --git a/static/image/applicant/__pycache__/urls.cpython-312.pyc b/static/image/applicant/__pycache__/urls.cpython-312.pyc new file mode 100644 index 0000000..7ae23f9 Binary files /dev/null and b/static/image/applicant/__pycache__/urls.cpython-312.pyc differ diff --git a/static/image/applicant/__pycache__/views.cpython-312.pyc b/static/image/applicant/__pycache__/views.cpython-312.pyc new file mode 100644 index 0000000..58cd1ef Binary files /dev/null and b/static/image/applicant/__pycache__/views.cpython-312.pyc differ diff --git a/static/image/applicant/admin.py b/static/image/applicant/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/static/image/applicant/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/static/image/applicant/apps.py b/static/image/applicant/apps.py new file mode 100644 index 0000000..27badf7 --- /dev/null +++ b/static/image/applicant/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApplicantConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'applicant' diff --git a/static/image/applicant/forms.py b/static/image/applicant/forms.py new file mode 100644 index 0000000..5c5b0b5 --- /dev/null +++ b/static/image/applicant/forms.py @@ -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'}), + } \ No newline at end of file diff --git a/static/image/applicant/forms_builder.py b/static/image/applicant/forms_builder.py new file mode 100644 index 0000000..c3a43e7 --- /dev/null +++ b/static/image/applicant/forms_builder.py @@ -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) \ No newline at end of file diff --git a/static/image/applicant/migrations/0001_initial.py b/static/image/applicant/migrations/0001_initial.py new file mode 100644 index 0000000..d7437c3 --- /dev/null +++ b/static/image/applicant/migrations/0001_initial.py @@ -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'], + }, + ), + ] diff --git a/static/image/applicant/migrations/__init__.py b/static/image/applicant/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/static/image/applicant/migrations/__pycache__/0001_initial.cpython-312.pyc b/static/image/applicant/migrations/__pycache__/0001_initial.cpython-312.pyc new file mode 100644 index 0000000..3091ce8 Binary files /dev/null and b/static/image/applicant/migrations/__pycache__/0001_initial.cpython-312.pyc differ diff --git a/static/image/applicant/migrations/__pycache__/0002_formfield_field_name.cpython-312.pyc b/static/image/applicant/migrations/__pycache__/0002_formfield_field_name.cpython-312.pyc new file mode 100644 index 0000000..bb11c8f Binary files /dev/null and b/static/image/applicant/migrations/__pycache__/0002_formfield_field_name.cpython-312.pyc differ diff --git a/static/image/applicant/migrations/__pycache__/0003_applicantsubmission_score.cpython-312.pyc b/static/image/applicant/migrations/__pycache__/0003_applicantsubmission_score.cpython-312.pyc new file mode 100644 index 0000000..67abc7e Binary files /dev/null and b/static/image/applicant/migrations/__pycache__/0003_applicantsubmission_score.cpython-312.pyc differ diff --git a/static/image/applicant/migrations/__pycache__/0004_alter_applicantform_name_alter_formfield_choices_and_more.cpython-312.pyc b/static/image/applicant/migrations/__pycache__/0004_alter_applicantform_name_alter_formfield_choices_and_more.cpython-312.pyc new file mode 100644 index 0000000..2a2430a Binary files /dev/null and b/static/image/applicant/migrations/__pycache__/0004_alter_applicantform_name_alter_formfield_choices_and_more.cpython-312.pyc differ diff --git a/static/image/applicant/migrations/__pycache__/__init__.cpython-312.pyc b/static/image/applicant/migrations/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..e38639e Binary files /dev/null and b/static/image/applicant/migrations/__pycache__/__init__.cpython-312.pyc differ diff --git a/static/image/applicant/models.py b/static/image/applicant/models.py new file mode 100644 index 0000000..6b35d2f --- /dev/null +++ b/static/image/applicant/models.py @@ -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}" \ No newline at end of file diff --git a/static/image/applicant/templates/applicant/apply_form.html b/static/image/applicant/templates/applicant/apply_form.html new file mode 100644 index 0000000..eae2993 --- /dev/null +++ b/static/image/applicant/templates/applicant/apply_form.html @@ -0,0 +1,94 @@ +{% extends 'base.html' %} + +{% block title %} + Apply: {{ job.title }} +{% endblock %} + +{% block content %} +
+
+
+ + {# --- 1. Job Header and Overview (Fixed/Static Info) --- #} +
+

{{ job.title }}

+ +

+ Your final step to apply for this position. +

+ +
+
+ + Department: {{ job.department|default:"Not specified" }} +
+
+ + Location: {{ job.get_location_display }} +
+
+ + Type: {{ job.get_job_type_display }} • {{ job.get_workplace_type_display }} +
+
+
+ + {# --- 2. Application Form Section --- #} +
+

Application Details

+ + {% if applicant_form.description %} +

{{ applicant_form.description }}

+ {% endif %} + +
+ {% csrf_token %} + + {% for field in form %} +
+ {# Label Tag #} + + + {# The Field Widget (Assumes form-control is applied in backend) #} + {{ field }} + + {# Field Errors #} + {% if field.errors %} +
{{ field.errors }}
+ {% endif %} + + {# Help Text #} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} +
+ {% endfor %} + + {# General Form Errors (Non-field errors) #} + {% if form.non_field_errors %} +
+ {{ form.non_field_errors }} +
+ {% endif %} + + +
+
+ + + +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/static/image/applicant/templates/applicant/create_form.html b/static/image/applicant/templates/applicant/create_form.html new file mode 100644 index 0000000..e1c616a --- /dev/null +++ b/static/image/applicant/templates/applicant/create_form.html @@ -0,0 +1,68 @@ +{% extends 'base.html' %} + +{% block title %} + Define Form for {{ job.title }} +{% endblock %} + +{% block content %} +
+
+
+ +
+ +

+ 🛠️ New Application Form Configuration +

+ +

+ You are creating a new form structure for job: {{ job.title }} +

+ +
+ {% csrf_token %} + +
+ Form Metadata + +
+ + {# The field should already have form-control applied from the backend #} + {{ form.name }} + + {% if form.name.errors %} +
{{ form.name.errors }}
+ {% endif %} +
+ +
+ + {# The field should already have form-control applied from the backend #} + {{ form.description}} + + {% if form.description.errors %} +
{{ form.description.errors }}
+ {% endif %} +
+
+ +
+ + Cancel + + +
+ +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/static/image/applicant/templates/applicant/edit_form.html b/static/image/applicant/templates/applicant/edit_form.html new file mode 100644 index 0000000..e9ad842 --- /dev/null +++ b/static/image/applicant/templates/applicant/edit_form.html @@ -0,0 +1,1020 @@ +{% extends 'base.html' %} +{% load static i18n %} + +{% block title %} +Edit Application Form - {{ applicant_form.name }} +{% endblock %} + +{% block customCSS %} + +{% endblock %} + +{% block content %} +
+ {% if messages %} + + {% endif %} + +
+
+

Edit Application Form

+ +
+ +
+ +
+
+

Form Details

+
+
+ {% csrf_token %} +
+ + {{ form_details.name }} +
+
+ + {{ form_details.description }} +
+
+ +
+
+
+ +
+
+
+

+ Add Field

+
+ T Text Input +
+
+ @ Email +
+
+ Dropdown +
+
+ Checkbox +
+
+ Paragraph +
+
+ 📅 Date +
+
+ Radio Buttons +
+
+ +
+
+

Form Structure

+ +
+
+
+ + +

Drag fields from the left panel to build your form

+
+
+ +
+
+
+
+{% endblock %} + +{% block customJS %} + +{% endblock %} \ No newline at end of file diff --git a/static/image/applicant/templates/applicant/job_forms_list.html b/static/image/applicant/templates/applicant/job_forms_list.html new file mode 100644 index 0000000..7c7253f --- /dev/null +++ b/static/image/applicant/templates/applicant/job_forms_list.html @@ -0,0 +1,103 @@ +{% extends 'base.html' %} + +{% block title %} + Manage Forms | {{ job.title }} +{% endblock %} + +{% block content %} +
+
+ +
+
+

+ + Application Forms for "{{ job.title }}" +

+

+ Internal Job ID: **{{ job.internal_job_id }}** +

+
+ + {# Primary Action Button using the theme color #} + + Create New Form + +
+ + {% if forms %} + +
+ {% for form in forms %} + + {# Custom styling based on active state #} +
+ + {# Left Section: Form Details #} +
+

+ {{ form.name }} +

+ + {# Status Badge #} + {% if form.is_active %} + + Active Form + + {% else %} + + Inactive + + {% endif %} + +

+ {{ form.description|default:"— No description provided. —" }} +

+
+ + {# Right Section: Actions #} +
+ + {# Edit Structure Button #} + + Edit Structure + + + {# Conditional Activation Button #} + {% if not form.is_active %} + + Activate Form + + {% else %} + {# Active indicator/Deactivate button placeholder #} + + Current Form + + {% endif %} +
+
+ {% endfor %} +
+ + {% else %} +
+ +

No application forms have been created yet for this job.

+

Click the button above to define a new form structure.

+
+ {% endif %} + + + +
+
+{% endblock %} \ No newline at end of file diff --git a/static/image/applicant/templates/applicant/review_job_detail.html b/static/image/applicant/templates/applicant/review_job_detail.html new file mode 100644 index 0000000..44414b3 --- /dev/null +++ b/static/image/applicant/templates/applicant/review_job_detail.html @@ -0,0 +1,129 @@ +{% extends "base.html" %} + +{% block title %}{{ job.title }} - University ATS{% endblock %} + +{% block content %} +
+
+
+
+

{{ job.title }}

+ + {{ job.get_status_display }} + +
+
+ +
+
+ Department: {{ job.department|default:"Not specified" }} +
+
+ Position Number: {{ job.position_number|default:"Not specified" }} +
+
+ +
+
+ Job Type: {{ job.get_job_type_display }} +
+
+ Workplace: {{ job.get_workplace_type_display }} +
+
+ +
+
+ Location: {{ job.get_location_display }} +
+
+ Created By: {{ job.created_by|default:"Not specified" }} +
+
+ + {% if job.salary_range %} +
+
+ Salary Range: {{ job.salary_range }} +
+
+ {% endif %} + + {% if job.start_date %} +
+
+ Start Date: {{ job.start_date }} +
+
+ {% endif %} + + {% if job.application_deadline %} +
+
+ Application Deadline: {{ job.application_deadline }} + {% if job.is_expired %} + EXPIRED + {% endif %} +
+
+ {% endif %} + + + {% if job.description %} +
+
Description
+
{{ job.description|linebreaks }}
+
+ {% endif %} + + {% if job.qualifications %} +
+
Qualifications
+
{{ job.qualifications|linebreaks }}
+
+ {% endif %} + + {% if job.benefits %} +
+
Benefits
+
{{ job.benefits|linebreaks }}
+
+ {% endif %} + + {% if job.application_instructions %} +
+
Application Instructions
+
{{ job.application_instructions|linebreaks }}
+
+ {% endif %} + + +
+
+
+ +
+ + +
+
+
Ready to Apply?
+
+
+

Review the job details on the left, then click the button below to submit your application.

+ + Apply for this Position + +

+ You'll be redirected to our secure application form where you can upload your resume and provide additional details. +

+
+
+ + + +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/static/image/applicant/templates/applicant/thank_you.html b/static/image/applicant/templates/applicant/thank_you.html new file mode 100644 index 0000000..b93c945 --- /dev/null +++ b/static/image/applicant/templates/applicant/thank_you.html @@ -0,0 +1,35 @@ +{% extends 'base.html' %} +{% block title %}Application Submitted - {{ job.title }}{% endblock %} +{% block content %} +
+
+
+ + + +
+ +

Thank You!

+

Your application has been submitted successfully

+ +
+

Position: {{ job.title }}

+

Job ID: {{ job.internal_job_id }}

+

Department: {{ job.department|default:"Not specified" }}

+ {% if job.application_deadline %} +

Application Deadline: {{ job.application_deadline|date:"F j, Y" }}

+ {% endif %} +
+ +

+ 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. +

+ + {% comment %}
+ Apply to Another Position + View Job Details +
{% endcomment %} +
+
+{% endblock %} \ No newline at end of file diff --git a/static/image/applicant/templatetags/__init__.py b/static/image/applicant/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/static/image/applicant/templatetags/__pycache__/__init__.cpython-312.pyc b/static/image/applicant/templatetags/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..cc88fb9 Binary files /dev/null and b/static/image/applicant/templatetags/__pycache__/__init__.cpython-312.pyc differ diff --git a/static/image/applicant/templatetags/__pycache__/mytags.cpython-312.pyc b/static/image/applicant/templatetags/__pycache__/mytags.cpython-312.pyc new file mode 100644 index 0000000..eb9bf9e Binary files /dev/null and b/static/image/applicant/templatetags/__pycache__/mytags.cpython-312.pyc differ diff --git a/static/image/applicant/templatetags/__pycache__/signals.cpython-312.pyc b/static/image/applicant/templatetags/__pycache__/signals.cpython-312.pyc new file mode 100644 index 0000000..0cae73f Binary files /dev/null and b/static/image/applicant/templatetags/__pycache__/signals.cpython-312.pyc differ diff --git a/static/image/applicant/templatetags/mytags.py b/static/image/applicant/templatetags/mytags.py new file mode 100644 index 0000000..b60911d --- /dev/null +++ b/static/image/applicant/templatetags/mytags.py @@ -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) \ No newline at end of file diff --git a/static/image/applicant/templatetags/signals.py b/static/image/applicant/templatetags/signals.py new file mode 100644 index 0000000..8d5f22f --- /dev/null +++ b/static/image/applicant/templatetags/signals.py @@ -0,0 +1,14 @@ +# from django.db.models.signals import post_save +# from django.dispatch import receiver +# from . import models +# +# @receiver(post_save, sender=models.Candidate) +# def parse_resume(sender, instance, created, **kwargs): +# if instance.resume and not instance.summary: +# from .utils import extract_summary_from_pdf,match_resume_with_job_description +# summary = extract_summary_from_pdf(instance.resume.path) +# if 'error' not in summary: +# instance.summary = summary +# instance.save() +# +# # match_resume_with_job_description \ No newline at end of file diff --git a/static/image/applicant/tests.py b/static/image/applicant/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/static/image/applicant/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/static/image/applicant/urls.py b/static/image/applicant/urls.py new file mode 100644 index 0000000..fa4fe8a --- /dev/null +++ b/static/image/applicant/urls.py @@ -0,0 +1,18 @@ +from django.urls import path +from . import views + +app_name = 'applicant' + +urlpatterns = [ + # Form Management + path('job//forms/', views.job_forms_list, name='job_forms_list'), + path('job//forms/create/', views.create_form_for_job, name='create_form'), + path('form//edit/', views.edit_form, name='edit_form'), + path('field//delete/', views.delete_field, name='delete_field'), + path('form//activate/', views.activate_form, name='activate_form'), + + # Public Application + path('apply//', views.apply_form_view, name='apply_form'), + path('review/job/detail//',views.review_job_detail, name="review_job_detail"), + path('apply//thank-you/', views.thank_you_view, name='thank_you'), +] \ No newline at end of file diff --git a/static/image/applicant/utils.py b/static/image/applicant/utils.py new file mode 100644 index 0000000..4901d72 --- /dev/null +++ b/static/image/applicant/utils.py @@ -0,0 +1,34 @@ +import os +import fitz # PyMuPDF +import spacy +import requests +from recruitment import models +from django.conf import settings + +nlp = spacy.load("en_core_web_sm") + +def extract_text_from_pdf(pdf_path): + text = "" + with fitz.open(pdf_path) as doc: + for page in doc: + text += page.get_text() + return text + +def extract_summary_from_pdf(pdf_path): + if not os.path.exists(pdf_path): + return {'error': 'File not found'} + + text = extract_text_from_pdf(pdf_path) + doc = nlp(text) + summary = { + 'name': doc.ents[0].text if doc.ents else '', + 'skills': [chunk.text for chunk in doc.noun_chunks if len(chunk.text.split()) > 1], + 'summary': text[:500] + } + return summary + +def match_resume_with_job_description(resume, job_description,prompt=""): + resume_doc = nlp(resume) + job_doc = nlp(job_description) + similarity = resume_doc.similarity(job_doc) + return similarity \ No newline at end of file diff --git a/static/image/applicant/views.py b/static/image/applicant/views.py new file mode 100644 index 0000000..2cb4dc3 --- /dev/null +++ b/static/image/applicant/views.py @@ -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}) \ No newline at end of file diff --git a/static/image/hospital_logo.png b/static/image/hospital_logo.png new file mode 100644 index 0000000..4250a35 Binary files /dev/null and b/static/image/hospital_logo.png differ diff --git a/static/image/hospital_logo_1.png b/static/image/hospital_logo_1.png new file mode 100644 index 0000000..ff44820 Binary files /dev/null and b/static/image/hospital_logo_1.png differ diff --git a/static/image/hospital_logo_2.png b/static/image/hospital_logo_2.png new file mode 100644 index 0000000..a1698d4 Binary files /dev/null and b/static/image/hospital_logo_2.png differ diff --git a/static/image/hospital_logo_3.png b/static/image/hospital_logo_3.png new file mode 100644 index 0000000..5a9e883 Binary files /dev/null and b/static/image/hospital_logo_3.png differ diff --git a/static/image/vision.svg b/static/image/vision.svg new file mode 100644 index 0000000..43156d8 --- /dev/null +++ b/static/image/vision.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/static/media/form_uploads/Experience_-_Midwife_-_LD_-_King_Abdullah_bin_Abdulaziz_University_Hospital_BlxPqWL.pdf b/static/media/form_uploads/Experience_-_Midwife_-_LD_-_King_Abdullah_bin_Abdulaziz_University_Hospital_BlxPqWL.pdf new file mode 100644 index 0000000..38165e1 Binary files /dev/null and b/static/media/form_uploads/Experience_-_Midwife_-_LD_-_King_Abdullah_bin_Abdulaziz_University_Hospital_BlxPqWL.pdf differ diff --git a/static/media/form_uploads/jitendra.pdf b/static/media/form_uploads/jitendra.pdf new file mode 100644 index 0000000..fae2754 Binary files /dev/null and b/static/media/form_uploads/jitendra.pdf differ diff --git a/static/media/form_uploads/resume_juanjosecarin.pdf b/static/media/form_uploads/resume_juanjosecarin.pdf new file mode 100644 index 0000000..81b6fd7 Binary files /dev/null and b/static/media/form_uploads/resume_juanjosecarin.pdf differ diff --git a/templates/admin/index.html b/templates/admin/index.html deleted file mode 100644 index b4c0bd5..0000000 --- a/templates/admin/index.html +++ /dev/null @@ -1,86 +0,0 @@ -{% extends 'unfold/layouts/base.html' %} -{% load i18n unfold %} - -{% block breadcrumbs %}{% endblock %} - -{% block title %} - {% trans 'Dashboard' %} | {{ site_title|default:_('Django site admin') }} -{% endblock %} - -{% block branding %} -

- - {{ site_header }} - -

-{% endblock %} - -{% block content_before %} - {% component "unfold/helpers/header.html" %} - {% trans "Recruitment Dashboard" %} - {% endcomponent %} -{% endblock %} - -{% block content %} -
- {% component "unfold/components/card.html" with title="Total Jobs Posted" %} -

{{ total_jobs }}

- {% endcomponent %} - - {% component "unfold/components/card.html" with title="Total Candidates" %} -

{{ total_candidates }}

- {% endcomponent %} - - {% component "unfold/components/card.html" with title="Average Applications/Job" %} -

{{ average_applications }}

- {% endcomponent %} -
- -
-
-

Applications Per Job

-
- -
- - - -{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index a3ec383..1897f5a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,4 +1,4 @@ -{% load static i18n %} +{% comment %} {% load static i18n %} @@ -9,16 +9,16 @@ {% comment %} {% endcomment %} - + {% comment %} {% endcomment %} - + {% comment %} {% block extra_css %}{% endblock %} - -
+ {% endcomment %} + {% comment %}
@@ -37,8 +37,8 @@
-
- +
{% endcomment %} +{% comment %} + {% endcomment %} -
+ {% comment %}
{% if messages %}
{% for message in messages %} @@ -107,12 +107,12 @@ {% block content %} {% endblock %} -
+
{% endcomment %} - {% include 'includes/delete_modal.html' %} + {% comment %} {% include 'includes/delete_modal.html' %} {% endcomment %} - + {% comment %} - {% block extra_js %}{% endblock %} + {% block extra_js %}{% endblock %} {% endcomment %} + + +{% load static %} + + + + + + + {% block title %}University ATS{% endblock %} + + + + + + + + + {% block customCSS %}{% endblock %} + + + +
+
+ + +
+
+ + info@kaauh.edu.sa +
+
+ + +966 11 820 0000 +
+
+ +
+ Saudi Vision 2030 + King Abdullah Academic University Hospital +
+
+
+ + + +
+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + + {% block content %} + + {% endblock %} +
+ + {% include 'includes/delete_modal.html' %} +
+
+

+ © {% now "Y" %} King Abdullah Academic University Hospital (KAAUH).
+ All rights reserved. +

+
+
+ + + + + {% block customJS %}{% endblock %} + + \ No newline at end of file