diff --git a/locale/ar/LC_MESSAGES/django.po b/locale/ar/LC_MESSAGES/django.po index e112fa0..f28f365 100644 --- a/locale/ar/LC_MESSAGES/django.po +++ b/locale/ar/LC_MESSAGES/django.po @@ -1426,178 +1426,3 @@ msgstr "" #: templates/unfold/components/table.html:43 msgid "No data" msgstr "" - -# Source Management -msgid "Data Sources" -msgstr "مصادر البيانات" - -msgid "Create New Source" -msgstr "إنشاء مصدر جديد" - -msgid "Search by name, type, or description..." -msgstr "البحث بالاسم، النوع، أو الوصف..." - -msgid "Filter" -msgstr "تصفية" - -msgid "Available Sources" -msgstr "المصادر المتاحة" - -msgid "sources" -msgstr "مصادر" - -msgid "View Details" -msgstr "عرض التفاصيل" - -msgid "Edit" -msgstr "تعديل" - -msgid "Delete" -msgstr "حذف" - -msgid "First" -msgstr "الأول" - -msgid "Previous" -msgstr "السابق" - -msgid "Next" -msgstr "التالي" - -msgid "Last" -msgstr "الأخير" - -msgid "No sources found" -msgstr "لم يتم العثور على مصادر" - -msgid "Get started by creating your first data source." -msgstr "ابدأ بإنشاء أول مصدر بيانات لك." - -msgid "Network Configuration" -msgstr "تكوين الشبكة" - -msgid "Settings" -msgstr "الإعدادات" - -msgid "API Configuration" -msgstr "تكوين واجهة برمجة التطبيقات" - -msgid "API Keys" -msgstr "مفاتيح واجهة برمجة التطبيقات" - -msgid "Generate secure API keys for external integrations" -msgstr "إنشاء مفاتيح واجهة برمجة التطبيقات الآمنة للتكاملات الخارجية" - -msgid "Active" -msgstr "نشط" - -msgid "Inactive" -msgstr "غير نشط" - -msgid "Not generated" -msgstr "لم يتم إنشاؤه" - -msgid "Created By" -msgstr "أنشأ بواسطة" - -msgid "Created At" -msgstr "أنشأ في" - -msgid "Updated At" -msgstr "حدث في" - -msgid "IP Address" -msgstr "عنوان IP" - -msgid "Trusted IPs" -msgstr "عناوين IP الموثوقة" - -msgid "Integration Version" -msgstr "إصدار التكامل" - -msgid "API Key" -msgstr "مفتاح واجهة برمجة التطبيقات" - -msgid "API Secret" -msgstr "سر واجهة برمجة التطبيقات" - -msgid "Recent Integration Logs" -msgstr "سجلات التكامل الأخيرة" - -msgid "Time" -msgstr "الوقت" - -msgid "Action" -msgstr "الإجراء" - -msgid "Endpoint" -msgstr "نقطة النهاية" - -msgid "Method" -msgstr "الطريقة" - -msgid "Status" -msgstr "الحالة" - -msgid "Success" -msgstr "نجح" - -msgid "Failed" -msgstr "فشل" - -msgid "No integration logs found" -msgstr "لم يتم العثور على سجلات تكامل" - -msgid "Integration logs will appear here when this source is used for external integrations." -msgstr "ستظهر سجلات التكامل هنا عند استخدام هذا المصدر للتكاملات الخارجية." - -msgid "Delete Source" -msgstr "حذف المصدر" - -msgid "Confirm Deletion" -msgstr "تأكيد الحذف" - -msgid "Are you sure you want to delete the following source? This action cannot be undone." -msgstr "هل أنت متأكد من رغبتك في حذف المصدر التالي؟ لا يمكن التراجع عن هذا الإجراء." - -msgid "Important Note" -msgstr "ملاحظة هامة" - -msgid "All associated API keys will be permanently deleted." -msgstr "ستتم حذف جميع مفاتيح واجهة برمجة التطبيقات المرتبطة بشكل دائم." - -msgid "Integration logs related to this source will remain but will show 'Source deleted'." -msgstr "ستبقى سجلات التكامل المتعلقة بهذا المصدر ولكنها ستظهر 'تم حذف المصدر'." - -msgid "Any active integrations using this source will be disconnected." -msgstr "سيتم فصل أي تكاملات نشطة تستخدم هذا المصدر." - -msgid "Back to List" -msgstr "العودة إلى القائمة" - -msgid "Delete Source" -msgstr "حذف المصدر" - -msgid "Generate API Keys" -msgstr "إنشاء مفاتيح واجهة برمجة التطبيقات" - -msgid "Copy to Clipboard" -msgstr "نسخ إلى الحافظة" - -msgid "Failed to copy to clipboard" -msgstr "فشل نسخ إلى الحافظة" - -msgid "Generate random API key" -msgstr "إنشاء مفتاح واجهة برمجة تطبيقات عشوائي" - -msgid "Generate random API secret" -msgstr "إنشاء سر واجهة برمجة تطبيقات عشوائي" - -msgid "Source updated successfully." -msgstr "تم تحديث المصدر بنجاح." - -msgid "Source created successfully." -msgstr "تم إنشاء المصدر بنجاح." - -msgid "Source deleted successfully." -msgstr "تم حذف المصدر بنجاح." diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index 25de3e8..ecedf37 100644 --- a/locale/en/LC_MESSAGES/django.po +++ b/locale/en/LC_MESSAGES/django.po @@ -1425,178 +1425,3 @@ msgstr "" #: templates/unfold/components/table.html:43 msgid "No data" msgstr "" - -# Source Management -msgid "Data Sources" -msgstr "" - -msgid "Create New Source" -msgstr "" - -msgid "Search by name, type, or description..." -msgstr "" - -msgid "Filter" -msgstr "" - -msgid "Available Sources" -msgstr "" - -msgid "sources" -msgstr "" - -msgid "View Details" -msgstr "" - -msgid "Edit" -msgstr "" - -msgid "Delete" -msgstr "" - -msgid "First" -msgstr "" - -msgid "Previous" -msgstr "" - -msgid "Next" -msgstr "" - -msgid "Last" -msgstr "" - -msgid "No sources found" -msgstr "" - -msgid "Get started by creating your first data source." -msgstr "" - -msgid "Network Configuration" -msgstr "" - -msgid "Settings" -msgstr "" - -msgid "API Configuration" -msgstr "" - -msgid "API Keys" -msgstr "" - -msgid "Generate secure API keys for external integrations" -msgstr "" - -msgid "Active" -msgstr "" - -msgid "Inactive" -msgstr "" - -msgid "Not generated" -msgstr "" - -msgid "Created By" -msgstr "" - -msgid "Created At" -msgstr "" - -msgid "Updated At" -msgstr "" - -msgid "IP Address" -msgstr "" - -msgid "Trusted IPs" -msgstr "" - -msgid "Integration Version" -msgstr "" - -msgid "API Key" -msgstr "" - -msgid "API Secret" -msgstr "" - -msgid "Recent Integration Logs" -msgstr "" - -msgid "Time" -msgstr "" - -msgid "Action" -msgstr "" - -msgid "Endpoint" -msgstr "" - -msgid "Method" -msgstr "" - -msgid "Status" -msgstr "" - -msgid "Success" -msgstr "" - -msgid "Failed" -msgstr "" - -msgid "No integration logs found" -msgstr "" - -msgid "Integration logs will appear here when this source is used for external integrations." -msgstr "" - -msgid "Delete Source" -msgstr "" - -msgid "Confirm Deletion" -msgstr "" - -msgid "Are you sure you want to delete the following source? This action cannot be undone." -msgstr "" - -msgid "Important Note" -msgstr "" - -msgid "All associated API keys will be permanently deleted." -msgstr "" - -msgid "Integration logs related to this source will remain but will show 'Source deleted'." -msgstr "" - -msgid "Any active integrations using this source will be disconnected." -msgstr "" - -msgid "Back to List" -msgstr "" - -msgid "Delete Source" -msgstr "" - -msgid "Generate API Keys" -msgstr "" - -msgid "Copy to Clipboard" -msgstr "" - -msgid "Failed to copy to clipboard" -msgstr "" - -msgid "Generate random API key" -msgstr "" - -msgid "Generate random API secret" -msgstr "" - -msgid "Source updated successfully." -msgstr "" - -msgid "Source created successfully." -msgstr "" - -msgid "Source deleted successfully." -msgstr "" diff --git a/recruitment/__pycache__/forms.cpython-313.pyc b/recruitment/__pycache__/forms.cpython-313.pyc index a59ff1e..4d4c47a 100644 Binary files a/recruitment/__pycache__/forms.cpython-313.pyc and b/recruitment/__pycache__/forms.cpython-313.pyc differ diff --git a/recruitment/__pycache__/urls.cpython-313.pyc b/recruitment/__pycache__/urls.cpython-313.pyc index 097b279..0f0a705 100644 Binary files a/recruitment/__pycache__/urls.cpython-313.pyc and b/recruitment/__pycache__/urls.cpython-313.pyc differ diff --git a/recruitment/forms.py b/recruitment/forms.py index 83ca402..73fe94d 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -1,171 +1,20 @@ from django import forms +from .validators import validate_hash_tags from django.core.validators import URLValidator from django.forms.formsets import formset_factory from django.utils.translation import gettext_lazy as _ from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Submit, Row, Column, Field, Div from django.contrib.auth.models import User -from django.contrib.auth.forms import UserCreationForm -import re from .models import ( ZoomMeeting, Candidate,TrainingMaterial,JobPosting, FormTemplate,InterviewSchedule,BreakTime,JobPostingImage, - Profile,MeetingComment,ScheduledInterview,Source + Profile,MeetingComment,ScheduledInterview ) # from django_summernote.widgets import SummernoteWidget from django_ckeditor_5.widgets import CKEditor5Widget -import secrets -import string -from django.core.exceptions import ValidationError -def generate_api_key(length=32): - """Generate a secure API key""" - alphabet = string.ascii_letters + string.digits - return ''.join(secrets.choice(alphabet) for _ in range(length)) -def generate_api_secret(length=64): - """Generate a secure API secret""" - alphabet = string.ascii_letters + string.digits + '-._~' - return ''.join(secrets.choice(alphabet) for _ in range(length)) - -class SourceForm(forms.ModelForm): - """Form for creating and editing sources with API key generation""" - - # Hidden field to trigger API key generation - generate_keys = forms.CharField( - widget=forms.HiddenInput(), - required=False, - help_text="Set to 'true' to generate new API keys" - ) - - # Display fields for generated keys (read-only) - api_key_generated = forms.CharField( - label="Generated API Key", - required=False, - widget=forms.TextInput(attrs={'readonly': True, 'class': 'form-control'}) - ) - - api_secret_generated = forms.CharField( - label="Generated API Secret", - required=False, - widget=forms.TextInput(attrs={'readonly': True, 'class': 'form-control'}) - ) - - class Meta: - model = Source - fields = [ - 'name', 'source_type', 'description', 'ip_address', - 'trusted_ips', 'is_active', 'integration_version' - ] - widgets = { - 'name': forms.TextInput(attrs={ - 'class': 'form-control', - 'placeholder': 'e.g., ATS System, ERP Integration', - 'required': True - }), - 'source_type': forms.TextInput(attrs={ - 'class': 'form-control', - 'placeholder': 'e.g., ATS, ERP, API', - 'required': True - }), - 'description': forms.Textarea(attrs={ - 'class': 'form-control', - 'rows': 3, - 'placeholder': 'Brief description of the source system' - }), - 'ip_address': forms.TextInput(attrs={ - 'class': 'form-control', - 'placeholder': '192.168.1.100' - }), - 'trusted_ips': forms.Textarea(attrs={ - 'class': 'form-control', - 'rows': 2, - 'placeholder': 'Comma-separated IP addresses (e.g., 192.168.1.100, 10.0.0.1)' - }), - 'integration_version': forms.TextInput(attrs={ - 'class': 'form-control', - 'placeholder': 'v1.0, v2.1' - }), - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.form_method = 'post' - self.helper.form_class = 'form-horizontal' - self.helper.label_class = 'col-md-3' - self.helper.field_class = 'col-md-9' - - # Add generate keys button - self.helper.layout = Layout( - Field('name', css_class='form-control'), - Field('source_type', css_class='form-control'), - Field('description', css_class='form-control'), - Field('ip_address', css_class='form-control'), - Field('trusted_ips', css_class='form-control'), - Field('integration_version', css_class='form-control'), - Field('is_active', css_class='form-check-input'), - - # Hidden field for key generation trigger - Field('generate_keys', type='hidden'), - - # Display fields for generated keys - Field('api_key_generated', css_class='form-control'), - Field('api_secret_generated', css_class='form-control'), - - Submit('submit', 'Save Source', css_class='btn btn-primary mt-3') - ) - - def clean_name(self): - """Ensure source name is unique""" - name = self.cleaned_data.get('name') - if name: - # Check for duplicates excluding current instance if editing - instance = self.instance - if not instance.pk: # Creating new instance - if Source.objects.filter(name=name).exists(): - raise ValidationError('A source with this name already exists.') - else: # Editing existing instance - if Source.objects.filter(name=name).exclude(pk=instance.pk).exists(): - raise ValidationError('A source with this name already exists.') - return name - - def clean_trusted_ips(self): - """Validate and format trusted IP addresses""" - trusted_ips = self.cleaned_data.get('trusted_ips') - if trusted_ips: - # Split by comma and strip whitespace - ips = [ip.strip() for ip in trusted_ips.split(',') if ip.strip()] - - # Validate each IP address - for ip in ips: - try: - # Basic IP validation (can be enhanced) - if not (ip.replace('.', '').isdigit() and len(ip.split('.')) == 4): - raise ValidationError(f'Invalid IP address: {ip}') - except Exception: - raise ValidationError(f'Invalid IP address: {ip}') - - return ', '.join(ips) - return trusted_ips - - def clean(self): - """Custom validation for the form""" - cleaned_data = super().clean() - - # Check if we need to generate API keys - generate_keys = cleaned_data.get('generate_keys') - - if generate_keys == 'true': - # Generate new API key and secret - cleaned_data['api_key'] = generate_api_key() - cleaned_data['api_secret'] = generate_api_secret() - - # Set display fields for the frontend - cleaned_data['api_key_generated'] = cleaned_data['api_key'] - cleaned_data['api_secret_generated'] = cleaned_data['api_secret'] - - return cleaned_data class CandidateForm(forms.ModelForm): class Meta: @@ -223,6 +72,51 @@ class CandidateStageForm(forms.ModelForm): 'stage': forms.Select(attrs={'class': 'form-select'}), } + # def __init__(self, *args, **kwargs): + # # Get the current candidate instance for validation + # self.candidate = kwargs.pop('candidate', None) + # super().__init__(*args, **kwargs) + + # # Dynamically filter stage choices based on current stage + # if self.candidate and self.candidate.pk: + # current_stage = self.candidate.stage + # available_stages = self.candidate.get_available_stages() + + # # Filter choices to only include available stages + # choices = [(stage, self.candidate.Stage(stage).label) + # for stage in available_stages] + # self.fields['stage'].choices = choices + + # # Set initial value to current stage + # self.fields['stage'].initial = current_stage + # else: + # # For new candidates, only show 'Applied' stage + # self.fields['stage'].choices = [('Applied', _('Applied'))] + # self.fields['stage'].initial = 'Applied' + + # def clean_stage(self): + # """Validate stage transition""" + # new_stage = self.cleaned_data.get('stage') + # if not new_stage: + # raise forms.ValidationError(_('Please select a stage.')) + + # # Use model validation for stage transitions + # if self.candidate and self.candidate.pk: + # current_stage = self.candidate.stage + # if new_stage != current_stage: + # if not self.candidate.can_transition_to(new_stage): + # allowed_stages = self.candidate.get_available_stages() + # raise forms.ValidationError( + # _('Cannot transition from "%(current)s" to "%(new)s". ' + # 'Allowed transitions: %(allowed)s') % { + # 'current': current_stage, + # 'new': new_stage, + # 'allowed': ', '.join(allowed_stages) or 'None (final stage)' + # } + # ) + + # return new_stage + class ZoomMeetingForm(forms.ModelForm): class Meta: model = ZoomMeeting @@ -252,6 +146,8 @@ class ZoomMeetingForm(forms.ModelForm): Submit('submit', _('Create Meeting'), css_class='btn btn-primary') ) +# Old JobForm removed - replaced by JobPostingForm + class TrainingMaterialForm(forms.ModelForm): class Meta: model = TrainingMaterial @@ -264,11 +160,13 @@ class TrainingMaterialForm(forms.ModelForm): } widgets = { 'title': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Enter material title')}), - 'content': CKEditor5Widget(attrs={'placeholder': _('Enter material content')}), + # 💡 Use SummernoteWidget here + # 'content': SummernoteWidget(attrs={'placeholder': _('Enter material content')}), 'video_link': forms.URLInput(attrs={'class': 'form-control', 'placeholder': _('https://www.youtube.com/watch?v=...')}), 'file': forms.FileInput(attrs={'class': 'form-control'}), } + # The __init__ and FormHelper layout remains the same def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() @@ -277,7 +175,7 @@ class TrainingMaterialForm(forms.ModelForm): self.helper.layout = Layout( 'title', - 'content', + 'content', # Summernote is applied via the widgets dictionary Row( Column('video_link', css_class='col-md-6'), Column('file', css_class='col-md-6'), @@ -290,6 +188,7 @@ class TrainingMaterialForm(forms.ModelForm): ) ) + class JobPostingForm(forms.ModelForm): """Form for creating and editing job postings""" @@ -304,6 +203,7 @@ class JobPostingForm(forms.ModelForm): 'open_positions','hash_tags','max_applications' ] widgets = { + # Basic Information 'title': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Assistant Professor of Computer Science', @@ -321,6 +221,8 @@ class JobPostingForm(forms.ModelForm): 'class': 'form-select', 'required': True }), + + # Location 'location_city': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Boston' @@ -333,6 +235,8 @@ class JobPostingForm(forms.ModelForm): 'class': 'form-control', 'value': 'United States' }), + + 'salary_range': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': '$60,000 - $80,000' @@ -346,11 +250,24 @@ class JobPostingForm(forms.ModelForm): # 'required': True # }), + + + # Application Information + # 'application_url': forms.URLInput(attrs={ + # 'class': 'form-control', + # 'placeholder': 'https://university.edu/careers/job123', + # 'required': True + # }), + 'application_start_date': forms.DateInput(attrs={ + 'class': 'form-control', + 'type': 'date' + }), 'application_deadline': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date', 'required':True }), + 'open_positions': forms.NumberInput(attrs={ 'class': 'form-control', 'min': 1, @@ -359,7 +276,10 @@ class JobPostingForm(forms.ModelForm): 'hash_tags': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': '#hiring,#jobopening', + # 'validators':validate_hash_tags, # Assuming this is available }), + + # Internal Information 'position_number': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'UNIV-2025-001' @@ -397,7 +317,7 @@ class JobPostingForm(forms.ModelForm): if not tag.startswith('#'): raise forms.ValidationError("Each hashtag must start with '#' symbol and must be comma(,) sepearted.") return ','.join(tags) - return hash_tags + return hash_tags # Allow blank def clean_title(self): title=self.cleaned_data.get('title') @@ -411,7 +331,43 @@ class JobPostingForm(forms.ModelForm): description=self.cleaned_data.get('description') if not description or len(description.strip())<20: raise forms.ValidationError("Job description must be at least 20 characters long.") - return description.strip() + return description.strip() # to remove leading/trailing whitespace + + def clean_application_url(self): + url=self.cleaned_data.get('application_url') + if url: + validator=URLValidator() + try: + validator(url) + except forms.ValidationError: + raise forms.ValidationError('Please enter a valid URL (e.g., https://example.com)') + return url + + # def clean(self): + # """Cross-field validation""" + # cleaned_data = super().clean() + + # # Validate dates + # start_date = cleaned_data.get('start_date') + # application_deadline = cleaned_data.get('application_deadline') + + # # Perform cross-field validation only if both fields have values + # if start_date and application_deadline: + # if application_deadline > start_date: + # self.add_error('application_deadline', + # 'The application deadline must be set BEFORE the job start date.') + + # # # Validate that if status is ACTIVE, we have required fields + # # status = cleaned_data.get('status') + # # if status == 'ACTIVE': + # # if not cleaned_data.get('application_url'): + # # self.add_error('application_url', + # # 'Application URL is required for active jobs.') + # # if not cleaned_data.get('description'): + # # self.add_error('description', + # # 'Job description is required for active jobs.') + + # return cleaned_data class JobPostingImageForm(forms.ModelForm): class Meta: @@ -459,6 +415,90 @@ class FormTemplateForm(forms.ModelForm): Field('is_active', css_class='form-check-input'), Submit('submit', _('Create Template'), css_class='btn btn-primary mt-3') ) +# class BreakTimeForm(forms.ModelForm): +# class Meta: +# model = BreakTime +# fields = ['start_time', 'end_time'] +# widgets = { +# 'start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}), +# 'end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}), +# } + +# BreakTimeFormSet = formset_factory(BreakTimeForm, extra=1, can_delete=True) + +# class InterviewScheduleForm(forms.ModelForm): +# candidates = forms.ModelMultipleChoiceField( +# queryset=Candidate.objects.none(), +# widget=forms.CheckboxSelectMultiple, +# required=True +# ) +# working_days = forms.MultipleChoiceField( +# choices=[ +# (0, 'Monday'), +# (1, 'Tuesday'), +# (2, 'Wednesday'), +# (3, 'Thursday'), +# (4, 'Friday'), +# (5, 'Saturday'), +# (6, 'Sunday'), +# ], +# widget=forms.CheckboxSelectMultiple, +# required=True +# ) + +# class Meta: +# model = InterviewSchedule +# fields = [ +# 'candidates', 'start_date', 'end_date', 'working_days', +# 'start_time', 'end_time', 'interview_duration', 'buffer_time' +# ] +# widgets = { +# 'start_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), +# 'end_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), +# 'start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}), +# 'end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}), +# 'interview_duration': forms.NumberInput(attrs={'class': 'form-control'}), +# 'buffer_time': forms.NumberInput(attrs={'class': 'form-control'}), +# } + +# def __init__(self, slug, *args, **kwargs): +# super().__init__(*args, **kwargs) +# # Filter candidates based on the selected job +# self.fields['candidates'].queryset = Candidate.objects.filter( +# job__slug=slug, +# stage='Interview' +# ) + +# def clean_working_days(self): +# working_days = self.cleaned_data.get('working_days') +# # Convert string values to integers +# return [int(day) for day in working_days] + + +class JobPostingCancelReasonForm(forms.ModelForm): + class Meta: + model = JobPosting + fields = ['cancel_reason'] +class JobPostingStatusForm(forms.ModelForm): + class Meta: + model = JobPosting + fields = ['status'] + widgets = { + 'status': forms.Select(attrs={'class': 'form-select'}), + } +class FormTemplateIsActiveForm(forms.ModelForm): + class Meta: + model = FormTemplate + fields = ['is_active'] + +class CandidateExamDateForm(forms.ModelForm): + class Meta: + model = Candidate + fields = ['exam_date'] + widgets = { + 'exam_date': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}), + } + class BreakTimeForm(forms.Form): """ @@ -475,8 +515,10 @@ class BreakTimeForm(forms.Form): label="End Time" ) +# Use the non-model form for the formset factory BreakTimeFormSet = formset_factory(BreakTimeForm, extra=1, can_delete=True) +# --- InterviewScheduleForm remains unchanged --- class InterviewScheduleForm(forms.ModelForm): candidates = forms.ModelMultipleChoiceField( queryset=Candidate.objects.none(), @@ -512,6 +554,7 @@ class InterviewScheduleForm(forms.ModelForm): def __init__(self, slug, *args, **kwargs): super().__init__(*args, **kwargs) + # Filter candidates based on the selected job self.fields['candidates'].queryset = Candidate.objects.filter( job__slug=slug, stage='Interview' @@ -519,6 +562,7 @@ class InterviewScheduleForm(forms.ModelForm): def clean_working_days(self): working_days = self.cleaned_data.get('working_days') + # Convert string values to integers return [int(day) for day in working_days] class MeetingCommentForm(forms.ModelForm): @@ -548,7 +592,26 @@ class MeetingCommentForm(forms.ModelForm): Submit('submit', _('Add Comment'), css_class='btn btn-primary mt-3') ) +# --- ScheduleInterviewForCandiateForm remains unchanged --- +class ScheduleInterviewForCandiateForm(forms.ModelForm): + + class Meta: + model = InterviewSchedule + fields = ['start_date', 'end_date', 'start_time', 'end_time', 'interview_duration', 'buffer_time'] + widgets = { + 'start_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), + 'end_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), + 'start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}), + 'end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}), + 'interview_duration': forms.NumberInput(attrs={'class': 'form-control'}), + 'buffer_time': forms.NumberInput(attrs={'class': 'form-control'}), + 'break_start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}), + 'break_end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}), + } + + class InterviewForm(forms.ModelForm): + class Meta: model = ScheduledInterview fields = ['job','candidate'] @@ -558,6 +621,36 @@ class ProfileImageUploadForm(forms.ModelForm): model=Profile fields=['profile_image'] + + +# class UserEditForms(forms.ModelForm): +# class Meta: +# model = User +# fields = ['first_name', 'last_name'] + + +from django.contrib.auth.forms import UserCreationForm +# class StaffUserCreationForm(UserCreationForm): +# email = forms.EmailField(required=True) +# first_name = forms.CharField(max_length=30) +# last_name = forms.CharField(max_length=150) + +# class Meta: +# model = User +# fields = ("email", "first_name", "last_name", "password1", "password2") + +# def save(self, commit=True): +# user = super().save(commit=False) +# user.email = self.cleaned_data["email"] +# user.first_name = self.cleaned_data["first_name"] +# user.last_name = self.cleaned_data["last_name"] +# user.username = self.cleaned_data["email"] # or generate +# user.is_staff = True +# if commit: +# user.save() + # return user + +import re class StaffUserCreationForm(UserCreationForm): email = forms.EmailField(required=True) first_name = forms.CharField(max_length=30, required=True) @@ -591,37 +684,15 @@ class StaffUserCreationForm(UserCreationForm): user.email = self.cleaned_data["email"] user.first_name = self.cleaned_data["first_name"] user.last_name = self.cleaned_data["last_name"] - user.username = self.generate_username(user.email) + user.username = self.generate_username(user.email) # never use raw email if it has dots, etc. user.is_staff = True if commit: user.save() return user + + class ToggleAccountForm(forms.Form): pass -class JobPostingCancelReasonForm(forms.ModelForm): - class Meta: - model = JobPosting - fields = ['cancel_reason'] -class JobPostingStatusForm(forms.ModelForm): - class Meta: - model = JobPosting - fields = ['status'] - widgets = { - 'status': forms.Select(attrs={'class': 'form-select'}), - } - -class FormTemplateIsActiveForm(forms.ModelForm): - class Meta: - model = FormTemplate - fields = ['is_active'] - -class CandidateExamDateForm(forms.ModelForm): - class Meta: - model = Candidate - fields = ['exam_date'] - widgets = { - 'exam_date': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}), - } diff --git a/recruitment/urls.py b/recruitment/urls.py index 604ad08..4d391ff 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -2,7 +2,6 @@ from django.urls import path from . import views_frontend from . import views from . import views_integration -from . import views_source urlpatterns = [ path('', views_frontend.dashboard_view, name='dashboard'), @@ -127,15 +126,6 @@ urlpatterns = [ - # Source URLs - path('sources/', views_source.SourceListView.as_view(), name='source_list'), - path('sources/create/', views_source.SourceCreateView.as_view(), name='source_create'), - path('sources//', views_source.SourceDetailView.as_view(), name='source_detail'), - path('sources//update/', views_source.SourceUpdateView.as_view(), name='source_update'), - path('sources//delete/', views_source.SourceDeleteView.as_view(), name='source_delete'), - path('sources/api/generate-keys/', views_source.generate_api_keys_view, name='generate_api_keys'), - path('sources/api/copy-to-clipboard/', views_source.copy_to_clipboard_view, name='copy_to_clipboard'), - # Meeting Comments URLs path('meetings//comments/add/', views.add_meeting_comment, name='add_meeting_comment'), diff --git a/recruitment/views_source.py b/recruitment/views_source.py deleted file mode 100644 index 078e485..0000000 --- a/recruitment/views_source.py +++ /dev/null @@ -1,212 +0,0 @@ -from django.shortcuts import render, get_object_or_404, redirect -from django.views.generic import ListView, CreateView, UpdateView, DetailView, DeleteView -from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin -from django.urls import reverse_lazy -from django.contrib import messages -from django.db import transaction -from django.http import JsonResponse -from django.db import models -import secrets -import string -from .models import Source, IntegrationLog -from .forms import SourceForm, generate_api_key, generate_api_secret - -class SourceListView(LoginRequiredMixin, UserPassesTestMixin, ListView): - """List all sources""" - model = Source - template_name = 'recruitment/source_list.html' - context_object_name = 'sources' - paginate_by = 10 - - def test_func(self): - return self.request.user.is_staff - - def get_queryset(self): - queryset = super().get_queryset().order_by('name') - - # Search functionality - search_query = self.request.GET.get('search', '') - if search_query: - queryset = queryset.filter( - models.Q(name__icontains=search_query) | - models.Q(source_type__icontains=search_query) | - models.Q(description__icontains=search_query) - ) - - return queryset - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['search_query'] = self.request.GET.get('search', '') - return context - -class SourceCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView): - """Create a new source""" - model = Source - form_class = SourceForm - template_name = 'recruitment/source_form.html' - success_url = reverse_lazy('source_list') - - def test_func(self): - return self.request.user.is_staff - - def form_valid(self, form): - # Set initial values - form.instance.created_by = self.request.user.get_full_name() or self.request.user.username - - # Check if we need to generate API keys - if form.cleaned_data.get('generate_keys') == 'true': - form.instance.api_key = generate_api_key() - form.instance.api_secret = generate_api_secret() - - # Log the key generation - IntegrationLog.objects.create( - source=form.instance, - action=IntegrationLog.ActionChoices.CREATE, - endpoint='/api/sources/', - method='POST', - request_data={'name': form.instance.name}, - ip_address=self.request.META.get('REMOTE_ADDR'), - user_agent=self.request.META.get('HTTP_USER_AGENT', '') - ) - - response = super().form_valid(form) - - # Add success message - messages.success(self.request, f'Source "{form.instance.name}" created successfully!') - - return response - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['title'] = 'Create New Source' - context['generate_keys'] = self.request.GET.get('generate_keys', 'false') - return context - -class SourceDetailView(LoginRequiredMixin, UserPassesTestMixin, DetailView): - """View source details""" - model = Source - template_name = 'recruitment/source_detail.html' - context_object_name = 'source' - - def test_func(self): - return self.request.user.is_staff - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - # Mask API keys in display - source = self.object - if source.api_key: - masked_key = source.api_key[:8] + '*' * 24 - context['masked_api_key'] = masked_key - else: - context['masked_api_key'] = 'Not generated' - - if source.api_secret: - masked_secret = source.api_secret[:12] + '*' * 52 - context['masked_api_secret'] = masked_secret - else: - context['masked_api_secret'] = 'Not generated' - - # Get recent integration logs - context['recent_logs'] = IntegrationLog.objects.filter( - source=source - ).order_by('-created_at')[:10] - - return context - -class SourceUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView): - """Update an existing source""" - model = Source - form_class = SourceForm - template_name = 'recruitment/source_form.html' - success_url = reverse_lazy('source_list') - - def test_func(self): - return self.request.user.is_staff - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['title'] = f'Edit Source: {self.object.name}' - context['generate_keys'] = self.request.GET.get('generate_keys', 'false') - return context - - def form_valid(self, form): - # Check if we need to generate new API keys - if form.cleaned_data.get('generate_keys') == 'true': - form.instance.api_key = generate_api_key() - form.instance.api_secret = generate_api_secret() - - # Log the key regeneration - IntegrationLog.objects.create( - source=self.object, - action=IntegrationLog.ActionChoices.CREATE, - endpoint=f'/api/sources/{self.object.pk}/', - method='PUT', - request_data={'name': form.instance.name, 'regenerated_keys': True}, - ip_address=self.request.META.get('REMOTE_ADDR'), - user_agent=self.request.META.get('HTTP_USER_AGENT', '') - ) - - messages.success(self.request, 'New API keys generated successfully!') - - response = super().form_valid(form) - messages.success(self.request, f'Source "{form.instance.name}" updated successfully!') - return response - -class SourceDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView): - """Delete a source""" - model = Source - template_name = 'recruitment/source_confirm_delete.html' - success_url = reverse_lazy('source_list') - - def test_func(self): - return self.request.user.is_staff - - def delete(self, request, *args, **kwargs): - self.object = self.get_object() - success_url = self.get_success_url() - - # Log the deletion - IntegrationLog.objects.create( - source=self.object, - action=IntegrationLog.ActionChoices.SYNC, # Using SYNC for deletion - endpoint=f'/api/sources/{self.object.pk}/', - method='DELETE', - request_data={'name': self.object.name}, - ip_address=self.request.META.get('REMOTE_ADDR'), - user_agent=self.request.META.get('HTTP_USER_AGENT', '') - ) - - messages.success(request, f'Source "{self.object.name}" deleted successfully!') - return super().delete(request, *args, **kwargs) - -def generate_api_keys_view(request): - """API endpoint to generate API keys""" - if not request.user.is_staff: - return JsonResponse({'error': 'Permission denied'}, status=403) - - if request.method == 'POST': - api_key = generate_api_key() - api_secret = generate_api_secret() - - return JsonResponse({ - 'success': True, - 'api_key': api_key, - 'api_secret': api_secret, - 'message': 'API keys generated successfully' - }) - - return JsonResponse({'error': 'Invalid request method'}, status=405) - -def copy_to_clipboard_view(request): - """HTMX endpoint to copy text to clipboard""" - if request.method == 'POST': - text_to_copy = request.POST.get('text', '') - - return render(request, 'includes/copy_to_clipboard.html', { - 'text': text_to_copy - }) - - return JsonResponse({'error': 'Invalid request method'}, status=405) diff --git a/templates/base.html b/templates/base.html index d876b5b..b4a6c35 100644 --- a/templates/base.html +++ b/templates/base.html @@ -44,7 +44,7 @@
King Abdullah bin Abdulaziz University Hospital
- KAAUH Logo + KAAUH Logo @@ -55,11 +55,11 @@ {# --- MOBILE BRAND LOGIC: Show small logo on mobile, large on desktop (lg) --- #} - {% trans 'kaauh logo green bg' %} + {% trans 'kaauh logo green bg' %} - {% trans 'kaauh logo green bg' %} + {% trans 'kaauh logo green bg' %} {# Toggler: order-lg-0 ensures it's before navigation links on desktop, but it stays where it is on mobile #} @@ -216,21 +216,13 @@ - {% comment %}