Add better plugin description + new example plugin

Add better plugin description + new example plugin
This commit is contained in:
Master3395
2025-09-11 20:04:09 +02:00
parent 41ead838ef
commit dfbbccf073
15 changed files with 3801 additions and 0 deletions

2
testPlugin/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
default_app_config = 'testPlugin.apps.TestPluginConfig'

20
testPlugin/admin.py Normal file
View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from django.contrib import admin
from .models import TestPluginSettings, TestPluginLog
@admin.register(TestPluginSettings)
class TestPluginSettingsAdmin(admin.ModelAdmin):
list_display = ['user', 'plugin_enabled', 'test_count', 'last_test_time']
list_filter = ['plugin_enabled', 'last_test_time']
search_fields = ['user__username', 'custom_message']
readonly_fields = ['last_test_time']
@admin.register(TestPluginLog)
class TestPluginLogAdmin(admin.ModelAdmin):
list_display = ['timestamp', 'action', 'message', 'user']
list_filter = ['action', 'timestamp', 'user']
search_fields = ['action', 'message', 'user__username']
readonly_fields = ['timestamp']
date_hierarchy = 'timestamp'

11
testPlugin/apps.py Normal file
View File

@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
from django.apps import AppConfig
class TestPluginConfig(AppConfig):
name = 'testPlugin'
verbose_name = 'Test Plugin'
def ready(self):
# Import signal handlers
import testPlugin.signals

305
testPlugin/install.sh Normal file
View File

@@ -0,0 +1,305 @@
#!/bin/bash
# Test Plugin Installation Script for CyberPanel
# This script installs the test plugin from GitHub
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
PLUGIN_NAME="testPlugin"
PLUGIN_DIR="/home/cyberpanel/plugins"
CYBERPANEL_DIR="/usr/local/CyberCP"
GITHUB_REPO="https://github.com/cyberpanel/testPlugin.git"
TEMP_DIR="/tmp/cyberpanel_plugin_install"
# Function to print colored output
print_status() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Function to check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
print_error "This script must be run as root"
exit 1
fi
}
# Function to check if CyberPanel is installed
check_cyberpanel() {
if [ ! -d "$CYBERPANEL_DIR" ]; then
print_error "CyberPanel is not installed at $CYBERPANEL_DIR"
exit 1
fi
print_success "CyberPanel installation found"
}
# Function to create necessary directories
create_directories() {
print_status "Creating plugin directories..."
# Create plugins directory if it doesn't exist
mkdir -p "$PLUGIN_DIR"
chown -R cyberpanel:cyberpanel "$PLUGIN_DIR"
chmod 755 "$PLUGIN_DIR"
# Create temp directory
mkdir -p "$TEMP_DIR"
print_success "Directories created"
}
# Function to download plugin from GitHub
download_plugin() {
print_status "Downloading plugin from GitHub..."
# Remove existing temp directory
rm -rf "$TEMP_DIR"
# Clone the repository
if command -v git &> /dev/null; then
git clone "$GITHUB_REPO" "$TEMP_DIR"
else
print_error "Git is not installed. Please install git first."
exit 1
fi
print_success "Plugin downloaded"
}
# Function to install plugin files
install_plugin() {
print_status "Installing plugin files..."
# Copy plugin files to CyberPanel directory
cp -r "$TEMP_DIR" "$CYBERPANEL_DIR/$PLUGIN_NAME"
# Set proper permissions
chown -R cyberpanel:cyberpanel "$CYBERPANEL_DIR/$PLUGIN_NAME"
chmod -R 755 "$CYBERPANEL_DIR/$PLUGIN_NAME"
# Create symlink in plugins directory
ln -sf "$CYBERPANEL_DIR/$PLUGIN_NAME" "$PLUGIN_DIR/$PLUGIN_NAME"
print_success "Plugin files installed"
}
# Function to update Django settings
update_django_settings() {
print_status "Updating Django settings..."
SETTINGS_FILE="$CYBERPANEL_DIR/cyberpanel/settings.py"
if [ -f "$SETTINGS_FILE" ]; then
# Check if plugin is already in INSTALLED_APPS
if ! grep -q "testPlugin" "$SETTINGS_FILE"; then
# Add plugin to INSTALLED_APPS
sed -i '/^INSTALLED_APPS = \[/a\ "testPlugin",' "$SETTINGS_FILE"
print_success "Added testPlugin to INSTALLED_APPS"
else
print_warning "testPlugin already in INSTALLED_APPS"
fi
else
print_error "Django settings file not found at $SETTINGS_FILE"
exit 1
fi
}
# Function to update URL configuration
update_urls() {
print_status "Updating URL configuration..."
URLS_FILE="$CYBERPANEL_DIR/cyberpanel/urls.py"
if [ -f "$URLS_FILE" ]; then
# Check if plugin URLs are already included
if ! grep -q "testPlugin.urls" "$URLS_FILE"; then
# Add plugin URLs
sed -i '/^urlpatterns = \[/a\ path("testPlugin/", include("testPlugin.urls")),' "$URLS_FILE"
print_success "Added testPlugin URLs"
else
print_warning "testPlugin URLs already configured"
fi
else
print_error "URLs file not found at $URLS_FILE"
exit 1
fi
}
# Function to run Django migrations
run_migrations() {
print_status "Running Django migrations..."
cd "$CYBERPANEL_DIR"
# Run migrations
python3 manage.py makemigrations testPlugin
python3 manage.py migrate testPlugin
print_success "Migrations completed"
}
# Function to collect static files
collect_static() {
print_status "Collecting static files..."
cd "$CYBERPANEL_DIR"
python3 manage.py collectstatic --noinput
print_success "Static files collected"
}
# Function to restart CyberPanel services
restart_services() {
print_status "Restarting CyberPanel services..."
# Restart LiteSpeed
systemctl restart lscpd
# Restart CyberPanel
systemctl restart cyberpanel
print_success "Services restarted"
}
# Function to verify installation
verify_installation() {
print_status "Verifying installation..."
# Check if plugin files exist
if [ -d "$CYBERPANEL_DIR/$PLUGIN_NAME" ]; then
print_success "Plugin directory exists"
else
print_error "Plugin directory not found"
exit 1
fi
# Check if symlink exists
if [ -L "$PLUGIN_DIR/$PLUGIN_NAME" ]; then
print_success "Plugin symlink created"
else
print_error "Plugin symlink not found"
exit 1
fi
# Check if meta.xml exists
if [ -f "$CYBERPANEL_DIR/$PLUGIN_NAME/meta.xml" ]; then
print_success "Plugin metadata found"
else
print_error "Plugin metadata not found"
exit 1
fi
print_success "Installation verified successfully"
}
# Function to clean up
cleanup() {
print_status "Cleaning up temporary files..."
rm -rf "$TEMP_DIR"
print_success "Cleanup completed"
}
# Function to show installation summary
show_summary() {
echo ""
echo "=========================================="
echo "Test Plugin Installation Summary"
echo "=========================================="
echo "Plugin Name: $PLUGIN_NAME"
echo "Installation Directory: $CYBERPANEL_DIR/$PLUGIN_NAME"
echo "Plugin Directory: $PLUGIN_DIR/$PLUGIN_NAME"
echo "Access URL: https://your-domain:8090/testPlugin/"
echo ""
echo "Features Installed:"
echo "✓ Enable/Disable Toggle"
echo "✓ Test Button with Popup Messages"
echo "✓ Settings Page"
echo "✓ Activity Logs"
echo "✓ Inline Integration"
echo "✓ Complete Documentation"
echo "✓ Official CyberPanel Guide"
echo "✓ Advanced Development Guide"
echo ""
echo "To uninstall, run: $0 --uninstall"
echo "=========================================="
}
# Function to uninstall plugin
uninstall_plugin() {
print_status "Uninstalling testPlugin..."
# Remove plugin files
rm -rf "$CYBERPANEL_DIR/$PLUGIN_NAME"
rm -f "$PLUGIN_DIR/$PLUGIN_NAME"
# Remove from Django settings
SETTINGS_FILE="$CYBERPANEL_DIR/cyberpanel/settings.py"
if [ -f "$SETTINGS_FILE" ]; then
sed -i '/testPlugin/d' "$SETTINGS_FILE"
fi
# Remove from URLs
URLS_FILE="$CYBERPANEL_DIR/cyberpanel/urls.py"
if [ -f "$URLS_FILE" ]; then
sed -i '/testPlugin/d' "$URLS_FILE"
fi
# Restart services
restart_services
print_success "Plugin uninstalled successfully"
}
# Main installation function
main() {
echo "=========================================="
echo "CyberPanel Test Plugin Installer"
echo "=========================================="
echo ""
# Check for uninstall flag
if [ "$1" = "--uninstall" ]; then
uninstall_plugin
exit 0
fi
# Run installation steps
check_root
check_cyberpanel
create_directories
download_plugin
install_plugin
update_django_settings
update_urls
run_migrations
collect_static
restart_services
verify_installation
cleanup
show_summary
print_success "Test Plugin installation completed successfully!"
}
# Run main function with all arguments
main "$@"

