Merge branch 'main' of http://10.10.1.136:3000/marwan/kaauh_ats into frontend
This commit is contained in:
commit
7e5dfad1dd
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -31,7 +31,70 @@ def generate_api_secret(length=64):
|
|||||||
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
||||||
|
|
||||||
class SourceForm(forms.ModelForm):
|
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
|
# Hidden field to trigger API key generation
|
||||||
generate_keys = forms.CharField(
|
generate_keys = forms.CharField(
|
||||||
|
|||||||
@ -141,7 +141,8 @@ urlpatterns = [
|
|||||||
path('sources/<int:pk>/', views_source.SourceDetailView.as_view(), name='source_detail'),
|
path('sources/<int:pk>/', views_source.SourceDetailView.as_view(), name='source_detail'),
|
||||||
path('sources/<int:pk>/update/', views_source.SourceUpdateView.as_view(), name='source_update'),
|
path('sources/<int:pk>/update/', views_source.SourceUpdateView.as_view(), name='source_update'),
|
||||||
path('sources/<int:pk>/delete/', views_source.SourceDeleteView.as_view(), name='source_delete'),
|
path('sources/<int:pk>/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/<int:pk>/generate-keys/', views_source.generate_api_keys_view, name='generate_api_keys'),
|
||||||
|
path('sources/<int:pk>/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'),
|
path('sources/api/copy-to-clipboard/', views_source.copy_to_clipboard_view, name='copy_to_clipboard'),
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -42,7 +42,8 @@ from .forms import (
|
|||||||
AgencyJobAssignmentForm,
|
AgencyJobAssignmentForm,
|
||||||
LinkedPostContentForm,
|
LinkedPostContentForm,
|
||||||
ParticipantsSelectForm,
|
ParticipantsSelectForm,
|
||||||
CandidateEmailForm
|
CandidateEmailForm,
|
||||||
|
SourceForm
|
||||||
)
|
)
|
||||||
from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent
|
from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
@ -80,7 +81,8 @@ from .models import (
|
|||||||
Profile,MeetingComment,HiringAgency,
|
Profile,MeetingComment,HiringAgency,
|
||||||
AgencyJobAssignment,
|
AgencyJobAssignment,
|
||||||
AgencyAccessLink,
|
AgencyAccessLink,
|
||||||
Notification
|
Notification,
|
||||||
|
Source
|
||||||
)
|
)
|
||||||
import logging
|
import logging
|
||||||
from datastar_py.django import (
|
from datastar_py.django import (
|
||||||
@ -3818,3 +3820,170 @@ def compose_candidate_email(request, job_slug, candidate_slug):
|
|||||||
'job': job,
|
'job': job,
|
||||||
'candidate': candidate
|
'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'})
|
||||||
|
|||||||
@ -182,24 +182,90 @@ class SourceDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
|
|||||||
messages.success(request, f'Source "{self.object.name}" deleted successfully!')
|
messages.success(request, f'Source "{self.object.name}" deleted successfully!')
|
||||||
return super().delete(request, *args, **kwargs)
|
return super().delete(request, *args, **kwargs)
|
||||||
|
|
||||||
def generate_api_keys_view(request):
|
def generate_api_keys_view(request, pk):
|
||||||
"""API endpoint to generate API keys"""
|
"""Generate new API keys for a specific source"""
|
||||||
if not request.user.is_staff:
|
if not request.user.is_staff:
|
||||||
return JsonResponse({'error': 'Permission denied'}, status=403)
|
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':
|
if request.method == 'POST':
|
||||||
api_key = generate_api_key()
|
# Generate new API keys
|
||||||
api_secret = generate_api_secret()
|
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({
|
return JsonResponse({
|
||||||
'success': True,
|
'success': True,
|
||||||
'api_key': api_key,
|
'api_key': new_api_key,
|
||||||
'api_secret': api_secret,
|
'api_secret': new_api_secret,
|
||||||
'message': 'API keys generated successfully'
|
'message': 'API keys regenerated successfully'
|
||||||
})
|
})
|
||||||
|
|
||||||
return JsonResponse({'error': 'Invalid request method'}, status=405)
|
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):
|
def copy_to_clipboard_view(request):
|
||||||
"""HTMX endpoint to copy text to clipboard"""
|
"""HTMX endpoint to copy text to clipboard"""
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
--kaauh-info: #17a2b8;
|
--kaauh-info: #17a2b8;
|
||||||
--kaauh-danger: #dc3545;
|
--kaauh-danger: #dc3545;
|
||||||
--kaauh-warning: #ffc107;
|
--kaauh-warning: #ffc107;
|
||||||
|
--kaauh-gray-light: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Primary Color Overrides */
|
/* Primary Color Overrides */
|
||||||
@ -53,6 +54,17 @@
|
|||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
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 Styling */
|
||||||
.search-form {
|
.search-form {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
@ -71,6 +83,41 @@
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 600;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -115,7 +162,7 @@
|
|||||||
value="{{ search_query }}">
|
value="{{ search_query }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-md-1">
|
||||||
<button type="submit" class="btn btn-main-action w-100">
|
<button type="submit" class="btn btn-main-action w-100">
|
||||||
<i class="fas fa-search me-1"></i> {% trans "Search" %}
|
<i class="fas fa-search me-1"></i> {% trans "Search" %}
|
||||||
</button>
|
</button>
|
||||||
@ -125,78 +172,168 @@
|
|||||||
|
|
||||||
<!-- Agencies List -->
|
<!-- Agencies List -->
|
||||||
{% if page_obj %}
|
{% if page_obj %}
|
||||||
<div class="row">
|
<div id="agency-list">
|
||||||
{% for agency in page_obj %}
|
{% include "includes/_list_view_switcher.html" with list_id="agency-list" %}
|
||||||
<div class="col-lg-4 col-md-6 mb-4">
|
|
||||||
<div class="card kaauh-card agency-card h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<!-- Agency Header -->
|
|
||||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
|
||||||
<h5 class="card-title mb-0" style="color: var(--kaauh-teal-dark);">
|
|
||||||
{{ agency.name }}
|
|
||||||
</h5>
|
|
||||||
{% if agency.email %}
|
|
||||||
<a href="mailto:{{ agency.email }}" class="text-muted" title="{{ agency.email }}">
|
|
||||||
<i class="fas fa-envelope"></i>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contact Information -->
|
<!-- Table View -->
|
||||||
{% if agency.contact_person %}
|
<div class="table-view">
|
||||||
<p class="card-text mb-2">
|
<div class="table-responsive">
|
||||||
<i class="fas fa-user text-muted me-2"></i>
|
<table class="table table-hover align-middle mb-0">
|
||||||
<strong>{% trans "Contact:" %}</strong> {{ agency.contact_person }}
|
<thead>
|
||||||
</p>
|
<tr>
|
||||||
{% endif %}
|
<th scope="col">{% trans "Agency Name" %}</th>
|
||||||
|
<th scope="col">{% trans "Contact Person" %}</th>
|
||||||
|
<th scope="col">{% trans "Email" %}</th>
|
||||||
|
<th scope="col">{% trans "Phone" %}</th>
|
||||||
|
<th scope="col">{% trans "Country" %}</th>
|
||||||
|
<th scope="col">{% trans "Website" %}</th>
|
||||||
|
<th scope="col">{% trans "Created" %}</th>
|
||||||
|
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for agency in page_obj %}
|
||||||
|
<tr>
|
||||||
|
<td class="fw-medium">
|
||||||
|
<a href="{% url 'agency_detail' agency.slug %}"
|
||||||
|
class="text-decoration-none text-primary-theme">
|
||||||
|
{{ agency.name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ agency.contact_person|default:"-" }}</td>
|
||||||
|
<td>
|
||||||
|
{% if agency.email %}
|
||||||
|
<a href="mailto:{{ agency.email }}"
|
||||||
|
class="text-decoration-none"
|
||||||
|
title="{{ agency.email }}">
|
||||||
|
<i class="fas fa-envelope me-1"></i>
|
||||||
|
{{ agency.email|truncatechars:20 }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ agency.phone|default:"-" }}</td>
|
||||||
|
<td>
|
||||||
|
{% if agency.country %}
|
||||||
|
<i class="fas fa-globe text-muted me-1"></i>
|
||||||
|
{{ agency.get_country_display }}
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if agency.website %}
|
||||||
|
<a href="{{ agency.website }}"
|
||||||
|
target="_blank"
|
||||||
|
class="text-decoration-none text-secondary">
|
||||||
|
{{ agency.website|truncatechars:25 }}
|
||||||
|
<i class="fas fa-external-link-alt ms-1 small"></i>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="stats-badge">
|
||||||
|
{{ agency.created_at|date:"M d, Y" }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<a href="{% url 'agency_detail' agency.slug %}"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
title="{% trans 'View' %}">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'agency_update' agency.slug %}"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
title="{% trans 'Edit' %}">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if agency.phone %}
|
<!-- Card View -->
|
||||||
<p class="card-text mb-2">
|
<div class="card-view row g-4">
|
||||||
<i class="fas fa-phone text-muted me-2"></i>
|
{% for agency in page_obj %}
|
||||||
{{ agency.phone }}
|
<div class="col-lg-4 col-md-6 mb-4">
|
||||||
</p>
|
<div class="card kaauh-card agency-card h-100">
|
||||||
{% endif %}
|
<div class="card-body">
|
||||||
|
<!-- Agency Header -->
|
||||||
{% if agency.country %}
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||||
<p class="card-text mb-2">
|
<h5 class="card-title mb-0" style="color: var(--kaauh-teal-dark);">
|
||||||
<i class="fas fa-globe text-muted me-2"></i>
|
{{ agency.name }}
|
||||||
{{ agency.get_country_display }}
|
</h5>
|
||||||
</p>
|
{% if agency.email %}
|
||||||
{% endif %}
|
<a href="mailto:{{ agency.email }}" class="text-muted" title="{{ agency.email }}">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
<!-- Website Link -->
|
|
||||||
{% if agency.website %}
|
|
||||||
<p class="card-text mb-3">
|
|
||||||
<i class="fas fa-link text-muted me-2"></i>
|
|
||||||
<a href="{{ agency.website }}" target="_blank" class="text-decoration-none text-secondary">
|
|
||||||
{{ agency.website|truncatechars:30 }}
|
|
||||||
<i class="fas fa-external-link-alt ms-1 small"></i>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<div class="d-flex justify-content-between align-items-center mt-auto">
|
|
||||||
<div>
|
|
||||||
<a href="{% url 'agency_detail' agency.slug %}"
|
|
||||||
class="btn btn-main-action btn-sm me-2">
|
|
||||||
<i class="fas fa-eye me-1"></i> {% trans "View" %}
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'agency_update' agency.slug %}"
|
|
||||||
class="btn btn-outline-secondary btn-sm">
|
|
||||||
<i class="fas fa-edit me-1"></i> {% trans "Edit" %}
|
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<span class="stats-badge">
|
<!-- Contact Information -->
|
||||||
{% trans "Created" %} {{ agency.created_at|date:"M d, Y" }}
|
{% if agency.contact_person %}
|
||||||
</span>
|
<p class="card-text mb-2">
|
||||||
|
<i class="fas fa-user text-muted me-2"></i>
|
||||||
|
<strong>{% trans "Contact:" %}</strong> {{ agency.contact_person }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if agency.phone %}
|
||||||
|
<p class="card-text mb-2">
|
||||||
|
<i class="fas fa-phone text-muted me-2"></i>
|
||||||
|
{{ agency.phone }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if agency.country %}
|
||||||
|
<p class="card-text mb-2">
|
||||||
|
<i class="fas fa-globe text-muted me-2"></i>
|
||||||
|
{{ agency.get_country_display }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Website Link -->
|
||||||
|
{% if agency.website %}
|
||||||
|
<p class="card-text mb-3">
|
||||||
|
<i class="fas fa-link text-muted me-2"></i>
|
||||||
|
<a href="{{ agency.website }}" target="_blank" class="text-decoration-none text-secondary">
|
||||||
|
{{ agency.website|truncatechars:30 }}
|
||||||
|
<i class="fas fa-external-link-alt ms-1 small"></i>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center mt-auto">
|
||||||
|
<div>
|
||||||
|
<a href="{% url 'agency_detail' agency.slug %}"
|
||||||
|
class="btn btn-main-action btn-sm me-2">
|
||||||
|
<i class="fas fa-eye me-1"></i> {% trans "View" %}
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'agency_update' agency.slug %}"
|
||||||
|
class="btn btn-outline-secondary btn-sm">
|
||||||
|
<i class="fas fa-edit me-1"></i> {% trans "Edit" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="stats-badge">
|
||||||
|
{% trans "Created" %} {{ agency.created_at|date:"M d, Y" }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% endfor %}
|
||||||
{% endfor %}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
|
|||||||
@ -179,7 +179,7 @@
|
|||||||
.timeline-bg-offer { background-color: #28a745 !important; }
|
.timeline-bg-offer { background-color: #28a745 !important; }
|
||||||
.timeline-bg-rejected { background-color: #dc3545 !important; }
|
.timeline-bg-rejected { background-color: #dc3545 !important; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ------------------------------------------- */
|
/* ------------------------------------------- */
|
||||||
/* 1. Base Spinner Styling */
|
/* 1. Base Spinner Styling */
|
||||||
@ -272,7 +272,7 @@
|
|||||||
<li class="breadcrumb-item"><a href="{% url 'job_detail' candidate.job.slug %}" class="text-secondary">Job:({{candidate.job.title}})</a></li>
|
<li class="breadcrumb-item"><a href="{% url 'job_detail' candidate.job.slug %}" class="text-secondary">Job:({{candidate.job.title}})</a></li>
|
||||||
<li class="breadcrumb-item active" aria-current="page" class="text-secondary" style="
|
<li class="breadcrumb-item active" aria-current="page" class="text-secondary" style="
|
||||||
color: #F43B5E; /* Rosy Accent Color */
|
color: #F43B5E; /* Rosy Accent Color */
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
">Applicant Detail</li>
|
">Applicant Detail</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
@ -313,14 +313,14 @@
|
|||||||
<i class="fas fa-id-card me-1"></i> {% trans "Contact & Job" %}
|
<i class="fas fa-id-card me-1"></i> {% trans "Contact & Job" %}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
{# NEW TAB ADDED HERE #}
|
{# NEW TAB ADDED HERE #}
|
||||||
<button class="nav-link" id="timeline-tab" data-bs-toggle="tab" data-bs-target="#timeline-pane" type="button" role="tab" aria-controls="timeline-pane" aria-selected="false">
|
<button class="nav-link" id="timeline-tab" data-bs-toggle="tab" data-bs-target="#timeline-pane" type="button" role="tab" aria-controls="timeline-pane" aria-selected="false">
|
||||||
<i class="fas fa-route me-1"></i> {% trans "Journey Timeline" %}
|
<i class="fas fa-route me-1"></i> {% trans "Journey Timeline" %}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@ -367,7 +367,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# TAB 2 CONTENT: RESUME #}
|
{# TAB 2 CONTENT: RESUME #}
|
||||||
|
|
||||||
|
|
||||||
{# NEW TAB 3 CONTENT: CANDIDATE JOURNEY TIMELINE #}
|
{# NEW TAB 3 CONTENT: CANDIDATE JOURNEY TIMELINE #}
|
||||||
<div class="tab-pane fade" id="timeline-pane" role="tabpanel" aria-labelledby="timeline-tab">
|
<div class="tab-pane fade" id="timeline-pane" role="tabpanel" aria-labelledby="timeline-tab">
|
||||||
@ -629,9 +629,9 @@
|
|||||||
|
|
||||||
{# RIGHT COLUMN: ACTIONS AND CANDIDATE TIMELINE #}
|
{# RIGHT COLUMN: ACTIONS AND CANDIDATE TIMELINE #}
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
|
|
||||||
{# ACTIONS CARD #}
|
{# ACTIONS CARD #}
|
||||||
|
|
||||||
<div class="card shadow-sm mb-2 p-2">
|
<div class="card shadow-sm mb-2 p-2">
|
||||||
<h5 class="text-muted mb-3"><i class="fas fa-cog me-2"></i>{% trans "Management Actions" %}</h5>
|
<h5 class="text-muted mb-3"><i class="fas fa-cog me-2"></i>{% trans "Management Actions" %}</h5>
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
@ -645,7 +645,7 @@
|
|||||||
<i class="fas fa-arrow-left"></i> {% trans "Back to List" %}
|
<i class="fas fa-arrow-left"></i> {% trans "Back to List" %}
|
||||||
</a>
|
</a>
|
||||||
{% if candidate.resume %}
|
{% if candidate.resume %}
|
||||||
|
|
||||||
<a href="{{ candidate.resume.url }}" target="_blank" class="btn btn-outline-primary">
|
<a href="{{ candidate.resume.url }}" target="_blank" class="btn btn-outline-primary">
|
||||||
<i class="fas fa-eye me-1"></i>
|
<i class="fas fa-eye me-1"></i>
|
||||||
{% trans "View Actual Resume" %}
|
{% trans "View Actual Resume" %}
|
||||||
@ -654,22 +654,22 @@
|
|||||||
<i class="fas fa-download me-1"></i>
|
<i class="fas fa-download me-1"></i>
|
||||||
{% trans "Download Resume" %}
|
{% trans "Download Resume" %}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="{% url 'candidate_resume_template' candidate.slug %}" class="btn btn-outline-info">
|
<a href="{% url 'candidate_resume_template' candidate.slug %}" class="btn btn-outline-info">
|
||||||
<i class="fas fa-file-alt me-1"></i>
|
<i class="fas fa-file-alt me-1"></i>
|
||||||
{% trans "View Resume AI Overview" %}
|
{% trans "View Resume AI Overview" %}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card shadow-sm mb-4 p-2">
|
<div class="card shadow-sm mb-4 p-2">
|
||||||
<h5 class="text-muted mb-3"><i class="fas fa-clock me-2"></i>{% trans "Time to Hire: " %}{{candidate.time_to_hire|default:100}} days</h5>
|
<h5 class="text-muted mb-3"><i class="fas fa-clock me-2"></i>{% trans "Time to Hire: " %}{{candidate.time_to_hire|default:100}} days</h5>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -682,17 +682,18 @@
|
|||||||
{% if candidate.scoring_timeout %}
|
{% if candidate.scoring_timeout %}
|
||||||
<div style="display: flex; justify-content: center; align-items: center; height: 100%;" class="mb-2">
|
<div style="display: flex; justify-content: center; align-items: center; height: 100%;" class="mb-2">
|
||||||
<div class="ai-loading-container">
|
<div class="ai-loading-container">
|
||||||
<i class="fas fa-robot ai-robot-icon"></i>
|
<i class="fas fa-robot ai-robot-icon"></i>
|
||||||
<span>Resume is been Scoring...</span>
|
<span>Resume is been Scoring...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div style="display: flex; justify-content: center; align-items: center; height: 100%;">
|
<div style="display: flex; justify-content: center; align-items: center; height: 100%;">
|
||||||
<button type="submit" class="btn btn-sm btn-main-action" hx-get="{% url 'candidate_retry_scoring' candidate.slug %}" hx-select=".resume-parsed-section" hx-target=".resume-parsed-section" hx-swap="outerHTML" hx-on:click="this.disabled=true;this.innerHTML=`Scoring Resume , Please Wait.. <i class='fa-solid fa-spinner fa-spin'></i>`">
|
<button type="submit" class="btn btn-sm btn-main-action" hx-get="{% url 'candidate_retry_scoring' candidate.slug %}" hx-select=".resume-parsed-section" hx-target=".resume-parsed-section" hx-swap="outerHTML" hx-on:click="this.disabled=true;this.innerHTML=`Scoring Resume , Please Wait.. <i class='fa-solid fa-spinner fa-spin'></i>`">
|
||||||
{% trans "Retry AI Scoring" %}
|
<i class="fas fa-redo-alt me-1"></i>
|
||||||
</button>
|
{% trans "Unable to Parse Resume , click to retry" %}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -295,11 +295,9 @@
|
|||||||
<a href="{% url 'candidate_list' %}" class="text-decoration-none d-flex align-items-center gap-2">
|
<a href="{% url 'candidate_list' %}" class="text-decoration-none d-flex align-items-center gap-2">
|
||||||
<svg class="kaats-spinner" viewBox="0 0 50 50" style="width: 25px; height: 25px;">
|
<svg class="kaats-spinner" viewBox="0 0 50 50" style="width: 25px; height: 25px;">
|
||||||
<circle cx="25" cy="25" r="20"></circle>
|
<circle cx="25" cy="25" r="20"></circle>
|
||||||
<circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"
|
<circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"
|
||||||
style="animation: kaats-spinner-dash 1.5s ease-in-out infinite;"></circle>
|
style="animation: kaats-spinner-dash 1.5s ease-in-out infinite;"></circle>
|
||||||
</svg>
|
</svg>
|
||||||
{# CRITICAL: Remove the DIV and the text-nowrap class #}
|
|
||||||
<span class="text-teal-primary">{% trans "AI Scoring..." %}</span>
|
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -1,106 +1,105 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
{% load i18n %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}{{ title }}{% endblock %}
|
||||||
{% trans "Delete Source" %} | {% trans "Recruitment System" %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid py-4">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card shadow">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<div class="card-header py-3">
|
<h1 class="h3 mb-0">{{ title }}</h1>
|
||||||
<h6 class="m-0 font-weight-bold text-danger">
|
<a href="{% url 'source_detail' source.slug %}" class="btn btn-outline-secondary">
|
||||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
<i class="fas fa-arrow-left"></i> Back to Source
|
||||||
{% trans "Delete Source" %}
|
</a>
|
||||||
</h6>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<h5 class="alert-heading">{% trans "Confirm Deletion" %}</h5>
|
|
||||||
<p>{% trans "Are you sure you want to delete the following source? This action cannot be undone." %}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Source Information -->
|
<div class="row">
|
||||||
<div class="row mb-4">
|
<div class="col-md-8">
|
||||||
<div class="col-12">
|
<div class="card">
|
||||||
<h5>{% trans "Source Information" %}</h5>
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="alert alert-warning d-flex align-items-center">
|
||||||
<table class="table table-borderless">
|
<i class="fas fa-exclamation-triangle fa-2x me-3"></i>
|
||||||
<tr>
|
|
||||||
<th width="30%">{% trans "Name" %}</th>
|
|
||||||
<td>{{ source.name }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Type" %}</th>
|
|
||||||
<td>
|
|
||||||
<span class="badge bg-info">{{ source.source_type }}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Status" %}</th>
|
|
||||||
<td>
|
|
||||||
{% if source.is_active %}
|
|
||||||
<span class="badge bg-success">
|
|
||||||
<i class="fas fa-check-circle me-1"></i>
|
|
||||||
{% trans "Active" %}
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-danger">
|
|
||||||
<i class="fas fa-times-circle me-1"></i>
|
|
||||||
{% trans "Inactive" %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Created By" %}</th>
|
|
||||||
<td>{{ source.created_by }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Created At" %}</th>
|
|
||||||
<td>{{ source.created_at|date:"M d, Y H:i" }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Warning Messages -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
<h5 class="alert-heading">
|
|
||||||
<i class="fas fa-exclamation-circle me-2"></i>
|
|
||||||
{% trans "Important Note" %}
|
|
||||||
</h5>
|
|
||||||
<ul>
|
|
||||||
<li>{% trans "All associated API keys will be permanently deleted." %}</li>
|
|
||||||
<li>{% trans "Integration logs related to this source will remain but will show 'Source deleted'." %}</li>
|
|
||||||
<li>{% trans "Any active integrations using this source will be disconnected." %}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<div>
|
<div>
|
||||||
<a href="{% url 'source_detail' source.pk %}" class="btn btn-secondary">
|
<strong>Warning:</strong> This action cannot be undone.
|
||||||
<i class="fas fa-arrow-left me-2"></i>
|
Deleting this source will also remove all associated integration logs and API credentials.
|
||||||
{% trans "Cancel" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="">
|
</div>
|
||||||
{% csrf_token %}
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5>Source to be deleted:</h5>
|
||||||
|
<div class="card bg-light">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Name:</strong><br>
|
||||||
|
{{ source.name }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Type:</strong><br>
|
||||||
|
<span class="badge bg-info">{{ source.get_source_type_display }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if source.description %}
|
||||||
|
<hr>
|
||||||
|
<div>
|
||||||
|
<strong>Description:</strong><br>
|
||||||
|
{{ source.description|linebreaks }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<hr>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Created:</strong><br>
|
||||||
|
{{ source.created_at|date:"M d, Y H:i" }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Total API Calls:</strong><br>
|
||||||
|
{{ source.integration_logs.count }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" class="d-inline">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{{ cancel_url }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-times"></i> Cancel
|
||||||
|
</a>
|
||||||
<button type="submit" class="btn btn-danger">
|
<button type="submit" class="btn btn-danger">
|
||||||
<i class="fas fa-trash me-2"></i>
|
<i class="fas fa-trash"></i> Delete Source
|
||||||
{% trans "Delete Source" %}
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">Impact Summary</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Integration Logs</label>
|
||||||
|
<div class="h5 mb-0 text-danger">
|
||||||
|
{{ source.integration_logs.count }} will be deleted
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">API Credentials</label>
|
||||||
|
<div class="h5 mb-0 text-danger">
|
||||||
|
API Key & Secret will be permanently lost
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Active Integrations</label>
|
||||||
|
<div class="h5 mb-0 text-warning">
|
||||||
|
Any systems using this API will lose access
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -110,19 +109,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
|
||||||
<script>
|
|
||||||
// Add confirmation dialog
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const deleteForm = document.querySelector('form[action*="delete"]');
|
|
||||||
if (deleteForm) {
|
|
||||||
deleteForm.addEventListener('submit', function(e) {
|
|
||||||
if (!confirm('{% trans "Are you sure you want to delete this source? This action cannot be undone." %}')) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|||||||
@ -1,228 +1,287 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
{% load i18n %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}{{ source.name }} - Source Details{% endblock %}
|
||||||
{{ source.name }} | {% trans "Source Details" %} | {% trans "Recruitment System" %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid py-4">
|
<div class="container-fluid">
|
||||||
<!-- Page Header -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<div>
|
|
||||||
<h1 class="h3 mb-1">{{ source.name }}</h1>
|
|
||||||
<nav aria-label="breadcrumb">
|
|
||||||
<ol class="breadcrumb">
|
|
||||||
<li class="breadcrumb-item">
|
|
||||||
<a href="{% url 'source_list' %}">{% trans "Sources" %}</a>
|
|
||||||
</li>
|
|
||||||
<li class="breadcrumb-item active" aria-current="page">{{ source.name }}</li>
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
<div class="btn-group">
|
|
||||||
<a href="{% url 'source_update' source.pk %}" class="btn btn-primary">
|
|
||||||
<i class="fas fa-edit me-2"></i>
|
|
||||||
{% trans "Edit" %}
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'source_delete' source.pk %}" class="btn btn-danger">
|
|
||||||
<i class="fas fa-trash me-2"></i>
|
|
||||||
{% trans "Delete" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Source Information Card -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card shadow">
|
|
||||||
<div class="card-header py-3">
|
|
||||||
<h6 class="m-0 font-weight-bold text-primary">
|
|
||||||
<i class="fas fa-info-circle me-2"></i>
|
|
||||||
{% trans "Source Information" %}
|
|
||||||
</h6>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<table class="table table-borderless">
|
|
||||||
<tr>
|
|
||||||
<th width="30%">{% trans "Name" %}</th>
|
|
||||||
<td>{{ source.name }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Type" %}</th>
|
|
||||||
<td>
|
|
||||||
<span class="badge bg-info">{{ source.source_type }}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Status" %}</th>
|
|
||||||
<td>
|
|
||||||
{% if source.is_active %}
|
|
||||||
<span class="badge bg-success">
|
|
||||||
<i class="fas fa-check-circle me-1"></i>
|
|
||||||
{% trans "Active" %}
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-danger">
|
|
||||||
<i class="fas fa-times-circle me-1"></i>
|
|
||||||
{% trans "Inactive" %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<table class="table table-borderless">
|
|
||||||
<tr>
|
|
||||||
<th width="30%">{% trans "Created By" %}</th>
|
|
||||||
<td>{{ source.created_by }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Created At" %}</th>
|
|
||||||
<td>{{ source.created_at|date:"M d, Y H:i" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Updated At" %}</th>
|
|
||||||
<td>{{ source.updated_at|date:"M d, Y H:i" }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mt-3">
|
|
||||||
<div class="col-12">
|
|
||||||
<h6>{% trans "Description" %}</h6>
|
|
||||||
<p class="text-muted">{{ source.description|default:"-" }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Network Configuration -->
|
<div class="col-12">
|
||||||
<div class="col-lg-6 mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<div class="card shadow">
|
<h1 class="h3 mb-0">{{ source.name }}</h1>
|
||||||
<div class="card-header py-3">
|
<div class="btn-group">
|
||||||
<h6 class="m-0 font-weight-bold text-primary">
|
<a href="{% url 'source_update' source.pk %}" class="btn btn-outline-secondary">
|
||||||
<i class="fas fa-network-wired me-2"></i>
|
<i class="fas fa-edit"></i> Edit
|
||||||
{% trans "Network Configuration" %}
|
</a>
|
||||||
</h6>
|
<a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning">
|
||||||
|
<i class="fas fa-key"></i> Generate Keys
|
||||||
|
</a>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-outline-warning"
|
||||||
|
hx-post="{% url 'toggle_source_status' source.pk %}"
|
||||||
|
hx-confirm="Are you sure you want to {{ source.is_active|yesno:'deactivate,activate' }} this source?"
|
||||||
|
title="{{ source.is_active|yesno:'Deactivate,Activate' }}">
|
||||||
|
<i class="fas fa-{{ source.is_active|yesno:'pause,play' }}"></i>
|
||||||
|
{{ source.is_active|yesno:'Deactivate,Activate' }}
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'source_delete' source.pk %}" class="btn btn-outline-danger">
|
||||||
|
<i class="fas fa-trash"></i> Delete
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<strong>{% trans "IP Address" %}:</strong>
|
<!-- Source Information -->
|
||||||
<code>{{ source.ip_address|default:"Not specified" }}</code>
|
<div class="row">
|
||||||
</div>
|
<div class="col-md-8">
|
||||||
<div class="mb-3">
|
<div class="card mb-4">
|
||||||
<strong>{% trans "Trusted IPs" %}:</strong>
|
<div class="card-header">
|
||||||
<div class="mt-2">
|
<h6 class="mb-0">Source Information</h6>
|
||||||
{% if source.trusted_ips %}
|
</div>
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="card-body">
|
||||||
{% for ip in source.trusted_ips|split:"," %}
|
<div class="row">
|
||||||
<span class="badge bg-secondary">{{ ip|strip }}</span>
|
<div class="col-md-6">
|
||||||
{% endfor %}
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Name</label>
|
||||||
|
<div class="fw-bold">{{ source.name }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Type</label>
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-info">{{ source.get_source_type_display }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if source.description %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Description</label>
|
||||||
|
<div>{{ source.description|linebreaks }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Contact Email</label>
|
||||||
|
<div>
|
||||||
|
{% if source.contact_email %}
|
||||||
|
<a href="mailto:{{ source.contact_email }}">{{ source.contact_email }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">Not specified</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Contact Phone</label>
|
||||||
|
<div>
|
||||||
|
{% if source.contact_phone %}
|
||||||
|
{{ source.contact_phone }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">Not specified</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Status</label>
|
||||||
|
<div>
|
||||||
|
{% if source.is_active %}
|
||||||
|
<span class="badge bg-success">Active</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Inactive</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Requires Authentication</label>
|
||||||
|
<div>
|
||||||
|
{% if source.requires_auth %}
|
||||||
|
<span class="badge bg-warning">Yes</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">No</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if source.webhook_url %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Webhook URL</label>
|
||||||
|
<div><code>{{ source.webhook_url }}</code></div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if source.api_timeout %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">API Timeout</label>
|
||||||
|
<div>{{ source.api_timeout }} seconds</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if source.notes %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Notes</label>
|
||||||
|
<div>{{ source.notes|linebreaks }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Created</label>
|
||||||
|
<div>{{ source.created_at|date:"M d, Y H:i" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Last Updated</label>
|
||||||
|
<div>{{ source.updated_at|date:"M d, Y H:i" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<!-- API Credentials -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">API Credentials</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">API Key</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" value="{{ source.api_key }}" readonly>
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
hx-post="{% url 'copy_to_clipboard' %}"
|
||||||
|
hx-vals='{"text": "{{ source.api_key }}"}'
|
||||||
|
title="Copy to clipboard">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">API Secret</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" class="form-control" value="{{ source.api_secret }}" readonly id="api-secret">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" onclick="toggleSecretVisibility()">
|
||||||
|
<i class="fas fa-eye" id="secret-toggle-icon"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
hx-post="{% url 'copy_to_clipboard' %}"
|
||||||
|
hx-vals='{"text": "{{ source.api_secret }}"}'
|
||||||
|
title="Copy to clipboard">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning btn-sm">
|
||||||
|
<i class="fas fa-key"></i> Generate New Keys
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistics -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">Integration Statistics</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Total API Calls</label>
|
||||||
|
<div class="h5 mb-0">{{ total_logs }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Successful Calls</label>
|
||||||
|
<div class="h5 mb-0 text-success">{{ successful_logs }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Failed Calls</label>
|
||||||
|
<div class="h5 mb-0 text-danger">{{ failed_logs }}</div>
|
||||||
|
</div>
|
||||||
|
{% if total_logs > 0 %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Success Rate</label>
|
||||||
|
<div class="h5 mb-0">
|
||||||
|
{% widthratio successful_logs total_logs 100 %}%
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">Not specified</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- API Configuration -->
|
<!-- Integration Logs -->
|
||||||
<div class="col-lg-6 mb-4">
|
<div class="card">
|
||||||
<div class="card shadow">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<div class="card-header py-3">
|
<h6 class="mb-0">Recent Integration Logs</h6>
|
||||||
<h6 class="m-0 font-weight-bold text-primary">
|
<small class="text-muted">Last 10 logs</small>
|
||||||
<i class="fas fa-key me-2"></i>
|
|
||||||
{% trans "API Configuration" %}
|
|
||||||
</h6>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="mb-3">
|
{% if integration_logs %}
|
||||||
<strong>{% trans "Integration Version" %}:</strong>
|
|
||||||
<span class="badge bg-primary">{{ source.integration_version|default:"Not specified" }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<strong>{% trans "API Key" %}:</strong>
|
|
||||||
<div class="input-group mt-2">
|
|
||||||
<input type="text" class="form-control" id="apiKey" value="{{ masked_api_key }}" readonly>
|
|
||||||
<button class="btn btn-outline-secondary" type="button"
|
|
||||||
onclick="copyToClipboard('apiKey')">
|
|
||||||
<i class="fas fa-copy"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<strong>{% trans "API Secret" %}:</strong>
|
|
||||||
<div class="input-group mt-2">
|
|
||||||
<input type="text" class="form-control" id="apiSecret" value="{{ masked_api_secret }}" readonly>
|
|
||||||
<button class="btn btn-outline-secondary" type="button"
|
|
||||||
onclick="copyToClipboard('apiSecret')">
|
|
||||||
<i class="fas fa-copy"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Integration Logs -->
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card shadow">
|
|
||||||
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
|
||||||
<h6 class="m-0 font-weight-bold text-primary">
|
|
||||||
<i class="fas fa-history me-2"></i>
|
|
||||||
{% trans "Recent Integration Logs" %}
|
|
||||||
</h6>
|
|
||||||
<a href="{% url 'source_list' %}" class="btn btn-sm btn-secondary">
|
|
||||||
<i class="fas fa-arrow-left me-1"></i>
|
|
||||||
{% trans "Back to List" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
{% if recent_logs %}
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-bordered">
|
<table class="table table-sm">
|
||||||
<thead>
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Time" %}</th>
|
<th>Timestamp</th>
|
||||||
<th>{% trans "Action" %}</th>
|
<th>Method</th>
|
||||||
<th>{% trans "Endpoint" %}</th>
|
<th>Status</th>
|
||||||
<th>{% trans "Method" %}</th>
|
<th>Response Time</th>
|
||||||
<th>{% trans "Status" %}</th>
|
<th>Details</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for log in recent_logs %}
|
{% for log in integration_logs %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ log.created_at|date:"M d, Y H:i:s" }}</td>
|
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-info">{{ log.get_action_display }}</span>
|
<small>{{ log.created_at|date:"M d, Y H:i:s" }}</small>
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<code class="text-muted">{{ log.endpoint }}</code>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-secondary">{{ log.method }}</span>
|
<span class="badge bg-secondary">{{ log.method }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if log.success %}
|
{% if log.status_code >= 200 and log.status_code < 300 %}
|
||||||
<span class="badge bg-success">Success</span>
|
<span class="badge bg-success">{{ log.status_code }}</span>
|
||||||
|
{% elif log.status_code >= 400 %}
|
||||||
|
<span class="badge bg-danger">{{ log.status_code }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-danger">Failed</span>
|
<span class="badge bg-warning">{{ log.status_code }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if log.response_time_ms %}
|
||||||
|
<small>{{ log.response_time_ms }}ms</small>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if log.request_data %}
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-info"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#logDetailModal{{ log.id }}"
|
||||||
|
title="View details">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">No data</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -230,20 +289,10 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% if recent_logs.has_previous %}
|
|
||||||
<div class="text-center mt-3">
|
|
||||||
<a href="?page={{ recent_logs.previous_page_number }}" class="btn btn-sm btn-secondary">
|
|
||||||
<i class="fas fa-chevron-left"></i> {% trans "Previous" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center py-5">
|
<div class="text-center py-4">
|
||||||
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
|
<i class="fas fa-clipboard-list fa-2x text-muted mb-3"></i>
|
||||||
<h5 class="text-muted">{% trans "No integration logs found" %}</h5>
|
<p class="text-muted">No integration logs found</p>
|
||||||
<p class="text-muted">
|
|
||||||
{% trans "Integration logs will appear here when this source is used for external integrations." %}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -251,35 +300,119 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Log Detail Modals -->
|
||||||
|
{% for log in integration_logs %}
|
||||||
|
{% if log.request_data %}
|
||||||
|
<div class="modal fade" id="logDetailModal{{ log.id }}" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Integration Log Details</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Timestamp:</strong><br>
|
||||||
|
{{ log.created_at|date:"M d, Y H:i:s" }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Method:</strong><br>
|
||||||
|
<span class="badge bg-secondary">{{ log.method }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Status Code:</strong><br>
|
||||||
|
{% if log.status_code >= 200 and log.status_code < 300 %}
|
||||||
|
<span class="badge bg-success">{{ log.status_code }}</span>
|
||||||
|
{% elif log.status_code >= 400 %}
|
||||||
|
<span class="badge bg-danger">{{ log.status_code }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-warning">{{ log.status_code }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Response Time:</strong><br>
|
||||||
|
{% if log.response_time_ms %}
|
||||||
|
{{ log.response_time_ms }}ms
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>Request Data:</strong>
|
||||||
|
<pre class="bg-light p-2 rounded"><code>{{ log.request_data|pprint }}</code></pre>
|
||||||
|
</div>
|
||||||
|
{% if log.response_data %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>Response Data:</strong>
|
||||||
|
<pre class="bg-light p-2 rounded"><code>{{ log.response_data|pprint }}</code></pre>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if log.error_message %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>Error Message:</strong>
|
||||||
|
<div class="alert alert-danger">{{ log.error_message }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
// Make function available globally
|
function toggleSecretVisibility() {
|
||||||
window.copyToClipboard = function(elementId) {
|
const secretInput = document.getElementById('api-secret');
|
||||||
const element = document.getElementById(elementId);
|
const toggleIcon = document.getElementById('secret-toggle-icon');
|
||||||
if (element) {
|
|
||||||
const text = element.value;
|
|
||||||
navigator.clipboard.writeText(text).then(function() {
|
|
||||||
// Show success message
|
|
||||||
const button = event.target.closest('button');
|
|
||||||
const originalContent = button.innerHTML;
|
|
||||||
button.innerHTML = '<i class="fas fa-check"></i>';
|
|
||||||
button.classList.add('btn-success');
|
|
||||||
button.classList.remove('btn-outline-secondary');
|
|
||||||
|
|
||||||
setTimeout(function() {
|
if (secretInput.type === 'password') {
|
||||||
button.innerHTML = originalContent;
|
secretInput.type = 'text';
|
||||||
button.classList.remove('btn-success');
|
toggleIcon.classList.remove('fa-eye');
|
||||||
button.classList.add('btn-outline-secondary');
|
toggleIcon.classList.add('fa-eye-slash');
|
||||||
}, 2000);
|
|
||||||
}).catch(function(err) {
|
|
||||||
console.error('Failed to copy text: ', err);
|
|
||||||
alert('{% trans "Failed to copy to clipboard" %}');
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
console.error('Element not found:', elementId);
|
secretInput.type = 'password';
|
||||||
|
toggleIcon.classList.remove('fa-eye-slash');
|
||||||
|
toggleIcon.classList.add('fa-eye');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle HTMX copy to clipboard feedback
|
||||||
|
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
||||||
|
if (evt.detail.successful && evt.detail.target.matches('[hx-post*="copy_to_clipboard"]')) {
|
||||||
|
const button = evt.detail.target;
|
||||||
|
const originalIcon = button.innerHTML;
|
||||||
|
button.innerHTML = '<i class="fas fa-check"></i>';
|
||||||
|
button.classList.remove('btn-outline-secondary');
|
||||||
|
button.classList.add('btn-success');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
button.innerHTML = originalIcon;
|
||||||
|
button.classList.remove('btn-success');
|
||||||
|
button.classList.add('btn-outline-secondary');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-refresh after status toggle
|
||||||
|
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
||||||
|
if (evt.detail.successful && evt.detail.target.matches('[hx-post*="toggle_source_status"]')) {
|
||||||
|
// Reload the page after a short delay to show updated status
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -1,293 +1,177 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
{% load i18n %}
|
{% load static %}
|
||||||
|
{% load widget_tweaks %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}{{ title }}{% endblock %}
|
||||||
{% if title %}{{ title }} | {% endif %}{% trans "Source" %} | {% trans "Recruitment System" %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
<!-- Script to define functions globally before buttons are rendered -->
|
|
||||||
<script>
|
|
||||||
function copyToClipboard(elementId) {
|
|
||||||
const element = document.getElementById(elementId);
|
|
||||||
if (element) {
|
|
||||||
const text = element.value;
|
|
||||||
navigator.clipboard.writeText(text).then(function() {
|
|
||||||
const button = event.target.closest('button');
|
|
||||||
if (button) {
|
|
||||||
const orig = button.innerHTML;
|
|
||||||
button.innerHTML = '<i class="fas fa-check"></i>';
|
|
||||||
button.classList.add('btn-success');
|
|
||||||
button.classList.remove('btn-outline-secondary');
|
|
||||||
setTimeout(function() {
|
|
||||||
button.innerHTML = orig;
|
|
||||||
button.classList.remove('btn-success');
|
|
||||||
button.classList.add('btn-outline-secondary');
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}).catch(function(err) {
|
|
||||||
console.error('Failed to copy text: ', err);
|
|
||||||
alert('{% trans "Failed to copy to clipboard" %}');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error('Element not found:', elementId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateRandomKey(elementId, length) {
|
|
||||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
|
||||||
let result = '';
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
result += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
|
|
||||||
}
|
|
||||||
|
|
||||||
const element = document.getElementById(elementId);
|
|
||||||
if (element) {
|
|
||||||
element.value = result;
|
|
||||||
|
|
||||||
const button = event.target.closest('button');
|
|
||||||
if (button) {
|
|
||||||
const orig = button.innerHTML;
|
|
||||||
button.innerHTML = '<i class="fas fa-check"></i>';
|
|
||||||
button.classList.add('btn-success');
|
|
||||||
button.classList.remove('btn-outline-secondary');
|
|
||||||
setTimeout(function() {
|
|
||||||
button.innerHTML = orig;
|
|
||||||
button.classList.remove('btn-success');
|
|
||||||
button.classList.add('btn-outline-secondary');
|
|
||||||
}, 1500);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error('Element not found:', elementId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make functions globally available
|
|
||||||
window.copyToClipboard = copyToClipboard;
|
|
||||||
window.generateRandomKey = generateRandomKey;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="container-fluid py-4">
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card shadow">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<div class="card-header py-3">
|
<h1 class="h3 mb-0">{{ title }}</h1>
|
||||||
<h6 class="m-0 font-weight-bold text-primary">
|
<a href="{% url 'source_list' %}" class="btn btn-outline-secondary">
|
||||||
{% if title %}{{ title }}{% else %}{% trans "Create New Source" %}{% endif %}
|
<i class="fas fa-arrow-left"></i> Back to Sources
|
||||||
</h6>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post" id="sourceForm">
|
<form method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<!-- Form Messages -->
|
{% if form.non_field_errors %}
|
||||||
{% if form.errors %}
|
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
<h5 class="alert-heading">{% trans "Please correct the errors below:" %}</h5>
|
{% for error in form.non_field_errors %}
|
||||||
{% for field in form %}
|
{{ error }}
|
||||||
{% if field.errors %}
|
|
||||||
<p class="mb-0">{{ field.label }}: {{ field.errors|join:", " }}</p>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if messages %}
|
<div class="row">
|
||||||
{% for message in messages %}
|
<div class="col-md-6">
|
||||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
<div class="mb-3">
|
||||||
{{ message }}
|
<label for="{{ form.name.id_for_label }}" class="form-label">
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
{{ form.name.label }} <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
{{ form.name|add_class:"form-control" }}
|
||||||
|
{% if form.name.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.name.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="form-text">{{ form.name.help_text }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Basic Information -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<h5 class="mb-3">{% trans "Basic Information" %}</h5>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="mb-3">
|
||||||
{{ form.name.label_tag }}
|
<label for="{{ form.source_type.id_for_label }}" class="form-label">
|
||||||
{{ form.name }}
|
{{ form.source_type.label }} <span class="text-danger">*</span>
|
||||||
{% if form.name.help_text %}
|
</label>
|
||||||
<small class="form-text text-muted">{{ form.name.help_text }}</small>
|
{{ form.source_type|add_class:"form-select" }}
|
||||||
{% endif %}
|
{% if form.source_type.errors %}
|
||||||
{% if form.name.errors %}
|
<div class="invalid-feedback d-block">
|
||||||
<div class="invalid-feedback d-block">{{ form.name.errors }}</div>
|
{% for error in form.source_type.errors %}
|
||||||
{% endif %}
|
{{ error }}
|
||||||
</div>
|
{% endfor %}
|
||||||
|
</div>
|
||||||
<div class="col-md-6 mb-3">
|
{% endif %}
|
||||||
{{ form.source_type.label_tag }}
|
<div class="form-text">{{ form.source_type.help_text }}</div>
|
||||||
{{ form.source_type }}
|
|
||||||
{% if form.source_type.help_text %}
|
|
||||||
<small class="form-text text-muted">{{ form.source_type.help_text }}</small>
|
|
||||||
{% endif %}
|
|
||||||
{% if form.source_type.errors %}
|
|
||||||
<div class="invalid-feedback d-block">{{ form.source_type.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 mb-3">
|
|
||||||
{{ form.description.label_tag }}
|
|
||||||
{{ form.description }}
|
|
||||||
{% if form.description.help_text %}
|
|
||||||
<small class="form-text text-muted">{{ form.description.help_text }}</small>
|
|
||||||
{% endif %}
|
|
||||||
{% if form.description.errors %}
|
|
||||||
<div class="invalid-feedback d-block">{{ form.description.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Network Configuration -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<h5 class="mb-3">{% trans "Network Configuration" %}</h5>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
{{ form.ip_address.label_tag }}
|
|
||||||
{{ form.ip_address }}
|
|
||||||
{% if form.ip_address.help_text %}
|
|
||||||
<small class="form-text text-muted">{{ form.ip_address.help_text }}</small>
|
|
||||||
{% endif %}
|
|
||||||
{% if form.ip_address.errors %}
|
|
||||||
<div class="invalid-feedback d-block">{{ form.ip_address.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
{{ form.trusted_ips.label_tag }}
|
|
||||||
{{ form.trusted_ips }}
|
|
||||||
{% if form.trusted_ips.help_text %}
|
|
||||||
<small class="form-text text-muted">{{ form.trusted_ips.help_text }}</small>
|
|
||||||
{% endif %}
|
|
||||||
{% if form.trusted_ips.errors %}
|
|
||||||
<div class="invalid-feedback d-block">{{ form.trusted_ips.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Settings -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<h5 class="mb-3">{% trans "Settings" %}</h5>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
{{ form.integration_version.label_tag }}
|
|
||||||
{{ form.integration_version }}
|
|
||||||
{% if form.integration_version.help_text %}
|
|
||||||
<small class="form-text text-muted">{{ form.integration_version.help_text }}</small>
|
|
||||||
{% endif %}
|
|
||||||
{% if form.integration_version.errors %}
|
|
||||||
<div class="invalid-feedback d-block">{{ form.integration_version.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<div class="form-check form-switch">
|
|
||||||
{{ form.is_active }}
|
|
||||||
{{ form.is_active.label_tag }}
|
|
||||||
</div>
|
</div>
|
||||||
{% if form.is_active.help_text %}
|
|
||||||
<small class="form-text text-muted">{{ form.is_active.help_text }}</small>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- API Configuration -->
|
<div class="mb-3">
|
||||||
<div class="row mb-4">
|
<label for="{{ form.description.id_for_label }}" class="form-label">
|
||||||
<div class="col-12">
|
{{ form.description.label }}
|
||||||
<h5 class="mb-3">{% trans "API Configuration" %}</h5>
|
</label>
|
||||||
</div>
|
{{ form.description|add_class:"form-control" }}
|
||||||
|
{% if form.description.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.description.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="form-text">{{ form.description.help_text }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-12 mb-3">
|
<div class="row">
|
||||||
<div class="card">
|
<div class="col-md-6">
|
||||||
<div class="card-body">
|
<div class="mb-3">
|
||||||
<div class="row align-items-center">
|
<label for="{{ form.ip_address.id_for_label }}" class="form-label">
|
||||||
<div class="col-md-8">
|
{{ form.ip_address.label }}
|
||||||
<h6 class="mb-0">{% trans "API Keys" %}</h6>
|
</label>
|
||||||
<small class="text-muted">{% trans "Generate secure API keys for external integrations" %}</small>
|
{{ form.ip_address|add_class:"form-control" }}
|
||||||
|
{% if form.ip_address.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.ip_address.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="form-text">{{ form.ip_address.help_text }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.is_active|add_class:"form-check-input" }}
|
||||||
|
<label for="{{ form.is_active.id_for_label }}" class="form-check-label">
|
||||||
|
{{ form.is_active.label }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% if form.is_active.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.is_active.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="form-text">{{ form.is_active.help_text }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- API Credentials Section -->
|
||||||
|
{% if source %}
|
||||||
|
<div class="card bg-light mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">API Credentials</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">API Key</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" value="{{ source.api_key }}" readonly>
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
hx-post="{% url 'copy_to_clipboard' %}"
|
||||||
|
hx-vals='{"text": "{{ source.api_key }}"}'
|
||||||
|
title="Copy to clipboard">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-end">
|
</div>
|
||||||
<div class="form-check form-switch">
|
<div class="col-md-6">
|
||||||
{{ form.generate_keys }}
|
<div class="mb-3">
|
||||||
{{ form.generate_keys.label_tag }}
|
<label class="form-label">API Secret</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" class="form-control" value="{{ source.api_secret }}" readonly id="api-secret">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" onclick="toggleSecretVisibility()">
|
||||||
|
<i class="fas fa-eye" id="secret-toggle-icon"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
hx-post="{% url 'copy_to_clipboard' %}"
|
||||||
|
hx-vals='{"text": "{{ source.api_secret }}"}'
|
||||||
|
title="Copy to clipboard">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="text-end">
|
||||||
</div>
|
<a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning">
|
||||||
|
<i class="fas fa-key"></i> Generate New Keys
|
||||||
<!-- Generated API Key -->
|
</a>
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">{% trans "API Key" %}</label>
|
|
||||||
<div class="input-group">
|
|
||||||
{{ form.api_key_generated }}
|
|
||||||
<button class="btn btn-outline-secondary" type="button"
|
|
||||||
id="generateApiKey"
|
|
||||||
onclick="generateRandomKey('id_api_key_generated', 32)"
|
|
||||||
title="{% trans 'Generate random API key' %}">
|
|
||||||
<i class="fas fa-sync-alt"></i>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline-secondary" type="button"
|
|
||||||
id="copyApiKey"
|
|
||||||
onclick="copyToClipboard('id_api_key_generated')"
|
|
||||||
title="{% trans 'Copy to clipboard' %}">
|
|
||||||
<i class="fas fa-copy"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{% if form.api_key_generated.errors %}
|
|
||||||
<div class="invalid-feedback d-block">{{ form.api_key_generated.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Generated API Secret -->
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">{% trans "API Secret" %}</label>
|
|
||||||
<div class="input-group">
|
|
||||||
{{ form.api_secret_generated }}
|
|
||||||
<button class="btn btn-outline-secondary" type="button"
|
|
||||||
id="generateApiSecret"
|
|
||||||
onclick="generateRandomKey('id_api_secret_generated', 64)"
|
|
||||||
title="{% trans 'Generate random API secret' %}">
|
|
||||||
<i class="fas fa-sync-alt"></i>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline-secondary" type="button"
|
|
||||||
id="copyApiSecret"
|
|
||||||
onclick="copyToClipboard('id_api_secret_generated')"
|
|
||||||
title="{% trans 'Copy to clipboard' %}">
|
|
||||||
<i class="fas fa-copy"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{% if form.api_secret_generated.errors %}
|
|
||||||
<div class="invalid-feedback d-block">{{ form.api_secret_generated.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Form Actions -->
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<hr>
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<a href="{% url 'source_list' %}" class="btn btn-secondary">
|
|
||||||
<i class="fas fa-times me-2"></i>
|
|
||||||
{% trans "Cancel" %}
|
|
||||||
</a>
|
|
||||||
<div>
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
<i class="fas fa-save me-2"></i>
|
|
||||||
{% trans "Save Source" %}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{% url 'source_list' %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-times"></i> Cancel
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> {{ button_text }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -297,79 +181,37 @@ window.generateRandomKey = generateRandomKey;
|
|||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block customJS %}
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
// Function to copy text to clipboard
|
function toggleSecretVisibility() {
|
||||||
function copyToClipboard(elementId) {
|
const secretInput = document.getElementById('api-secret');
|
||||||
const element = document.getElementById(elementId);
|
const toggleIcon = document.getElementById('secret-toggle-icon');
|
||||||
if (element) {
|
|
||||||
const text = element.value;
|
|
||||||
navigator.clipboard.writeText(text).then(function() {
|
|
||||||
// Show success message
|
|
||||||
const button = event.target.closest('button');
|
|
||||||
if (button) {
|
|
||||||
const originalContent = button.innerHTML;
|
|
||||||
button.innerHTML = '<i class="fas fa-check"></i>';
|
|
||||||
button.classList.add('btn-success');
|
|
||||||
button.classList.remove('btn-outline-secondary');
|
|
||||||
|
|
||||||
setTimeout(function() {
|
if (secretInput.type === 'password') {
|
||||||
button.innerHTML = originalContent;
|
secretInput.type = 'text';
|
||||||
button.classList.remove('btn-success');
|
toggleIcon.classList.remove('fa-eye');
|
||||||
button.classList.add('btn-outline-secondary');
|
toggleIcon.classList.add('fa-eye-slash');
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}).catch(function(err) {
|
|
||||||
console.error('Failed to copy text: ', err);
|
|
||||||
alert('{% trans "Failed to copy to clipboard" %}');
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
console.error('Element not found:', elementId);
|
secretInput.type = 'password';
|
||||||
|
toggleIcon.classList.remove('fa-eye-slash');
|
||||||
|
toggleIcon.classList.add('fa-eye');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to generate random key
|
// Handle HTMX copy to clipboard feedback
|
||||||
function generateRandomKey(elementId, length) {
|
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
||||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
if (evt.detail.successful && evt.detail.target.matches('[hx-post*="copy_to_clipboard"]')) {
|
||||||
let result = '';
|
const button = evt.detail.target;
|
||||||
for (let i = 0; i < length; i++) {
|
const originalIcon = button.innerHTML;
|
||||||
result += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
|
button.innerHTML = '<i class="fas fa-check"></i>';
|
||||||
}
|
button.classList.remove('btn-outline-secondary');
|
||||||
console.log(elementId);
|
button.classList.add('btn-success');
|
||||||
const element = document.getElementById(elementId);
|
|
||||||
if (element) {
|
|
||||||
element.value = result;
|
|
||||||
|
|
||||||
// Show success animation on the generate button
|
setTimeout(() => {
|
||||||
const button = event.target.closest('button');
|
button.innerHTML = originalIcon;
|
||||||
if (button) {
|
button.classList.remove('btn-success');
|
||||||
const originalContent = button.innerHTML;
|
button.classList.add('btn-outline-secondary');
|
||||||
button.innerHTML = '<i class="fas fa-check"></i>';
|
}, 2000);
|
||||||
button.classList.add('btn-success');
|
|
||||||
button.classList.remove('btn-outline-secondary');
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
button.innerHTML = originalContent;
|
|
||||||
button.classList.remove('btn-success');
|
|
||||||
button.classList.add('btn-outline-secondary');
|
|
||||||
}, 1500);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error('Element not found:', elementId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize after DOM is fully loaded
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const generateKeysCheckbox = document.getElementById('id_generate_keys');
|
|
||||||
if (generateKeysCheckbox) {
|
|
||||||
// If API keys are already generated, show them
|
|
||||||
const apiKeyField = document.getElementById('id_api_key_generated');
|
|
||||||
const apiSecretField = document.getElementById('id_api_secret_generated');
|
|
||||||
|
|
||||||
if (apiKeyField && apiSecretField && (apiKeyField.value || apiSecretField.value)) {
|
|
||||||
generateKeysCheckbox.checked = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,200 +1,187 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
{% load i18n %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}{% trans "Sources" %} | {% trans "Recruitment System" %}{% endblock %}
|
{% block title %}Sources{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid py-4">
|
<div class="container-fluid">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="row">
|
||||||
<h1 class="h3 mb-0 text-gray-800">
|
<div class="col-12">
|
||||||
<i class="fas fa-database me-2"></i>
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
{% trans "Data Sources" %}
|
<h1 class="h3 mb-0">Sources</h1>
|
||||||
</h1>
|
<a href="{% url 'source_create' %}" class="btn btn-primary">
|
||||||
<a href="{% url 'source_create' %}" class="btn btn-primary">
|
<i class="fas fa-plus"></i> Create Source
|
||||||
<i class="fas fa-plus me-2"></i>
|
</a>
|
||||||
{% trans "Create New Source" %}
|
</div>
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Search Bar -->
|
<!-- Search and Filters -->
|
||||||
<div class="card shadow mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="get" class="row g-3">
|
<form method="get" class="row g-3">
|
||||||
<div class="col-md-10">
|
<div class="col-md-8">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text">
|
<span class="input-group-text">
|
||||||
<i class="fas fa-search"></i>
|
<i class="fas fa-search"></i>
|
||||||
</span>
|
</span>
|
||||||
<input type="text" name="search" class="form-control"
|
<input type="text" class="form-control" name="q"
|
||||||
placeholder="{% trans 'Search by name, type, or description...' %}"
|
placeholder="Search sources..." value="{{ search_query }}">
|
||||||
value="{{ search_query }}">
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-md-4">
|
||||||
<div class="col-md-2">
|
<button type="submit" class="btn btn-outline-primary">
|
||||||
<button type="submit" class="btn btn-secondary w-100">
|
<i class="fas fa-search"></i> Search
|
||||||
<i class="fas fa-filter me-2"></i>
|
</button>
|
||||||
{% trans "Filter" %}
|
{% if search_query %}
|
||||||
</button>
|
<a href="{% url 'source_list' %}" class="btn btn-outline-secondary">
|
||||||
</div>
|
<i class="fas fa-times"></i> Clear
|
||||||
</form>
|
</a>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sources List -->
|
|
||||||
<div class="card shadow">
|
|
||||||
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
|
||||||
<h6 class="m-0 font-weight-bold text-primary">
|
|
||||||
{% trans "Available Sources" %}
|
|
||||||
</h6>
|
|
||||||
<span class="badge bg-secondary">
|
|
||||||
{{ page_obj.paginator.count }} {% trans "sources" %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
{% if sources %}
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-bordered" id="dataTable" width="100%" cellspacing="0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Name" %}</th>
|
|
||||||
<th>{% trans "Type" %}</th>
|
|
||||||
<th>{% trans "Status" %}</th>
|
|
||||||
<th>{% trans "API Key" %}</th>
|
|
||||||
<th>{% trans "Created By" %}</th>
|
|
||||||
<th>{% trans "Created At" %}</th>
|
|
||||||
<th>{% trans "Actions" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for source in sources %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="me-3">
|
|
||||||
<div class="icon-circle bg-primary text-white">
|
|
||||||
<i class="fas fa-server"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="fw-bold">{{ source.name }}</div>
|
|
||||||
<small class="text-muted">{{ source.description|truncatewords:10 }}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge bg-info">{{ source.source_type }}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if source.is_active %}
|
|
||||||
<span class="badge bg-success">
|
|
||||||
<i class="fas fa-check-circle me-1"></i>
|
|
||||||
{% trans "Active" %}
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-danger">
|
|
||||||
<i class="fas fa-times-circle me-1"></i>
|
|
||||||
{% trans "Inactive" %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if source.api_key %}
|
|
||||||
<code class="text-muted">{{ source.api_key|slice:":8" }}...</code>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">
|
|
||||||
<i class="fas fa-key me-1"></i>
|
|
||||||
{% trans "Not generated" %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{ source.created_by }}</td>
|
|
||||||
<td>{{ source.created_at|date:"M d, Y" }}</td>
|
|
||||||
<td>
|
|
||||||
<div class="btn-group" role="group">
|
|
||||||
<a href="{% url 'source_detail' source.pk %}"
|
|
||||||
class="btn btn-sm btn-outline-primary"
|
|
||||||
title="{% trans 'View Details' %}">
|
|
||||||
<i class="fas fa-eye"></i>
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'source_update' source.pk %}"
|
|
||||||
class="btn btn-sm btn-outline-secondary"
|
|
||||||
title="{% trans 'Edit' %}">
|
|
||||||
<i class="fas fa-edit"></i>
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'source_delete' source.pk %}"
|
|
||||||
class="btn btn-sm btn-outline-danger"
|
|
||||||
title="{% trans 'Delete' %}">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pagination -->
|
|
||||||
{% if is_paginated %}
|
|
||||||
<nav aria-label="Page navigation">
|
|
||||||
<ul class="pagination justify-content-center">
|
|
||||||
{% if page_obj.has_previous %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page=1{% if search_query %}&search={{ search_query }}{% endif %}"
|
|
||||||
aria-label="{% trans 'First' %}">
|
|
||||||
<span aria-hidden="true">««</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}"
|
|
||||||
aria-label="{% trans 'Previous' %}">
|
|
||||||
<span aria-hidden="true">«</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% for num in page_obj.paginator.page_range %}
|
<!-- Results Summary -->
|
||||||
{% if page_obj.number == num %}
|
{% if search_query %}
|
||||||
<li class="page-item active">
|
<div class="alert alert-info">
|
||||||
<span class="page-link">{{ num }}</span>
|
Found {{ total_sources }} source{{ total_sources|pluralize }} matching "{{ search_query }}"
|
||||||
</li>
|
|
||||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ num }}{% if search_query %}&search={{ search_query }}{% endif %}">{{ num }}</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if page_obj.has_next %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}"
|
|
||||||
aria-label="{% trans 'Next' %}">
|
|
||||||
<span aria-hidden="true">»</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if search_query %}&search={{ search_query }}{% endif %}"
|
|
||||||
aria-label="{% trans 'Last' %}">
|
|
||||||
<span aria-hidden="true">»»</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
<div class="text-center py-5">
|
|
||||||
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
|
|
||||||
<h5 class="text-muted">{% trans "No sources found" %}</h5>
|
|
||||||
<p class="text-muted mb-4">
|
|
||||||
{% trans "Get started by creating your first data source." %}
|
|
||||||
</p>
|
|
||||||
<a href="{% url 'source_create' %}" class="btn btn-primary">
|
|
||||||
<i class="fas fa-plus me-2"></i>
|
|
||||||
{% trans "Create Source" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Sources Table -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
{% if page_obj %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>API Key</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for source in page_obj %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'source_detail' source.pk %}" class="text-decoration-none">
|
||||||
|
<strong>{{ source.name }}</strong>
|
||||||
|
</a>
|
||||||
|
{% if source.description %}
|
||||||
|
<br><small class="text-muted">{{ source.description|truncatechars:50 }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-info">{{ source.get_source_type_display }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if source.is_active %}
|
||||||
|
<span class="badge bg-success">Active</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Inactive</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code class="small">{{ source.api_key|truncatechars:20 }}</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<small class="text-muted">{{ source.created_at|date:"M d, Y" }}</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{% url 'source_detail' source.pk %}"
|
||||||
|
class="btn btn-sm btn-outline-primary" title="View">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'source_update' source.pk %}"
|
||||||
|
class="btn btn-sm btn-outline-secondary" title="Edit">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-warning"
|
||||||
|
hx-post="{% url 'toggle_source_status' source.pk %}"
|
||||||
|
hx-confirm="Are you sure you want to {{ source.is_active|yesno:'deactivate,activate' }} this source?"
|
||||||
|
title="{{ source.is_active|yesno:'Deactivate,Activate' }}">
|
||||||
|
<i class="fas fa-{{ source.is_active|yesno:'pause,play' }}"></i>
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'source_delete' source.pk %}"
|
||||||
|
class="btn btn-sm btn-outline-danger" title="Delete">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
{% if page_obj.has_other_pages %}
|
||||||
|
<nav aria-label="Sources pagination">
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page=1{% if search_query %}&q={{ search_query }}{% endif %}">
|
||||||
|
<i class="fas fa-angle-double-left"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}">
|
||||||
|
<i class="fas fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for num in page_obj.paginator.page_range %}
|
||||||
|
{% if page_obj.number == num %}
|
||||||
|
<li class="page-item active">
|
||||||
|
<span class="page-link">{{ num }}</span>
|
||||||
|
</li>
|
||||||
|
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ num }}{% if search_query %}&q={{ search_query }}{% endif %}">{{ num }}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}">
|
||||||
|
<i class="fas fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if search_query %}&q={{ search_query }}{% endif %}">
|
||||||
|
<i class="fas fa-angle-double-right"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-database fa-3x text-muted mb-3"></i>
|
||||||
|
<h5 class="text-muted">No sources found</h5>
|
||||||
|
<p class="text-muted">
|
||||||
|
{% if search_query %}
|
||||||
|
No sources match your search criteria.
|
||||||
|
{% else %}
|
||||||
|
Get started by creating your first source.
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<a href="{% url 'source_create' %}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Create Source
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -202,8 +189,14 @@
|
|||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
// Auto-refresh after status toggle
|
||||||
// Add any DataTables initialization or other JavaScript here
|
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
||||||
|
if (evt.detail.successful) {
|
||||||
|
// Reload the page after a short delay to show updated status
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user