diff --git a/.env b/.env index b9e2bf0..8d7fbd5 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ -DB_NAME=norahuniversity -DB_USER=norahuniversity -DB_PASSWORD=norahuniversity \ No newline at end of file +DB_NAME=haikal_db +DB_USER=faheed +DB_PASSWORD=Faheed@215 \ No newline at end of file diff --git a/recruitment/forms.py b/recruitment/forms.py index 82a5b80..d08c798 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -897,6 +897,15 @@ class JobPostingStatusForm(forms.ModelForm): widgets = { "status": forms.Select(attrs={"class": "form-select"}), } + + def clean_status(self): + status = self.cleaned_data.get("status") + if status == "ACTIVE": + if self.instance and self.instance.pk: + print(self.instance.assigned_to) + if not self.instance.assigned_to: + raise ValidationError("Please assign the job posting before setting it to Active.") + return status class LinkedPostContentForm(forms.ModelForm): @@ -2096,23 +2105,19 @@ class CandidateEmailForm(forms.Form): - +from django.forms import HiddenInput class MessageForm(forms.ModelForm): """Form for creating and editing messages between users""" class Meta: model = Message - fields = ["recipient", "job", "subject", "content", "message_type"] + fields = ["job","recipient", "subject", "content", "message_type"] widgets = { "recipient": forms.Select( attrs={"class": "form-select", "placeholder": "Select recipient","required": True,} ), "job": forms.Select( - attrs={"class": "form-select", "placeholder": "Select job", - "hx-get": "/en/messages/create/", - "hx-target": "#id_recipient", - "hx-select": "#id_recipient", - "hx-swap": "outerHTML",} + attrs={"class": "form-select", "placeholder": "Select job"} ), "subject": forms.TextInput( attrs={ @@ -2216,6 +2221,8 @@ class MessageForm(forms.ModelForm): self.fields["recipient"].queryset = User.objects.filter( user_type="staff" ).order_by("username") + + def clean(self): """Validate message form data""" diff --git a/recruitment/migrations/0006_emailcontent_alter_interview_details_url_note_and_more.py b/recruitment/migrations/0006_emailcontent_alter_interview_details_url_note_and_more.py new file mode 100644 index 0000000..b76acc2 --- /dev/null +++ b/recruitment/migrations/0006_emailcontent_alter_interview_details_url_note_and_more.py @@ -0,0 +1,56 @@ +# Generated by Django 5.2.7 on 2025-12-02 10:28 + +import django.db.models.deletion +import django_ckeditor_5.fields +import django_extensions.db.fields +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0005_merge_20251202_1308'), + ] + + operations = [ + migrations.CreateModel( + name='EmailContent', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('subject', models.CharField(max_length=255, verbose_name='Subject')), + ('message', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Message Body')), + ], + options={ + 'verbose_name': 'Email Content', + 'verbose_name_plural': 'Email Contents', + }, + ), + migrations.AlterField( + model_name='interview', + name='details_url', + field=models.JSONField(blank=True, null=True), + ), + migrations.CreateModel( + name='Note', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('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')), + ('note_type', models.CharField(choices=[('Feedback', 'Candidate Feedback'), ('Logistics', 'Logistical Note'), ('General', 'General Comment')], default='Feedback', max_length=50, verbose_name='Note Type')), + ('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Content/Feedback')), + ('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='recruitment.application', verbose_name='Application')), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_notes', to=settings.AUTH_USER_MODEL, verbose_name='Author')), + ('interview', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='recruitment.interview', verbose_name='Scheduled Interview')), + ], + options={ + 'verbose_name': 'Interview Note', + 'verbose_name_plural': 'Interview Notes', + 'ordering': ['created_at'], + }, + ), + migrations.DeleteModel( + name='InterviewNote', + ), + ] diff --git a/recruitment/views.py b/recruitment/views.py index ada62cb..11f9114 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -519,6 +519,7 @@ def job_detail(request, slug): job_status = status_form.cleaned_data["status"] form_template = job.form_template if job_status == "ACTIVE": + form_template.is_active = True form_template.save(update_fields=["is_active"]) else: @@ -535,7 +536,9 @@ def job_detail(request, slug): return redirect("job_detail", slug=slug) else: - messages.error(request, "Failed to update status due to validation errors.") + error_messages = status_form.errors.get('status', []) + formatted_errors = "
".join(error_messages) + messages.error(request, f"{formatted_errors}") # --- 2. Quality Metrics (JSON Aggregation) --- @@ -607,10 +610,13 @@ def job_detail(request, slug): if avg_t_in_exam_duration else 0 ) - + category_data = ( - applications.filter(ai_analysis_data__analysis_data_en__category__isnull=False) - .values("ai_analysis_data__analysis_data_en__category") + applications.filter( + ai_analysis_data__analysis_data_en__category__isnull=False + ).exclude( + ai_analysis_data__analysis_data_en__category__exact=None + ).values("ai_analysis_data__analysis_data_en__category") .annotate( application_count=Count("id"), category=Cast( @@ -619,6 +625,7 @@ def job_detail(request, slug): ) .order_by("ai_analysis_data__analysis_data_en__category") ) + # Prepare data for Chart.js categories = [item["category"] for item in category_data] applications_count = [item["application_count"] for item in category_data] @@ -4688,7 +4695,7 @@ def message_detail(request, message_id): @login_required def message_create(request): """Create a new message""" - from .email_service import EmailService + from .email_service import EmailService if request.method == "POST": form = MessageForm(request.user, request.POST) @@ -4699,10 +4706,7 @@ def message_create(request): # Send email if message_type is 'email' and recipient has email if message.recipient and message.recipient.email: - try: - - email_result = async_task('recruitment.tasks._task_send_individual_email', subject=message.subject, body_message=message.content, @@ -4730,9 +4734,35 @@ def message_create(request): messages.error(request, "Please correct the errors below.") else: - form = MessageForm(request.user) + + form.fields["job"].widget.attrs.update({"hx-get": "/en/messages/create/", + "hx-target": "#id_recipient", + "hx-select": "#id_recipient", + "hx-swap": "outerHTML",}) + if request.user.user_type == "staff": + job_id = request.GET.get("job") + if job_id: + job = get_object_or_404(JobPosting, id=job_id) + applications=job.applications.all() + applicant_users = User.objects.filter(person_profile__in=applications.values_list('person', flat=True)) + agency_users = User.objects.filter(id__in=AgencyJobAssignment.objects.filter(job=job).values_list('agency__user', flat=True)) + form.fields["recipient"].queryset = applicant_users | agency_users + + # form.fields["recipient"].queryset = User.objects.filter(person_profile__) + else: + + form.fields['recipient'].widget = HiddenInput() + if request.method == "GET" and "HX-Request" in request.headers and request.user.user_type in ["candidate","agency"]: + print() + job_id = request.GET.get("job") + if job_id: + job = get_object_or_404(JobPosting, id=job_id) + form.fields["recipient"].queryset = User.objects.filter(id=job.assigned_to.id) + form.fields["recipient"].initial = job.assigned_to + + context = { "form": form, } diff --git a/templates/messages/application_message_form.html b/templates/messages/application_message_form.html index cf9f760..0dc122d 100644 --- a/templates/messages/application_message_form.html +++ b/templates/messages/application_message_form.html @@ -1,5 +1,5 @@ {% extends "portal_base.html" %} -{% load static %} +{% load static crispy_forms_tags %} {% load i18n %} {% block title %}{% if form.instance.pk %}{% trans "Reply to Message" %}{% else %}{% trans "Compose Message" %}{% endif %}{% endblock %} @@ -11,23 +11,23 @@
{% if form.instance.pk %} - Reply to Message + {% trans "Reply to Message" %} {% else %} - Compose Message + {% trans "Compose Message" %} {% endif %}
{% if form.instance.parent_message %}
- Replying to: {{ form.instance.parent_message.subject }} + {% trans "Replying to:" %} {{ form.instance.parent_message.subject }}
- From {{ form.instance.parent_message.sender.get_full_name|default:form.instance.parent_message.sender.username }} - on {{ form.instance.parent_message.created_at|date:"M d, Y H:i" }} + {% trans "From" %} {{ form.instance.parent_message.sender.get_full_name|default:form.instance.parent_message.sender.username }} + {% trans "on" %} {{ form.instance.parent_message.created_at|date:"M d, Y H:i" }}
- Original message: + {% trans "Original message:" %}
{{ form.instance.parent_message.content|linebreaks }}
@@ -38,99 +38,17 @@
{% csrf_token %} -
-
-
- - {{ form.job }} - {% if form.job.errors %} -
- {{ form.job.errors.0 }} -
- {% endif %} -
- Select a job if this message is related to a specific position -
-
-
-
-
- - {{ form.recipient }} - - {% if form.recipient.errors %} -
- {{ form.recipient.errors.0 }} -
- {% endif %} -
- Select the user who will receive this message -
-
-
-
-
- - {{ form.message_type }} - {% if form.message_type.errors %} -
- {{ form.message_type.errors.0 }} -
- {% endif %} -
- Select the type of message you're sending -
-
-
-
- -
-
-
- - {{ form.subject }} - {% if form.subject.errors %} -
- {{ form.subject.errors.0 }} -
- {% endif %} -
-
-
- -
- - {{ form.content }} - {% if form.content.errors %} -
- {{ form.content.errors.0 }} -
- {% endif %} -
- Write your message here. You can use line breaks and basic formatting. -
-
- + {{form|crispy}}
- - Cancel + + {% trans "Cancel" %}
@@ -184,6 +102,7 @@ document.addEventListener('DOMContentLoaded', function() { // Character counter for subject const subjectField = document.getElementById('id_subject'); const maxLength = 200; + const charsLabel = "{% trans 'characters' %}"; if (subjectField) { // Add character counter display @@ -194,7 +113,7 @@ document.addEventListener('DOMContentLoaded', function() { function updateCounter() { const remaining = maxLength - subjectField.value.length; - counter.textContent = `${subjectField.value.length}/${maxLength} characters`; + counter.textContent = `${subjectField.value.length}/${maxLength} ${charsLabel}`; if (remaining < 20) { counter.className = 'text-warning'; } else { @@ -216,19 +135,19 @@ document.addEventListener('DOMContentLoaded', function() { if (!recipient) { e.preventDefault(); - alert('Please select a recipient.'); + alert("{% trans 'Please select a recipient.' %}"); return false; } if (!subject) { e.preventDefault(); - alert('Please enter a subject.'); + alert("{% trans 'Please enter a subject.' %}"); return false; } if (!content) { e.preventDefault(); - alert('Please enter a message.'); + alert("{% trans 'Please enter a message.' %}"); return false; } });