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:
pacnpal
2025-12-21 17:33:24 -05:00
parent b9063ff4f8
commit 7ba0004c93
74 changed files with 11134 additions and 198 deletions

View 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);