mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-07-06 17:14:07 +00:00
Added move clients feature
All checks were successful
Docker hub build / docker (push) Successful in 9m13s
All checks were successful
Docker hub build / docker (push) Successful in 9m13s
This commit is contained in:
39
vpn/admin.py
39
vpn/admin.py
@ -85,7 +85,7 @@ class ServerAdmin(PolymorphicParentModelAdmin):
|
|||||||
|
|
||||||
def move_clients_action(self, request, queryset):
|
def move_clients_action(self, request, queryset):
|
||||||
if queryset.count() == 0:
|
if queryset.count() == 0:
|
||||||
self.message_user(request, "Select at least one server.", level=messages.ERROR)
|
self.message_user(request, "Select al least two servers.", level=messages.ERROR)
|
||||||
return
|
return
|
||||||
|
|
||||||
selected_ids = ','.join(str(server.id) for server in queryset)
|
selected_ids = ','.join(str(server.id) for server in queryset)
|
||||||
@ -103,17 +103,24 @@ class ServerAdmin(PolymorphicParentModelAdmin):
|
|||||||
return redirect('admin:vpn_server_changelist')
|
return redirect('admin:vpn_server_changelist')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Only work with database objects, don't check server connectivity
|
||||||
servers = Server.objects.filter(id__in=server_ids)
|
servers = Server.objects.filter(id__in=server_ids)
|
||||||
all_servers = Server.objects.all()
|
all_servers = Server.objects.all()
|
||||||
|
|
||||||
# Get ACL links for selected servers with related data
|
# Get ACL links for selected servers with related data
|
||||||
|
# This is purely database operation, no server connectivity required
|
||||||
links_by_server = {}
|
links_by_server = {}
|
||||||
for server in servers:
|
for server in servers:
|
||||||
|
try:
|
||||||
# Get all ACL links for this server with user and ACL data
|
# Get all ACL links for this server with user and ACL data
|
||||||
links = ACLLink.objects.filter(
|
links = ACLLink.objects.filter(
|
||||||
acl__server=server
|
acl__server=server
|
||||||
).select_related('acl__user', 'acl__server').order_by('acl__user__username', 'comment')
|
).select_related('acl__user', 'acl__server').order_by('acl__user__username', 'comment')
|
||||||
links_by_server[server] = links
|
links_by_server[server] = links
|
||||||
|
except Exception as e:
|
||||||
|
# Log the error but continue with other servers
|
||||||
|
messages.warning(request, f"Warning: Could not load links for server {server.name}: {e}")
|
||||||
|
links_by_server[server] = []
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'title': 'Move Client Links Between Servers',
|
'title': 'Move Client Links Between Servers',
|
||||||
@ -125,11 +132,11 @@ class ServerAdmin(PolymorphicParentModelAdmin):
|
|||||||
return render(request, 'admin/move_clients.html', context)
|
return render(request, 'admin/move_clients.html', context)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messages.error(request, f"Error loading data: {e}")
|
messages.error(request, f"Database error while loading data: {e}")
|
||||||
return redirect('admin:vpn_server_changelist')
|
return redirect('admin:vpn_server_changelist')
|
||||||
|
|
||||||
elif request.method == 'POST':
|
elif request.method == 'POST':
|
||||||
# Process the transfer of ACL links
|
# Process the transfer of ACL links - purely database operations
|
||||||
try:
|
try:
|
||||||
source_server_id = request.POST.get('source_server')
|
source_server_id = request.POST.get('source_server')
|
||||||
target_server_id = request.POST.get('target_server')
|
target_server_id = request.POST.get('target_server')
|
||||||
@ -147,13 +154,19 @@ class ServerAdmin(PolymorphicParentModelAdmin):
|
|||||||
messages.error(request, "Please select at least one link to move.")
|
messages.error(request, "Please select at least one link to move.")
|
||||||
return redirect(request.get_full_path())
|
return redirect(request.get_full_path())
|
||||||
|
|
||||||
|
# Get server objects from database only
|
||||||
|
try:
|
||||||
source_server = Server.objects.get(id=source_server_id)
|
source_server = Server.objects.get(id=source_server_id)
|
||||||
target_server = Server.objects.get(id=target_server_id)
|
target_server = Server.objects.get(id=target_server_id)
|
||||||
|
except Server.DoesNotExist:
|
||||||
|
messages.error(request, "One of the selected servers was not found in database.")
|
||||||
|
return redirect('admin:vpn_server_changelist')
|
||||||
|
|
||||||
moved_count = 0
|
moved_count = 0
|
||||||
errors = []
|
errors = []
|
||||||
users_processed = set()
|
users_processed = set()
|
||||||
|
|
||||||
|
# Process each selected link - database operations only
|
||||||
for link_id in selected_link_ids:
|
for link_id in selected_link_ids:
|
||||||
try:
|
try:
|
||||||
# Get the ACL link with related ACL and user data
|
# Get the ACL link with related ACL and user data
|
||||||
@ -174,7 +187,7 @@ class ServerAdmin(PolymorphicParentModelAdmin):
|
|||||||
target_acl.save(auto_create_link=False)
|
target_acl.save(auto_create_link=False)
|
||||||
created = True
|
created = True
|
||||||
|
|
||||||
# Move the link to target ACL
|
# Move the link to target ACL - pure database operation
|
||||||
acl_link.acl = target_acl
|
acl_link.acl = target_acl
|
||||||
acl_link.save()
|
acl_link.save()
|
||||||
|
|
||||||
@ -187,16 +200,19 @@ class ServerAdmin(PolymorphicParentModelAdmin):
|
|||||||
except ACLLink.DoesNotExist:
|
except ACLLink.DoesNotExist:
|
||||||
errors.append(f"Link with ID {link_id} not found on source server")
|
errors.append(f"Link with ID {link_id} not found on source server")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append(f"Error moving link {link_id}: {e}")
|
errors.append(f"Database error moving link {link_id}: {e}")
|
||||||
|
|
||||||
# Clean up empty ACLs on source server
|
# Clean up empty ACLs on source server - database operation only
|
||||||
# (ACLs that have no more links after the move)
|
try:
|
||||||
empty_acls = ACL.objects.filter(
|
empty_acls = ACL.objects.filter(
|
||||||
server=source_server,
|
server=source_server,
|
||||||
links__isnull=True
|
links__isnull=True
|
||||||
)
|
)
|
||||||
deleted_acls_count = empty_acls.count()
|
deleted_acls_count = empty_acls.count()
|
||||||
empty_acls.delete()
|
empty_acls.delete()
|
||||||
|
except Exception as e:
|
||||||
|
messages.warning(request, f"Warning: Could not clean up empty ACLs: {e}")
|
||||||
|
deleted_acls_count = 0
|
||||||
|
|
||||||
if moved_count > 0:
|
if moved_count > 0:
|
||||||
messages.success(request,
|
messages.success(request,
|
||||||
@ -211,11 +227,8 @@ class ServerAdmin(PolymorphicParentModelAdmin):
|
|||||||
|
|
||||||
return redirect('admin:vpn_server_changelist')
|
return redirect('admin:vpn_server_changelist')
|
||||||
|
|
||||||
except Server.DoesNotExist:
|
|
||||||
messages.error(request, "One of the selected servers was not found.")
|
|
||||||
return redirect('admin:vpn_server_changelist')
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messages.error(request, f"Error during link transfer: {e}")
|
messages.error(request, f"Database error during link transfer: {e}")
|
||||||
return redirect('admin:vpn_server_changelist')
|
return redirect('admin:vpn_server_changelist')
|
||||||
|
|
||||||
@admin.display(description='User Count', ordering='user_count')
|
@admin.display(description='User Count', ordering='user_count')
|
||||||
@ -224,12 +237,16 @@ class ServerAdmin(PolymorphicParentModelAdmin):
|
|||||||
|
|
||||||
@admin.display(description='Status')
|
@admin.display(description='Status')
|
||||||
def server_status_inline(self, obj):
|
def server_status_inline(self, obj):
|
||||||
|
try:
|
||||||
status = obj.get_server_status()
|
status = obj.get_server_status()
|
||||||
if 'error' in status:
|
if 'error' in status:
|
||||||
return mark_safe(f"<span style='color: red;'>Error: {status['error']}</span>")
|
return mark_safe(f"<span style='color: red;'>Error: {status['error']}</span>")
|
||||||
import json
|
import json
|
||||||
pretty_status = ", ".join(f"{key}: {value}" for key, value in status.items())
|
pretty_status = ", ".join(f"{key}: {value}" for key, value in status.items())
|
||||||
return mark_safe(f"<pre>{pretty_status}</pre>")
|
return mark_safe(f"<pre>{pretty_status}</pre>")
|
||||||
|
except Exception as e:
|
||||||
|
# Don't let server connectivity issues break the admin interface
|
||||||
|
return mark_safe(f"<span style='color: orange;'>Status unavailable: {e}</span>")
|
||||||
server_status_inline.short_description = "Status"
|
server_status_inline.short_description = "Status"
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
|
@ -1,37 +1,332 @@
|
|||||||
{% extends "admin/base.html" %}
|
{% extends "admin/base_site.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
|
{% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
|
||||||
|
|
||||||
{% block branding %}
|
{% block content %}
|
||||||
<h1 id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('Django administration') }}</a></h1>
|
<div class="content-main">
|
||||||
{% endblock %}
|
<h1>{{ title }}</h1>
|
||||||
|
|
||||||
{% block footer %}
|
<div class="alert alert-info" style="margin: 10px 0; padding: 10px; border-radius: 4px; background-color: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460;">
|
||||||
<div id="footer" style="margin-top: 20px; padding: 10px; border-top: 1px solid #ccc; font-size: 12px; color: #666;">
|
<strong>Note:</strong> This operation only affects the database and works even if servers are unreachable.
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
Server connectivity is not required for moving links between servers.
|
||||||
<div>
|
|
||||||
<strong>OutFleet VPN Manager</strong>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: right;">
|
|
||||||
{% if VERSION_INFO %}
|
{% if messages %}
|
||||||
<div>
|
{% for message in messages %}
|
||||||
<strong>Version:</strong>
|
<div class="alert alert-{{ message.tags }}" style="margin: 10px 0; padding: 10px; border-radius: 4px;
|
||||||
<code>{{ VERSION_INFO.git_commit_short }}</code>
|
{% if message.tags == 'error' %}background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;
|
||||||
{% if VERSION_INFO.is_development %}
|
{% elif message.tags == 'success' %}background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724;
|
||||||
<span style="color: #e74c3c;">(Development)</span>
|
{% elif message.tags == 'warning' %}background-color: #fff3cd; border: 1px solid #ffeaa7; color: #856404;
|
||||||
|
{% elif message.tags == 'info' %}background-color: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460;
|
||||||
|
{% endif %}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" id="move-clients-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="form-row" style="margin-bottom: 20px;">
|
||||||
|
<div style="display: flex; gap: 20px;">
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<label for="source_server"><strong>Source Server:</strong></label>
|
||||||
|
<select id="source_server" name="source_server" style="width: 100%; padding: 5px;" onchange="updateLinksList()">
|
||||||
|
<option value="">-- Select Source Server --</option>
|
||||||
|
{% for server in servers %}
|
||||||
|
<option value="{{ server.id }}">{{ server.name }} ({{ server.server_type }})</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{% if not VERSION_INFO.is_development %}
|
|
||||||
<div style="margin-top: 2px;">
|
<div style="flex: 1;">
|
||||||
<strong>Built:</strong> {{ VERSION_INFO.build_date }}
|
<label for="target_server"><strong>Target Server:</strong></label>
|
||||||
</div>
|
<select id="target_server" name="target_server" style="width: 100%; padding: 5px;" onchange="updateSubmitButton()">
|
||||||
<div style="margin-top: 2px;">
|
<option value="">-- Select Target Server --</option>
|
||||||
<strong>Commit:</strong>
|
{% for server in all_servers %}
|
||||||
<code style="font-size: 10px;">{{ VERSION_INFO.git_commit }}</code>
|
<option value="{{ server.id }}">{{ server.name }} ({{ server.server_type }})</option>
|
||||||
</div>
|
{% endfor %}
|
||||||
{% endif %}
|
</select>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="links-list" style="margin-bottom: 20px;">
|
||||||
|
<h3>Select Client Links to Move:</h3>
|
||||||
|
<div id="links-container">
|
||||||
|
<p style="color: #666;">Please select a source server first.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="submit-row">
|
||||||
|
<input type="submit" value="Move Selected Links" class="default" id="submit-btn" disabled>
|
||||||
|
<a href="{% url 'admin:vpn_server_changelist' %}" class="button cancel">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Client links data for each server
|
||||||
|
var linksByServer = {
|
||||||
|
{% for server, links in links_by_server.items %}
|
||||||
|
"{{ server.id }}": [
|
||||||
|
{% for link in links %}
|
||||||
|
{
|
||||||
|
"id": {{ link.id }},
|
||||||
|
"link": "{{ link.link|escapejs }}",
|
||||||
|
"comment": "{{ link.comment|escapejs }}",
|
||||||
|
"username": "{{ link.acl.user.username|escapejs }}",
|
||||||
|
"user_comment": "{{ link.acl.user.comment|escapejs }}",
|
||||||
|
"created_at": "{{ link.acl.created_at|date:'Y-m-d H:i' }}",
|
||||||
|
"full_url": "{{ EXTERNAL_ADDRESS }}/ss/{{ link.link }}#{{ link.acl.server.name|escapejs }}"
|
||||||
|
},
|
||||||
|
{% endfor %}
|
||||||
|
],
|
||||||
|
{% endfor %}
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateLinksList() {
|
||||||
|
var sourceServerId = document.getElementById('source_server').value;
|
||||||
|
var linksContainer = document.getElementById('links-container');
|
||||||
|
var submitBtn = document.getElementById('submit-btn');
|
||||||
|
|
||||||
|
if (!sourceServerId) {
|
||||||
|
linksContainer.innerHTML = '<p style="color: #666;">Please select a source server first.</p>';
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var links = linksByServer[sourceServerId] || [];
|
||||||
|
|
||||||
|
if (links.length === 0) {
|
||||||
|
linksContainer.innerHTML = '<p style="color: #666;">No client links found for this server.</p>';
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group links by user for better organization
|
||||||
|
var linksByUser = {};
|
||||||
|
links.forEach(function(link) {
|
||||||
|
if (!linksByUser[link.username]) {
|
||||||
|
linksByUser[link.username] = {
|
||||||
|
user_comment: link.user_comment,
|
||||||
|
created_at: link.created_at,
|
||||||
|
links: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
linksByUser[link.username].links.push(link);
|
||||||
|
});
|
||||||
|
|
||||||
|
var html = '<div style="max-height: 500px; overflow-y: auto; border: 1px solid #ddd; padding: 10px;">';
|
||||||
|
html += '<div style="margin-bottom: 10px;">';
|
||||||
|
html += '<button type="button" onclick="toggleAllLinks()" style="padding: 5px 10px; margin-right: 10px;">Select All</button>';
|
||||||
|
html += '<button type="button" onclick="toggleAllLinks(false)" style="padding: 5px 10px;">Deselect All</button>';
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
html += '<table style="width: 100%; border-collapse: collapse;">';
|
||||||
|
html += '<thead><tr style="background-color: #f5f5f5;">';
|
||||||
|
html += '<th style="padding: 8px; border: 1px solid #ddd; width: 50px;">Select</th>';
|
||||||
|
html += '<th style="padding: 8px; border: 1px solid #ddd;">Username</th>';
|
||||||
|
html += '<th style="padding: 8px; border: 1px solid #ddd;">Link Comment</th>';
|
||||||
|
html += '<th style="padding: 8px; border: 1px solid #ddd;">Link ID</th>';
|
||||||
|
html += '<th style="padding: 8px; border: 1px solid #ddd;">User Comment</th>';
|
||||||
|
html += '<th style="padding: 8px; border: 1px solid #ddd;">Created</th>';
|
||||||
|
html += '</tr></thead><tbody>';
|
||||||
|
|
||||||
|
// Sort users alphabetically
|
||||||
|
var sortedUsers = Object.keys(linksByUser).sort();
|
||||||
|
|
||||||
|
sortedUsers.forEach(function(username) {
|
||||||
|
var userData = linksByUser[username];
|
||||||
|
var userLinks = userData.links;
|
||||||
|
|
||||||
|
// Add user row header if user has multiple links
|
||||||
|
if (userLinks.length > 1) {
|
||||||
|
html += '<tr style="background-color: #f9f9f9;">';
|
||||||
|
html += '<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">';
|
||||||
|
html += '<input type="checkbox" class="user-checkbox" data-username="' + username + '" onchange="toggleUserLinks(\'' + username + '\')">';
|
||||||
|
html += '</td>';
|
||||||
|
html += '<td colspan="5" style="padding: 8px; border: 1px solid #ddd;"><strong>' + username + '</strong> (' + userLinks.length + ' links)</td>';
|
||||||
|
html += '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add individual link rows
|
||||||
|
userLinks.forEach(function(link) {
|
||||||
|
html += '<tr class="link-row" data-username="' + username + '">';
|
||||||
|
html += '<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">';
|
||||||
|
html += '<input type="checkbox" name="selected_links" value="' + link.id + '" class="link-checkbox" data-username="' + username + '" onchange="updateSubmitButton(); updateUserCheckbox(\'' + username + '\')">';
|
||||||
|
html += '</td>';
|
||||||
|
html += '<td style="padding: 8px; border: 1px solid #ddd;">' + (userLinks.length === 1 ? '<strong>' + username + '</strong>' : '↳') + '</td>';
|
||||||
|
html += '<td style="padding: 8px; border: 1px solid #ddd;">' + (link.comment || '<em>No comment</em>') + '</td>';
|
||||||
|
html += '<td style="padding: 8px; border: 1px solid #ddd; font-family: monospace; font-size: 12px;">' + link.link + '</td>';
|
||||||
|
html += '<td style="padding: 8px; border: 1px solid #ddd;">' + (userData.user_comment || '') + '</td>';
|
||||||
|
html += '<td style="padding: 8px; border: 1px solid #ddd;">' + userData.created_at + '</td>';
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
linksContainer.innerHTML = html;
|
||||||
|
|
||||||
|
updateSubmitButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAllLinks(selectAll = true) {
|
||||||
|
var checkboxes = document.getElementsByName('selected_links');
|
||||||
|
var userCheckboxes = document.getElementsByClassName('user-checkbox');
|
||||||
|
|
||||||
|
for (var i = 0; i < checkboxes.length; i++) {
|
||||||
|
checkboxes[i].checked = selectAll;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < userCheckboxes.length; i++) {
|
||||||
|
userCheckboxes[i].checked = selectAll;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSubmitButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleUserLinks(username) {
|
||||||
|
var userCheckbox = document.querySelector('.user-checkbox[data-username="' + username + '"]');
|
||||||
|
var userLinkCheckboxes = document.querySelectorAll('.link-checkbox[data-username="' + username + '"]');
|
||||||
|
|
||||||
|
for (var i = 0; i < userLinkCheckboxes.length; i++) {
|
||||||
|
userLinkCheckboxes[i].checked = userCheckbox.checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSubmitButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUserCheckbox(username) {
|
||||||
|
var userLinkCheckboxes = document.querySelectorAll('.link-checkbox[data-username="' + username + '"]');
|
||||||
|
var userCheckbox = document.querySelector('.user-checkbox[data-username="' + username + '"]');
|
||||||
|
|
||||||
|
if (!userCheckbox) return; // No user checkbox for single-link users
|
||||||
|
|
||||||
|
var allChecked = true;
|
||||||
|
var noneChecked = true;
|
||||||
|
|
||||||
|
for (var i = 0; i < userLinkCheckboxes.length; i++) {
|
||||||
|
if (userLinkCheckboxes[i].checked) {
|
||||||
|
noneChecked = false;
|
||||||
|
} else {
|
||||||
|
allChecked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allChecked) {
|
||||||
|
userCheckbox.checked = true;
|
||||||
|
userCheckbox.indeterminate = false;
|
||||||
|
} else if (noneChecked) {
|
||||||
|
userCheckbox.checked = false;
|
||||||
|
userCheckbox.indeterminate = false;
|
||||||
|
} else {
|
||||||
|
userCheckbox.checked = false;
|
||||||
|
userCheckbox.indeterminate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSubmitButton() {
|
||||||
|
var checkboxes = document.getElementsByName('selected_links');
|
||||||
|
var sourceServer = document.getElementById('source_server').value;
|
||||||
|
var targetServer = document.getElementById('target_server').value;
|
||||||
|
var submitBtn = document.getElementById('submit-btn');
|
||||||
|
|
||||||
|
var hasSelected = false;
|
||||||
|
for (var i = 0; i < checkboxes.length; i++) {
|
||||||
|
if (checkboxes[i].checked) {
|
||||||
|
hasSelected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
submitBtn.disabled = !(hasSelected && sourceServer && targetServer && sourceServer !== targetServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form submission confirmation
|
||||||
|
document.getElementById('move-clients-form').addEventListener('submit', function(e) {
|
||||||
|
var checkboxes = document.getElementsByName('selected_links');
|
||||||
|
var selectedCount = 0;
|
||||||
|
var selectedUsers = new Set();
|
||||||
|
|
||||||
|
for (var i = 0; i < checkboxes.length; i++) {
|
||||||
|
if (checkboxes[i].checked) {
|
||||||
|
selectedCount++;
|
||||||
|
selectedUsers.add(checkboxes[i].getAttribute('data-username'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceServerName = document.getElementById('source_server').selectedOptions[0].text;
|
||||||
|
var targetServerName = document.getElementById('target_server').selectedOptions[0].text;
|
||||||
|
|
||||||
|
var confirmMessage = 'Are you sure you want to move ' + selectedCount + ' link(s) for ' + selectedUsers.size + ' user(s) from "' + sourceServerName + '" to "' + targetServerName + '"?\n\n';
|
||||||
|
confirmMessage += 'This action will:\n';
|
||||||
|
confirmMessage += '- Transfer selected links to target server\n';
|
||||||
|
confirmMessage += '- Create ACLs for users who don\'t have access to target server\n';
|
||||||
|
confirmMessage += '- Remove empty ACLs from source server\n';
|
||||||
|
confirmMessage += '- Preserve all link settings and comments\n\n';
|
||||||
|
confirmMessage += 'This cannot be undone.';
|
||||||
|
|
||||||
|
if (!confirm(confirmMessage)) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.alert {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-row {
|
||||||
|
padding: 12px 14px;
|
||||||
|
margin: 0 0 20px;
|
||||||
|
background: #f8f8f8;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-row input[type="submit"] {
|
||||||
|
background: #417690;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-row input[type="submit"]:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-row .cancel {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-row .cancel:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-row:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"]:indeterminate {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Reference in New Issue
Block a user