diff --git a/recruitment/__pycache__/forms.cpython-313.pyc b/recruitment/__pycache__/forms.cpython-313.pyc index cbf6857..4a127ba 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 4fb5e79..eac1470 100644 Binary files a/recruitment/__pycache__/models.cpython-313.pyc and b/recruitment/__pycache__/models.cpython-313.pyc differ diff --git a/recruitment/__pycache__/urls.cpython-313.pyc b/recruitment/__pycache__/urls.cpython-313.pyc index 02a6254..a4bcbcd 100644 Binary files a/recruitment/__pycache__/urls.cpython-313.pyc and b/recruitment/__pycache__/urls.cpython-313.pyc differ diff --git a/recruitment/__pycache__/views.cpython-313.pyc b/recruitment/__pycache__/views.cpython-313.pyc index 4ce6a38..a45f0cd 100644 Binary files a/recruitment/__pycache__/views.cpython-313.pyc and b/recruitment/__pycache__/views.cpython-313.pyc differ diff --git a/recruitment/__pycache__/views_frontend.cpython-313.pyc b/recruitment/__pycache__/views_frontend.cpython-313.pyc index df9b9a7..b90a714 100644 Binary files a/recruitment/__pycache__/views_frontend.cpython-313.pyc and b/recruitment/__pycache__/views_frontend.cpython-313.pyc differ diff --git a/recruitment/forms.py b/recruitment/forms.py index 0501c2b..87b373b 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -31,7 +31,70 @@ def generate_api_secret(length=64): return ''.join(secrets.choice(alphabet) for _ in range(length)) class SourceForm(forms.ModelForm): - """Form for creating and editing sources with API key generation""" + """Simple form for creating and editing sources""" + + class Meta: + model = Source + fields = [ + 'name', 'source_type', 'description', 'ip_address', 'is_active' + ] + 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' + }), + 'is_active': forms.CheckboxInput(attrs={ + 'class': 'form-check-input' + }), + } + + 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' + + self.helper.layout = Layout( + Field('name', css_class='form-control'), + Field('source_type', css_class='form-control'), + Field('ip_address', css_class='form-control'), + Field('is_active', css_class='form-check-input'), + 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 + +class SourceAdvancedForm(forms.ModelForm): + """Advanced form for creating and editing sources with API key generation""" # Hidden field to trigger API key generation generate_keys = forms.CharField( diff --git a/recruitment/urls.py b/recruitment/urls.py index 25c9eed..55b72eb 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -141,7 +141,8 @@ urlpatterns = [ 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//generate-keys/', views_source.generate_api_keys_view, name='generate_api_keys'), + path('sources//toggle-status/', views_source.toggle_source_status_view, name='toggle_source_status'), path('sources/api/copy-to-clipboard/', views_source.copy_to_clipboard_view, name='copy_to_clipboard'), diff --git a/recruitment/views.py b/recruitment/views.py index c96c524..b56ea09 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -42,7 +42,8 @@ from .forms import ( AgencyJobAssignmentForm, LinkedPostContentForm, ParticipantsSelectForm, - CandidateEmailForm + CandidateEmailForm, + SourceForm ) from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent from rest_framework import viewsets @@ -80,7 +81,8 @@ from .models import ( Profile,MeetingComment,HiringAgency, AgencyJobAssignment, AgencyAccessLink, - Notification + Notification, + Source ) import logging from datastar_py.django import ( @@ -3818,3 +3820,170 @@ def compose_candidate_email(request, job_slug, candidate_slug): 'job': job, 'candidate': candidate }) + + +# Source CRUD Views +@login_required +def source_list(request): + """List all sources with search and pagination""" + search_query = request.GET.get('q', '') + sources = Source.objects.all() + + if search_query: + sources = sources.filter( + Q(name__icontains=search_query) | + Q(source_type__icontains=search_query) | + Q(description__icontains=search_query) + ) + + # Order by most recently created + sources = sources.order_by('-created_at') + + # Pagination + paginator = Paginator(sources, 15) # Show 15 sources per page + page_number = request.GET.get('page') + page_obj = paginator.get_page(page_number) + + context = { + 'page_obj': page_obj, + 'search_query': search_query, + 'total_sources': sources.count(), + } + return render(request, 'recruitment/source_list.html', context) + + +@login_required +def source_create(request): + """Create a new source""" + if request.method == 'POST': + form = SourceForm(request.POST) + if form.is_valid(): + source = form.save() + messages.success(request, f'Source "{source.name}" created successfully!') + return redirect('source_detail', slug=source.slug) + else: + messages.error(request, 'Please correct the errors below.') + else: + form = SourceForm() + + context = { + 'form': form, + 'title': 'Create New Source', + 'button_text': 'Create Source', + } + return render(request, 'recruitment/source_form.html', context) + + +@login_required +def source_detail(request, slug): + """View details of a specific source""" + source = get_object_or_404(Source, slug=slug) + + # Get integration logs for this source + integration_logs = source.integration_logs.order_by('-created_at')[:10] # Show recent 10 logs + + # Statistics + total_logs = source.integration_logs.count() + successful_logs = source.integration_logs.filter(method='POST').count() + failed_logs = source.integration_logs.filter(method='POST', status_code__gte=400).count() + + context = { + 'source': source, + 'integration_logs': integration_logs, + 'total_logs': total_logs, + 'successful_logs': successful_logs, + 'failed_logs': failed_logs, + } + return render(request, 'recruitment/source_detail.html', context) + + +@login_required +def source_update(request, slug): + """Update an existing source""" + source = get_object_or_404(Source, slug=slug) + + if request.method == 'POST': + form = SourceForm(request.POST, instance=source) + if form.is_valid(): + source = form.save() + messages.success(request, f'Source "{source.name}" updated successfully!') + return redirect('source_detail', slug=source.slug) + else: + messages.error(request, 'Please correct the errors below.') + else: + form = SourceForm(instance=source) + + context = { + 'form': form, + 'source': source, + 'title': f'Edit Source: {source.name}', + 'button_text': 'Update Source', + } + return render(request, 'recruitment/source_form.html', context) + + +@login_required +def source_delete(request, slug): + """Delete a source""" + source = get_object_or_404(Source, slug=slug) + + if request.method == 'POST': + source_name = source.name + source.delete() + messages.success(request, f'Source "{source_name}" deleted successfully!') + return redirect('source_list') + + context = { + 'source': source, + 'title': 'Delete Source', + 'message': f'Are you sure you want to delete the source "{source.name}"?', + 'cancel_url': reverse('source_detail', kwargs={'slug': source.slug}), + } + return render(request, 'recruitment/source_confirm_delete.html', context) + + +@login_required +def source_generate_keys(request, slug): + """Generate new API keys for a source""" + source = get_object_or_404(Source, slug=slug) + + if request.method == 'POST': + # Generate new API key and secret + from .forms import generate_api_key, generate_api_secret + source.api_key = generate_api_key() + source.api_secret = generate_api_secret() + source.save(update_fields=['api_key', 'api_secret']) + + messages.success(request, f'New API keys generated for "{source.name}"!') + return redirect('source_detail', slug=source.slug) + + # For GET requests, show confirmation page + context = { + 'source': source, + 'title': 'Generate New API Keys', + 'message': f'Are you sure you want to generate new API keys for "{source.name}"? This will invalidate the existing keys.', + 'cancel_url': reverse('source_detail', kwargs={'slug': source.slug}), + } + return render(request, 'recruitment/source_confirm_generate_keys.html', context) + + +@login_required +def source_toggle_status(request, slug): + """Toggle active status of a source""" + source = get_object_or_404(Source, slug=slug) + + if request.method == 'POST': + source.is_active = not source.is_active + source.save(update_fields=['is_active']) + + status_text = 'activated' if source.is_active else 'deactivated' + messages.success(request, f'Source "{source.name}" has been {status_text}!') + + # Handle HTMX requests + if 'HX-Request' in request.headers: + return HttpResponse(status=200) # HTMX success response + + return redirect('source_detail', slug=source.slug) + + # For GET requests, return error + return JsonResponse({'success': False, 'error': 'Method not allowed'}) diff --git a/recruitment/views_source.py b/recruitment/views_source.py index 078e485..f594a6f 100644 --- a/recruitment/views_source.py +++ b/recruitment/views_source.py @@ -182,24 +182,90 @@ class SourceDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView): 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""" +def generate_api_keys_view(request, pk): + """Generate new API keys for a specific source""" if not request.user.is_staff: return JsonResponse({'error': 'Permission denied'}, status=403) + try: + source = get_object_or_404(Source, pk=pk) + except Source.DoesNotExist: + return JsonResponse({'error': 'Source not found'}, status=404) + if request.method == 'POST': - api_key = generate_api_key() - api_secret = generate_api_secret() + # Generate new API keys + new_api_key = generate_api_key() + new_api_secret = generate_api_secret() + + # Update the source with new keys + old_api_key = source.api_key + source.api_key = new_api_key + source.api_secret = new_api_secret + 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', '') + ) return JsonResponse({ 'success': True, - 'api_key': api_key, - 'api_secret': api_secret, - 'message': 'API keys generated successfully' + 'api_key': new_api_key, + 'api_secret': new_api_secret, + 'message': 'API keys regenerated successfully' }) return JsonResponse({'error': 'Invalid request method'}, status=405) +def toggle_source_status_view(request, pk): + """Toggle the active status of a source""" + if not request.user.is_staff: + return JsonResponse({'error': 'Permission denied'}, status=403) + + try: + source = get_object_or_404(Source, pk=pk) + except Source.DoesNotExist: + return JsonResponse({'error': 'Source not found'}, status=404) + + if request.method == 'POST': + # Toggle the status + old_status = source.is_active + source.is_active = not source.is_active + 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', '') + ) + + 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' + }) + def copy_to_clipboard_view(request): """HTMX endpoint to copy text to clipboard""" if request.method == 'POST': diff --git a/templates/recruitment/agency_list.html b/templates/recruitment/agency_list.html index 2702b8d..0f5bbce 100644 --- a/templates/recruitment/agency_list.html +++ b/templates/recruitment/agency_list.html @@ -15,6 +15,7 @@ --kaauh-info: #17a2b8; --kaauh-danger: #dc3545; --kaauh-warning: #ffc107; + --kaauh-gray-light: #f8f9fa; } /* Primary Color Overrides */ @@ -53,6 +54,17 @@ box-shadow: 0 4px 8px rgba(0,0,0,0.15); } + /* Secondary Button Style */ + .btn-outline-secondary { + color: var(--kaauh-teal-dark); + border-color: var(--kaauh-teal); + } + .btn-outline-secondary:hover { + background-color: var(--kaauh-teal-dark); + color: white; + border-color: var(--kaauh-teal-dark); + } + /* Search Form Styling */ .search-form { background-color: #f8f9fa; @@ -71,6 +83,41 @@ font-size: 0.875rem; font-weight: 600; } + + /* Table View Styling */ + .table-view .table thead th { + background-color: var(--kaauh-teal-dark); + color: white; + font-weight: 600; + border-color: var(--kaauh-border); + text-transform: uppercase; + font-size: 0.8rem; + letter-spacing: 0.5px; + padding: 1rem; + } + .table-view .table tbody td { + vertical-align: middle; + padding: 1rem; + border-color: var(--kaauh-border); + } + .table-view .table tbody tr:hover { + background-color: var(--kaauh-gray-light); + } + + /* Card View Specific Styles */ + .card-view .card { + height: 100%; + } + .card-view .card-title { + color: var(--kaauh-teal-dark); + font-weight: 700; + } + .card-view .card-body { + display: flex; + flex-direction: column; + justify-content: space-between; + flex-grow: 1; + } {% endblock %} @@ -115,7 +162,7 @@ value="{{ search_query }}"> -
+
@@ -125,78 +172,168 @@ {% if page_obj %} -
- {% for agency in page_obj %} -
-
-
- -
-
- {{ agency.name }} -
- {% if agency.email %} - - - - {% endif %} -
+
+ {% include "includes/_list_view_switcher.html" with list_id="agency-list" %} - - {% if agency.contact_person %} -

- - {% trans "Contact:" %} {{ agency.contact_person }} -

- {% endif %} + +
+
+ + + + + + + + + + + + + + + {% for agency in page_obj %} + + + + + + + + + + + {% endfor %} + +
{% trans "Agency Name" %}{% trans "Contact Person" %}{% trans "Email" %}{% trans "Phone" %}{% trans "Country" %}{% trans "Website" %}{% trans "Created" %}{% trans "Actions" %}
+ + {{ agency.name }} + + {{ agency.contact_person|default:"-" }} + {% if agency.email %} + + + {{ agency.email|truncatechars:20 }} + + {% else %} + - + {% endif %} + {{ agency.phone|default:"-" }} + {% if agency.country %} + + {{ agency.get_country_display }} + {% else %} + - + {% endif %} + + {% if agency.website %} + + {{ agency.website|truncatechars:25 }} + + + {% else %} + - + {% endif %} + + + {{ agency.created_at|date:"M d, Y" }} + + + +
+
+
- {% if agency.phone %} -

- - {{ agency.phone }} -

- {% endif %} - - {% if agency.country %} -

- - {{ agency.get_country_display }} -

- {% endif %} - - - {% if agency.website %} -

- - - {{ agency.website|truncatechars:30 }} - - -

- {% endif %} - - -
-
- - {% trans "View" %} - - - {% trans "Edit" %} + +
+ {% for agency in page_obj %} +
+
+
+ + -
- - {% trans "Created" %} {{ agency.created_at|date:"M d, Y" }} - + + + {% if agency.contact_person %} +

+ + {% trans "Contact:" %} {{ agency.contact_person }} +

+ {% endif %} + + {% if agency.phone %} +

+ + {{ agency.phone }} +

+ {% endif %} + + {% if agency.country %} +

+ + {{ agency.get_country_display }} +

+ {% endif %} + + + {% if agency.website %} +

+ + + {{ agency.website|truncatechars:30 }} + + +

+ {% endif %} + + +
+ +
+ + {% trans "Created" %} {{ agency.created_at|date:"M d, Y" }} + +
-
- {% endfor %} + {% endfor %} +
diff --git a/templates/recruitment/candidate_detail.html b/templates/recruitment/candidate_detail.html index d93eb22..c0de7fb 100644 --- a/templates/recruitment/candidate_detail.html +++ b/templates/recruitment/candidate_detail.html @@ -179,7 +179,7 @@ .timeline-bg-offer { background-color: #28a745 !important; } .timeline-bg-rejected { background-color: #dc3545 !important; } - + /* ------------------------------------------- */ /* 1. Base Spinner Styling */ @@ -272,7 +272,7 @@ @@ -313,14 +313,14 @@ {% trans "Contact & Job" %} - + - +
@@ -367,7 +367,7 @@
{# TAB 2 CONTENT: RESUME #} - + {# NEW TAB 3 CONTENT: CANDIDATE JOURNEY TIMELINE #}
@@ -629,9 +629,9 @@ {# RIGHT COLUMN: ACTIONS AND CANDIDATE TIMELINE #}
- + {# ACTIONS CARD #} - +
{% trans "Management Actions" %}
@@ -645,7 +645,7 @@ {% trans "Back to List" %} {% if candidate.resume %} - + {% trans "View Actual Resume" %} @@ -654,22 +654,22 @@ {% trans "Download Resume" %} - + {% trans "View Resume AI Overview" %} - + {% endif %}
{% trans "Time to Hire: " %}{{candidate.time_to_hire|default:100}} days
- +
- - + +
@@ -682,17 +682,18 @@ {% if candidate.scoring_timeout %}
- + Resume is been Scoring... -
+
{% else %}
+ + {% trans "Unable to Parse Resume , click to retry" %} +
- {% endif %} + {% endif %} {% endif %}
diff --git a/templates/recruitment/candidate_list.html b/templates/recruitment/candidate_list.html index dc2de8b..270c7f9 100644 --- a/templates/recruitment/candidate_list.html +++ b/templates/recruitment/candidate_list.html @@ -295,11 +295,9 @@ - - {# CRITICAL: Remove the DIV and the text-nowrap class #} - {% trans "AI Scoring..." %} {% endif %} diff --git a/templates/recruitment/source_confirm_delete.html b/templates/recruitment/source_confirm_delete.html index a06cf21..7f19c90 100644 --- a/templates/recruitment/source_confirm_delete.html +++ b/templates/recruitment/source_confirm_delete.html @@ -1,106 +1,105 @@ -{% extends 'base.html' %} -{% load i18n %} +{% extends "base.html" %} +{% load static %} -{% block title %} - {% trans "Delete Source" %} | {% trans "Recruitment System" %} -{% endblock %} +{% block title %}{{ title }}{% endblock %} {% block content %} -
+
-
-
-
- - {% trans "Delete Source" %} -
-
-
-
-
{% trans "Confirm Deletion" %}
-

{% trans "Are you sure you want to delete the following source? This action cannot be undone." %}

-
+
+

{{ title }}

+ + Back to Source + +
- -
-
-
{% trans "Source Information" %}
-
- - - - - - - - - - - - - - - - - - - - - -
{% trans "Name" %}{{ source.name }}
{% trans "Type" %} - {{ source.source_type }} -
{% trans "Status" %} - {% if source.is_active %} - - - {% trans "Active" %} - - {% else %} - - - {% trans "Inactive" %} - - {% endif %} -
{% trans "Created By" %}{{ source.created_by }}
{% trans "Created At" %}{{ source.created_at|date:"M d, Y H:i" }}
-
-
-
- - -
-
-
-
- - {% trans "Important Note" %} -
-
    -
  • {% trans "All associated API keys will be permanently deleted." %}
  • -
  • {% trans "Integration logs related to this source will remain but will show 'Source deleted'." %}
  • -
  • {% trans "Any active integrations using this source will be disconnected." %}
  • -
-
-
-
- - -
-
-
+
+
+
+
+
+
- - - {% trans "Cancel" %} - + Warning: This action cannot be undone. + Deleting this source will also remove all associated integration logs and API credentials.
-
- {% csrf_token %} +
+ +
+
Source to be deleted:
+
+
+
+
+ Name:
+ {{ source.name }} +
+
+ Type:
+ {{ source.get_source_type_display }} +
+
+ {% if source.description %} +
+
+ Description:
+ {{ source.description|linebreaks }} +
+ {% endif %} +
+
+
+ Created:
+ {{ source.created_at|date:"M d, Y H:i" }} +
+
+ Total API Calls:
+ {{ source.integration_logs.count }} +
+
+
+
+
+ + + {% csrf_token %} +
+ + Cancel + - +
+ +
+
+
+ +
+
+
+
Impact Summary
+
+
+
+ +
+ {{ source.integration_logs.count }} will be deleted +
+
+
+ +
+ API Key & Secret will be permanently lost +
+
+
+ +
+ Any systems using this API will lose access +
@@ -110,19 +109,3 @@
{% endblock %} - -{% block extra_js %} - -{% endblock %} diff --git a/templates/recruitment/source_detail.html b/templates/recruitment/source_detail.html index 5203f82..05cf88c 100644 --- a/templates/recruitment/source_detail.html +++ b/templates/recruitment/source_detail.html @@ -1,228 +1,287 @@ -{% extends 'base.html' %} -{% load i18n %} +{% extends "base.html" %} +{% load static %} -{% block title %} - {{ source.name }} | {% trans "Source Details" %} | {% trans "Recruitment System" %} -{% endblock %} +{% block title %}{{ source.name }} - Source Details{% endblock %} {% block content %} -
- -
-
-
-
-

{{ source.name }}

- -
- -
-
-
- - -
-
-
-
-
- - {% trans "Source Information" %} -
-
-
-
-
- - - - - - - - - - - - - -
{% trans "Name" %}{{ source.name }}
{% trans "Type" %} - {{ source.source_type }} -
{% trans "Status" %} - {% if source.is_active %} - - - {% trans "Active" %} - - {% else %} - - - {% trans "Inactive" %} - - {% endif %} -
-
-
- - - - - - - - - - - - - -
{% trans "Created By" %}{{ source.created_by }}
{% trans "Created At" %}{{ source.created_at|date:"M d, Y H:i" }}
{% trans "Updated At" %}{{ source.updated_at|date:"M d, Y H:i" }}
-
-
-
-
-
{% trans "Description" %}
-

{{ source.description|default:"-" }}

-
-
-
-
-
-
- +
- -
-
-
-
- - {% trans "Network Configuration" %} -
+
+
+

{{ source.name }}

+
+ + Edit + + + Generate Keys + + + + Delete +
-
-
- {% trans "IP Address" %}: - {{ source.ip_address|default:"Not specified" }} -
-
- {% trans "Trusted IPs" %}: -
- {% if source.trusted_ips %} -
- {% for ip in source.trusted_ips|split:"," %} - {{ ip|strip }} - {% endfor %} +
+ + +
+
+
+
+
Source Information
+
+
+
+
+
+ +
{{ source.name }}
+
+
+
+
+ +
+ {{ source.get_source_type_display }} +
+
+
+
+ + {% if source.description %} +
+ +
{{ source.description|linebreaks }}
+
+ {% endif %} + +
+
+
+ +
+ {% if source.contact_email %} + {{ source.contact_email }} + {% else %} + Not specified + {% endif %} +
+
+
+
+
+ +
+ {% if source.contact_phone %} + {{ source.contact_phone }} + {% else %} + Not specified + {% endif %} +
+
+
+
+ +
+
+
+ +
+ {% if source.is_active %} + Active + {% else %} + Inactive + {% endif %} +
+
+
+
+
+ +
+ {% if source.requires_auth %} + Yes + {% else %} + No + {% endif %} +
+
+
+
+ + {% if source.webhook_url %} +
+ +
{{ source.webhook_url }}
+
+ {% endif %} + + {% if source.api_timeout %} +
+ +
{{ source.api_timeout }} seconds
+
+ {% endif %} + + {% if source.notes %} +
+ +
{{ source.notes|linebreaks }}
+
+ {% endif %} + +
+
+
+ +
{{ source.created_at|date:"M d, Y H:i" }}
+
+
+
+
+ +
{{ source.updated_at|date:"M d, Y H:i" }}
+
+
+
+
+
+
+ +
+ +
+
+
API Credentials
+
+
+
+ +
+ + +
+
+
+ +
+ + + +
+
+ +
+
+ + +
+
+
Integration Statistics
+
+
+
+ +
{{ total_logs }}
+
+
+ +
{{ successful_logs }}
+
+
+ +
{{ failed_logs }}
+
+ {% if total_logs > 0 %} +
+ +
+ {% widthratio successful_logs total_logs 100 %}% +
- {% else %} - Not specified {% endif %}
-
- -
-
-
-
- - {% trans "API Configuration" %} -
+ +
+
+
Recent Integration Logs
+ Last 10 logs
-
- {% trans "Integration Version" %}: - {{ source.integration_version|default:"Not specified" }} -
-
- {% trans "API Key" %}: -
- - -
-
-
- {% trans "API Secret" %}: -
- - -
-
-
-
-
-
- - -
-
-
-
-
- - {% trans "Recent Integration Logs" %} -
- - - {% trans "Back to List" %} - -
-
- {% if recent_logs %} + {% if integration_logs %}
- - +
+ - - - - - + + + + + - {% for log in recent_logs %} + {% for log in integration_logs %} - - + + @@ -230,20 +289,10 @@
{% trans "Time" %}{% trans "Action" %}{% trans "Endpoint" %}{% trans "Method" %}{% trans "Status" %}TimestampMethodStatusResponse TimeDetails
{{ log.created_at|date:"M d, Y H:i:s" }} - {{ log.get_action_display }} - - {{ log.endpoint }} + {{ log.created_at|date:"M d, Y H:i:s" }} {{ log.method }} - {% if log.success %} - Success + {% if log.status_code >= 200 and log.status_code < 300 %} + {{ log.status_code }} + {% elif log.status_code >= 400 %} + {{ log.status_code }} {% else %} - Failed + {{ log.status_code }} + {% endif %} + + {% if log.response_time_ms %} + {{ log.response_time_ms }}ms + {% else %} + - + {% endif %} + + {% if log.request_data %} + + {% else %} + No data {% endif %}
- {% if recent_logs.has_previous %} - - {% endif %} {% else %} -
- -
{% trans "No integration logs found" %}
-

- {% trans "Integration logs will appear here when this source is used for external integrations." %} -

+
+ +

No integration logs found

{% endif %}
@@ -251,35 +300,119 @@
+ + +{% for log in integration_logs %} + {% if log.request_data %} + + {% endif %} +{% endfor %} {% endblock %} {% block extra_js %} {% endblock %} diff --git a/templates/recruitment/source_form.html b/templates/recruitment/source_form.html index 05f789a..c630388 100644 --- a/templates/recruitment/source_form.html +++ b/templates/recruitment/source_form.html @@ -1,293 +1,177 @@ -{% extends 'base.html' %} -{% load i18n %} +{% extends "base.html" %} +{% load static %} +{% load widget_tweaks %} -{% block title %} - {% if title %}{{ title }} | {% endif %}{% trans "Source" %} | {% trans "Recruitment System" %} -{% endblock %} +{% block title %}{{ title }}{% endblock %} {% block content %} - - - - -
+
-
-
-
- {% if title %}{{ title }}{% else %}{% trans "Create New Source" %}{% endif %} -
-
+
+

{{ title }}

+ + Back to Sources + +
+ +
-
+ {% csrf_token %} - - {% if form.errors %} + {% if form.non_field_errors %}
-
{% trans "Please correct the errors below:" %}
- {% for field in form %} - {% if field.errors %} -

{{ field.label }}: {{ field.errors|join:", " }}

- {% endif %} + {% for error in form.non_field_errors %} + {{ error }} {% endfor %}
{% endif %} - {% if messages %} - {% for message in messages %} -