24
testPlugin/meta.xml Normal file
View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<plugin>
<name>Test Plugin</name>
<type>Utility</type>
<description>A comprehensive test plugin for CyberPanel with enable/disable functionality, test button, popup messages, and inline integration</description>
<version>1.0.0</version>
<author>CyberPanel Development Team</author>
<website>https://github.com/cyberpanel/testPlugin</website>
<license>MIT</license>
<dependencies>
<python>3.6+</python>
<django>2.2+</django>
</dependencies>
<permissions>
<admin>true</admin>
<user>false</user>
</permissions>
<settings>
<enable_toggle>true</enable_toggle>
<test_button>true</test_button>
<popup_messages>true</popup_messages>
<inline_integration>true</inline_integration>
</settings>
</plugin>

35
testPlugin/models.py Normal file
View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
from django.db import models
from django.contrib.auth.models import User
class TestPluginSettings(models.Model):
"""Model to store plugin settings and enable/disable state"""
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
plugin_enabled = models.BooleanField(default=True, help_text="Enable or disable the plugin")
test_count = models.IntegerField(default=0, help_text="Number of times test button was clicked")
last_test_time = models.DateTimeField(auto_now=True, help_text="Last time test button was clicked")
custom_message = models.TextField(default="Test plugin is working!", help_text="Custom message for popup")
class Meta:
verbose_name = "Test Plugin Settings"
verbose_name_plural = "Test Plugin Settings"
def __str__(self):
return f"Test Plugin Settings - Enabled: {self.plugin_enabled}"
class TestPluginLog(models.Model):
"""Model to store plugin activity logs"""
timestamp = models.DateTimeField(auto_now_add=True)
action = models.CharField(max_length=100)
message = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
class Meta:
verbose_name = "Test Plugin Log"
verbose_name_plural = "Test Plugin Logs"
ordering = ['-timestamp']
def __str__(self):
return f"{self.timestamp} - {self.action}: {self.message}"

15
testPlugin/signals.py Normal file
View File

@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from .models import TestPluginSettings
@receiver(post_save, sender=User)
def create_user_settings(sender, instance, created, **kwargs):
"""Create default plugin settings when a new user is created"""
if created:
TestPluginSettings.objects.create(
user=instance,
plugin_enabled=True
)

View File

