From edd2d5201581a70031072a8ff599ce1e5abefb43 Mon Sep 17 00:00:00 2001 From: ismail Date: Tue, 2 Dec 2025 16:55:06 +0300 Subject: [PATCH] more updates --- recruitment/forms.py | 4 +- recruitment/migrations/0001_initial.py | 58 ++++++++++++------- .../migrations/0002_alter_person_user.py | 20 ------- ...terviewtemplate_schedule_interview_type.py | 18 ------ ..._bulkinterviewtemplate_physical_address.py | 18 ------ .../0004_bulkinterviewtemplate_topic.py | 19 ------ .../migrations/0005_merge_20251202_1308.py | 14 ----- recruitment/tasks.py | 7 ++- recruitment/views.py | 20 ++++--- templates/base.html | 8 +-- templates/includes/document_list.html | 42 +++++++++----- templates/recruitment/application_detail.html | 4 +- 12 files changed, 86 insertions(+), 146 deletions(-) delete mode 100644 recruitment/migrations/0002_alter_person_user.py delete mode 100644 recruitment/migrations/0002_bulkinterviewtemplate_schedule_interview_type.py delete mode 100644 recruitment/migrations/0003_bulkinterviewtemplate_physical_address.py delete mode 100644 recruitment/migrations/0004_bulkinterviewtemplate_topic.py delete mode 100644 recruitment/migrations/0005_merge_20251202_1308.py diff --git a/recruitment/forms.py b/recruitment/forms.py index 82a5b80..c52b544 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -812,7 +812,7 @@ class NoteForm(forms.ModelForm): ), } labels = { - "content": _("Comment"), + "content": _("Note"), } # def __init__(self, *args, **kwargs): @@ -2186,7 +2186,7 @@ class MessageForm(forms.ModelForm): self.fields["job"].queryset = JobPosting.objects.filter( id__in=job_ids ).order_by("-created_at") - + print("Agency user job queryset:", self.fields["job"].queryset) elif self.user.user_type == "candidate": # Candidates can only see jobs they applied for diff --git a/recruitment/migrations/0001_initial.py b/recruitment/migrations/0001_initial.py index 918503b..ff9ae1d 100644 --- a/recruitment/migrations/0001_initial.py +++ b/recruitment/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.7 on 2025-11-27 15:36 +# Generated by Django 5.2.6 on 2025-12-02 10:27 import django.contrib.auth.models import django.contrib.auth.validators @@ -31,6 +31,18 @@ class Migration(migrations.Migration): ('end_time', models.TimeField(verbose_name='End Time')), ], ), + 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.CreateModel( name='FormStage', fields=[ @@ -57,7 +69,6 @@ class Migration(migrations.Migration): ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), ('location_type', models.CharField(choices=[('Remote', 'Remote (e.g., Zoom, Google Meet)'), ('Onsite', 'In-Person (Physical Location)')], db_index=True, max_length=10, verbose_name='Location Type')), ('topic', models.CharField(blank=True, help_text="e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room'", max_length=255, verbose_name='Meeting/Location Topic')), - ('details_url', models.URLField(blank=True, max_length=2048, null=True, verbose_name='Meeting/Location URL')), ('timezone', models.CharField(default='UTC', max_length=50, verbose_name='Timezone')), ('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')), ('duration', models.PositiveIntegerField(verbose_name='Duration (minutes)')), @@ -65,6 +76,7 @@ class Migration(migrations.Migration): ('meeting_id', models.CharField(blank=True, max_length=50, null=True, unique=True, verbose_name='External Meeting ID')), ('password', models.CharField(blank=True, max_length=20, null=True)), ('zoom_gateway_response', models.JSONField(blank=True, null=True)), + ('details_url', models.JSONField(blank=True, null=True)), ('participant_video', models.BooleanField(default=True)), ('join_before_host', models.BooleanField(default=False)), ('host_email', models.CharField(blank=True, max_length=255, null=True)), @@ -278,24 +290,6 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'Applications', }, ), - migrations.CreateModel( - name='InterviewNote', - 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')), - ('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(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.CreateModel( name='JobPosting', fields=[ @@ -363,12 +357,15 @@ class Migration(migrations.Migration): ('start_date', models.DateField(db_index=True, verbose_name='Start Date')), ('end_date', models.DateField(db_index=True, verbose_name='End Date')), ('working_days', models.JSONField(verbose_name='Working Days')), + ('topic', models.CharField(max_length=255, verbose_name='Interview Topic')), ('start_time', models.TimeField(verbose_name='Start Time')), ('end_time', models.TimeField(verbose_name='End Time')), ('break_start_time', models.TimeField(blank=True, null=True, verbose_name='Break Start Time')), ('break_end_time', models.TimeField(blank=True, null=True, verbose_name='Break End Time')), ('interview_duration', models.PositiveIntegerField(verbose_name='Interview Duration (minutes)')), ('buffer_time', models.PositiveIntegerField(default=0, verbose_name='Buffer Time (minutes)')), + ('schedule_interview_type', models.CharField(choices=[('Remote', 'Remote (e.g., Zoom)'), ('Onsite', 'In-Person (Physical Location)')], default='Onsite', max_length=10, verbose_name='Interview Type')), + ('physical_address', models.CharField(blank=True, max_length=255, null=True)), ('applications', models.ManyToManyField(blank=True, related_name='interview_schedules', to='recruitment.application')), ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('interview', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='schedule_templates', to='recruitment.interview', verbose_name='Location Template (Zoom/Onsite)')), @@ -438,6 +435,25 @@ class Migration(migrations.Migration): 'ordering': ['-created_at'], }, ), + 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.CreateModel( name='Notification', fields=[ @@ -478,7 +494,7 @@ class Migration(migrations.Migration): ('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size], verbose_name='Profile Image')), ('linkedin_profile', models.URLField(blank=True, null=True, verbose_name='LinkedIn Profile URL')), ('agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='recruitment.hiringagency', verbose_name='Hiring Agency')), - ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='person_profile', to=settings.AUTH_USER_MODEL, verbose_name='User Account')), + ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='person_profile', to=settings.AUTH_USER_MODEL, verbose_name='User Account')), ], options={ 'verbose_name': 'Person', diff --git a/recruitment/migrations/0002_alter_person_user.py b/recruitment/migrations/0002_alter_person_user.py deleted file mode 100644 index 0010b0c..0000000 --- a/recruitment/migrations/0002_alter_person_user.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-28 10:24 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='person', - name='user', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='person_profile', to=settings.AUTH_USER_MODEL, verbose_name='User Account'), - ), - ] diff --git a/recruitment/migrations/0002_bulkinterviewtemplate_schedule_interview_type.py b/recruitment/migrations/0002_bulkinterviewtemplate_schedule_interview_type.py deleted file mode 100644 index fd0de2c..0000000 --- a/recruitment/migrations/0002_bulkinterviewtemplate_schedule_interview_type.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-12-01 12:30 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='bulkinterviewtemplate', - name='schedule_interview_type', - field=models.CharField(choices=[('Remote', 'Remote (e.g., Zoom)'), ('Onsite', 'In-Person (Physical Location)')], default='Onsite', max_length=10, verbose_name='Interview Type'), - ), - ] diff --git a/recruitment/migrations/0003_bulkinterviewtemplate_physical_address.py b/recruitment/migrations/0003_bulkinterviewtemplate_physical_address.py deleted file mode 100644 index ffdfd45..0000000 --- a/recruitment/migrations/0003_bulkinterviewtemplate_physical_address.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-12-01 13:02 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0002_bulkinterviewtemplate_schedule_interview_type'), - ] - - operations = [ - migrations.AddField( - model_name='bulkinterviewtemplate', - name='physical_address', - field=models.CharField(blank=True, max_length=255, null=True), - ), - ] diff --git a/recruitment/migrations/0004_bulkinterviewtemplate_topic.py b/recruitment/migrations/0004_bulkinterviewtemplate_topic.py deleted file mode 100644 index e420b8b..0000000 --- a/recruitment/migrations/0004_bulkinterviewtemplate_topic.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.6 on 2025-12-01 14:43 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0003_bulkinterviewtemplate_physical_address'), - ] - - operations = [ - migrations.AddField( - model_name='bulkinterviewtemplate', - name='topic', - field=models.CharField(default='', max_length=255, verbose_name='Interview Topic'), - preserve_default=False, - ), - ] diff --git a/recruitment/migrations/0005_merge_20251202_1308.py b/recruitment/migrations/0005_merge_20251202_1308.py deleted file mode 100644 index 01fde32..0000000 --- a/recruitment/migrations/0005_merge_20251202_1308.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 5.2.6 on 2025-12-02 10:08 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0002_alter_person_user'), - ('recruitment', '0004_bulkinterviewtemplate_topic'), - ] - - operations = [ - ] diff --git a/recruitment/tasks.py b/recruitment/tasks.py index 7defa5b..23bbf0c 100644 --- a/recruitment/tasks.py +++ b/recruitment/tasks.py @@ -27,9 +27,9 @@ except ImportError: logger = logging.getLogger(__name__) OPENROUTER_API_KEY ='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' -# OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free' +OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct' -OPENROUTER_MODEL = 'openai/gpt-oss-20b' +# OPENROUTER_MODEL = 'qwen/qwen-2.5-7b-instruct' # OPENROUTER_MODEL = 'openai/gpt-oss-20b' # OPENROUTER_MODEL = 'mistralai/mistral-small-3.2-24b-instruct:free' @@ -623,7 +623,8 @@ def handle_resume_parsing_and_scoring(pk: int): }} If a top-level key or its required fields are missing, set the field to null, an empty list, or an empty object as appropriate. - + Be Clear and Direct Avoid overly indirect politeness which can add confusion. + Be strict,objective and concise and critical in your responses, and don't give inflated scores to weak candidates. Output only valid JSON—no markdown, no extra text. """ diff --git a/recruitment/views.py b/recruitment/views.py index ada62cb..56cb4f9 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -713,7 +713,7 @@ def request_cvs_download(request, slug): if not job.applications.exists(): messages.warning(request, _("No applications found for this job. ZIP file generation skipped.")) return redirect('job_detail', slug=slug) - + async_task('recruitment.tasks.generate_and_save_cv_zip', job.id) # Provide user feedback and redirect @@ -4868,7 +4868,7 @@ def message_delete(request, message_id): Redirects to the message list on success (either via standard redirect or HTMX's hx-redirect header). """ - + # 1. Retrieve the message # Use select_related to fetch linked objects efficiently for checks/logging message = get_object_or_404( @@ -4879,13 +4879,13 @@ def message_delete(request, message_id): # Only the sender or recipient can delete the message if message.sender != request.user and message.recipient != request.user: messages.error(request, "You don't have permission to delete this message.") - + # HTMX requests should handle redirection via client-side logic (hx-redirect) if "HX-Request" in request.headers: # Returning 403 or 400 is ideal, but 200 with an empty body is often accepted # by HTMX and the message is shown on the next page/refresh. - return HttpResponse(status=403) - + return HttpResponse(status=403) + # Standard navigation redirect return redirect("message_list") @@ -4899,7 +4899,7 @@ def message_delete(request, message_id): # 1. Set the HTMX response header for redirection response = HttpResponse(status=200) response["HX-Redirect"] = reverse("message_list") # <--- EXPLICIT HEADER - return response + return response # Standard navigation fallback return redirect("message_list") @@ -5069,7 +5069,8 @@ def document_upload(request, slug): if upload_target == 'person': return redirect("applicant_portal_dashboard") else: - return redirect("applicant_application_detail", slug=application.slug) + return render(request, 'recruitment/application_detail.html', {'application': application}) + # return redirect("application_detail", slug=application.slug) # Handle GET request for AJAX if request.headers.get("X-Requested-With") == "XMLHttpRequest": @@ -5081,7 +5082,6 @@ def document_upload(request, slug): def document_delete(request, document_id): """Delete a document""" document = get_object_or_404(Document, id=document_id) - print(document) # Initialize variables for redirection outside of the complex logic is_htmx = "HX-Request" in request.headers @@ -5144,7 +5144,9 @@ def document_delete(request, document_id): if is_htmx or request.headers.get("X-Requested-With") == "XMLHttpRequest": # For HTMX, return a 200 OK. The front-end is expected to use hx-swap='outerHTML' # to remove the element, or hx-redirect to navigate. - return HttpResponse(status=200) + response = HttpResponse(status=200) + response["HX-Refresh"] = "true" # Instruct HTMX to refresh the current view + return response # --- Standard Navigation Fallback --- else: diff --git a/templates/base.html b/templates/base.html index 0076768..eeddbc3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -50,7 +50,7 @@