mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 16:11:09 -05:00
chore: fix pghistory migration deps and improve htmx utilities
- Update pghistory dependency from 0007 to 0006 in account migrations - Add docstrings and remove unused imports in htmx_forms.py - Add DJANGO_SETTINGS_MODULE bash commands to Claude settings - Add state transition definitions for ride statuses
This commit is contained in:
377
backend/static/js/moderation/history.js
Normal file
377
backend/static/js/moderation/history.js
Normal file
@@ -0,0 +1,377 @@
|
||||
/**
|
||||
* Moderation Transition History JavaScript
|
||||
* Handles AJAX loading and display of FSM transition history
|
||||
*/
|
||||
|
||||
let currentPage = 1;
|
||||
let nextPageUrl = null;
|
||||
let previousPageUrl = null;
|
||||
|
||||
/**
|
||||
* Format timestamp to human-readable format
|
||||
*/
|
||||
function formatTimestamp(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CSRF token from cookie
|
||||
*/
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and display transition history
|
||||
*/
|
||||
function loadHistory(url = null, filters = {}) {
|
||||
const tbody = document.getElementById('history-tbody');
|
||||
tbody.innerHTML = '<tr class="loading-row"><td colspan="7" class="text-center"><div class="spinner"></div> Loading history...</td></tr>';
|
||||
|
||||
// Build URL
|
||||
let fetchUrl = url || '/api/moderation/reports/all_history/';
|
||||
|
||||
// Add filters to URL if no custom URL provided
|
||||
if (!url && Object.keys(filters).length > 0) {
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(filters)) {
|
||||
if (value) {
|
||||
params.append(key, value);
|
||||
}
|
||||
}
|
||||
fetchUrl += '?' + params.toString();
|
||||
}
|
||||
|
||||
fetch(fetchUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
renderHistoryTable(data.results || data);
|
||||
updatePagination(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading history:', error);
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="text-center" style="color: red;">Error loading history. Please try again.</td></tr>';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render history table rows
|
||||
*/
|
||||
function renderHistoryTable(logs) {
|
||||
const tbody = document.getElementById('history-tbody');
|
||||
|
||||
if (!logs || logs.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="text-center">No transition history found.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = logs.map(log => `
|
||||
<tr>
|
||||
<td>${formatTimestamp(log.timestamp)}</td>
|
||||
<td><span class="badge badge-model">${log.model}</span></td>
|
||||
<td><a href="/moderation/${log.model}/${log.object_id}" class="object-link">${log.object_id}</a></td>
|
||||
<td><span class="badge badge-transition">${log.transition || '-'}</span></td>
|
||||
<td><span class="badge badge-state badge-state-${log.state}">${log.state}</span></td>
|
||||
<td>${log.user || '<em>System</em>'}</td>
|
||||
<td><button onclick="viewDetails(${log.id})" class="btn btn-sm btn-view">View</button></td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update pagination controls
|
||||
*/
|
||||
function updatePagination(data) {
|
||||
nextPageUrl = data.next || null;
|
||||
previousPageUrl = data.previous || null;
|
||||
|
||||
const prevBtn = document.getElementById('prev-page');
|
||||
const nextBtn = document.getElementById('next-page');
|
||||
const pageInfo = document.getElementById('page-info');
|
||||
|
||||
prevBtn.disabled = !previousPageUrl;
|
||||
nextBtn.disabled = !nextPageUrl;
|
||||
|
||||
// Calculate page number from count
|
||||
if (data.count) {
|
||||
const resultsPerPage = data.results ? data.results.length : 0;
|
||||
const totalPages = Math.ceil(data.count / (resultsPerPage || 1));
|
||||
pageInfo.textContent = `Page ${currentPage} of ${totalPages}`;
|
||||
} else {
|
||||
pageInfo.textContent = `Page ${currentPage}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* View details modal
|
||||
*/
|
||||
function viewDetails(logId) {
|
||||
const modal = document.getElementById('details-modal');
|
||||
const modalBody = document.getElementById('modal-body');
|
||||
|
||||
modalBody.innerHTML = '<div class="spinner"></div> Loading details...';
|
||||
modal.style.display = 'flex';
|
||||
|
||||
// Fetch detailed information filtered by id
|
||||
fetch(`/api/moderation/reports/all_history/?id=${logId}`, {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Handle both paginated and non-paginated responses
|
||||
let log = null;
|
||||
if (data.results && data.results.length > 0) {
|
||||
log = data.results[0];
|
||||
} else if (Array.isArray(data) && data.length > 0) {
|
||||
log = data[0];
|
||||
} else if (data.id) {
|
||||
// Single object response
|
||||
log = data;
|
||||
}
|
||||
|
||||
if (log) {
|
||||
modalBody.innerHTML = `
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<strong>ID:</strong> ${log.id}
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Timestamp:</strong> ${formatTimestamp(log.timestamp)}
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Model:</strong> ${log.model}
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Object ID:</strong> ${log.object_id}
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Transition:</strong> ${log.transition || '-'}
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>From State:</strong> ${log.from_state || '-'}
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>To State:</strong> ${log.to_state || log.state || '-'}
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>User:</strong> ${log.user || 'System'}
|
||||
</div>
|
||||
${log.reason ? `<div class="detail-item full-width"><strong>Reason:</strong><br>${log.reason}</div>` : ''}
|
||||
${log.description ? `<div class="detail-item full-width"><strong>Description:</strong><br>${log.description}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
modalBody.innerHTML = '<p>No log entry found with this ID.</p>';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading details:', error);
|
||||
modalBody.innerHTML = '<p style="color: red;">Error loading details.</p>';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal
|
||||
*/
|
||||
function closeModal() {
|
||||
const modal = document.getElementById('details-modal');
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current filters
|
||||
*/
|
||||
function getCurrentFilters() {
|
||||
return {
|
||||
model_type: document.getElementById('model-filter').value,
|
||||
state: document.getElementById('state-filter').value,
|
||||
start_date: document.getElementById('start-date').value,
|
||||
end_date: document.getElementById('end-date').value,
|
||||
user_id: document.getElementById('user-filter').value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listeners
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Apply filters button
|
||||
document.getElementById('apply-filters').addEventListener('click', () => {
|
||||
currentPage = 1;
|
||||
const filters = getCurrentFilters();
|
||||
loadHistory(null, filters);
|
||||
});
|
||||
|
||||
// Clear filters button
|
||||
document.getElementById('clear-filters').addEventListener('click', () => {
|
||||
document.getElementById('model-filter').value = '';
|
||||
document.getElementById('state-filter').value = '';
|
||||
document.getElementById('start-date').value = '';
|
||||
document.getElementById('end-date').value = '';
|
||||
document.getElementById('user-filter').value = '';
|
||||
currentPage = 1;
|
||||
loadHistory();
|
||||
});
|
||||
|
||||
// Pagination buttons
|
||||
document.getElementById('prev-page').addEventListener('click', () => {
|
||||
if (previousPageUrl) {
|
||||
currentPage--;
|
||||
loadHistory(previousPageUrl);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('next-page').addEventListener('click', () => {
|
||||
if (nextPageUrl) {
|
||||
currentPage++;
|
||||
loadHistory(nextPageUrl);
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal on background click
|
||||
document.getElementById('details-modal').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'details-modal') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Initial load
|
||||
loadHistory();
|
||||
});
|
||||
|
||||
// Additional CSS for badges (inline styles)
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.badge-model {
|
||||
background-color: #e7f3ff;
|
||||
color: #0066cc;
|
||||
}
|
||||
|
||||
.badge-transition {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.badge-state {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.badge-state-PENDING {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.badge-state-APPROVED {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.badge-state-REJECTED {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.badge-state-IN_PROGRESS {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.badge-state-COMPLETED {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.badge-state-ESCALATED {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.object-link {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.object-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn-view {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-view:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
padding: 10px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.detail-item.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.detail-item strong {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #666;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
Reference in New Issue
Block a user