@@ -0,0 +1,336 @@
/* Test Plugin CSS - Additional styles for better integration */
/* Popup Message Animations */
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
/* Enhanced Button Styles */
.btn-test {
position: relative;
overflow: hidden;
}
.btn-test::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.btn-test:active::before {
width: 300px;
height: 300px;
}
/* Toggle Switch Enhanced */
.toggle-switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
input:checked + .slider {
background-color: #5856d6;
}
input:checked + .slider:before {
transform: translateX(26px);
}
/* Loading States */
.loading {
opacity: 0.6;
pointer-events: none;
}
.loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin: -10px 0 0 -10px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Card Hover Effects */
.plugin-card {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.plugin-card:hover {
transform: translateY(-8px);
box-shadow: 0 12px 32px rgba(0,0,0,0.15);
}
/* Status Indicators */
.status-indicator {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-indicator.enabled {
background: #e8f5e8;
color: #388e3c;
}
.status-indicator.disabled {
background: #ffebee;
color: #d32f2f;
}
/* Responsive Enhancements */
@media (max-width: 768px) {
.control-row {
flex-direction: column;
align-items: stretch;
}
.control-group {
justify-content: space-between;
margin-bottom: 15px;
}
.stats-grid {
grid-template-columns: 1fr;
gap: 15px;
}
.logs-table {
font-size: 12px;
}
.logs-table th,
.logs-table td {
padding: 8px 4px;
}
}
@media (max-width: 480px) {
.test-plugin-wrapper {
padding: 10px;
}
.plugin-header,
.control-panel,
.settings-form,
.logs-content {
padding: 15px;
}
.plugin-header h1 {
font-size: 24px;
flex-direction: column;
text-align: center;
}
.btn-test,
.btn-secondary {
width: 100%;
justify-content: center;
margin-bottom: 10px;
}
}
/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--text-primary: #ffffff;
--text-secondary: #b3b3b3;
--text-tertiary: #808080;
--border-primary: #404040;
--shadow-md: 0 2px 8px rgba(0,0,0,0.3);
--shadow-lg: 0 8px 24px rgba(0,0,0,0.4);
}
}
/* Print Styles */
@media print {
.test-plugin-wrapper {
background: white !important;
color: black !important;
}
.btn-test,
.btn-secondary,
.toggle-switch {
display: none !important;
}
.popup-message {
display: none !important;
}
}
/* Additional styles for inline elements */
.popup-message {
position: fixed;
top: 20px;
right: 20px;
background: white;
border-radius: 8px;
padding: 16px 20px;
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
border-left: 4px solid #10b981;
z-index: 9999;
max-width: 400px;
transform: translateX(100%);
transition: transform 0.3s ease;
}
.popup-message.show {
transform: translateX(0);
}
.popup-message.error {
border-left-color: #ef4444;
}
.popup-message.warning {
border-left-color: #f59e0b;
}
.popup-title {
font-weight: 600;
color: var(--text-primary, #2f3640);
margin-bottom: 4px;
}
.popup-content {
font-size: 14px;
color: var(--text-secondary, #64748b);
margin-bottom: 8px;
}
.popup-time {
font-size: 12px;
color: var(--text-tertiary, #9ca3af);
}
.popup-close {
position: absolute;
top: 8px;
right: 8px;
background: none;
border: none;
font-size: 18px;
cursor: pointer;
color: var(--text-tertiary, #9ca3af);
}
.notification {
position: fixed;
top: 20px;
right: 20px;
background: white;
border-radius: 8px;
padding: 16px 20px;
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
border-left: 4px solid #10b981;
z-index: 9999;
max-width: 400px;
transform: translateX(100%);
transition: transform 0.3s ease;
}
.notification.show {
transform: translateX(0);
}
.notification.error {
border-left-color: #ef4444;
}
.notification-title {
font-weight: 600;
color: var(--text-primary, #2f3640);
margin-bottom: 4px;
}
.notification-content {
font-size: 14px;
color: var(--text-secondary, #64748b);
}
.popup-container {
position: fixed;
top: 0;
right: 0;
z-index: 9999;
pointer-events: none;
}

View File

@@ -0,0 +1,323 @@
/**
* Test Plugin JavaScript
* Handles all client-side functionality for the test plugin
*/
class TestPlugin {
constructor() {
this.init();
}
init() {
this.bindEvents();
this.initializeComponents();
}
bindEvents() {
// Toggle switch functionality
const toggleSwitch = document.getElementById('plugin-toggle');
if (toggleSwitch) {
toggleSwitch.addEventListener('change', (e) => this.handleToggle(e));
}
// Test button functionality
const testButton = document.getElementById('test-button');
if (testButton) {
testButton.addEventListener('click', (e) => this.handleTestClick(e));
}
// Settings form
const settingsForm = document.getElementById('settings-form');
if (settingsForm) {
settingsForm.addEventListener('submit', (e) => this.handleSettingsSubmit(e));
}
// Log filter
const actionFilter = document.getElementById('action-filter');
if (actionFilter) {
actionFilter.addEventListener('change', (e) => this.handleLogFilter(e));
}
}
initializeComponents() {
// Initialize any components that need setup
this.initializeTooltips();
this.initializeAnimations();
}
async handleToggle(event) {
const toggleSwitch = event.target;
const testButton = document.getElementById('test-button');
try {
const response = await fetch('/testPlugin/toggle/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCSRFToken()
}
});
const data = await response.json();
if (data.status === 1) {
if (testButton) {
testButton.disabled = !data.enabled;
}
this.showNotification('success', 'Plugin Toggle', data.message);
// Update status indicator if exists
this.updateStatusIndicator(data.enabled);
// Reload page after a short delay to update UI
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
this.showNotification('error', 'Error', data.error_message);
// Revert toggle state
toggleSwitch.checked = !toggleSwitch.checked;
}
} catch (error) {
this.showNotification('error', 'Error', 'Failed to toggle plugin');
// Revert toggle state
toggleSwitch.checked = !toggleSwitch.checked;
}
}
async handleTestClick(event) {
const testButton = event.target;
if (testButton.disabled) return;
// Add loading state
testButton.classList.add('loading');
testButton.disabled = true;
const originalContent = testButton.innerHTML;
testButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Testing...';
try {
const response = await fetch('/testPlugin/test/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCSRFToken()
}
});
const data = await response.json();
if (data.status === 1) {
// Update test count
const testCountElement = document.getElementById('test-count');
if (testCountElement) {
testCountElement.textContent = data.test_count;
}
// Show popup message
this.showPopup(
data.popup_message.type,
data.popup_message.title,
data.popup_message.message
);
// Add success animation
testButton.style.background = 'linear-gradient(135deg, #10b981, #059669)';
setTimeout(() => {
testButton.style.background = '';
}, 2000);
} else {
this.showNotification('error', 'Error', data.error_message);
}
} catch (error) {
this.showNotification('error', 'Error', 'Failed to execute test');
} finally {
// Remove loading state
testButton.classList.remove('loading');
testButton.disabled = false;
testButton.innerHTML = originalContent;
}
}
async handleSettingsSubmit(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const data = {
custom_message: formData.get('custom_message')
};
try {
const response = await fetch('/testPlugin/update-settings/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCSRFToken()
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.status === 1) {
this.showNotification('success', 'Settings Updated', result.message);
} else {
this.showNotification('error', 'Error', result.error_message);
}
} catch (error) {
this.showNotification('error', 'Error', 'Failed to update settings');
}
}
handleLogFilter(event) {
const selectedAction = event.target.value;
const logRows = document.querySelectorAll('.log-row');
logRows.forEach(row => {
if (selectedAction === '' || row.dataset.action === selectedAction) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
}
showPopup(type, title, message) {
const popupContainer = document.getElementById('popup-container') || this.createPopupContainer();
const popup = document.createElement('div');
popup.className = `popup-message ${type}`;
popup.innerHTML = `
<button class="popup-close" onclick="this.parentElement.remove()">&times;</button>
<div class="popup-title">${title}</div>
<div class="popup-content">${message}</div>
<div class="popup-time">${new Date().toLocaleTimeString()}</div>
`;
popupContainer.appendChild(popup);
// Show popup with animation
setTimeout(() => popup.classList.add('show'), 100);
// Auto remove after 5 seconds
setTimeout(() => {
popup.classList.remove('show');
setTimeout(() => popup.remove(), 300);
}, 5000);
}
showNotification(type, title, message) {
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.innerHTML = `
<div class="notification-title">${title}</div>
<div class="notification-content">${message}</div>
`;
document.body.appendChild(notification);
// Show notification
setTimeout(() => notification.classList.add('show'), 100);
// Auto remove after 3 seconds
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => notification.remove(), 300);
}, 3000);
}
createPopupContainer() {
const container = document.createElement('div');
container.id = 'popup-container';
container.className = 'popup-container';
document.body.appendChild(container);
return container;
}
updateStatusIndicator(enabled) {
const statusElements = document.querySelectorAll('.status-indicator');
statusElements.forEach(element => {
element.className = `status-indicator ${enabled ? 'enabled' : 'disabled'}`;
element.innerHTML = enabled ?
'<i class="fas fa-check-circle"></i> Enabled' :
'<i class="fas fa-times-circle"></i> Disabled';
});
}
initializeTooltips() {
// Add tooltips to buttons and controls
const elements = document.querySelectorAll('[data-tooltip]');
elements.forEach(element => {
element.addEventListener('mouseenter', (e) => this.showTooltip(e));
element.addEventListener('mouseleave', (e) => this.hideTooltip(e));
});
}
showTooltip(event) {
const element = event.target;
const tooltipText = element.dataset.tooltip;
if (!tooltipText) return;
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.textContent = tooltipText;
tooltip.style.cssText = `
position: absolute;
background: #333;
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
z-index: 10000;
pointer-events: none;
white-space: nowrap;
`;
document.body.appendChild(tooltip);
const rect = element.getBoundingClientRect();
tooltip.style.left = rect.left + (rect.width / 2) - (tooltip.offsetWidth / 2) + 'px';
tooltip.style.top = rect.top - tooltip.offsetHeight - 8 + 'px';
element._tooltip = tooltip;
}
hideTooltip(event) {
const element = event.target;
if (element._tooltip) {
element._tooltip.remove();
delete element._tooltip;
}
}
initializeAnimations() {
// Add entrance animations to cards
const cards = document.querySelectorAll('.plugin-card, .stat-card, .log-item');
cards.forEach((card, index) => {
card.style.opacity = '0';
card.style.transform = 'translateY(20px)';
card.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
setTimeout(() => {
card.style.opacity = '1';
card.style.transform = 'translateY(0)';
}, index * 100);
});
}
getCSRFToken() {
const token = document.querySelector('[name=csrfmiddlewaretoken]');
return token ? token.value : '';
}
}
// Initialize the plugin when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
new TestPlugin();
});
// Export for potential external use
window.TestPlugin = TestPlugin;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,566 @@
{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Test Plugin - CyberPanel" %}{% endblock %}
{% block header_scripts %}
<style>
/* Test Plugin Specific Styles */
.test-plugin-wrapper {
background: transparent;
padding: 20px;
}
.test-plugin-container {
max-width: 1200px;
margin: 0 auto;
}
/* Page Header */
.plugin-header {
background: var(--bg-primary, white);
border-radius: 12px;
padding: 25px;
margin-bottom: 25px;
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
border: 1px solid var(--border-primary, #e8e9ff);
}
.plugin-header h1 {
font-size: 28px;
font-weight: 700;
color: var(--text-primary, #2f3640);
margin: 0 0 10px 0;
display: flex;
align-items: center;
gap: 15px;
}
.plugin-header .icon {
width: 48px;
height: 48px;
background: #5856d6;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
box-shadow: 0 4px 12px rgba(88,86,214,0.3);
}
.plugin-header p {
font-size: 15px;
color: var(--text-secondary, #64748b);
margin: 0;
}
/* Control Panel */
.control-panel {
background: var(--bg-primary, white);
border-radius: 12px;
padding: 25px;
margin-bottom: 25px;
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
border: 1px solid var(--border-primary, #e8e9ff);
}
.control-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
.control-group {
display: flex;
align-items: center;
gap: 15px;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #5856d6;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.btn-test {
background: linear-gradient(135deg, #5856d6, #4a90e2);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn-test:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(88,86,214,0.3);
}
.btn-test:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: #6c757d;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-secondary:hover {
background: #5a6268;
color: white;
text-decoration: none;
}
/* Stats Cards */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 25px;
}
.stat-card {
background: var(--bg-primary, white);
border-radius: 12px;
padding: 20px;
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
border: 1px solid var(--border-primary, #e8e9ff);
text-align: center;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #5856d6;
margin-bottom: 8px;
}
.stat-label {
font-size: 14px;
color: var(--text-secondary, #64748b);
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Recent Logs */
.logs-section {
background: var(--bg-primary, white);
border-radius: 12px;
padding: 25px;
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
border: 1px solid var(--border-primary, #e8e9ff);
}
.logs-list {
max-height: 300px;
overflow-y: auto;
}
.log-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid var(--border-primary, #e8e9ff);
}
.log-item:last-child {
border-bottom: none;
}
.log-icon {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
font-size: 14px;
}
.log-icon.info {
background: #e3f2fd;
color: #1976d2;
}
.log-icon.success {
background: #e8f5e8;
color: #388e3c;
}
.log-icon.warning {
background: #fff3e0;
color: #f57c00;
}
.log-content {
flex: 1;
}
.log-action {
font-weight: 600;
color: var(--text-primary, #2f3640);
margin-bottom: 4px;
}
.log-message {
font-size: 14px;
color: var(--text-secondary, #64748b);
}
.log-time {
font-size: 12px;
color: var(--text-tertiary, #9ca3af);
margin-left: 12px;
}
/* Popup Message Styles */
.popup-message {
position: fixed;
top: 20px;
right: 20px;
background: white;
border-radius: 8px;
padding: 16px 20px;
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
border-left: 4px solid #10b981;
z-index: 9999;
max-width: 400px;
transform: translateX(100%);
transition: transform 0.3s ease;
}
.popup-message.show {
transform: translateX(0);
}
.popup-message.error {
border-left-color: #ef4444;
}
.popup-message.warning {
border-left-color: #f59e0b;
}
.popup-title {
font-weight: 600;
color: var(--text-primary, #2f3640);
margin-bottom: 4px;
}
.popup-content {
font-size: 14px;
color: var(--text-secondary, #64748b);
margin-bottom: 8px;
}
.popup-time {
font-size: 12px;
color: var(--text-tertiary, #9ca3af);
}
.popup-close {
position: absolute;
top: 8px;
right: 8px;
background: none;
border: none;
font-size: 18px;
cursor: pointer;
color: var(--text-tertiary, #9ca3af);
}
/* Responsive */
@media (max-width: 768px) {
.test-plugin-wrapper {
padding: 15px;
}
.control-row {
flex-direction: column;
align-items: stretch;
}
.control-group {
justify-content: space-between;
}
.stats-grid {
grid-template-columns: 1fr;
}
.popup-message {
right: 10px;
left: 10px;
max-width: none;
}
}
</style>
{% endblock %}
{% block content %}
<div class="test-plugin-wrapper">
<div class="test-plugin-container">
<!-- Plugin Header -->
<div class="plugin-header">
<h1>
<div class="icon">
<i class="fas fa-vial"></i>
</div>
{% trans "Test Plugin" %}
</h1>
<p>{% trans "A comprehensive test plugin with enable/disable functionality, test button, and popup messages" %}</p>
</div>
<!-- Control Panel -->
<div class="control-panel">
<div class="control-row">
<div class="control-group">
<label for="plugin-toggle" style="font-weight: 600; color: var(--text-primary, #2f3640);">
{% trans "Enable Plugin" %}
</label>
<label class="toggle-switch">
<input type="checkbox" id="plugin-toggle" {% if plugin_enabled %}checked{% endif %}>
<span class="slider"></span>
</label>
</div>
<div class="control-group">
<button class="btn-test" id="test-button" {% if not plugin_enabled %}disabled{% endif %}>
<i class="fas fa-play"></i>
{% trans "Test Button" %}
</button>
<a href="{% url 'testPlugin:plugin_settings' %}" class="btn-secondary">
<i class="fas fa-cog"></i>
{% trans "Settings" %}
</a>
<a href="{% url 'testPlugin:plugin_logs' %}" class="btn-secondary">
<i class="fas fa-list"></i>
{% trans "Logs" %}
</a>
<a href="{% url 'testPlugin:plugin_docs' %}" class="btn-secondary">
<i class="fas fa-book"></i>
{% trans "Documentation" %}
</a>
</div>
</div>
</div>
<!-- Stats Cards -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="test-count">{{ settings.test_count|default:0 }}</div>
<div class="stat-label">{% trans "Test Clicks" %}</div>
</div>
<div class="stat-card">
<div class="stat-value">
{% if plugin_enabled %}
<i class="fas fa-check-circle" style="color: #10b981;"></i>
{% else %}
<i class="fas fa-times-circle" style="color: #ef4444;"></i>
{% endif %}
</div>
<div class="stat-label">{% trans "Plugin Status" %}</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ recent_logs|length }}</div>
<div class="stat-label">{% trans "Recent Activities" %}</div>
</div>
</div>
<!-- Recent Logs -->
<div class="logs-section">
<h3 style="margin-bottom: 20px; color: var(--text-primary, #2f3640);">
<i class="fas fa-history" style="margin-right: 8px; color: #5856d6;"></i>
{% trans "Recent Activity" %}
</h3>
<div class="logs-list">
{% for log in recent_logs %}
<div class="log-item">
<div class="log-icon {% if 'error' in log.action %}warning{% elif 'success' in log.action or 'click' in log.action %}success{% else %}info{% endif %}">
{% if 'click' in log.action %}
<i class="fas fa-mouse-pointer"></i>
{% elif 'toggle' in log.action %}
<i class="fas fa-toggle-on"></i>
{% elif 'settings' in log.action %}
<i class="fas fa-cog"></i>
{% else %}
<i class="fas fa-info-circle"></i>
{% endif %}
</div>
<div class="log-content">
<div class="log-action">{{ log.action|title }}</div>
<div class="log-message">{{ log.message }}</div>
</div>
<div class="log-time">{{ log.timestamp|date:"M d, H:i" }}</div>
</div>
{% empty %}
<div style="text-align: center; padding: 40px; color: var(--text-secondary, #64748b);">
<i class="fas fa-inbox" style="font-size: 48px; margin-bottom: 16px; opacity: 0.5;"></i>
<p>{% trans "No recent activity" %}</p>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<!-- Popup Message Container -->
<div id="popup-container"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const toggleSwitch = document.getElementById('plugin-toggle');
const testButton = document.getElementById('test-button');
const testCountElement = document.getElementById('test-count');
// Toggle switch functionality
toggleSwitch.addEventListener('change', function() {
fetch('{% url "testPlugin:toggle_plugin" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
}
})
.then(response => response.json())
.then(data => {
if (data.status === 1) {
testButton.disabled = !data.enabled;
showPopup('success', 'Plugin Toggle', data.message);
location.reload(); // Refresh to update UI
} else {
showPopup('error', 'Error', data.error_message);
}
})
.catch(error => {
showPopup('error', 'Error', 'Failed to toggle plugin');
});
});
// Test button functionality
testButton.addEventListener('click', function() {
if (testButton.disabled) return;
testButton.disabled = true;
testButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Testing...';
fetch('{% url "testPlugin:test_button" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
}
})
.then(response => response.json())
.then(data => {
if (data.status === 1) {
testCountElement.textContent = data.test_count;
showPopup(data.popup_message.type, data.popup_message.title, data.popup_message.message);
} else {
showPopup('error', 'Error', data.error_message);
}
})
.catch(error => {
showPopup('error', 'Error', 'Failed to execute test');
})
.finally(() => {
testButton.disabled = false;
testButton.innerHTML = '<i class="fas fa-play"></i> Test Button';
});
});
// Popup message function
function showPopup(type, title, message) {
const popupContainer = document.getElementById('popup-container');
const popup = document.createElement('div');
popup.className = `popup-message ${type}`;
popup.innerHTML = `
<button class="popup-close" onclick="this.parentElement.remove()">&times;</button>
<div class="popup-title">${title}</div>
<div class="popup-content">${message}</div>
<div class="popup-time">${new Date().toLocaleTimeString()}</div>
`;
popupContainer.appendChild(popup);
// Show popup
setTimeout(() => popup.classList.add('show'), 100);
// Auto remove after 5 seconds
setTimeout(() => {
popup.classList.remove('show');
setTimeout(() => popup.remove(), 300);
}, 5000);
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,286 @@
{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Test Plugin Logs - CyberPanel" %}{% endblock %}
{% block header_scripts %}
<style>
.logs-wrapper {
background: transparent;
padding: 20px;
}
.logs-container {
max-width: 1200px;
margin: 0 auto;
}
.logs-header {
background: var(--bg-primary, white);
border-radius: 12px;
padding: 25px;
margin-bottom: 25px;
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
border: 1px solid var(--border-primary, #e8e9ff);
}
.logs-content {
background: var(--bg-primary, white);
border-radius: 12px;
padding: 25px;
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
border: 1px solid var(--border-primary, #e8e9ff);
}
.logs-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.logs-table th,
.logs-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid var(--border-primary, #e8e9ff);
}
.logs-table th {
background: var(--bg-secondary, #f8f9ff);
font-weight: 600;
color: var(--text-primary, #2f3640);
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.logs-table td {
font-size: 14px;
color: var(--text-secondary, #64748b);
}
.log-action {
font-weight: 600;
color: var(--text-primary, #2f3640);
}
.log-timestamp {
color: var(--text-tertiary, #9ca3af);
font-size: 13px;
}
.log-icon {
display: inline-block;
width: 24px;
height: 24px;
border-radius: 50%;
text-align: center;
line-height: 24px;
font-size: 12px;
margin-right: 8px;
}
.log-icon.info {
background: #e3f2fd;
color: #1976d2;
}
.log-icon.success {
background: #e8f5e8;
color: #388e3c;
}
.log-icon.warning {
background: #fff3e0;
color: #f57c00;
}
.log-icon.error {
background: #ffebee;
color: #d32f2f;
}
.btn-secondary {
background: #6c757d;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
margin-right: 10px;
}
.btn-secondary:hover {
background: #5a6268;
color: white;
text-decoration: none;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-secondary, #64748b);
}
.empty-state i {
font-size: 64px;
margin-bottom: 20px;
opacity: 0.5;
}
.empty-state h3 {
font-size: 24px;
margin-bottom: 10px;
color: var(--text-primary, #2f3640);
}
.empty-state p {
font-size: 16px;
margin: 0;
}
.filter-controls {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
align-items: center;
}
.filter-select {
padding: 8px 12px;
border: 1px solid var(--border-primary, #e8e9ff);
border-radius: 6px;
background: white;
font-size: 14px;
}
.filter-select:focus {
outline: none;
border-color: #5856d6;
box-shadow: 0 0 0 3px rgba(88,86,214,0.1);
}
@media (max-width: 768px) {
.logs-table {
font-size: 12px;
}
.logs-table th,
.logs-table td {
padding: 8px;
}
.filter-controls {
flex-direction: column;
align-items: stretch;
}
}
</style>
{% endblock %}
{% block content %}
<div class="logs-wrapper">
<div class="logs-container">
<!-- Logs Header -->
<div class="logs-header">
<h1>
<i class="fas fa-list" style="margin-right: 12px; color: #5856d6;"></i>
{% trans "Test Plugin Logs" %}
</h1>
<p>{% trans "View detailed activity logs for the test plugin" %}</p>
</div>
<!-- Logs Content -->
<div class="logs-content">
<div class="filter-controls">
<select class="filter-select" id="action-filter" title="{% trans 'Filter logs by action type' %}">
<option value="">{% trans "All Actions" %}</option>
<option value="test_button_click">{% trans "Test Button Clicks" %}</option>
<option value="plugin_toggle">{% trans "Plugin Toggle" %}</option>
<option value="settings_update">{% trans "Settings Update" %}</option>
<option value="page_visit">{% trans "Page Visits" %}</option>
</select>
<a href="{% url 'testPlugin:plugin_home' %}" class="btn-secondary">
<i class="fas fa-arrow-left"></i>
{% trans "Back to Plugin" %}
</a>
<a href="{% url 'testPlugin:plugin_docs' %}" class="btn-secondary">
<i class="fas fa-book"></i>
{% trans "Documentation" %}
</a>
</div>
{% if logs %}
<table class="logs-table">
<thead>
<tr>
<th>{% trans "Action" %}</th>
<th>{% trans "Message" %}</th>
<th>{% trans "Timestamp" %}</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr class="log-row" data-action="{{ log.action }}">
<td>
<span class="log-icon {% if 'error' in log.action %}error{% elif 'success' in log.action or 'click' in log.action %}success{% elif 'warning' in log.action %}warning{% else %}info{% endif %}">
{% if 'click' in log.action %}
<i class="fas fa-mouse-pointer"></i>
{% elif 'toggle' in log.action %}
<i class="fas fa-toggle-on"></i>
{% elif 'settings' in log.action %}
<i class="fas fa-cog"></i>
{% elif 'visit' in log.action %}
<i class="fas fa-eye"></i>
{% else %}
<i class="fas fa-info-circle"></i>
{% endif %}
</span>
<span class="log-action">{{ log.action|title|replace:"_":" " }}</span>
</td>
<td>{{ log.message }}</td>
<td class="log-timestamp">{{ log.timestamp|date:"M d, Y H:i:s" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">
<i class="fas fa-inbox"></i>
<h3>{% trans "No Logs Found" %}</h3>
<p>{% trans "No activity logs available yet. Start using the plugin to see logs here." %}</p>
</div>
{% endif %}
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const actionFilter = document.getElementById('action-filter');
const logRows = document.querySelectorAll('.log-row');
actionFilter.addEventListener('change', function() {
const selectedAction = this.value;
logRows.forEach(row => {
if (selectedAction === '' || row.dataset.action === selectedAction) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,259 @@
{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Test Plugin Settings - CyberPanel" %}{% endblock %}
{% block header_scripts %}
<style>
.settings-wrapper {
background: transparent;
padding: 20px;
}
.settings-container {
max-width: 800px;
margin: 0 auto;
}
.settings-header {
background: var(--bg-primary, white);
border-radius: 12px;
padding: 25px;
margin-bottom: 25px;
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
border: 1px solid var(--border-primary, #e8e9ff);
}
.settings-form {
background: var(--bg-primary, white);
border-radius: 12px;
padding: 25px;
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
border: 1px solid var(--border-primary, #e8e9ff);
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
font-weight: 600;
color: var(--text-primary, #2f3640);
margin-bottom: 8px;
}
.form-control {
width: 100%;
padding: 12px;
border: 1px solid var(--border-primary, #e8e9ff);
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.form-control:focus {
outline: none;
border-color: #5856d6;
box-shadow: 0 0 0 3px rgba(88,86,214,0.1);
}
.btn-primary {
background: linear-gradient(135deg, #5856d6, #4a90e2);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(88,86,214,0.3);
}
.btn-secondary {
background: #6c757d;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
margin-right: 10px;
}
.btn-secondary:hover {
background: #5a6268;
color: white;
text-decoration: none;
}
</style>
{% endblock %}
{% block content %}
<div class="settings-wrapper">
<div class="settings-container">
<!-- Settings Header -->
<div class="settings-header">
<h1>
<i class="fas fa-cog" style="margin-right: 12px; color: #5856d6;"></i>
{% trans "Test Plugin Settings" %}
</h1>
<p>{% trans "Configure your test plugin settings and preferences" %}</p>
</div>
<!-- Settings Form -->
<div class="settings-form">
<form id="settings-form">
{% csrf_token %}
<div class="form-group">
<label for="custom_message" class="form-label">
{% trans "Custom Test Message" %}
</label>
<textarea
id="custom_message"
name="custom_message"
class="form-control"
rows="3"
placeholder="Enter your custom message for the test button popup..."
>{{ settings.custom_message }}</textarea>
<small style="color: var(--text-secondary, #64748b); margin-top: 5px; display: block;">
{% trans "This message will be displayed when you click the test button" %}
</small>
</div>
<div class="form-group">
<label class="form-label">
{% trans "Plugin Status" %}
</label>
<div style="padding: 12px; background: var(--bg-secondary, #f8f9ff); border-radius: 8px; border: 1px solid var(--border-primary, #e8e9ff);">
<strong style="color: {% if settings.plugin_enabled %}#10b981{% else %}#ef4444{% endif %};">
{% if settings.plugin_enabled %}
<i class="fas fa-check-circle"></i> {% trans "Enabled" %}
{% else %}
<i class="fas fa-times-circle"></i> {% trans "Disabled" %}
{% endif %}
</strong>
<p style="margin: 8px 0 0 0; color: var(--text-secondary, #64748b); font-size: 14px;">
{% trans "Use the toggle switch on the main page to enable/disable the plugin" %}
</p>
</div>
</div>
<div class="form-group">
<label class="form-label">
{% trans "Test Statistics" %}
</label>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
<div style="padding: 12px; background: var(--bg-secondary, #f8f9ff); border-radius: 8px; text-align: center;">
<div style="font-size: 24px; font-weight: 700; color: #5856d6;">{{ settings.test_count }}</div>
<div style="font-size: 14px; color: var(--text-secondary, #64748b);">{% trans "Total Tests" %}</div>
</div>
<div style="padding: 12px; background: var(--bg-secondary, #f8f9ff); border-radius: 8px; text-align: center;">
<div style="font-size: 24px; font-weight: 700; color: #5856d6;">{{ settings.last_test_time|date:"M d" }}</div>
<div style="font-size: 14px; color: var(--text-secondary, #64748b);">{% trans "Last Test" %}</div>
</div>
</div>
</div>
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid var(--border-primary, #e8e9ff);">
<button type="submit" class="btn-primary">
<i class="fas fa-save"></i>
{% trans "Save Settings" %}
</button>
<a href="{% url 'testPlugin:plugin_home' %}" class="btn-secondary">
<i class="fas fa-arrow-left"></i>
{% trans "Back to Plugin" %}
</a>
<a href="{% url 'testPlugin:plugin_docs' %}" class="btn-secondary">
<i class="fas fa-book"></i>
{% trans "Documentation" %}
</a>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('settings-form');
form.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(form);
const data = {
custom_message: formData.get('custom_message')
};
fetch('{% url "testPlugin:update_settings" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
if (data.status === 1) {
showNotification('success', 'Settings Updated', data.message);
} else {
showNotification('error', 'Error', data.error_message);
}
})
.catch(error => {
showNotification('error', 'Error', 'Failed to update settings');
});
});
function showNotification(type, title, message) {
// Create notification element
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: white;
border-radius: 8px;
padding: 16px 20px;
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
border-left: 4px solid ${type === 'success' ? '#10b981' : '#ef4444'};
z-index: 9999;
max-width: 400px;
transform: translateX(100%);
transition: transform 0.3s ease;
`;
notification.innerHTML = `
<div style="font-weight: 600; color: var(--text-primary, #2f3640); margin-bottom: 4px;">${title}</div>
<div style="font-size: 14px; color: var(--text-secondary, #64748b);">${message}</div>
`;
document.body.appendChild(notification);
// Show notification
setTimeout(() => notification.style.transform = 'translateX(0)', 100);
// Auto remove after 3 seconds
setTimeout(() => {
notification.style.transform = 'translateX(100%)';
setTimeout(() => notification.remove(), 300);
}, 3000);
}
});
</script>
{% endblock %}

17
testPlugin/urls.py Normal file
View File

@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
from django.urls import path
from . import views
app_name = 'testPlugin'
urlpatterns = [
path('', views.plugin_home, name='plugin_home'),
path('test/', views.test_button, name='test_button'),
path('toggle/', views.toggle_plugin, name='toggle_plugin'),
path('settings/', views.plugin_settings, name='plugin_settings'),
path('update-settings/', views.update_settings, name='update_settings'),
path('install/', views.install_plugin, name='install_plugin'),
path('uninstall/', views.uninstall_plugin, name='uninstall_plugin'),
path('logs/', views.plugin_logs, name='plugin_logs'),
path('docs/', views.plugin_docs, name='plugin_docs'),
]

246
testPlugin/views.py Normal file
View File

@@ -0,0 +1,246 @@
# -*- coding: utf-8 -*-
import json
import os
from django.shortcuts import render, get_object_or_404
from django.http import JsonResponse, HttpResponse
from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.contrib import messages
from django.utils import timezone
from plogical.httpProc import httpProc
from .models import TestPluginSettings, TestPluginLog
@login_required
def plugin_home(request):
"""Main plugin page with inline integration"""
try:
# Get or create plugin settings
settings, created = TestPluginSettings.objects.get_or_create(
user=request.user,
defaults={'plugin_enabled': True}
)
# Get recent logs
recent_logs = TestPluginLog.objects.filter(user=request.user).order_by('-timestamp')[:10]
context = {
'settings': settings,
'recent_logs': recent_logs,
'plugin_enabled': settings.plugin_enabled,
}
# Log page visit
TestPluginLog.objects.create(
user=request.user,
action='page_visit',
message='Visited plugin home page'
)
proc = httpProc(request, 'testPlugin/plugin_home.html', context, 'admin')
return proc.render()
except Exception as e:
return JsonResponse({'status': 0, 'error_message': str(e)})
@login_required
@require_http_methods(["POST"])
def test_button(request):
"""Handle test button click and show popup message"""
try:
settings, created = TestPluginSettings.objects.get_or_create(
user=request.user,
defaults={'plugin_enabled': True}
)
if not settings.plugin_enabled:
return JsonResponse({
'status': 0,
'error_message': 'Plugin is disabled. Please enable it first.'
})
# Increment test count
settings.test_count += 1
settings.save()
# Create log entry
TestPluginLog.objects.create(
user=request.user,
action='test_button_click',
message=f'Test button clicked (count: {settings.test_count})'
)
# Prepare popup message
popup_message = {
'type': 'success',
'title': 'Test Successful!',
'message': f'{settings.custom_message} (Clicked {settings.test_count} times)',
'timestamp': timezone.now().strftime('%Y-%m-%d %H:%M:%S')
}
return JsonResponse({
'status': 1,
'popup_message': popup_message,
'test_count': settings.test_count
})
except Exception as e:
return JsonResponse({'status': 0, 'error_message': str(e)})
@login_required
@require_http_methods(["POST"])
def toggle_plugin(request):
"""Toggle plugin enable/disable state"""
try:
settings, created = TestPluginSettings.objects.get_or_create(
user=request.user,
defaults={'plugin_enabled': True}
)
# Toggle the state
settings.plugin_enabled = not settings.plugin_enabled
settings.save()
# Log the action
action = 'enabled' if settings.plugin_enabled else 'disabled'
TestPluginLog.objects.create(
user=request.user,
action='plugin_toggle',
message=f'Plugin {action}'
)
return JsonResponse({
'status': 1,
'enabled': settings.plugin_enabled,
'message': f'Plugin {action} successfully'
})
except Exception as e:
return JsonResponse({'status': 0, 'error_message': str(e)})
@login_required
def plugin_settings(request):
"""Plugin settings page"""
try:
settings, created = TestPluginSettings.objects.get_or_create(
user=request.user,
defaults={'plugin_enabled': True}
)
context = {
'settings': settings,
}
proc = httpProc(request, 'testPlugin/plugin_settings.html', context, 'admin')
return proc.render()
except Exception as e:
return JsonResponse({'status': 0, 'error_message': str(e)})
@login_required
@require_http_methods(["POST"])
def update_settings(request):
"""Update plugin settings"""
try:
settings, created = TestPluginSettings.objects.get_or_create(
user=request.user,
defaults={'plugin_enabled': True}
)
data = json.loads(request.body)
custom_message = data.get('custom_message', settings.custom_message)
settings.custom_message = custom_message
settings.save()
# Log the action
TestPluginLog.objects.create(
user=request.user,
action='settings_update',
message=f'Settings updated: custom_message="{custom_message}"'
)
return JsonResponse({
'status': 1,
'message': 'Settings updated successfully'
})
except Exception as e:
return JsonResponse({'status': 0, 'error_message': str(e)})
@login_required
@require_http_methods(["POST"])
def install_plugin(request):
"""Install plugin (placeholder for future implementation)"""
try:
# Log the action
TestPluginLog.objects.create(
user=request.user,
action='plugin_install',
message='Plugin installation requested'
)
return JsonResponse({
'status': 1,
'message': 'Plugin installation completed successfully'
})
except Exception as e:
return JsonResponse({'status': 0, 'error_message': str(e)})
@login_required
@require_http_methods(["POST"])
def uninstall_plugin(request):
"""Uninstall plugin (placeholder for future implementation)"""
try:
# Log the action
TestPluginLog.objects.create(
user=request.user,
action='plugin_uninstall',
message='Plugin uninstallation requested'
)
return JsonResponse({
'status': 1,
'message': 'Plugin uninstallation completed successfully'
})
except Exception as e:
return JsonResponse({'status': 0, 'error_message': str(e)})
@login_required
def plugin_logs(request):
"""View plugin logs"""
try:
logs = TestPluginLog.objects.filter(user=request.user).order_by('-timestamp')[:50]
context = {
'logs': logs,
}
proc = httpProc(request, 'testPlugin/plugin_logs.html', context, 'admin')
return proc.render()
except Exception as e:
return JsonResponse({'status': 0, 'error_message': str(e)})
@login_required
def plugin_docs(request):
"""View plugin documentation"""
try:
context = {}
proc = httpProc(request, 'testPlugin/plugin_docs.html', context, 'admin')
return proc.render()
except Exception as e:
return JsonResponse({'status': 0, 'error_message': str(e)})