delte message fix

This commit is contained in:
Faheed 2025-11-25 15:13:50 +03:00
parent 8bc3747afe
commit 6961a27985
30 changed files with 39 additions and 2162 deletions

View File

@ -230,6 +230,8 @@ class PersonDeleteView(StaffRequiredMixin, DeleteView):
success_url = reverse_lazy("person_list")
class JobPostingViewSet(viewsets.ModelViewSet):
queryset = JobPosting.objects.all()
serializer_class = JobPostingSerializer

View File

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class ApplicantConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'applicant'

View File

@ -1,22 +0,0 @@
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'}),
}

View File

@ -1,49 +0,0 @@
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)

View File

@ -1,70 +0,0 @@
# 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'],
},
),
]

View File

@ -1,144 +0,0 @@
# 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}"

View File

@ -1,94 +0,0 @@
{% extends 'base.html' %}
{% block title %}
Apply: {{ job.title }}
{% endblock %}
{% block content %}
<div class="container my-5">
<div class="row justify-content-center">
<div class="col-lg-8">
{# --- 1. Job Header and Overview (Fixed/Static Info) --- #}
<div class="card bg-light-subtle mb-4 p-4 border-0 rounded-3 shadow-sm">
<h1 class="h2 fw-bold text-primary mb-1">{{ job.title }}</h1>
<p class="mb-3 text-muted">
Your final step to apply for this position.
</p>
<div class="d-flex gap-4 small text-secondary">
<div>
<i class="fas fa-building me-1"></i>
<strong>Department:</strong> {{ job.department|default:"Not specified" }}
</div>
<div>
<i class="fas fa-map-marker-alt me-1"></i>
<strong>Location:</strong> {{ job.get_location_display }}
</div>
<div>
<i class="fas fa-briefcase me-1"></i>
<strong>Type:</strong> {{ job.get_job_type_display }} • {{ job.get_workplace_type_display }}
</div>
</div>
</div>
{# --- 2. Application Form Section --- #}
<div class="card p-5 border-0 rounded-3 shadow">
<h2 class="h3 fw-semibold mb-3">Application Details</h2>
{% if applicant_form.description %}
<p class="text-muted mb-4">{{ applicant_form.description }}</p>
{% endif %}
<form method="post" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="form-group mb-4">
{# Label Tag #}
<label for="{{ field.id_for_label }}" class="form-label">
{{ field.label }}
{% if field.field.required %}<span class="text-danger">*</span>{% endif %}
</label>
{# The Field Widget (Assumes form-control is applied in backend) #}
{{ field }}
{# Field Errors #}
{% if field.errors %}
<div class="invalid-feedback d-block">{{ field.errors }}</div>
{% endif %}
{# Help Text #}
{% if field.help_text %}
<div class="form-text">{{ field.help_text }}</div>
{% endif %}
</div>
{% endfor %}
{# General Form Errors (Non-field errors) #}
{% if form.non_field_errors %}
<div class="alert alert-danger mb-4">
{{ form.non_field_errors }}
</div>
{% endif %}
<button type="submit" class="btn btn-primary btn-lg mt-3 w-100">
<i class="fas fa-paper-plane me-2"></i> Submit Application
</button>
</form>
</div>
<footer class="mt-4 text-center">
<a href="{% url 'applicant:review_job_detail' job.internal_job_id %}"
class="btn btn-link text-secondary">
&larr; Review Job Details
</a>
</footer>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,68 +0,0 @@
{% extends 'base.html' %}
{% block title %}
Define Form for {{ job.title }}
{% endblock %}
{% block content %}
<div class="container my-5">
<div class="row justify-content-center">
<div class="col-lg-8 col-md-10">
<div class="card shadow-lg border-0 p-4 p-md-5">
<h2 class="card-title text-center mb-4 text-dark">
🛠️ New Application Form Configuration
</h2>
<p class="text-center text-muted mb-4 border-bottom pb-3">
You are creating a new form structure for job: <strong>{{ job.title }}</strong>
</p>
<form method="post" novalidate>
{% csrf_token %}
<fieldset class="mb-5">
<legend class="h5 mb-3 text-secondary">Form Metadata</legend>
<div class="form-group mb-4">
<label for="{{ form.name.id_for_label }}" class="form-label required">
Form Name
</label>
{# The field should already have form-control applied from the backend #}
{{ form.name }}
{% if form.name.errors %}
<div class="text-danger small mt-1">{{ form.name.errors }}</div>
{% endif %}
</div>
<div class="form-group mb-4">
<label for="{{ form.description.id_for_label }}" class="form-label">
Description
</label>
{# The field should already have form-control applied from the backend #}
{{ form.description}}
{% if form.description.errors %}
<div class="text-danger small mt-1">{{ form.description.errors }}</div>
{% endif %}
</div>
</fieldset>
<div class="d-flex justify-content-end gap-3 pt-3">
<a href="{% url 'applicant:job_forms_list' job.internal_job_id %}"
class="btn btn-outline-secondary">
Cancel
</a>
<button type="submit" class="btn univ-color btn-lg">
Create Form & Continue &rarr;
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@ -1,103 +0,0 @@
{% extends 'base.html' %}
{% block title %}
Manage Forms | {{ job.title }}
{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-10">
<header class="mb-5 pb-3 border-bottom d-flex flex-column flex-md-row justify-content-between align-items-md-center">
<div>
<h2 class="h3 mb-1 ">
<i class="fas fa-clipboard-list me-2 text-secondary"></i>
Application Forms for <span class="text-success fw-bold">"{{ job.title }}"</span>
</h2>
<p class="text-muted small">
Internal Job ID: **{{ job.internal_job_id }}**
</p>
</div>
{# Primary Action Button using the theme color #}
<a href="{% url 'applicant:create_form' job_id=job.internal_job_id %}"
class="btn univ-color btn-lg shadow-sm mt-3 mt-md-0">
<i class="fas fa-plus me-1"></i> Create New Form
</a>
</header>
{% if forms %}
<div class="list-group">
{% for form in forms %}
{# Custom styling based on active state #}
<div class="list-group-item d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center p-3 mb-3 rounded shadow-sm
{% if form.is_active %}border-success border-3 bg-light{% else %}border-secondary border-1{% endif %}">
{# Left Section: Form Details #}
<div class="flex-grow-1 me-4 mb-2 mb-sm-0">
<h4 class="h5 mb-1 d-inline-block">
{{ form.name }}
</h4>
{# Status Badge #}
{% if form.is_active %}
<span class="badge bg-success ms-2">
<i class="fas fa-check-circle me-1"></i> Active Form
</span>
{% else %}
<span class="badge bg-secondary ms-2">
<i class="fas fa-times-circle me-1"></i> Inactive
</span>
{% endif %}
<p class="text-muted mt-1 mb-1 small">
{{ form.description|default:"— No description provided. —" }}
</p>
</div>
{# Right Section: Actions #}
<div class="d-flex gap-2 align-items-center flex-wrap">
{# Edit Structure Button #}
<a href="{% url 'applicant:edit_form' form.id %}"
class="btn btn-sm btn-outline-secondary">
<i class="fas fa-pen me-1"></i> Edit Structure
</a>
{# Conditional Activation Button #}
{% if not form.is_active %}
<a href="{% url 'applicant:activate_form' form.id %}"
class="btn btn-sm univ-color">
<i class="fas fa-bolt me-1"></i> Activate Form
</a>
{% else %}
{# Active indicator/Deactivate button placeholder #}
<a href="#" class="btn btn-sm btn-outline-success" disabled>
<i class="fas fa-star me-1"></i> Current Form
</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5 bg-light rounded shadow-sm">
<i class="fas fa-file-alt fa-4x text-muted mb-3"></i>
<p class="lead mb-0">No application forms have been created yet for this job.</p>
<p class="mt-2 mb-0 text-secondary">Click the button above to define a new form structure.</p>
</div>
{% endif %}
<footer class="text-end mt-5 pt-3 border-top">
<a href="{% url 'jobs:job_detail' job.internal_job_id %}"
class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> Back to Job Details
</a>
</footer>
</div>
</div>
{% endblock %}

View File

@ -1,129 +0,0 @@
{% extends "base.html" %}
{% block title %}{{ job.title }} - University ATS{% endblock %}
{% block content %}
<div class="row mb-5">
<div class="col-lg-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2>{{ job.title }}</h2>
<span class="badge bg-{{ job.status|lower }} status-badge">
{{ job.get_status_display }}
</span>
</div>
<div class="card-body">
<!-- Job Details -->
<div class="row mb-3">
<div class="col-md-6">
<strong>Department:</strong> {{ job.department|default:"Not specified" }}
</div>
<div class="col-md-6">
<strong>Position Number:</strong> {{ job.position_number|default:"Not specified" }}
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<strong>Job Type:</strong> {{ job.get_job_type_display }}
</div>
<div class="col-md-6">
<strong>Workplace:</strong> {{ job.get_workplace_type_display }}
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<strong>Location:</strong> {{ job.get_location_display }}
</div>
<div class="col-md-6">
<strong>Created By:</strong> {{ job.created_by|default:"Not specified" }}
</div>
</div>
{% if job.salary_range %}
<div class="row mb-3">
<div class="col-12">
<strong>Salary Range:</strong> {{ job.salary_range }}
</div>
</div>
{% endif %}
{% if job.start_date %}
<div class="row mb-3">
<div class="col-12">
<strong>Start Date:</strong> {{ job.start_date }}
</div>
</div>
{% endif %}
{% if job.application_deadline %}
<div class="row mb-3">
<div class="col-12">
<strong>Application Deadline:</strong> {{ job.application_deadline }}
{% if job.is_expired %}
<span class="badge bg-danger">EXPIRED</span>
{% endif %}
</div>
</div>
{% endif %}
<!-- Description -->
{% if job.description %}
<div class="mb-3">
<h5>Description</h5>
<div>{{ job.description|linebreaks }}</div>
</div>
{% endif %}
{% if job.qualifications %}
<div class="mb-3">
<h5>Qualifications</h5>
<div>{{ job.qualifications|linebreaks }}</div>
</div>
{% endif %}
{% if job.benefits %}
<div class="mb-3">
<h5>Benefits</h5>
<div>{{ job.benefits|linebreaks }}</div>
</div>
{% endif %}
{% if job.application_instructions %}
<div class="mb-3">
<h5>Application Instructions</h5>
<div>{{ job.application_instructions|linebreaks }}</div>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-lg-4">
<!-- Add this section below your existing job details -->
<div class="card mt-4">
<div class="card-header bg-success text-white">
<h5><i class="fas fa-file-signature"></i> Ready to Apply?</h5>
</div>
<div class="card-body">
<p>Review the job details on the left, then click the button below to submit your application.</p>
<a href="{% url 'applicant:apply_form' job.internal_job_id %}" class="btn btn-success btn-lg w-100">
<i class="fas fa-paper-plane"></i> Apply for this Position
</a>
<p class="text-muted mt-2">
<small>You'll be redirected to our secure application form where you can upload your resume and provide additional details.</small>
</p>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,35 +0,0 @@
{% extends 'base.html' %}
{% block title %}Application Submitted - {{ job.title }}{% endblock %}
{% block content %}
<div class="card">
<div style="text-align: center; padding: 30px 0;">
<div style="width: 80px; height: 80px; background: #d4edda; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 20px;">
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="#28a745" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
</svg>
</div>
<h1 style="color: #28a745; margin-bottom: 15px;">Thank You!</h1>
<h2>Your application has been submitted successfully</h2>
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 25px 0; text-align: left;">
<p><strong>Position:</strong> {{ job.title }}</p>
<p><strong>Job ID:</strong> {{ job.internal_job_id }}</p>
<p><strong>Department:</strong> {{ job.department|default:"Not specified" }}</p>
{% if job.application_deadline %}
<p><strong>Application Deadline:</strong> {{ job.application_deadline|date:"F j, Y" }}</p>
{% endif %}
</div>
<p style="font-size: 18px; line-height: 1.6;">
We appreciate your interest in joining our team. Our hiring team will review your application
and contact you if there's a potential match for this position.
</p>
{% comment %} <div style="margin-top: 30px;">
<a href="/" class="btn btn-primary" style="margin-right: 10px;">Apply to Another Position</a>
<a href="{% url 'jobs:job_detail' job.internal_job_id %}" class="btn btn-outline">View Job Details</a>
</div> {% endcomment %}
</div>
</div>
{% endblock %}

View File

@ -1,24 +0,0 @@
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)

View File

@ -1,14 +0,0 @@
# 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

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,18 +0,0 @@
from django.urls import path
from . import views
app_name = 'applicant'
urlpatterns = [
# Form Management
path('job/<str:job_id>/forms/', views.job_forms_list, name='job_forms_list'),
path('job/<str:job_id>/forms/create/', views.create_form_for_job, name='create_form'),
path('form/<int:form_id>/edit/', views.edit_form, name='edit_form'),
path('field/<int:field_id>/delete/', views.delete_field, name='delete_field'),
path('form/<int:form_id>/activate/', views.activate_form, name='activate_form'),
# Public Application
path('apply/<str:job_id>/', views.apply_form_view, name='apply_form'),
path('review/job/detail/<str:job_id>/',views.review_job_detail, name="review_job_detail"),
path('apply/<str:job_id>/thank-you/', views.thank_you_view, name='thank_you'),
]

View File

@ -1,34 +0,0 @@
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

View File

@ -1,175 +0,0 @@
# 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})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

View File

@ -1,120 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.425 132.744" style="version:1">
<switch>
<foreignObject width="1" height="1" x="0" y="0" requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/"/>
<g>
<path fill="#ADA9AE" d="M141.468 75.372v0.145c0 9.575-6.977 16.611-18.315 16.611 -9.157 0-15.48-3.627-19.84-8.777l7.704-7.327c3.488 3.918 7.122 6.093 12.282 6.093 4.215 0 7.195-2.393 7.195-6.165 -0.056-0.1 0.822-6.6-9.957-6.6h-4.651l-1.745-7.11 16.32-16.385 -6.54 4.2h-17.628v-9.574h34.375v8.414l-12.863 12.258C134.709 62.316 141.468 65.942 141.468 75.372M109.563 17.265c0-4.597 3.302-8.297 7.936-8.297h5.222l-1.367 0.681c0.062 12.6-0.25 14.187 0.556 16.029l-4.353-0.001c-4.634 0-7.994-3.758-7.994-8.354V17.265zM117.499 27.788h4.637v-1.667c0.338 0.586 0.793 1.041 1.464 1.359 1.475 0.7 0.403 0.26 42.703 0.39V6.858h-2.231v18.785c0 0-38.678 0.149-39.413-0.181 -0.638-0.287-0.87-0.943-0.976-1.642 -0.169-1.135-0.066-1.156-0.097-16.962h-6.029c-6.169 0-10.369 4.885-10.369 10.465v0.058C107.188 22.96 111.33 27.788 117.499 27.788M182.746 4.63h-4.888c0.106-1.616-0.642-4.63 2.651-4.63h1.574v1.552c-1.524 0.113-2.401-0.371-2.401 0.846v0.515h3.064V4.63zM188.396 28.097c-0.595 0-1.2-0.083-1.813-0.251l0.586-2.143c1.009 0.287 2.299 0.304 2.831-0.598 0.621-1.068 0.399-1.444 0.432-18.247h2.231c-0.064 15.685 0.154 16.123-0.209 17.941C192.026 26.926 190.736 28.097 188.396 28.097M123.586 4.63h-2.232V2.403h2.232V4.63zM172.692 12.314c1.047-2.039 3.245-3.174 5.492-3.198V9.112h3.605l-1.274 0.635v10c-1.756-0.052-2.99 0.167-4.616-0.349 -2.22-0.698-3.724-2.406-3.724-4.885C172.175 13.716 172.347 12.982 172.692 12.314M172.692 20.206c2.905 2.07 6.866 1.728 7.073 1.767l0.75-1.571c-0.121 1.523 0.419 3.651-1.004 4.717 -1.203 0.899-3.348 0.87-4.672 0.556 -0.548-0.129-1.176-0.352-1.883-0.667l-0.697 2.143c1.414 0.521 2.766 0.946 4.267 0.946 2.176 0 3.756-0.515 4.742-1.544 0.985-1.031 1.478-2.418 1.478-4.162V6.858c-2.746 0.086-4.829-0.277-7.335 0.585C168.696 9.752 168.562 17.266 172.692 20.206M119.123 4.63h-2.232V2.403h2.232V4.63zM96.028 6.834h2.304v20.911h-1.886L81.607 8.936l1.312 3.048v15.761h-2.305V6.834h2.215l14.17 17.99 -0.971-2.566V6.834zM9.009 27.894L0 6.834h2.634c0.134 0.324 7.314 17.679 8.042 19.439l-0.228-2.298 7.121-17.141h2.543l-9.008 21.06H9.009zM48.932 6.834h2.364v20.91h-2.364V6.834zM74.29 17.349c0 4.75-3.411 8.573-8.2 8.573s-8.261-3.883-8.261-8.632v-0.061c0-4.749 3.413-8.572 8.201-8.572 4.789 0 8.26 3.883 8.26 8.633V17.349zM66.09 6.476c-6.375 0-10.715 5.048-10.715 10.814v0.059c0 5.765 4.28 10.754 10.655 10.754s10.715-5.048 10.715-10.813v-0.061C76.745 11.465 72.465 6.476 66.09 6.476M45.031 22.069v0.059c0 3.584-2.993 5.915-7.153 5.915 -3.322 0-6.045-1.105-8.559-3.346l1.466-1.732c2.185 1.971 4.28 2.956 7.184 2.956 2.813 0 4.668-1.492 4.668-3.554v-0.059c0-5.836-12.48-1.986-12.48-10.008v-0.059c0-3.286 2.903-5.706 6.884-5.706 3.052 0 5.237 0.867 7.362 2.57l-1.376 1.821c-4.75-3.864-10.506-2.384-10.506 1.106v0.06c0 1.971 1.077 3.076 5.687 4.062C42.877 17.17 45.031 18.873 45.031 22.069M25.688 27.745h-2.364V6.835h2.364V27.745zM163.293 30.096h2.232v2.227h-2.232V30.096zM158.83 30.096h2.232v2.227h-2.232V30.096zM18.71 81.609h19.398v9.649H0.461v-8.85c18.811-15.964 25.728-19.084 25.728-26.04 0-4.28-2.835-6.601-6.832-6.601 -3.925 0-6.614 2.175-10.393 6.817l-7.849-6.309C6.13 43.456 11 39.756 20.084 39.756c10.538 0 17.515 6.166 17.515 15.669v0.144c0 11.916-9.021 15.757-24.49 28.223l-2.16 1.664L18.71 81.609zM186.725 66.015c0 8.704-6.25 15.813-15.263 15.813 -9.012 0-15.407-7.254-15.407-15.959v-0.145c0-8.704 6.251-15.812 15.262-15.812 9.011 0 15.408 7.254 15.408 15.957V66.015zM171.462 39.612c-15.697 0-27.108 11.823-27.108 26.257v0.146c0 14.435 11.264 26.113 26.963 26.113s27.108-11.824 27.108-26.259v-0.145C198.425 51.29 187.16 39.612 171.462 39.612"/>
<path fill="#29367B" d="M68.518 36.272L71.095 38.74 68.622 41.311 66.046 38.843"/>
<path fill="#B1AEB3" d="M74.415 38.843L76.99 41.311 79.463 38.74 76.887 36.272"/>
<path fill="#D0DA33" d="M70.787 42.084L72.794 44.005 74.718 42.003 72.713 40.082"/>
<path fill="#71BA44" d="M78.142 43.183L80.148 45.105 82.072 43.103 80.066 41.181"/>
<path fill="#D0DA33" d="M67.507 45.145L69.513 47.067 71.437 45.065 69.433 43.143"/>
<path fill="#27B8BE" d="M74.238 45.378L76.243 47.3 78.168 45.298 76.162 43.376"/>
<path fill="#71BA44" d="M71.605 47.681L73.016 49.033 74.37 47.624 72.959 46.272"/>
<path fill="#58B75E" d="M76.928 48.742L78.339 50.094 79.694 48.685 78.283 47.334"/>
<path fill="#3088C8" d="M88.918 37.593H93.36800000000001V42.035000000000004H88.918z" transform="rotate(-10.39 91.135 39.818)"/>
<path fill="#1E3871" d="M83.133 39.508H86.70299999999999V43.072H83.133z" transform="rotate(-10.376 84.927 41.294)"/>
<path fill="#27B8BE" d="M89.92 44.395H93.49V47.959H89.92z" transform="rotate(-10.39 91.7 46.17)"/>
<path fill="#D0DA33" d="M85.014 44.984H87.794V47.758H85.014z" transform="rotate(-10.382 86.413 46.375)"/>
<path fill="#B1AEB3" d="M90.334 50.169H93.114V52.943999999999996H90.334z" transform="rotate(-10.382 91.733 51.56)"/>
<path fill="#27B8BE" d="M80.917 46.094H83.69800000000001V48.869H80.917z" transform="rotate(-10.382 82.315 47.485)"/>
<path fill="#71BA44" d="M85.88 49.669H88.66V52.443999999999996H85.88z" transform="rotate(-10.382 87.28 51.06)"/>
<path fill="#B1AEB3" d="M82.329 50.08H84.285V52.032H82.329z" transform="rotate(-10.39 83.314 51.066)"/>
<path fill="#58B75E" d="M86.024 54.05H87.979V56.001H86.024z" transform="rotate(-10.36 87.01 55.033)"/>
<path fill="#29367B" d="M101.572 52.342L99.659 56.354 103.679 58.262 105.59 54.251"/>
<path fill="#27B8BE" d="M95.867 50.489L94.333 53.707 97.557 55.238 99.091 52.02"/>
<path fill="#71BA44" d="M98.505 58.416L96.971 61.635 100.196 63.165 101.73 59.947"/>
<path fill="#D0DA33" d="M94.28 56.002L93.086 58.508 95.597 59.7 96.792 57.194"/>
<path fill="#D0DA33" d="M95.555 63.315L94.36 65.821 96.87 67.013 98.066 64.507"/>
<path fill="#3088C8" d="M90.306 54.51L89.111 57.016 91.622 58.208 92.817 55.702"/>
<path fill="#B1AEB3" d="M92.237 60.308L91.042 62.814 93.552 64.006 94.748 61.5"/>
<path fill="#71BA44" d="M89.217 58.541L88.376 60.304 90.142 61.142 90.983 59.38"/>
<path fill="#27B8BE" d="M89.372 64.241H91.324V66.196H89.372z" transform="rotate(-64.552 90.342 65.217)"/>
<path fill="#3088C8" d="M103.737 71.551L99.835 73.687 101.976 77.582 105.878 75.446"/>
<path fill="#27B8BE" d="M100.198 66.717L97.068 68.431 98.785 71.555 101.915 69.841"/>
<path fill="#27B8BE" d="M97.691 74.686L94.56 76.4 96.278 79.525 99.408 77.811"/>
<path fill="#29367B" d="M95.679 70.261L93.242 71.596 94.578 74.028 97.017 72.694"/>
<path fill="#58B75E" d="M92.426 76.936L89.988 78.271 91.325 80.703 93.763 79.369"/>
<path fill="#1E3871" d="M93.331 66.731L90.893 68.065 92.23 70.499 94.669 69.164"/>
<path fill="#D0DA33" d="M91.498 72.56L89.06 73.895 90.397 76.327 92.835 74.993"/>
<path fill="#B1AEB3" d="M90.084 69.364L88.369 70.303 89.31 72.014 91.024 71.076"/>
<path fill="#27B8BE" d="M87.475 74.114L85.76 75.053 86.7 76.764 88.415 75.826"/>
<path fill="#71BA44" d="M89.529 88.1H93.972V92.55099999999999H89.529z" transform="rotate(-82.958 91.74 90.32)"/>
<path fill="#1E3871" d="M90.424 82.17H93.988V85.74H90.424z" transform="rotate(-82.935 92.21 83.955)"/>
<path fill="#27B8BE" d="M83.719 87.169H87.28299999999999V90.74H83.719z" transform="rotate(-82.935 85.504 88.955)"/>
<path fill="#71BA44" d="M85.516 82.459H88.29100000000001V85.239H85.516z" transform="rotate(-82.952 86.912 83.852)"/>
<path fill="#B1AEB3" d="M78.966 85.97H81.741V88.75H78.966z" transform="rotate(-82.952 80.36 87.365)"/>
<path fill="#B1AEB3" d="M85.681 78.224H88.457V81.00500000000001H85.681z" transform="rotate(-82.935 87.072 79.615)"/>
<path fill="#71BA44" d="M80.778 81.88H83.55300000000001V84.661H80.778z" transform="rotate(-82.935 82.168 83.27)"/>
<path fill="#B1AEB3" d="M82.377 78.518H84.329V80.473H82.377z" transform="rotate(-82.946 83.36 79.498)"/>
<path fill="#B1AEB3" d="M77.476 80.846H79.428V82.802H77.476z" transform="rotate(-82.952 78.455 81.827)"/>
<path fill="#D0DA33" d="M76.652 96.502L73.39 93.479 70.362 96.735 73.624 99.757"/>
<path fill="#71BA44" d="M76.051 34.279L72.789 31.256 69.761 34.512 73.023 37.534"/>
<path fill="#27B8BE" d="M80.133 91.625L77.516 89.2 75.087 91.812 77.704 94.236"/>
<path fill="#71BA44" d="M71.765 91.764L69.147 89.339 66.718 91.951 69.335 94.375"/>
<path fill="#3088C8" d="M75.337 88.463L73.299 86.575 71.408 88.608 73.445 90.496"/>
<path fill="#D0DA33" d="M67.965 87.486L65.928 85.598 64.037 87.631 66.075 89.52"/>
<path fill="#D0DA33" d="M77.954 85.126L75.916 83.238 74.025 85.271 76.062 87.159"/>
<path fill="#B1AEB3" d="M71.833 85.227L69.795 83.339 67.904 85.372 69.941 87.26"/>
<path fill="#1E3871" d="M74.426 82.881L72.993 81.552 71.663 82.983 73.096 84.311"/>
<path fill="#B1AEB3" d="M69.087 81.907L67.654 80.579 66.323 82.009 67.756 83.337"/>
<path fill="#3088C8" d="M52.797 88.848H57.248V93.28999999999999H52.797z" transform="rotate(-11.333 55.022 91.072)"/>
<path fill="#27B8BE" d="M59.437 87.709H63.007V91.273H59.437z" transform="rotate(-11.333 61.207 89.493)"/>
<path fill="#71BA44" d="M52.569 82.934H56.139V86.49799999999999H52.569z" transform="rotate(-11.344 54.34 84.698)"/>
<path fill="#D0DA33" d="M58.262 83.048H61.042V85.82300000000001H58.262z" transform="rotate(-11.333 59.64 84.44)"/>
<path fill="#29367B" d="M62.34 81.869H65.12V84.645H62.34z" transform="rotate(-11.333 63.72 83.263)"/>
<path fill="#3088C8" d="M57.317 78.377H60.097V81.152H57.317z" transform="rotate(-11.333 58.696 79.77)"/>
<path fill="#71BA44" d="M61.693 78.724H63.649V80.676H61.693z" transform="rotate(-11.314 62.682 79.71)"/>
<path fill="#B1AEB3" d="M57.934 74.815H59.888999999999996V76.767H57.934z" transform="rotate(-11.344 58.916 75.795)"/>
<path fill="#27B8BE" d="M44.388 78.715L46.233 74.673 42.182 72.831 40.337 76.873"/>
<path fill="#D0DA33" d="M50.122 80.474L51.602 77.231 48.353 75.754 46.872 78.997"/>
<path fill="#3088C8" d="M47.351 72.592L48.832 69.349 45.582 67.871 44.102 71.115"/>
<path fill="#27B8BE" d="M49.54 71.708H52.316V74.487H49.54z" transform="rotate(-65.485 50.93 73.102)"/>
<path fill="#B1AEB3" d="M55.002 81.249L56.155 78.724 53.625 77.574 52.472 80.099"/>
<path fill="#29367B" d="M48.145 64.418H50.92100000000001V67.197H48.145z" transform="rotate(-65.474 49.535 65.806)"/>
<path fill="#58B75E" d="M53.538 73.134H56.31399999999999V75.913H53.538z" transform="rotate(-65.474 54.928 74.522)"/>
<path fill="#D0DA33" d="M51.512 67.369H54.288V70.148H51.512z" transform="rotate(-65.455 52.905 68.762)"/>
<path fill="#1E3871" d="M56.637 72.312L57.448 70.536 55.668 69.726 54.857 71.503"/>
<path fill="#71BA44" d="M55.879 66.948L56.69 65.171 54.91 64.362 54.099 66.139"/>
<path fill="#27B8BE" d="M41.902 59.544L45.769 57.344 43.563 53.485 39.697 55.685"/>
<path fill="#71BA44" d="M45.522 64.32L48.624 62.555 46.854 59.459 43.753 61.225"/>
<path fill="#1E3871" d="M47.897 56.31L50.999 54.545 49.229 51.448 46.128 53.214"/>
<path fill="#3088C8" d="M49.982 60.701L52.396 59.326 51.019 56.916 48.604 58.29"/>
<path fill="#71BA44" d="M53.124 53.974L55.538 52.599 54.161 50.189 51.746 51.563"/>
<path fill="#27B8BE" d="M52.387 64.192L54.803 62.818 53.426 60.407 51.01 61.782"/>
<path fill="#D0DA33" d="M54.124 58.334L56.539 56.96 55.162 54.549 52.747 55.924"/>
<path fill="#B1AEB3" d="M55.591 61.506L57.29 60.539 56.321 58.844 54.622 59.81"/>
<path fill="#71BA44" d="M58.12 56.713L59.82 55.746 58.851 54.051 57.151 55.018"/>
<path fill="#71BA44" d="M51.354 38.35H55.796V42.800000000000004H51.354z" transform="rotate(-83.898 53.578 40.576)"/>
<path fill="#B1AEB3" d="M51.445 45.166H55.008V48.736999999999995H51.445z" transform="rotate(-83.892 53.227 46.953)"/>
<path fill="#27B8BE" d="M58.066 40.057H61.63V43.628H58.066z" transform="rotate(-83.892 59.848 41.844)"/>
<path fill="#D0DA33" d="M57.142 45.581H59.917V48.361000000000004H57.142z" transform="rotate(-83.886 58.533 46.972)"/>
<path fill="#3088C8" d="M63.634 41.961H66.409V44.741H63.634z" transform="rotate(-83.91 65.016 43.35)"/>
<path fill="#27B8BE" d="M57.047 49.818H59.821999999999996V52.598H57.047z" transform="rotate(-83.886 58.435 51.21)"/>
<path fill="#71BA44" d="M61.889 46.08H64.664V48.86H61.889z" transform="rotate(-83.886 63.278 47.47)"/>
<path fill="#1E3871" d="M61.176 50.287H63.128V52.243H61.176z" transform="rotate(-83.892 62.153 51.266)"/>
<path fill="#B1AEB3" d="M66.039 47.877H67.991V49.834H66.039z" transform="rotate(-83.927 67.016 48.854)"/>
<path fill="#727A82" d="M86.941 68.235c-2.33-1.342-5.572-1.732-8.931-0.502 -1.468 0.555-2.938 1.524-4.178 2.473 -0.781-4.735-0.4-9.13-0.218-11.289 0.001-0.012 0.015-0.018 0.026-0.011 1.677 1.055 1.207 4.003 0.855 4.846 -0.007 0.017 0.018 0.028 0.03 0.014 2.09-2.468 1.069-5.193 0.64-5.883 -0.078-0.125 0.395 0.254 0.783 0.751 1.461 1.86 0.786 3.949 0.949 3.9 0.004-0.001 0.009 0 0.011-0.005 0.971-1.905 0.659-4.321-1.338-6.066 -0.091-0.077 0.265 0.039 0.378 0.079 1.703 0.591 2.591 2.345 2.579 3.909 0 0.344 1.15-2.938-1.63-4.606 -0.52-0.311-1.054-0.482-1.403-0.524 -0.272-0.029 1.489-0.294 2.726 0.544 0.743 0.501 0.938 1.107 0.919 0.937 -0.216-2.197-2.806-3.065-4.376-2.406 -0.166 0.083 0.618-1.079 2.134-0.989 0.292 0.017 0.562 0.08 0.778 0.183 0.015 0.007 0.029-0.013 0.018-0.026 -1.145-1.309-3.029-1.336-4.029 0.178 -0.007 0.01-0.022 0.01-0.028 0 -0.379-0.679-0.696-1.548-0.833-2.368 -0.033-0.21-0.103 1.015-0.863 2.368 -0.006 0.01-0.02 0.01-0.027 0 -1.003-1.518-2.885-1.488-4.029-0.178 -0.012 0.013 0.002 0.033 0.018 0.026 0.764-0.363 2.207-0.256 2.93 0.781 0.009 0.014-0.004 0.032-0.02 0.025 -1.618-0.68-4.16 0.243-4.375 2.406 -0.001 0.018 0.023 0.028 0.032 0.012 0.072-0.135 0.25-0.423 0.583-0.715 1.294-1.14 3.329-0.81 3.03-0.778 -0.795 0.098-2.427 0.806-3.03 2.424 -0.493 1.322-0.018 3.063 0 2.632 0.016-1.471 0.859-3.441 2.94-3.943 0.016-0.004 0.028 0.018 0.015 0.029 -3.234 2.843-1.278 6.334-1.308 6.058 -0.1-1.127-0.183-3.139 1.686-4.658 0.014-0.011 0.036 0.004 0.026 0.02 -0.429 0.69-1.449 3.415 0.64 5.883 0.012 0.014 0.037 0.003 0.03-0.014 -0.351-0.843-0.821-3.791 0.855-4.846 0.011-0.007 0.025-0.001 0.026 0.011 0.183 2.159 0.564 6.554-0.218 11.289 -2.656-2.03-5.271-3.361-8.639-3.213 -1.694 0.074-3.301 0.567-4.47 1.242 -0.042 0.024-0.011 0.085 0.034 0.071 4.899-1.566 9.139 0.334 13.081 3.614 -1.991 1.735-3.798 3.551-4.848 4.099 -0.245-0.335-0.67-0.528-1.135-0.427 -0.876 0.193-1.085 1.29-0.61 1.762 1.721 1.717 4.412-1.69 7.631-4.538 3.098 2.738 5.913 6.254 7.631 4.538 0.137-0.137 0.296-0.589 0.214-0.948 -0.207-0.896-1.398-1.155-1.959-0.387 -1.01-0.509-2.783-2.301-4.848-4.099 3.97-3.309 8.164-5.156 13.08-3.614C86.952 68.32 86.983 68.259 86.941 68.235"/>
<path fill="#ADA9AE" d="M71.265 115.108h1.277v-11.436h-1.277V115.108zM37.804 113.833l-1.995-4.891c2.064-1.747 4.625-1.426 6.273 0l-1.995 4.891H37.804zM30.764 113.833c-1.004-0.03-1.71 0.095-2.642-0.198 -3.451-1.09-2.513-5.888 1.349-5.888h1.293V113.833zM66.268 113.833c-2.057-0.063-3.91 0.363-4.853-1.274 0.186-0.818 0.084-1.552 0.112-5.306H60.25v4.318c0 3.169-3.824 2.963-4.726 1.06 -0.362-0.765-0.141-1.085-0.207-5.378h-1.278v4.414c0 1.44-0.901 2.166-2.155 2.166H49.41v-6.58h-1.277v6.58h-6.705l2.235-5.337c-1.268-1.108-3.033-2.151-4.725-2.151 -1.719 0-3.45 0.998-4.726 2.151l2.251 5.337h-4.422v-7.376h-2.043c-2.888 0-5.285 1.66-5.285 4.381 0 4.843 5.941 4.246 6.051 4.27 -0.02 0.157 0.159 1.251-0.575 1.801 -0.686 0.514-1.911 0.499-2.674 0.319 -0.314-0.075-0.673-0.202-1.077-0.383l-0.399 1.227c0.81 0.298 1.583 0.542 2.442 0.542 2.434 0 3.56-1.214 3.56-3.267v-0.239c20.842-0.131 20.226 0.3 21.496-0.398 0.484-0.265 0.875-0.621 1.173-1.068 0.309 0.51 0.729 0.909 1.261 1.196 1.117 0.599 2.821 0.543 3.856 0.031 0.537-0.265 0.95-0.674 1.237-1.227 0.493 0.842 1.258 1.383 2.794 1.451v0.015h3.687v-11.436h-1.277V113.833zM180.858 113.268c-1.13 2.641-5.02 2.717-6.146-0.04 -0.331-0.809-0.331-1.768 0-2.589 1.363-3.378 6.41-2.408 6.41 1.299C181.122 112.415 181.034 112.859 180.858 113.268M168.47 113.268c-0.977 2.27-3.921 2.606-5.46 1.02 -1.263-1.306-1.226-3.433-0.007-4.716 0.993-1.055 2.524-1.225 3.695-0.742C168.544 109.6 169.159 111.667 168.47 113.268M137.412 113.833c-0.977-0.03-1.685 0.098-2.611-0.206 -3.363-1.105-2.561-5.896 1.349-5.896h1.262V113.833zM186.389 113.833H182c1.37-3.007-0.758-6.532-4.263-6.532 -3.228 0-5.486 3.308-4.167 6.532h-3.959c1.335-2.919-0.688-6.532-4.262-6.532 -3.23 0-5.486 3.309-4.167 6.532h-4.055v-10.161h-1.277v10.161h-4.07c-0.07-1.309 0.297-3.246-0.799-4.469 -0.743-0.838-1.57-1.099-2.698-1.099h-5.651l2.404-4.593h-1.389l-2.452 4.577c0.079 0.508 0.233 0.905 0.575 1.29 6.387 0.054 6.804-0.134 7.559 0.184 0.592 0.245 0.953 0.692 1.078 1.235 0.152 0.654 0.075 1.146 0.096 2.875h-11.814v-7.376h-2.012c-3.384 0-5.268 2.013-5.268 4.333 0 2.907 2.338 4.318 5.268 4.318h25.335c1.879 2.024 5.042 1.852 6.769 0h5.619c1.873 2.018 5.033 1.859 6.77 0h6.497v-11.436h-1.278V113.833zM191.386 115.108h1.277v-11.436h-1.277V115.108zM97.288 117.914H98.6v-1.309h-1.312V117.914zM109.774 113.833l-1.996-4.891c2.048-1.734 4.605-1.441 6.274 0l-1.995 4.891H109.774zM118.825 113.833h-5.427l2.234-5.337c-1.271-1.114-3.042-2.151-4.725-2.151 -1.725 0-3.451 1.003-4.726 2.151l2.251 5.337h-4.756v-7.376h-1.278v8.922c0 1.127-0.082 1.96-1.213 1.96 -0.263 0-0.402-0.023-0.654-0.096l-0.335 1.228c1.067 0.292 2.327 0.194 2.953-0.726 0.566-0.833 0.499-1.983 0.527-2.637h16.427v-11.436h-1.278V113.833zM21.872 107.572c-1.42-1.55-3.064-0.988-3.624-1.115v1.274c0.932 0.036 1.363-0.103 1.995 0.199 0.532 0.256 0.908 0.669 1.062 1.251 0.173 0.669 0.076 1.191 0.104 4.652h-5.987v1.275h7.264c-0.053-4.83 0.132-5.342-0.2-6.421C22.353 108.252 22.148 107.88 21.872 107.572M92.619 117.929h1.312v-1.309h-1.312V117.929zM84.854 113.833c-0.975-0.03-1.684 0.098-2.611-0.206 -3.367-1.106-2.556-5.896 1.349-5.896h1.262V113.833zM98.583 106.457h-1.277v7.376h-4.821v-7.376h-1.277v7.376h-5.077v-7.376c-1.606 0.05-4.002-0.325-5.843 1.234 -2.61 2.211-1.937 7.417 3.832 7.417h14.463V106.457zM90.382 117.929h1.312v-1.309h-1.312V117.929zM5.037 103.672H3.725v1.309h1.312V103.672zM136.446 103.672h-1.312v1.309h1.312V103.672zM86.125 103.672h-1.312v1.309h1.312V103.672zM12.571 117.929h1.312v-1.309h-1.312V117.929zM83.888 103.672h-1.312v1.309h1.312V103.672zM7.274 103.672H5.962v1.309h1.312V103.672zM10.334 117.929h1.312v-1.309h-1.312V117.929zM6.003 113.833c-0.973-0.03-1.686 0.098-2.61-0.206 -3.361-1.102-2.567-5.896 1.349-5.896h1.261V113.833zM13.634 106.457h-1.277v7.376H7.28v-7.376c-1.531 0.047-2.748-0.153-4.183 0.326 -1.203 0.404-2.186 1.147-2.722 2.287 -0.558 1.186-0.457 2.696 0.057 3.664 0.925 1.746 2.86 2.374 4.836 2.374h8.366V106.457zM123.822 115.108h1.277v-11.436h-1.277V115.108zM138.682 103.672h-1.312v1.309h1.312V103.672z"/>
<path fill="#ADA9AE" d="M1.251 123.484L2.854 123.484 2.854 128.062 7.193 123.484 9.161 123.484 5.369 127.399 9.33 132.588 7.389 132.588 4.275 128.491 2.854 129.948 2.854 132.588 1.251 132.588"/>
<path fill="#ADA9AE" d="M10.792 123.484H12.395V132.588H10.792z"/>
<path fill="#ADA9AE" d="M14.863 123.484L16.349 123.484 21.236 129.779 21.236 123.484 22.812 123.484 22.812 132.588 21.47 132.588 16.44 126.111 16.44 132.588 14.863 132.588"/>
<path fill="#ADA9AE" d="M24.797 128.062v-0.026c0-2.549 1.955-4.709 4.704-4.709 1.59 0 2.568 0.443 3.506 1.236l-1.016 1.21c-2.47-2.098-5.512-0.587-5.512 2.237v0.026c0 1.873 1.237 3.251 3.1 3.251 0.861 0 1.643-0.273 2.203-0.689v-1.704h-2.333v-1.391h3.884v3.823C30.071 134.113 24.797 132.681 24.797 128.062"/>
<path fill="#ADA9AE" d="M38.948 131.131c4.254 0 4.203-6.191 0-6.191H37.15v6.191H38.948zM35.547 123.484h3.401c6.508 0 6.42 9.104 0 9.104h-3.401V123.484z"/>
<path fill="#ADA9AE" d="M53.155 128.062v-0.026c0-1.769-1.29-3.238-3.102-3.238 -1.811 0-3.075 1.443-3.075 3.212v0.026c0 1.769 1.29 3.239 3.101 3.239C51.891 131.275 53.155 129.831 53.155 128.062M45.297 128.062v-0.026c0-2.562 1.981-4.709 4.782-4.709 2.802 0 4.757 2.121 4.757 4.683v0.026c0 2.562-1.98 4.708-4.783 4.708C47.252 132.744 45.297 130.624 45.297 128.062"/>
<path fill="#ADA9AE" d="M56.827 123.484L58.534 123.484 61.31 127.789 64.085 123.484 65.792 123.484 65.792 132.588 64.189 132.588 64.189 126.059 61.31 130.351 61.258 130.351 58.404 126.085 58.404 132.588 56.827 132.588"/>
<path fill="#ADA9AE" d="M79.506 128.062v-0.026c0-1.769-1.29-3.238-3.102-3.238 -1.811 0-3.075 1.443-3.075 3.212v0.026c0 1.769 1.29 3.239 3.101 3.239C78.242 131.275 79.506 129.831 79.506 128.062M71.648 128.062v-0.026c0-2.562 1.981-4.709 4.782-4.709 2.802 0 4.757 2.121 4.757 4.683v0.026c0 2.562-1.981 4.708-4.783 4.708C73.603 132.744 71.648 130.624 71.648 128.062"/>
<path fill="#ADA9AE" d="M83.179 123.484L89.968 123.484 89.968 124.941 84.782 124.941 84.782 127.425 89.382 127.425 89.382 128.881 84.782 128.881 84.782 132.588 83.179 132.588"/>
<path fill="#ADA9AE" d="M95.027 131.262l0.964-1.145c0.873 0.755 1.746 1.184 2.88 1.184 0.99 0 1.616-0.455 1.616-1.145v-0.025c0-2.034-5.096-0.646-5.096-4.111v-0.026c0-2.794 3.911-3.507 6.425-1.495l-0.86 1.209c-2.098-1.561-3.961-0.947-3.961 0.144v0.026c0 2.005 5.095 0.737 5.095 4.096V130C102.09 133.192 97.564 133.521 95.027 131.262"/>
<path fill="#ADA9AE" d="M109.296 128.973l-1.577-3.642 -1.564 3.642H109.296zM107.002 123.419h1.485l4.014 9.169h-1.694l-0.925-2.198h-4.314l-0.937 2.198h-1.643L107.002 123.419z"/>
<path fill="#ADA9AE" d="M113.444 128.726v-5.242h1.603v5.177c0 1.691 0.873 2.601 2.306 2.601 1.421 0 2.294-0.858 2.294-2.536v-5.242h1.603v5.164c0 2.718-1.538 4.084-3.923 4.084C114.955 132.732 113.444 131.366 113.444 128.726"/>
<path fill="#ADA9AE" d="M126.997 131.131c4.254 0 4.204-6.191 0-6.191h-1.798v6.191H126.997zM123.596 123.484h3.401c6.508 0 6.42 9.104 0 9.104h-3.401V123.484z"/>
<path fill="#ADA9AE" d="M133.875 123.484H135.478V132.588H133.875z"/>
<path fill="#ADA9AE" d="M147.352 128.973l-1.577-3.642 -1.564 3.642H147.352zM145.058 123.419h1.485l4.014 9.169h-1.694l-0.925-2.198h-4.314l-0.937 2.198h-1.643L145.058 123.419z"/>
<path fill="#ADA9AE" d="M156.02 127.997c1.147 0 1.877-0.598 1.877-1.522v-0.026c0-0.975-0.704-1.509-1.89-1.509h-2.332v3.057H156.02zM152.072 123.484h4.065c1.928 0 3.389 0.969 3.389 2.874 -0.037 0.1 0.19 2.108-2.177 2.784l2.463 3.446h-1.89l-2.241-3.174c-0.175 0-2.109 0-2.006 0v3.174h-1.603V123.484z"/>
<path fill="#ADA9AE" d="M167.299 128.973l-1.577-3.642 -1.564 3.642H167.299zM165.005 123.419h1.486l4.013 9.169h-1.694l-0.925-2.198h-4.314l-0.937 2.198h-1.643L165.005 123.419z"/>
<path fill="#ADA9AE" d="M176.253 131.171c1.095 0 1.759-0.43 1.759-1.249v-0.026c0-0.767-0.612-1.223-1.876-1.223h-2.541v2.498H176.253zM175.784 127.321c1.029 0 1.72-0.403 1.72-1.236v-0.026c0-0.715-0.573-1.157-1.603-1.157h-2.306v2.419H175.784zM172.018 123.484h4.092c1.604 0 2.997 0.75 2.997 2.315 -0.037 0.1 0.164 1.294-1.303 2.055 1.069 0.364 1.811 0.976 1.811 2.211v0.026c0 1.626-1.342 2.497-3.375 2.497h-4.222V123.484z"/>
<path fill="#ADA9AE" d="M181.6 123.484H183.203V132.588H181.6z"/>
<path fill="#ADA9AE" d="M191.041 128.973l-1.577-3.642 -1.564 3.642H191.041zM188.747 123.419h1.485l4.014 9.169h-1.694l-0.925-2.198h-4.314l-0.937 2.198h-1.643L188.747 123.419z"/>
</g>
</switch>
</svg>

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@ -195,8 +195,8 @@
{% if request.user.is_superuser %}
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'admin_settings' %}"><i class="fas fa-cog me-3 fs-5"></i> <span>{% trans "Settings" %}</span></a></li>
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'source_list' %}"><i class="fas fa-cog me-3 fs-5"></i> <span>{% trans "Integration" %}</span></a></li>
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'admin_settings' %}"><i class="fas fa-cog me-3 fs-5"></i> <span>{% trans "Staff Settings" %}</span></a></li>
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'source_list' %}"><i class="fas fa-cog me-3 fs-5"></i> <span>{% trans "Integration Settings" %}</span></a></li>
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'easy_logs' %}"><i class="fas fa-history me-3 fs-5"></i> <span>{% trans "Activity Log" %}</span></a></li>
{% comment %} <li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="#"><i class="fas fa-question-circle me-3 text-primary fs-5"></i> <span>{% trans "Help & Support" %}</span></a></li> {% endcomment %}
{% endif %}

View File

@ -1,81 +1,85 @@
{% extends "base.html" %}
{% load static %}
{% load static i18n %}
{% block title %}{{ message.subject }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h1>{% trans "Message Detail" %}</h1>
<div class="row" id="message-{{ message.id }}">
<div class="col-12" >
<!-- Message Header -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
{{ message.subject }}
{% if message.parent_message %}
<span class="badge bg-secondary ms-2">Reply</span>
<span class="badge bg-secondary ms-2">{% trans "Reply" %}</span>
{% endif %}
</h5>
<div class="btn-group" role="group">
<a href="{% url 'message_reply' message.id %}" class="btn btn-outline-info">
<i class="fas fa-reply"></i> Reply
<i class="fas fa-reply"></i> {% trans "Reply" %}
</a>
{% if message.recipient == request.user %}
<a href="{% url 'message_mark_unread' message.id %}"
class="btn btn-outline-warning"
hx-post="{% url 'message_mark_unread' message.id %}">
<i class="fas fa-envelope"></i> Mark Unread
<i class="fas fa-envelope"></i> {% trans "Mark Unread" %}
</a>
{% endif %}
<a href="{% url 'message_delete' message.id %}"
class="btn btn-outline-danger"
hx-get="{% url 'message_delete' message.id %}"
hx-confirm="Are you sure you want to delete this message?">
<i class="fas fa-trash"></i> Delete
class="btn btn-sm btn-outline-danger"
hx-post="{% url 'message_delete' message.id %}"
hx-confirm="{% trans 'Are you sure you want to delete this message?' %}"
hx-target="#message-{{ message.id }}"
hx-swap="outerHTML"
title="{% trans 'Delete' %}">
<i class="fas fa-trash"></i>
</a>
<a href="{% url 'message_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Messages
<i class="fas fa-arrow-left"></i> {% trans "Back to Messages" %}
</a>
</div>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6">
<strong>From:</strong>
<strong>{% trans "From:" %}</strong>
<span class="text-primary">{{ message.sender.get_full_name|default:message.sender.username }}</span>
</div>
<div class="col-md-6">
<strong>To:</strong>
<strong>{% trans "To:" %}</strong>
<span class="text-primary">{{ message.recipient.get_full_name|default:message.recipient.username }}</span>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<strong>Type:</strong>
<strong>{% trans "Type:" %}</strong>
<span class="badge bg-{{ message.message_type|lower }}">
{{ message.get_message_type_display }}
</span>
</div>
<div class="col-md-6">
<strong>Status:</strong>
<strong>{% trans "Status:" %}</strong>
{% if message.is_read %}
<span class="badge bg-success">Read</span>
<span class="badge bg-success">{% trans "Read" %}</span>
{% if message.read_at %}
<small class="text-muted">({{ message.read_at|date:"M d, Y H:i" }})</small>
{% endif %}
{% else %}
<span class="badge bg-warning">Unread</span>
<span class="badge bg-warning">{% trans "Unread" %}</span>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<strong>Created:</strong>
<strong>{% trans "Created:" %}</strong>
<span>{{ message.created_at|date:"M d, Y H:i" }}</span>
</div>
{% if message.job %}
<div class="col-md-6">
<strong>Related Job:</strong>
<div class="col-md-6">
<strong>{% trans "Related Job:" %}</strong>
<a href="{% url 'job_detail' message.job.slug %}" class="text-primary">
{{ message.job.title }}
</a>
@ -84,13 +88,13 @@
</div>
{% if message.parent_message %}
<div class="alert alert-info">
<strong>In reply to:</strong>
<strong>{% trans "In reply to:" %}</strong>
<a href="{% url 'message_detail' message.parent_message.id %}">
{{ message.parent_message.subject }}
</a>
<small class="text-muted d-block">
From {{ message.parent_message.sender.get_full_name|default:message.parent_message.sender.username }}
on {{ message.parent_message.created_at|date:"M d, Y H:i" }}
{% trans "From" %} {{ message.parent_message.sender.get_full_name|default:message.parent_message.sender.username }}
{% trans "on" %} {{ message.parent_message.created_at|date:"M d, Y H:i" }}
</small>
</div>
{% endif %}
@ -100,7 +104,7 @@
<!-- Message Content -->
<div class="card">
<div class="card-header">
<h6 class="mb-0">Message</h6>
<h6 class="mb-0">{% trans "Message" %}</h6>
</div>
<div class="card-body">
<div class="message-content">
@ -114,7 +118,7 @@
<div class="card mt-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-comments"></i> Replies ({{ message.replies.count }})
<i class="fas fa-comments"></i> {% trans "Replies" %} ({{ message.replies.count }})
</h6>
</div>
<div class="card-body">
@ -136,7 +140,7 @@
</div>
<div class="mt-2">
<a href="{% url 'message_reply' reply.id %}" class="btn btn-sm btn-outline-info">
<i class="fas fa-reply"></i> Reply to this
<i class="fas fa-reply"></i> {% trans "Reply to this" %}
</a>
</div>
</div>

View File

@ -93,7 +93,7 @@
</thead>
<tbody>
{% for message in page_obj %}
<tr class="{% if not message.is_read %}table-secondary{% endif %}">
<tr class="{% if not message.is_read %}table-secondary{% endif %}" id="message-{{ message.id }}">
<td>
<a href="{% url 'message_detail' message.id %}"
class="{% if not message.is_read %}fw-bold {% endif %}">
@ -136,10 +136,12 @@
class="btn btn-sm btn-outline-primary" title="{% trans 'Reply' %}">
<i class="fas fa-reply"></i>
</a>
<a href="{% url 'message_delete' message.id %}"
<a href="{% url 'message_delete' message.id %}"
class="btn btn-sm btn-outline-danger"
hx-get="{% url 'message_delete' message.id %}"
hx-post="{% url 'message_delete' message.id %}"
hx-confirm="{% trans 'Are you sure you want to delete this message?' %}"
hx-target="#message-{{ message.id }}"
hx-swap="outerHTML"
title="{% trans 'Delete' %}">
<i class="fas fa-trash"></i>
</a>