diff --git a/NorahUniversity/settings.py b/NorahUniversity/settings.py index e0387a6..ca20ac3 100644 --- a/NorahUniversity/settings.py +++ b/NorahUniversity/settings.py @@ -487,3 +487,6 @@ MESSAGE_TAGS = { # Custom User Model AUTH_USER_MODEL = "recruitment.CustomUser" + + +ZOOM_WEBHOOK_API_KEY = "2GNDC5Rvyw9AHoGikHXsQB" \ No newline at end of file diff --git a/recruitment/__pycache__/forms.cpython-313.pyc b/recruitment/__pycache__/forms.cpython-313.pyc index 3a76149..91980bc 100644 Binary files a/recruitment/__pycache__/forms.cpython-313.pyc and b/recruitment/__pycache__/forms.cpython-313.pyc differ diff --git a/recruitment/__pycache__/models.cpython-313.pyc b/recruitment/__pycache__/models.cpython-313.pyc index b5e1511..5057120 100644 Binary files a/recruitment/__pycache__/models.cpython-313.pyc and b/recruitment/__pycache__/models.cpython-313.pyc differ diff --git a/recruitment/__pycache__/signals.cpython-313.pyc b/recruitment/__pycache__/signals.cpython-313.pyc index e613a9b..933adb4 100644 Binary files a/recruitment/__pycache__/signals.cpython-313.pyc and b/recruitment/__pycache__/signals.cpython-313.pyc differ diff --git a/recruitment/__pycache__/views.cpython-313.pyc b/recruitment/__pycache__/views.cpython-313.pyc index 7d346a3..ab80ab9 100644 Binary files a/recruitment/__pycache__/views.cpython-313.pyc and b/recruitment/__pycache__/views.cpython-313.pyc differ diff --git a/recruitment/forms.py b/recruitment/forms.py index 677bdcb..1a6ba6a 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -55,7 +55,7 @@ class SourceForm(forms.ModelForm): class Meta: model = Source - fields = ["name", "source_type", "description", "ip_address", "is_active"] + fields = ["name", "source_type", "description", "ip_address","trusted_ips", "is_active"] widgets = { "name": forms.TextInput( attrs={ @@ -81,6 +81,9 @@ class SourceForm(forms.ModelForm): "ip_address": forms.TextInput( attrs={"class": "form-control", "placeholder": "192.168.1.100"} ), + "trusted_ips":forms.TextInput( + attrs={"class": "form-control", "placeholder": "192.168.1.100","required": False} + ), "is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}), } @@ -2228,7 +2231,7 @@ class CandidateSignupForm(forms.ModelForm): 'last_name': forms.TextInput(attrs={'class': 'form-control'}), 'email': forms.EmailInput(attrs={'class': 'form-control'}), 'phone': forms.TextInput(attrs={'class': 'form-control'}), - 'gpa': forms.TextInput(attrs={'class': 'form-control'}), + # 'gpa': forms.TextInput(attrs={'class': 'form-control'}), "nationality": forms.Select(attrs={'class': 'form-control select2'}), 'date_of_birth': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}), 'gender': forms.Select(attrs={'class': 'form-control'}), diff --git a/recruitment/models.py b/recruitment/models.py index f00cb80..b05faad 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -99,9 +99,9 @@ class JobPosting(Base): # Core Fields title = models.CharField(max_length=200) department = models.CharField(max_length=100, blank=True) - job_type = models.CharField(max_length=20, choices=JOB_TYPES, default="FULL_TIME") + job_type = models.CharField(max_length=20, choices=JOB_TYPES, default="Full-time") workplace_type = models.CharField( - max_length=20, choices=WORKPLACE_TYPES, default="ON_SITE" + max_length=20, choices=WORKPLACE_TYPES, default="On-site" ) # Location diff --git a/recruitment/signals.py b/recruitment/signals.py index 51b73b6..ab502eb 100644 --- a/recruitment/signals.py +++ b/recruitment/signals.py @@ -18,7 +18,9 @@ from .models import ( Notification, HiringAgency, Person, + Source, ) +from .forms import generate_api_key, generate_api_secret from django.contrib.auth import get_user_model logger = logging.getLogger(__name__) @@ -29,11 +31,10 @@ User = get_user_model() @receiver(post_save, sender=JobPosting) def format_job(sender, instance, created, **kwargs): if created or not instance.ai_parsed: - try: - form_template = instance.form_template - except FormTemplate.DoesNotExist: + form = getattr(instance, "form_template", None) + if not form: FormTemplate.objects.get_or_create( - job=instance, is_active=False, name=instance.title + job=instance, is_active=True, name=instance.title ) async_task( "recruitment.tasks.format_job_description", @@ -469,3 +470,27 @@ def person_created(sender, instance, created, **kwargs): ) instance.user = user instance.save() + + +@receiver(post_save, sender=Source) +def source_created(sender, instance, created, **kwargs): + """ + Automatically generate API key and API secret when a new Source is created. + """ + if created: + # Only generate keys if they don't already exist + if not instance.api_key and not instance.api_secret: + logger.info(f"Generating API keys for new Source: {instance.pk} - {instance.name}") + + # Generate API key and secret using existing secure functions + api_key = generate_api_key() + api_secret = generate_api_secret() + + # Update the source with generated keys + instance.api_key = api_key + instance.api_secret = api_secret + instance.save(update_fields=['api_key', 'api_secret']) + + logger.info(f"API keys generated successfully for Source: {instance.name} (Key: {api_key[:8]}...)") + else: + logger.info(f"Source {instance.name} already has API keys, skipping generation") diff --git a/recruitment/tasks.py b/recruitment/tasks.py index b4cd871..7eaf1d4 100644 --- a/recruitment/tasks.py +++ b/recruitment/tasks.py @@ -28,7 +28,7 @@ logger = logging.getLogger(__name__) OPENROUTER_API_KEY ='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' # OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free' -OPENROUTER_MODEL = 'openai/gpt-oss-20b' +OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct' # OPENROUTER_MODEL = 'openai/gpt-oss-20b' # OPENROUTER_MODEL = 'mistralai/mistral-small-3.2-24b-instruct:free' @@ -506,6 +506,7 @@ def handle_zoom_webhook_event(payload): Background task to process a Zoom webhook event and update the local ZoomMeeting status. It handles: created, updated, started, ended, and deleted events. """ + print(payload) event_type = payload.get('event') object_data = payload['payload']['object'] @@ -534,7 +535,9 @@ def handle_zoom_webhook_event(payload): # elif event_type == 'meeting.updated': # Only update time fields if they are in the payload print(object_data) - meeting_instance.start_time = object_data.get('start_time', meeting_instance.start_time) + meeting_start_time = object_data.get('start_time', meeting_instance.start_time) + if meeting_start_time: + meeting_instance.start_time = datetime.fromisoformat(meeting_start_time) meeting_instance.duration = object_data.get('duration', meeting_instance.duration) meeting_instance.timezone = object_data.get('timezone', meeting_instance.timezone) diff --git a/recruitment/views.py b/recruitment/views.py index 8201e30..675ee16 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -1336,6 +1336,7 @@ def application_submit(request, template_slug): # email = submission.responses.get(field__label="Email Address") # phone = submission.responses.get(field__label="Phone Number") # address = submission.responses.get(field__label="Address") + gpa = submission.responses.get(field__label="GPA") resume = submission.responses.get(field__label="Resume Upload") @@ -1346,6 +1347,8 @@ def application_submit(request, template_slug): submission.save() # time=timezone.now() person = request.user.person_profile + person.gpa = gpa.value if gpa else None + person.save() Application.objects.create( person = person, resume=resume.get_file if resume.is_file else None, @@ -1806,7 +1809,7 @@ def candidate_screening_view(request, slug): min_experience_str = request.GET.get("min_experience") screening_rating = request.GET.get("screening_rating") tier1_count_str = request.GET.get("tier1_count") - gpa = request.GET.get("gpa") + gpa = request.GET.get("GPA") try: # Check if the string value exists and is not an empty string before conversion @@ -1854,8 +1857,9 @@ def candidate_screening_view(request, slug): ) if gpa: candidates = candidates.filter( - person__gpa = gpa + person__gpa__gt= gpa ) + print(candidates) if tier1_count > 0: candidates = candidates[:tier1_count] @@ -3018,7 +3022,6 @@ def is_superuser_check(user): def create_staff_user(request): if request.method == "POST": form = StaffUserCreationForm(request.POST) - print(form) if form.is_valid(): form.save() messages.success( @@ -3034,7 +3037,7 @@ def create_staff_user(request): @staff_user_required def admin_settings(request): - staffs = User.objects.filter(is_superuser=False) + staffs = User.objects.filter(user_type="staff",is_superuser=False) form = ToggleAccountForm() context = {"staffs": staffs, "form": form} return render(request, "user/admin_settings.html", context) @@ -3097,12 +3100,11 @@ def account_toggle_status(request, pk): @csrf_exempt -@staff_user_required def zoom_webhook_view(request): - print(request.headers) - print(settings.ZOOM_WEBHOOK_API_KEY) - # if api_key != settings.ZOOM_WEBHOOK_API_KEY: - # return HttpResponse(status=405) + api_key = request.headers.get("X-Zoom-API-KEY") + if api_key != settings.ZOOM_WEBHOOK_API_KEY: + return HttpResponse(status=405) + if request.method == "POST": try: payload = json.loads(request.body) @@ -5497,7 +5499,7 @@ def candidate_signup(request, slug): gender = form.cleaned_data["gender"] nationality = form.cleaned_data["nationality"] address = form.cleaned_data["address"] - gpa = form.cleaned_data["gpa"] + # gpa = form.cleaned_data["gpa"] password = form.cleaned_data["password"] user = User.objects.create_user( @@ -5512,7 +5514,7 @@ def candidate_signup(request, slug): phone=phone, gender=gender, nationality=nationality, - gpa=gpa, + # gpa=gpa, address=address, user = user ) diff --git a/recruitment/views_source.py b/recruitment/views_source.py index f594a6f..2f60d2e 100644 --- a/recruitment/views_source.py +++ b/recruitment/views_source.py @@ -204,26 +204,27 @@ def generate_api_keys_view(request, pk): source.save() # Log the key regeneration - IntegrationLog.objects.create( - source=source, - action=IntegrationLog.ActionChoices.CREATE, - endpoint=f'/api/sources/{source.pk}/generate-keys/', - method='POST', - request_data={ - 'name': source.name, - 'old_api_key': old_api_key[:8] + '...' if old_api_key else None, - 'new_api_key': new_api_key[:8] + '...' - }, - ip_address=request.META.get('REMOTE_ADDR'), - user_agent=request.META.get('HTTP_USER_AGENT', '') - ) + # IntegrationLog.objects.create( + # source=source, + # action=IntegrationLog.ActionChoices.CREATE, + # endpoint=f'/api/sources/{source.pk}/generate-keys/', + # method='POST', + # request_data={ + # 'name': source.name, + # 'old_api_key': old_api_key[:8] + '...' if old_api_key else None, + # 'new_api_key': new_api_key[:8] + '...' + # }, + # ip_address=request.META.get('REMOTE_ADDR'), + # user_agent=request.META.get('HTTP_USER_AGENT', '') + # ) - return JsonResponse({ - 'success': True, - 'api_key': new_api_key, - 'api_secret': new_api_secret, - 'message': 'API keys regenerated successfully' - }) + return redirect('source_detail', pk=source.pk) + # return JsonResponse({ + # 'success': True, + # 'api_key': new_api_key, + # 'api_secret': new_api_secret, + # 'message': 'API keys regenerated successfully' + # }) return JsonResponse({'error': 'Invalid request method'}, status=405) @@ -244,27 +245,28 @@ def toggle_source_status_view(request, pk): source.save() # Log the status change - IntegrationLog.objects.create( - source=source, - action=IntegrationLog.ActionChoices.SYNC, - endpoint=f'/api/sources/{source.pk}/toggle-status/', - method='POST', - request_data={ - 'name': source.name, - 'old_status': old_status, - 'new_status': source.is_active - }, - ip_address=request.META.get('REMOTE_ADDR'), - user_agent=request.META.get('HTTP_USER_AGENT', '') - ) + # IntegrationLog.objects.create( + # source=source, + # action=IntegrationLog.ActionChoices.SYNC, + # endpoint=f'/api/sources/{source.pk}/toggle-status/', + # method='POST', + # request_data={ + # 'name': source.name, + # 'old_status': old_status, + # 'new_status': source.is_active + # }, + # ip_address=request.META.get('REMOTE_ADDR'), + # user_agent=request.META.get('HTTP_USER_AGENT', '') + # ) status_text = 'activated' if source.is_active else 'deactivated' - return JsonResponse({ - 'success': True, - 'is_active': source.is_active, - 'message': f'Source "{source.name}" {status_text} successfully' - }) + return redirect('source_detail', pk=source.pk) + # return JsonResponse({ + # 'success': True, + # 'is_active': source.is_active, + # 'message': f'Source "{source.name}" {status_text} successfully' + # }) def copy_to_clipboard_view(request): """HTMX endpoint to copy text to clipboard""" diff --git a/templates/applicant/partials/candidate_facing_base.html b/templates/applicant/partials/candidate_facing_base.html index 1c7c277..785d21c 100644 --- a/templates/applicant/partials/candidate_facing_base.html +++ b/templates/applicant/partials/candidate_facing_base.html @@ -32,8 +32,8 @@ --gray-text: #6c757d; --kaauh-border: #d0d7de; /* Cleaner border color */ --kaauh-shadow: 0 15px 35px rgba(0, 0, 0, 0.1); /* Deeper shadow for premium look */ - --kaauh-dark-bg: #0d0d0d; - --kaauh-dark-contrast: #1c1c1c; + --kaauh-dark-bg: #0d0d0d; + --kaauh-dark-contrast: #1c1c1c; /* CALCULATED STICKY HEIGHTS (As provided in base) */ --navbar-height: 56px; @@ -43,17 +43,145 @@ body { min-height: 100vh; - background-color: #f0f0f5; + background-color: #f0f0f5; padding-top: 0; } .text-primary-theme { color: var(--kaauh-teal) !important; } .text-primary-theme-hover:hover { color: var(--kaauh-teal-dark) !important; } + /* Language Dropdown Styles */ + .language-toggle-btn { + background-color: transparent; + border: 1px solid var(--kaauh-border); + color: var(--kaauh-teal); + padding: 0.5rem 1rem; + border-radius: 0.5rem; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 500; + min-width: 120px; + justify-content: center; + } + + .language-toggle-btn:hover { + background-color: var(--kaauh-teal-light); + border-color: var(--kaauh-teal); + color: var(--kaauh-teal-dark); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 99, 110, 0.15); + } + + .language-toggle-btn:focus { + outline: none; + box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25); + border-color: var(--kaauh-teal); + } + + .language-toggle-btn::after { + margin-left: 0.5rem; + } + + .dropdown-menu { + border: 1px solid var(--kaauh-border); + border-radius: 0.5rem; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); + padding: 0.5rem; + min-width: 180px; + } + + .dropdown-menu .dropdown-item { + padding: 0.75rem 1rem; + transition: all 0.2s ease; + border-radius: 0.375rem; + margin: 0.25rem 0; + display: flex; + align-items: center; + gap: 0.75rem; + font-weight: 500; + border: none; + background: transparent; + width: 100%; + text-align: left; + } + + .dropdown-menu .dropdown-item:hover { + background-color: var(--kaauh-teal); + color: white; + transform: translateX(4px); + } + + .dropdown-menu .dropdown-item:focus { + outline: none; + box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25); + } + + .dropdown-menu .dropdown-item.active { + background-color: var(--kaauh-teal); + color: white; + font-weight: 600; + box-shadow: 0 2px 4px rgba(0, 99, 110, 0.2); + } + + .dropdown-menu .dropdown-item.active:hover { + background-color: var(--kaauh-teal-dark); + } + + .flag-emoji { + font-size: 1.2rem; + line-height: 1; + min-width: 24px; + text-align: center; + } + + .language-text { + font-size: 0.9rem; + font-weight: 500; + } + + /* RTL Support for Language Dropdown */ + html[dir="rtl"] .language-toggle-btn { + flex-direction: row-reverse; + } + + html[dir="rtl"] .dropdown-menu .dropdown-item { + flex-direction: row-reverse; + text-align: right; + } + + html[dir="rtl"] .dropdown-menu .dropdown-item:hover { + transform: translateX(-4px); + } + + /* Mobile Responsiveness */ + @media (max-width: 768px) { + .language-toggle-btn { + min-width: 100px; + padding: 0.4rem 0.8rem; + font-size: 0.9rem; + } + + .dropdown-menu { + min-width: 160px; + } + + .dropdown-menu .dropdown-item { + padding: 0.6rem 0.8rem; + font-size: 0.85rem; + } + + .flag-emoji { + font-size: 1rem; + min-width: 20px; + } + } + .bg-kaauh-teal { background-color: #00636e; } - + .btn-main-action { background-color: var(--kaauh-teal); color: white; @@ -67,7 +195,7 @@ background-color: var(--kaauh-teal-dark); color: white; transform: translateY(-2px); /* More pronounced lift */ - box-shadow: 0 10px 20px rgba(0, 99, 110, 0.5); + box-shadow: 0 10px 20px rgba(0, 99, 110, 0.5); } /* ---------------------------------------------------------------------- */ /* 1. DARK HERO STYLING (High Contrast) */ @@ -75,16 +203,16 @@ .hero-section { background: linear-gradient(135deg, var(--kaauh-dark-contrast) 0%, var(--kaauh-dark-bg) 100%); padding: 4rem 0; /* Reduced from 8rem to 4rem */ - margin-top: -1px; - color: white; - position: relative; + margin-top: -1px; + color: white; + position: relative; overflow: hidden; } .hero-title { font-size: 2.5rem; /* Reduced from 3.5rem to 2.5rem */ font-weight: 800; /* Extra bold */ line-height: 1.1; - letter-spacing: -0.05em; + letter-spacing: -0.05em; max-width: 900px; } .hero-section .lead { @@ -104,7 +232,7 @@ padding: 10rem 0; } .hero-title { - font-size: 5.5rem; + font-size: 5.5rem; } } @@ -153,20 +281,20 @@ background-color: #f0f0f5; /* Separates the job list from the white path section */ padding-top: 3rem; } - + .job-listing-card { - border: 1px solid var(--kaauh-border); - border-left: 6px solid var(--kaauh-teal); + border: 1px solid var(--kaauh-border); + border-left: 6px solid var(--kaauh-teal); border-radius: 0.75rem; - padding: 2rem !important; + padding: 2rem !important; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); /* Lighter default shadow */ } .job-listing-card:hover { transform: translateY(-3px); /* Increased lift */ box-shadow: 0 12px 25px rgba(0, 99, 110, 0.15); /* Stronger hover shadow */ - background-color: var(--kaauh-teal-light); + background-color: var(--kaauh-teal-light); } - + .card.sticky-top-filters { box-shadow: var(--kaauh-shadow); /* Uses the deeper card shadow */ } @@ -215,7 +343,8 @@
{% csrf_token %}
@@ -224,7 +353,8 @@
{% csrf_token %}
@@ -239,7 +369,7 @@
{# Use responsive columns matching the main content block for alignment #} -
+
{% for message in messages %}