Compare commits
No commits in common. "c369b8bedcf1c608ab1231d29fe5c08cd0671d6d" and "eb122da0374e34fed5b76d8792ee99861c6a8d29" have entirely different histories.
c369b8bedc
...
eb122da037
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -31,70 +31,7 @@ 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):
|
||||||
"""Simple form for creating and editing sources"""
|
"""Form for creating and editing sources with API key generation"""
|
||||||
|
|
||||||
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(
|
||||||
|
|||||||
@ -140,8 +140,7 @@ 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/<int:pk>/generate-keys/', views_source.generate_api_keys_view, name='generate_api_keys'),
|
path('sources/api/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,8 +42,7 @@ 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
|
||||||
@ -81,8 +80,7 @@ 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 (
|
||||||
@ -3802,170 +3800,3 @@ 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,90 +182,24 @@ 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, pk):
|
def generate_api_keys_view(request):
|
||||||
"""Generate new API keys for a specific source"""
|
"""API endpoint to generate API keys"""
|
||||||
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':
|
||||||
# Generate new API keys
|
api_key = generate_api_key()
|
||||||
new_api_key = generate_api_key()
|
api_secret = generate_api_secret()
|
||||||
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': new_api_key,
|
'api_key': api_key,
|
||||||
'api_secret': new_api_secret,
|
'api_secret': api_secret,
|
||||||
'message': 'API keys regenerated successfully'
|
'message': 'API keys generated 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,7 +15,6 @@
|
|||||||
--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 */
|
||||||
@ -54,17 +53,6 @@
|
|||||||
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;
|
||||||
@ -83,41 +71,6 @@
|
|||||||
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 %}
|
||||||
|
|
||||||
@ -162,7 +115,7 @@
|
|||||||
value="{{ search_query }}">
|
value="{{ search_query }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-1">
|
<div class="col-md-2">
|
||||||
<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>
|
||||||
@ -172,168 +125,78 @@
|
|||||||
|
|
||||||
<!-- Agencies List -->
|
<!-- Agencies List -->
|
||||||
{% if page_obj %}
|
{% if page_obj %}
|
||||||
<div id="agency-list">
|
<div class="row">
|
||||||
{% include "includes/_list_view_switcher.html" with list_id="agency-list" %}
|
{% for agency in page_obj %}
|
||||||
|
<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>
|
||||||
|
|
||||||
<!-- Table View -->
|
<!-- Contact Information -->
|
||||||
<div class="table-view">
|
{% if agency.contact_person %}
|
||||||
<div class="table-responsive">
|
<p class="card-text mb-2">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<i class="fas fa-user text-muted me-2"></i>
|
||||||
<thead>
|
<strong>{% trans "Contact:" %}</strong> {{ agency.contact_person }}
|
||||||
<tr>
|
</p>
|
||||||
<th scope="col">{% trans "Agency Name" %}</th>
|
{% endif %}
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- Card View -->
|
{% if agency.phone %}
|
||||||
<div class="card-view row g-4">
|
<p class="card-text mb-2">
|
||||||
{% for agency in page_obj %}
|
<i class="fas fa-phone text-muted me-2"></i>
|
||||||
<div class="col-lg-4 col-md-6 mb-4">
|
{{ agency.phone }}
|
||||||
<div class="card kaauh-card agency-card h-100">
|
</p>
|
||||||
<div class="card-body">
|
{% endif %}
|
||||||
<!-- Agency Header -->
|
|
||||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
{% if agency.country %}
|
||||||
<h5 class="card-title mb-0" style="color: var(--kaauh-teal-dark);">
|
<p class="card-text mb-2">
|
||||||
{{ agency.name }}
|
<i class="fas fa-globe text-muted me-2"></i>
|
||||||
</h5>
|
{{ agency.get_country_display }}
|
||||||
{% if agency.email %}
|
</p>
|
||||||
<a href="mailto:{{ agency.email }}" class="text-muted" title="{{ agency.email }}">
|
{% endif %}
|
||||||
<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>
|
||||||
<!-- Contact Information -->
|
<span class="stats-badge">
|
||||||
{% if agency.contact_person %}
|
{% trans "Created" %} {{ agency.created_at|date:"M d, Y" }}
|
||||||
<p class="card-text mb-2">
|
</span>
|
||||||
<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>
|
||||||
{% endfor %}
|
</div>
|
||||||
</div>
|
{% endfor %}
|
||||||
</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">
|
||||||
@ -615,9 +615,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">
|
||||||
@ -631,7 +631,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" %}
|
||||||
@ -640,22 +640,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>
|
||||||
@ -668,18 +668,17 @@
|
|||||||
{% 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>`">
|
||||||
<i class="fas fa-redo-alt me-1"></i>
|
{% trans "Retry AI Scoring" %}
|
||||||
{% trans "Unable to Parse Resume , click to retry" %}
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -295,9 +295,11 @@
|
|||||||
<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,105 +1,106 @@
|
|||||||
{% extends "base.html" %}
|
{% extends 'base.html' %}
|
||||||
{% load static %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}{{ title }}{% endblock %}
|
{% block title %}
|
||||||
|
{% trans "Delete Source" %} | {% trans "Recruitment System" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid py-4">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="card shadow">
|
||||||
<h1 class="h3 mb-0">{{ title }}</h1>
|
<div class="card-header py-3">
|
||||||
<a href="{% url 'source_detail' source.slug %}" class="btn btn-outline-secondary">
|
<h6 class="m-0 font-weight-bold text-danger">
|
||||||
<i class="fas fa-arrow-left"></i> Back to Source
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||||
</a>
|
{% trans "Delete Source" %}
|
||||||
</div>
|
</h6>
|
||||||
|
</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>
|
||||||
|
|
||||||
<div class="row">
|
<!-- Source Information -->
|
||||||
<div class="col-md-8">
|
<div class="row mb-4">
|
||||||
<div class="card">
|
<div class="col-12">
|
||||||
<div class="card-body">
|
<h5>{% trans "Source Information" %}</h5>
|
||||||
<div class="alert alert-warning d-flex align-items-center">
|
<div class="table-responsive">
|
||||||
<i class="fas fa-exclamation-triangle fa-2x me-3"></i>
|
<table class="table table-borderless">
|
||||||
<div>
|
<tr>
|
||||||
<strong>Warning:</strong> This action cannot be undone.
|
<th width="30%">{% trans "Name" %}</th>
|
||||||
Deleting this source will also remove all associated integration logs and API credentials.
|
<td>{{ source.name }}</td>
|
||||||
</div>
|
</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 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">
|
|
||||||
<i class="fas fa-trash"></i> Delete Source
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-4">
|
<!-- Warning Messages -->
|
||||||
<div class="card">
|
<div class="row mb-4">
|
||||||
<div class="card-header">
|
<div class="col-12">
|
||||||
<h6 class="mb-0">Impact Summary</h6>
|
<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>
|
||||||
<div class="card-body">
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label text-muted">Integration Logs</label>
|
<!-- Action Buttons -->
|
||||||
<div class="h5 mb-0 text-danger">
|
<div class="row">
|
||||||
{{ source.integration_logs.count }} will be deleted
|
<div class="col-12">
|
||||||
</div>
|
<div class="d-flex justify-content-between">
|
||||||
</div>
|
<div>
|
||||||
<div class="mb-3">
|
<a href="{% url 'source_detail' source.pk %}" class="btn btn-secondary">
|
||||||
<label class="form-label text-muted">API Credentials</label>
|
<i class="fas fa-arrow-left me-2"></i>
|
||||||
<div class="h5 mb-0 text-danger">
|
{% trans "Cancel" %}
|
||||||
API Key & Secret will be permanently lost
|
</a>
|
||||||
</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>
|
||||||
|
<form method="post" action="">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-danger">
|
||||||
|
<i class="fas fa-trash me-2"></i>
|
||||||
|
{% trans "Delete Source" %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -109,3 +110,19 @@
|
|||||||
</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,287 +1,228 @@
|
|||||||
{% extends "base.html" %}
|
{% extends 'base.html' %}
|
||||||
{% load static %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}{{ source.name }} - Source Details{% endblock %}
|
{% block title %}
|
||||||
|
{{ source.name }} | {% trans "Source Details" %} | {% trans "Recruitment System" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid py-4">
|
||||||
<div class="row">
|
<!-- Page Header -->
|
||||||
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<h1 class="h3 mb-0">{{ source.name }}</h1>
|
<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">
|
<div class="btn-group">
|
||||||
<a href="{% url 'source_update' source.pk %}" class="btn btn-outline-secondary">
|
<a href="{% url 'source_update' source.pk %}" class="btn btn-primary">
|
||||||
<i class="fas fa-edit"></i> Edit
|
<i class="fas fa-edit me-2"></i>
|
||||||
|
{% trans "Edit" %}
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning">
|
<a href="{% url 'source_delete' source.pk %}" class="btn btn-danger">
|
||||||
<i class="fas fa-key"></i> Generate Keys
|
<i class="fas fa-trash me-2"></i>
|
||||||
</a>
|
{% trans "Delete" %}
|
||||||
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Source Information -->
|
<!-- Source Information Card -->
|
||||||
<div class="row">
|
<div class="row mb-4">
|
||||||
<div class="col-md-8">
|
<div class="col-12">
|
||||||
<div class="card mb-4">
|
<div class="card shadow">
|
||||||
<div class="card-header">
|
<div class="card-header py-3">
|
||||||
<h6 class="mb-0">Source Information</h6>
|
<h6 class="m-0 font-weight-bold text-primary">
|
||||||
</div>
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
<div class="card-body">
|
{% trans "Source Information" %}
|
||||||
<div class="row">
|
</h6>
|
||||||
<div class="col-md-6">
|
|
||||||
<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>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Integration Logs -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h6 class="mb-0">Recent Integration Logs</h6>
|
|
||||||
<small class="text-muted">Last 10 logs</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if integration_logs %}
|
<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">
|
||||||
|
<!-- Network Configuration -->
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">
|
||||||
|
<i class="fas fa-network-wired me-2"></i>
|
||||||
|
{% trans "Network Configuration" %}
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>{% trans "IP Address" %}:</strong>
|
||||||
|
<code>{{ source.ip_address|default:"Not specified" }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>{% trans "Trusted IPs" %}:</strong>
|
||||||
|
<div class="mt-2">
|
||||||
|
{% if source.trusted_ips %}
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
{% for ip in source.trusted_ips|split:"," %}
|
||||||
|
<span class="badge bg-secondary">{{ ip|strip }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">Not specified</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API Configuration -->
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">
|
||||||
|
<i class="fas fa-key me-2"></i>
|
||||||
|
{% trans "API Configuration" %}
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<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-sm">
|
<table class="table table-bordered">
|
||||||
<thead class="table-light">
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Timestamp</th>
|
<th>{% trans "Time" %}</th>
|
||||||
<th>Method</th>
|
<th>{% trans "Action" %}</th>
|
||||||
<th>Status</th>
|
<th>{% trans "Endpoint" %}</th>
|
||||||
<th>Response Time</th>
|
<th>{% trans "Method" %}</th>
|
||||||
<th>Details</th>
|
<th>{% trans "Status" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for log in integration_logs %}
|
{% for log in recent_logs %}
|
||||||
<tr>
|
<tr>
|
||||||
|
<td>{{ log.created_at|date:"M d, Y H:i:s" }}</td>
|
||||||
<td>
|
<td>
|
||||||
<small>{{ log.created_at|date:"M d, Y H:i:s" }}</small>
|
<span class="badge bg-info">{{ log.get_action_display }}</span>
|
||||||
|
</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.status_code >= 200 and log.status_code < 300 %}
|
{% if log.success %}
|
||||||
<span class="badge bg-success">{{ log.status_code }}</span>
|
<span class="badge bg-success">Success</span>
|
||||||
{% elif log.status_code >= 400 %}
|
|
||||||
<span class="badge bg-danger">{{ log.status_code }}</span>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-warning">{{ log.status_code }}</span>
|
<span class="badge bg-danger">Failed</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>
|
||||||
@ -289,10 +230,20 @@
|
|||||||
</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-4">
|
<div class="text-center py-5">
|
||||||
<i class="fas fa-clipboard-list fa-2x text-muted mb-3"></i>
|
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
|
||||||
<p class="text-muted">No integration logs found</p>
|
<h5 class="text-muted">{% trans "No integration logs found" %}</h5>
|
||||||
|
<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>
|
||||||
@ -300,119 +251,35 @@
|
|||||||
</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>
|
||||||
function toggleSecretVisibility() {
|
// Make function available globally
|
||||||
const secretInput = document.getElementById('api-secret');
|
window.copyToClipboard = function(elementId) {
|
||||||
const toggleIcon = document.getElementById('secret-toggle-icon');
|
const element = document.getElementById(elementId);
|
||||||
|
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');
|
||||||
|
|
||||||
if (secretInput.type === 'password') {
|
setTimeout(function() {
|
||||||
secretInput.type = 'text';
|
button.innerHTML = originalContent;
|
||||||
toggleIcon.classList.remove('fa-eye');
|
button.classList.remove('btn-success');
|
||||||
toggleIcon.classList.add('fa-eye-slash');
|
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 {
|
} else {
|
||||||
secretInput.type = 'password';
|
console.error('Element not found:', elementId);
|
||||||
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,177 +1,293 @@
|
|||||||
{% extends "base.html" %}
|
{% extends 'base.html' %}
|
||||||
{% load static %}
|
{% load i18n %}
|
||||||
{% load widget_tweaks %}
|
|
||||||
|
|
||||||
{% block title %}{{ title }}{% endblock %}
|
{% block title %}
|
||||||
|
{% 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="d-flex justify-content-between align-items-center mb-4">
|
<div class="card shadow">
|
||||||
<h1 class="h3 mb-0">{{ title }}</h1>
|
<div class="card-header py-3">
|
||||||
<a href="{% url 'source_list' %}" class="btn btn-outline-secondary">
|
<h6 class="m-0 font-weight-bold text-primary">
|
||||||
<i class="fas fa-arrow-left"></i> Back to Sources
|
{% if title %}{{ title }}{% else %}{% trans "Create New Source" %}{% endif %}
|
||||||
</a>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post" novalidate>
|
<form method="post" id="sourceForm">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
{% if form.non_field_errors %}
|
<!-- Form Messages -->
|
||||||
|
{% if form.errors %}
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
{% for error in form.non_field_errors %}
|
<h5 class="alert-heading">{% trans "Please correct the errors below:" %}</h5>
|
||||||
{{ error }}
|
{% for field in form %}
|
||||||
|
{% if field.errors %}
|
||||||
|
<p class="mb-0">{{ field.label }}: {{ field.errors|join:", " }}</p>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="row">
|
{% if messages %}
|
||||||
<div class="col-md-6">
|
{% for message in messages %}
|
||||||
<div class="mb-3">
|
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||||
<label for="{{ form.name.id_for_label }}" class="form-label">
|
{{ message }}
|
||||||
{{ form.name.label }} <span class="text-danger">*</span>
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
</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>
|
||||||
</div>
|
{% endfor %}
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.source_type.id_for_label }}" class="form-label">
|
|
||||||
{{ form.source_type.label }} <span class="text-danger">*</span>
|
|
||||||
</label>
|
|
||||||
{{ form.source_type|add_class:"form-select" }}
|
|
||||||
{% if form.source_type.errors %}
|
|
||||||
<div class="invalid-feedback d-block">
|
|
||||||
{% for error in form.source_type.errors %}
|
|
||||||
{{ error }}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="form-text">{{ form.source_type.help_text }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.description.id_for_label }}" class="form-label">
|
|
||||||
{{ form.description.label }}
|
|
||||||
</label>
|
|
||||||
{{ 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="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.ip_address.id_for_label }}" class="form-label">
|
|
||||||
{{ form.ip_address.label }}
|
|
||||||
</label>
|
|
||||||
{{ 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-6">
|
|
||||||
<div class="mb-3">
|
|
||||||
<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 class="text-end">
|
|
||||||
<a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning">
|
|
||||||
<i class="fas fa-key"></i> Generate New Keys
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
<!-- Basic Information -->
|
||||||
<a href="{% url 'source_list' %}" class="btn btn-outline-secondary">
|
<div class="row mb-4">
|
||||||
<i class="fas fa-times"></i> Cancel
|
<div class="col-12">
|
||||||
</a>
|
<h5 class="mb-3">{% trans "Basic Information" %}</h5>
|
||||||
<button type="submit" class="btn btn-primary">
|
</div>
|
||||||
<i class="fas fa-save"></i> {{ button_text }}
|
|
||||||
</button>
|
<div class="col-md-6 mb-3">
|
||||||
|
{{ form.name.label_tag }}
|
||||||
|
{{ form.name }}
|
||||||
|
{% if form.name.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ form.name.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
{% if form.name.errors %}
|
||||||
|
<div class="invalid-feedback d-block">{{ form.name.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
{{ form.source_type.label_tag }}
|
||||||
|
{{ 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>
|
||||||
|
{% if form.is_active.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ form.is_active.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API Configuration -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<h5 class="mb-3">{% trans "API Configuration" %}</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 mb-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h6 class="mb-0">{% trans "API Keys" %}</h6>
|
||||||
|
<small class="text-muted">{% trans "Generate secure API keys for external integrations" %}</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 text-end">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
{{ form.generate_keys }}
|
||||||
|
{{ form.generate_keys.label_tag }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Generated API Key -->
|
||||||
|
<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>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -181,37 +297,79 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block customJS %}
|
||||||
<script>
|
<script>
|
||||||
function toggleSecretVisibility() {
|
// Function to copy text to clipboard
|
||||||
const secretInput = document.getElementById('api-secret');
|
function copyToClipboard(elementId) {
|
||||||
const toggleIcon = document.getElementById('secret-toggle-icon');
|
const element = document.getElementById(elementId);
|
||||||
|
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');
|
||||||
|
|
||||||
if (secretInput.type === 'password') {
|
setTimeout(function() {
|
||||||
secretInput.type = 'text';
|
button.innerHTML = originalContent;
|
||||||
toggleIcon.classList.remove('fa-eye');
|
button.classList.remove('btn-success');
|
||||||
toggleIcon.classList.add('fa-eye-slash');
|
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 {
|
} else {
|
||||||
secretInput.type = 'password';
|
console.error('Element not found:', elementId);
|
||||||
toggleIcon.classList.remove('fa-eye-slash');
|
|
||||||
toggleIcon.classList.add('fa-eye');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle HTMX copy to clipboard feedback
|
// Function to generate random key
|
||||||
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
function generateRandomKey(elementId, length) {
|
||||||
if (evt.detail.successful && evt.detail.target.matches('[hx-post*="copy_to_clipboard"]')) {
|
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
||||||
const button = evt.detail.target;
|
let result = '';
|
||||||
const originalIcon = button.innerHTML;
|
for (let i = 0; i < length; i++) {
|
||||||
button.innerHTML = '<i class="fas fa-check"></i>';
|
result += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
|
||||||
button.classList.remove('btn-outline-secondary');
|
}
|
||||||
button.classList.add('btn-success');
|
console.log(elementId);
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (element) {
|
||||||
|
element.value = result;
|
||||||
|
|
||||||
setTimeout(() => {
|
// Show success animation on the generate button
|
||||||
button.innerHTML = originalIcon;
|
const button = event.target.closest('button');
|
||||||
button.classList.remove('btn-success');
|
if (button) {
|
||||||
button.classList.add('btn-outline-secondary');
|
const originalContent = button.innerHTML;
|
||||||
}, 2000);
|
button.innerHTML = '<i class="fas fa-check"></i>';
|
||||||
|
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,187 +1,200 @@
|
|||||||
{% extends "base.html" %}
|
{% extends 'base.html' %}
|
||||||
{% load static %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}Sources{% endblock %}
|
{% block title %}{% trans "Sources" %} | {% trans "Recruitment System" %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid py-4">
|
||||||
<div class="row">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<div class="col-12">
|
<h1 class="h3 mb-0 text-gray-800">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<i class="fas fa-database me-2"></i>
|
||||||
<h1 class="h3 mb-0">Sources</h1>
|
{% trans "Data Sources" %}
|
||||||
<a href="{% url 'source_create' %}" class="btn btn-primary">
|
</h1>
|
||||||
<i class="fas fa-plus"></i> Create Source
|
<a href="{% url 'source_create' %}" class="btn btn-primary">
|
||||||
</a>
|
<i class="fas fa-plus me-2"></i>
|
||||||
</div>
|
{% trans "Create New Source" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Search and Filters -->
|
<!-- Search Bar -->
|
||||||
<div class="card mb-4">
|
<div class="card shadow 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-8">
|
<div class="col-md-10">
|
||||||
<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" class="form-control" name="q"
|
<input type="text" name="search" class="form-control"
|
||||||
placeholder="Search sources..." value="{{ search_query }}">
|
placeholder="{% trans 'Search by name, type, or description...' %}"
|
||||||
</div>
|
value="{{ search_query }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
|
||||||
<button type="submit" class="btn btn-outline-primary">
|
|
||||||
<i class="fas fa-search"></i> Search
|
|
||||||
</button>
|
|
||||||
{% if search_query %}
|
|
||||||
<a href="{% url 'source_list' %}" class="btn btn-outline-secondary">
|
|
||||||
<i class="fas fa-times"></i> Clear
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-md-2">
|
||||||
|
<button type="submit" class="btn btn-secondary w-100">
|
||||||
|
<i class="fas fa-filter me-2"></i>
|
||||||
|
{% trans "Filter" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Results Summary -->
|
<!-- Sources List -->
|
||||||
{% if search_query %}
|
<div class="card shadow">
|
||||||
<div class="alert alert-info">
|
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
||||||
Found {{ total_sources }} source{{ total_sources|pluralize }} matching "{{ search_query }}"
|
<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 %}
|
||||||
|
|
||||||
|
{% 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 %}&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>
|
||||||
@ -189,14 +202,8 @@
|
|||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
// Auto-refresh after status toggle
|
$(document).ready(function() {
|
||||||
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
// Add any DataTables initialization or other JavaScript here
|
||||||
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