Merge pull request 'small fix' (#115) from frontend into main
Reviewed-on: #115
This commit is contained in:
commit
85170c8dfa
@ -1588,6 +1588,9 @@ class MessageForm(forms.ModelForm):
|
||||
# Validate messaging permissions
|
||||
if self.user and cleaned_data.get("recipient"):
|
||||
self._validate_messaging_permissions(cleaned_data)
|
||||
|
||||
if self.cleaned_data.get('recipient')==self.user:
|
||||
raise forms.ValidationError(_("You cannot message yourself"))
|
||||
|
||||
return cleaned_data
|
||||
|
||||
@ -2015,8 +2018,13 @@ class SettingsForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Settings
|
||||
fields = ['key', 'value']
|
||||
fields = ['name','key', 'value']
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={
|
||||
'class': 'form-control mb-3',
|
||||
'placeholder': 'e.g., Zoom',
|
||||
'required': True
|
||||
}),
|
||||
'key': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Enter setting key',
|
||||
|
||||
18
recruitment/migrations/0004_settings_name.py
Normal file
18
recruitment/migrations/0004_settings_name.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2025-12-15 14:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0003_interview_interview_result_interview_result_comments'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='settings',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, help_text="A human-readable name (e.g., 'Zoom')", max_length=100, null=True, verbose_name='Friendly Name'),
|
||||
),
|
||||
]
|
||||
@ -2593,6 +2593,12 @@ class Document(Base):
|
||||
|
||||
class Settings(Base):
|
||||
"""Model to store key-value pair settings"""
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name=_("Friendly Name"),
|
||||
help_text=_("A human-readable name (e.g., 'Zoom')"),
|
||||
null=True, blank=True
|
||||
)
|
||||
|
||||
key = models.CharField(
|
||||
max_length=100,
|
||||
@ -2604,6 +2610,7 @@ class Settings(Base):
|
||||
verbose_name=_("Setting Value"),
|
||||
help_text=_("Value for the setting"),
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Setting")
|
||||
|
||||
@ -51,7 +51,7 @@ def get_setting(key, default=None):
|
||||
return default
|
||||
|
||||
|
||||
def set_setting(key, value):
|
||||
def set_setting(key, value,name):
|
||||
"""
|
||||
Set a setting value in the database
|
||||
|
||||
@ -62,8 +62,9 @@ def set_setting(key, value):
|
||||
Returns:
|
||||
Settings: The created or updated setting object
|
||||
"""
|
||||
print(key,value)
|
||||
setting, created = Settings.objects.update_or_create(
|
||||
key=key, defaults={"value": str(value)}
|
||||
key=key, value=value,name=name
|
||||
)
|
||||
return setting
|
||||
|
||||
@ -262,26 +263,35 @@ def initialize_default_settings():
|
||||
"""
|
||||
# Zoom settings
|
||||
zoom_settings = {
|
||||
"ZOOM_ACCOUNT_ID": getattr(settings, "ZOOM_ACCOUNT_ID", ""),
|
||||
"ZOOM_CLIENT_ID": getattr(settings, "ZOOM_CLIENT_ID", ""),
|
||||
"ZOOM_CLIENT_SECRET": getattr(settings, "ZOOM_CLIENT_SECRET", ""),
|
||||
"ZOOM_WEBHOOK_API_KEY": getattr(settings, "ZOOM_WEBHOOK_API_KEY", ""),
|
||||
"SECRET_TOKEN": getattr(settings, "SECRET_TOKEN", ""),
|
||||
"ZOOM_ACCOUNT_ID": "",
|
||||
"ZOOM_CLIENT_ID": "",
|
||||
"ZOOM_CLIENT_SECRET": "",
|
||||
"ZOOM_WEBHOOK_API_KEY": "",
|
||||
"SECRET_TOKEN": "",
|
||||
}
|
||||
|
||||
# LinkedIn settings
|
||||
linkedin_settings = {
|
||||
"LINKEDIN_CLIENT_ID": getattr(settings, "LINKEDIN_CLIENT_ID", ""),
|
||||
"LINKEDIN_CLIENT_SECRET": getattr(settings, "LINKEDIN_CLIENT_SECRET", ""),
|
||||
"LINKEDIN_REDIRECT_URI": getattr(settings, "LINKEDIN_REDIRECT_URI", ""),
|
||||
"LINKEDIN_CLIENT_ID": "",
|
||||
"LINKEDIN_CLIENT_SECRET": "",
|
||||
"LINKEDIN_REDIRECT_URI": "",
|
||||
}
|
||||
|
||||
# Create settings if they don't exist
|
||||
all_settings = {**zoom_settings, **linkedin_settings}
|
||||
openrouter_settings = {
|
||||
"OPENROUTER_API_URL":"",
|
||||
"OPENROUTER_API_KEY":"",
|
||||
"OPENROUTER_MODEL":""
|
||||
}
|
||||
|
||||
|
||||
|
||||
# Create settings if they don't exist
|
||||
all_settings = {**zoom_settings, **linkedin_settings,**openrouter_settings}
|
||||
names=['ZOOM','ZOOM','ZOOM','ZOOM','ZOOM','LINKEDIN','LINKEDIN','LINKEDIN','OPENROUTER','OPENROUTER','OPENROUTER']
|
||||
i=0
|
||||
for key, value in all_settings.items():
|
||||
if value: # Only set if value exists
|
||||
set_setting(key, value)
|
||||
set_setting(key, value,names[i])
|
||||
i=i+1
|
||||
|
||||
|
||||
#####################################
|
||||
|
||||
@ -4290,12 +4290,10 @@ def update_interview_result(request,slug):
|
||||
form = InterviewResultForm(request.POST, instance=interview)
|
||||
|
||||
if form.is_valid():
|
||||
|
||||
interview.save(update_fields=['interview_result', 'result_comments'])
|
||||
|
||||
|
||||
form.save() # Saves form data
|
||||
|
||||
messages.success(request, _("Interview cancelled successfully."))
|
||||
messages.success(request, _(f"Interview result updated successfully to {interview.interview_result}."))
|
||||
return redirect("interview_detail", slug=schedule.slug)
|
||||
else:
|
||||
error_list = [
|
||||
|
||||
@ -523,11 +523,34 @@
|
||||
</button>
|
||||
|
||||
{% if schedule.status == 'completed' %}
|
||||
<button type="button" class="btn btn-outline-success btn-sm"
|
||||
<button type="button" class="btn btn-outline-success btn-sm w-100"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#resultModal">
|
||||
<i class="fas fa-check-circle me-1"></i> {% trans "Update Result" %}
|
||||
</button>
|
||||
<div class="w-100 text-center">
|
||||
{% if interview.interview_result %}
|
||||
{% trans 'Interview Result : ' %}
|
||||
{% if interview.interview_result == 'passed' %}
|
||||
<span class="badge bg-success text-white p-1">
|
||||
<i class="fas fa-check-circle me-1"></i> {{ interview.interview_result }}
|
||||
</span>
|
||||
{% elif interview.interview_result == 'failed' %}
|
||||
<span class="badge bg-danger text-white p-1 fs-5">
|
||||
<i class="fas fa-times-circle me-1"></i> {{ interview.interview_result }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-info text-dark p-1">
|
||||
<i class="fas fa-info-circle me-1"></i> {{ interview.interview_result }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="badge rounded-pill bg-secondary text-white">
|
||||
{% trans "No Result Yet" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,82 +1,153 @@
|
||||
{% extends "base.html" %}
|
||||
{% load widget_tweaks %}
|
||||
{% block title %}Setting Details{% endblock %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Setting Details" %} | {{ setting.key }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'settings_list' %}" class="text-decoration-none text-muted">
|
||||
<i class="fas fa-sliders-h me-1"></i> {% trans "Integrations" %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" style="color: #F43B5E; font-weight: 600;">{{ setting.key }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1 text-dark fw-bold">{% trans "Setting Details" %}</h1>
|
||||
<p class="text-muted small mb-0">{{ setting.name|default:setting.key }}</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'settings_update' setting.pk %}" class="btn btn-main-action shadow-sm">
|
||||
<i class="fas fa-edit me-1"></i> {% trans "Edit" %}
|
||||
</a>
|
||||
<a href="{% url 'settings_list' %}" class="btn btn-outline-secondary shadow-sm">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<!-- Breadcrumb Navigation -->
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'settings_list' %}" class="text-decoration-none text-secondary">
|
||||
<i class="fas fa-cog me-1"></i> Settings
|
||||
</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page"
|
||||
style="color: #F43B5E; font-weight: 600;">{{ setting.key }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0 text-primary-theme">
|
||||
<i class="fas fa-cog me-2"></i>
|
||||
Setting Details
|
||||
</h1>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'settings_update' pk=setting.pk %}" class="btn btn-main-action">
|
||||
<i class="fas fa-edit me-1"></i> Edit Setting
|
||||
</a>
|
||||
<a href="{% url 'settings_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i> Back to List
|
||||
</a>
|
||||
<div class="col-lg-8">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-bottom-0 pt-4 px-4">
|
||||
<h5 class="card-title text-primary-theme fw-bold mb-0">{% trans "Configuration" %}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="mb-4">
|
||||
<label class="text-muted small fw-bold text-uppercase tracking-wider mb-2">{% trans "Internal Key" %}</label>
|
||||
<div class="p-2 bg-light rounded border d-flex justify-content-between align-items-center">
|
||||
<code class="text-primary-theme fs-5">{{ setting.key }}</code>
|
||||
<button class="btn btn-link btn-sm text-secondary" onclick="copyToClipboard('{{ setting.key|escapejs }}', this)">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Setting Details Card -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
<div class="mb-0">
|
||||
<label class="text-muted small fw-bold text-uppercase tracking-wider mb-2">{% trans "Sensitive Value" %}</label>
|
||||
<div class="p-3 bg-light rounded border">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="font-monospace text-break" id="revealContainer" style="word-break: break-all;">
|
||||
<span class="reveal-toggle text-muted fs-5"
|
||||
data-full-value="{{ setting.value }}"
|
||||
data-masked="••••••••••••••••••••••••"
|
||||
style="user-select: none;">
|
||||
••••••••••••••••••••••••
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-light btn-sm border" onclick="toggleSecret(document.querySelector('.reveal-toggle'))" title="{% trans 'Show/Hide' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-light btn-sm border" onclick="copyToClipboard('{{ setting.value|escapejs }}', this)" title="{% trans 'Copy' %}">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-primary-theme"><strong>Key:</strong></h6>
|
||||
<p><code class="text-primary-theme">{{ setting.key }}</code></p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-primary-theme"><strong>Value:</strong></h6>
|
||||
<p>{{ setting.value|default:"-" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-4">
|
||||
<h6 class="text-primary-theme"><strong>Created:</strong></h6>
|
||||
<p>{{ setting.created_at|date:"Y-m-d H:i" }}</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h6 class="text-primary-theme"><strong>Last Updated:</strong></h6>
|
||||
<p>{{ setting.updated_at|date:"Y-m-d H:i" }}</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h6 class="text-primary-theme"><strong>Updated By:</strong></h6>
|
||||
<p>{{ setting.updated_by.get_full_name|default:"System" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h6 class="text-dark fw-bold mb-3">{% trans "Metadata" %}</h6>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="text-muted small d-block">{% trans "Category" %}</label>
|
||||
<span class="badge rounded-pill bg-light text-dark border">{{ setting.get_category_display }}</span>
|
||||
</div>
|
||||
|
||||
<hr class="text-light">
|
||||
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="flex-shrink-0 bg-light p-2 rounded-circle me-3">
|
||||
<i class="fas fa-calendar-alt text-muted"></i>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-muted small d-block">{% trans "Created" %}</label>
|
||||
<span class="text-dark small">{{ setting.created_at|date:"M d, Y H:i" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="flex-shrink-0 bg-light p-2 rounded-circle me-3">
|
||||
<i class="fas fa-history text-muted"></i>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-muted small d-block">{% trans "Last Updated" %}</label>
|
||||
<span class="text-dark small">{{ setting.updated_at|date:"M d, Y H:i" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0 bg-light p-2 rounded-circle me-3">
|
||||
<i class="fas fa-user text-muted"></i>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-muted small d-block">{% trans "Modified By" %}</label>
|
||||
<span class="text-dark small">{{ setting.updated_by.get_full_name|default:"System" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
function toggleSecret(element) {
|
||||
const fullValue = element.getAttribute('data-full-value');
|
||||
const maskedValue = element.getAttribute('data-masked');
|
||||
const icon = event.currentTarget.querySelector('i');
|
||||
|
||||
if (element.textContent.trim() === maskedValue) {
|
||||
element.textContent = fullValue;
|
||||
element.classList.replace('text-muted', 'text-dark');
|
||||
if(icon) icon.classList.replace('fa-eye', 'fa-eye-slash');
|
||||
} else {
|
||||
element.textContent = maskedValue;
|
||||
element.classList.replace('text-dark', 'text-muted');
|
||||
if(icon) icon.classList.replace('fa-eye-slash', 'fa-eye');
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard(text, btn) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
const icon = btn.querySelector('i');
|
||||
const originalClass = icon.className;
|
||||
icon.className = 'fas fa-check text-success';
|
||||
setTimeout(() => { icon.className = originalClass; }, 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -1,188 +1,175 @@
|
||||
{% extends "base.html" %}
|
||||
{% load widget_tweaks %}
|
||||
{% load i18n %}
|
||||
{% block title %}Settings{% endblock %}
|
||||
|
||||
{% block title %}{% trans "Integration Settings" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<nav aria-label="breadcrumb">
|
||||
<div class="container-fluid py-4">
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'settings' %}" class="text-decoration-none text-secondary">{% trans "Settings" %}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page" style="
|
||||
color: #F43B5E; /* Rosy Accent Color */
|
||||
font-weight: 600;
|
||||
">{% trans "Integration Settings" %}</li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'settings' %}" class="text-decoration-none text-muted">{% trans "Settings" %}</a></li>
|
||||
<li class="breadcrumb-item active" style="color: #F43B5E; font-weight: 600;">{% trans "Integrations" %}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0 text-primary-theme">
|
||||
<i class="fas fa-cog me-2"></i>
|
||||
{% trans "Integration Settings" %}
|
||||
</h1>
|
||||
<a href="{% url 'settings_create' %}" class="btn btn-main-action">
|
||||
<i class="fas fa-plus me-2"></i>
|
||||
Create New Setting
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filters -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<form method="get" class="row g-3">
|
||||
<div class="col-md-8">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<i class="fas fa-search"></i>
|
||||
</span>
|
||||
<input type="text" class="form-control" name="q"
|
||||
placeholder="Search settings..." 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 'settings_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times"></i> Clear
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1 text-dark fw-bold">
|
||||
<i class="fas fa-sliders-h me-2 text-primary-theme"></i>
|
||||
{% trans "Integration Settings" %}
|
||||
</h1>
|
||||
<p class="text-muted small mb-0">{% trans "Manage API keys, Webhooks, and external service configurations." %}</p>
|
||||
</div>
|
||||
<a href="{% url 'settings_create' %}" class="btn btn-main-action shadow-sm">
|
||||
<i class="fas fa-plus-circle me-2"></i>{% trans "Add New Setting" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Results Summary -->
|
||||
{% if search_query %}
|
||||
<div class="alert alert-info">
|
||||
Found {{ page_obj.paginator.count }} setting{{ page_obj.paginator.count|pluralize }} matching "{{ search_query }}"
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Settings 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 style="width: 45%;">Key</th>
|
||||
<th style="width: 45%;">Value</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for setting in page_obj %}
|
||||
<tr>
|
||||
<td>
|
||||
<code class="text-primary-theme">{{ setting.key }}</code>
|
||||
</td>
|
||||
<td>{{ setting.value|truncatechars:50 }}</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{% url 'settings_detail' pk=setting.pk %}"
|
||||
class="btn btn-sm btn-outline-primary" title="View Details">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'settings_update' pk=setting.pk %}"
|
||||
class="btn btn-sm btn-outline-secondary" title="Edit Setting">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
{% comment %} <form method="post" action="{% url 'settings_delete' pk=setting.pk %}"
|
||||
onsubmit="return confirm('Are you sure you want to delete this setting?');"
|
||||
style="display: inline;">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete Setting">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form> {% endcomment %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if page_obj.has_other_pages %}
|
||||
<nav aria-label="Settings 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-cog fa-3x text-muted mb-3"></i>
|
||||
<h5 class="text-muted">No settings found</h5>
|
||||
<p class="text-muted">
|
||||
{% if search_query %}
|
||||
No settings match your search criteria "{{ search_query }}".
|
||||
{% else %}
|
||||
Get started by creating your first setting.
|
||||
{% endif %}
|
||||
</p>
|
||||
<a href="{% url 'settings_create' %}" class="btn btn-main-action">
|
||||
<i class="fas fa-plus"></i> Create Setting
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<small class="text-muted">
|
||||
{% if page_obj %}
|
||||
Showing {{ page_obj.start_index }}-{{ page_obj.end_index }} of {{ page_obj.paginator.count }} settings
|
||||
{% if search_query %}
|
||||
(filtered by: "{{ search_query }}")
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</small>
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-3">
|
||||
<form method="get" class="row g-2">
|
||||
<div class="col-md-9">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-white border-end-0"><i class="fas fa-search text-muted"></i></span>
|
||||
<input type="text" class="form-control border-start-0 ps-0" name="q"
|
||||
placeholder="{% trans 'Search by name, key, or category...' %}" value="{{ search_query }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 d-grid gap-2 d-md-flex">
|
||||
<button type="submit" class="btn btn-main-action px-4">{% trans "Search" %}</button>
|
||||
{% if search_query %}
|
||||
<a href="{% url 'settings_list' %}" class="btn btn-light border">{% trans "Clear" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if page_obj %}
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-4" style="width: 40%;">{% trans "Setting Name & Key" %}</th>
|
||||
<th style="width: 40%;">{% trans "Value" %}</th>
|
||||
<th class="text-end pe-4">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for setting in page_obj %}
|
||||
<tr>
|
||||
<td class="ps-4">
|
||||
<div class="fw-bold text-dark">{{ setting.name|default:setting.key }}</div>
|
||||
<code class="small text-muted" style="font-size: 0.75rem;">{{ setting.key }}</code>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="text-truncate" style="max-width: 300px; min-width: 150px;">
|
||||
<span class="font-monospace small text-muted reveal-toggle"
|
||||
data-full-value="{{ setting.value }}"
|
||||
data-masked="••••••••••••••••"
|
||||
onclick="toggleSecret(this)"
|
||||
style="cursor: pointer; user-select: none;">
|
||||
••••••••••••••••
|
||||
</span>
|
||||
</div>
|
||||
<div class="ms-2 d-flex">
|
||||
<button type="button" class="btn btn-link btn-sm p-1 text-decoration-none text-secondary"
|
||||
onclick="toggleSecret(this.parentElement.previousElementSibling.firstElementChild)"
|
||||
title="{% trans 'Show/Hide' %}">
|
||||
<i class="fas fa-eye fa-xs"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-link btn-sm p-1 text-decoration-none text-secondary"
|
||||
onclick="copyToClipboard('{{ setting.value|escapejs }}', this)"
|
||||
title="{% trans 'Copy to Clipboard' %}">
|
||||
<i class="fas fa-copy fa-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end pe-4">
|
||||
<div class="btn-group shadow-sm">
|
||||
<a href="{% url 'settings_detail' setting.pk %}" class="btn btn-white btn-sm border" title="{% trans 'View' %}">
|
||||
<i class="fas fa-eye text-primary"></i>
|
||||
</a>
|
||||
<a href="{% url 'settings_update' setting.pk %}" class="btn btn-white btn-sm border" title="{% trans 'Edit' %}">
|
||||
<i class="fas fa-edit text-secondary"></i>
|
||||
</a>
|
||||
{% comment %} <button type="button" class="btn btn-white btn-sm border"
|
||||
onclick="deleteDocument(this, {{ setting.pk }})" title="{% trans 'Delete' %}">
|
||||
<i class="fas fa-trash-alt text-danger"></i>
|
||||
</button> {% endcomment %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if page_obj.has_other_pages %}
|
||||
<div class="card-footer bg-white border-top-0 py-3">
|
||||
{% include "includes/paginator.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card border-0 shadow-sm py-5">
|
||||
<div class="card-body text-center">
|
||||
<div class="mb-4">
|
||||
<i class="fas fa-tools fa-4x text-light"></i>
|
||||
</div>
|
||||
<h4 class="text-dark">{% trans "No settings found" %}</h4>
|
||||
<p class="text-muted mx-auto" style="max-width: 400px;">
|
||||
{% if search_query %}
|
||||
{% blocktrans %}We couldn't find any settings matching "{{ search_query }}". Please try a different term.{% endblocktrans %}
|
||||
{% else %}
|
||||
{% trans "You haven't added any integration settings yet. Get started by adding your first API key or configuration." %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<a href="{% url 'settings_create' %}" class="btn btn-main-action mt-2">
|
||||
<i class="fas fa-plus me-2"></i>{% trans "Create First Setting" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
.reveal-toggle { transition: color 0.2s; }
|
||||
.reveal-toggle:hover { color: #212529 !important; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
function toggleSecret(element) {
|
||||
const fullValue = element.getAttribute('data-full-value');
|
||||
const maskedValue = element.getAttribute('data-masked');
|
||||
// Find the icon inside the button next to the div container
|
||||
const icon = element.parentElement.parentElement.querySelector('.fa-eye, .fa-eye-slash');
|
||||
|
||||
if (element.textContent.trim() === maskedValue) {
|
||||
element.textContent = fullValue;
|
||||
element.classList.replace('text-muted', 'text-dark');
|
||||
if(icon) icon.classList.replace('fa-eye', 'fa-eye-slash');
|
||||
} else {
|
||||
element.textContent = maskedValue;
|
||||
element.classList.replace('text-dark', 'text-muted');
|
||||
if(icon) icon.classList.replace('fa-eye-slash', 'fa-eye');
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard(text, btn) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
const icon = btn.querySelector('i');
|
||||
const originalClass = icon.className;
|
||||
icon.className = 'fas fa-check text-success fa-xs';
|
||||
setTimeout(() => { icon.className = originalClass; }, 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Loading…
x
Reference in New Issue
Block a user