Enhance AJAX navigation and pagination across dashboard templates

- Implemented AJAX-based navigation for links and forms to improve user experience.
- Added loading indicators during AJAX requests to enhance feedback.
- Refactored data tables and search results to load content dynamically via AJAX.
- Created partial templates for data tables and search results to streamline rendering.
- Updated pagination links to work with AJAX, maintaining browser history.
- Added JavaScript files for handling AJAX navigation and pagination.
- Improved session detail view with conditional rendering for action buttons.
- Updated Docker Compose file for consistency in version formatting.
- Created a TODO list for future enhancements and features.
This commit is contained in:
2025-05-17 02:28:38 +02:00
parent fe69bdbc94
commit 482bea1ba5
21 changed files with 1275 additions and 565 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ indent_size = 2
# CSS, JavaScript, and JSON files # CSS, JavaScript, and JSON files
[*.{css,scss,js,json}] [*.{css,scss,js,json}]
indent_size = 2 indent_size = 4
# Markdown files # Markdown files
[*.md] [*.md]
+24 -28
View File
@@ -1,30 +1,26 @@
{ {
"arrowParens": "always", "arrowParens": "always",
"bracketSpacing": true, "bracketSpacing": true,
"embeddedLanguageFormatting": "auto", "embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css", "htmlWhitespaceSensitivity": "css",
"insertPragma": false, "insertPragma": false,
"jsxSingleQuote": false, "jsxSingleQuote": false,
"printWidth": 100, "printWidth": 100,
"proseWrap": "preserve", "proseWrap": "preserve",
"quoteProps": "as-needed", "quoteProps": "as-needed",
"requirePragma": false, "requirePragma": false,
"semi": true, "semi": true,
"singleQuote": false, "singleQuote": false,
"tabWidth": 2, "tabWidth": 2,
"trailingComma": "es5", "trailingComma": "es5",
"useTabs": false, "useTabs": false,
"overrides": [ "overrides": [
{ {
"files": [ "files": ["*.html"],
"*.html" "options": {
], "parser": "jinja-template"
"options": { }
"parser": "jinja-template" }
} ],
} "plugins": ["prettier-plugin-jinja-template"]
],
"plugins": [
"prettier-plugin-jinja-template"
]
} }
+8
View File
@@ -3,16 +3,19 @@
## Core Features Implemented ## Core Features Implemented
1. **Multi-Tenant Architecture**: 1. **Multi-Tenant Architecture**:
- Companies have isolated data and user access - Companies have isolated data and user access
- Users belong to specific companies - Users belong to specific companies
- Role-based permissions (admin, company admin, regular user) - Role-based permissions (admin, company admin, regular user)
2. **Data Management**: 2. **Data Management**:
- CSV file upload and processing - CSV file upload and processing
- Data source management - Data source management
- Chat session records with comprehensive metadata - Chat session records with comprehensive metadata
3. **Dashboard Visualization**: 3. **Dashboard Visualization**:
- Interactive charts using Plotly.js - Interactive charts using Plotly.js
- Key metrics and KPIs - Key metrics and KPIs
- Time-series analysis - Time-series analysis
@@ -21,18 +24,21 @@
- Category distribution - Category distribution
4. **Search and Analysis**: 4. **Search and Analysis**:
- Full-text search across chat sessions - Full-text search across chat sessions
- Filtering by various attributes - Filtering by various attributes
- Detailed view of individual chat sessions - Detailed view of individual chat sessions
- Transcript viewing - Transcript viewing
5. **User Management**: 5. **User Management**:
- User registration and authentication - User registration and authentication
- Profile management - Profile management
- Password change functionality - Password change functionality
- Role assignment - Role assignment
6. **Admin Interface**: 6. **Admin Interface**:
- Company management - Company management
- User administration - User administration
- Data source oversight - Data source oversight
@@ -67,6 +73,7 @@
### Data Flow ### Data Flow
1. **Upload Process**: 1. **Upload Process**:
- File validation - File validation
- CSV parsing - CSV parsing
- Data normalization - Data normalization
@@ -74,6 +81,7 @@
- Association with company - Association with company
2. **Dashboard Generation**: 2. **Dashboard Generation**:
- Data aggregation - Data aggregation
- Statistical calculations - Statistical calculations
- Chart data preparation - Chart data preparation
+1 -4
View File
@@ -55,10 +55,7 @@ If you need to prevent Prettier from formatting a section of your template:
```html ```html
{# prettier-ignore #} {# prettier-ignore #}
<div> <div>This section will not be formatted by Prettier.</div>
This section will not be formatted
by Prettier.
</div>
<!-- prettier-ignore --> <!-- prettier-ignore -->
<div> <div>
+2
View File
@@ -70,10 +70,12 @@ This will create:
## Usage Flow ## Usage Flow
1. **Admin Setup**: 1. **Admin Setup**:
- Admin creates companies - Admin creates companies
- Admin creates users and assigns them to companies - Admin creates users and assigns them to companies
2. **Company Admin**: 2. **Company Admin**:
- Uploads CSV files with chat data - Uploads CSV files with chat data
- Creates and configures dashboards - Creates and configures dashboards
- Manages company users - Manages company users
+25 -19
View File
@@ -92,10 +92,12 @@ This will create:
### Admin Tasks ### Admin Tasks
1. **Access Admin Panel**: 1. **Access Admin Panel**:
- Go to <http://localhost/admin/> - Go to <http://localhost/admin/>
- Login with your admin credentials - Login with your admin credentials
2. **Create a Company**: 2. **Create a Company**:
- Go to Companies > Add Company - Go to Companies > Add Company
- Fill in the company details and save - Fill in the company details and save
@@ -108,10 +110,12 @@ This will create:
### Company Admin Tasks ### Company Admin Tasks
1. **Login to Dashboard**: 1. **Login to Dashboard**:
- Go to <http://localhost/> - Go to <http://localhost/>
- Login with your company admin credentials - Login with your company admin credentials
2. **Upload Chat Data**: 2. **Upload Chat Data**:
- Click on "Upload Data" in the sidebar - Click on "Upload Data" in the sidebar
- Fill in the data source details - Fill in the data source details
- Select a CSV file containing chat data - Select a CSV file containing chat data
@@ -126,11 +130,13 @@ This will create:
### Regular User Tasks ### Regular User Tasks
1. **View Dashboard**: 1. **View Dashboard**:
- Login with your user credentials - Login with your user credentials
- The dashboard will show automatically - The dashboard will show automatically
- Select different dashboards from the sidebar - Select different dashboards from the sidebar
2. **Search Chat Sessions**: 2. **Search Chat Sessions**:
- Click on "Search" in the top navigation - Click on "Search" in the top navigation
- Enter search terms - Enter search terms
- Use filters to refine results - Use filters to refine results
@@ -143,25 +149,25 @@ This will create:
Your CSV files should include the following columns: Your CSV files should include the following columns:
| Column | Description | Type | | Column | Description | Type |
| ----------------- | ------------------------------- | -------- | | ------------------- | ------------------------------- | -------- |
| session_id | Unique ID for the chat | String | | `session_id` | Unique ID for the chat | String |
| start_time | Session start time | Datetime | | `start_time` | Session start time | Datetime |
| end_time | Session end time | Datetime | | `end_time` | Session end time | Datetime |
| ip_address | User's IP address | String | | `ip_address` | User's IP address | String |
| country | User's country | String | | `country` | User's country | String |
| language | Chat language | String | | `language` | Chat language | String |
| messages_sent | Number of messages | Integer | | `messages_sent` | Number of messages | Integer |
| sentiment | Sentiment analysis result | String | | `sentiment` | Sentiment analysis result | String |
| escalated | Whether chat was escalated | Boolean | | `escalated` | Whether chat was escalated | Boolean |
| forwarded_hr | Whether chat was sent to HR | Boolean | | `forwarded_hr` | Whether chat was sent to HR | Boolean |
| full_transcript | Complete chat text | Text | | `full_transcript` | Complete chat text | Text |
| avg_response_time | Average response time (seconds) | Float | | `avg_response_time` | Average response time (seconds) | Float |
| tokens | Number of tokens used | Integer | | `tokens` | Number of tokens used | Integer |
| tokens_eur | Cost in EUR | Float | | `tokens_eur` | Cost in EUR | Float |
| category | Chat category | String | | `category` | Chat category | String |
| initial_msg | First user message | Text | | `initial_msg` | First user message | Text |
| user_rating | User satisfaction rating | String | | `user_rating` | User satisfaction rating | String |
Example CSV row: Example CSV row:
+21 -19
View File
@@ -32,7 +32,7 @@ A Django application that creates an analytics dashboard for chat session data.
2. Create a virtual environment and activate it: 2. Create a virtual environment and activate it:
```sh ```sh
uv venv uv venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate source .venv/bin/activate # On Windows: .venv\Scripts\activate
``` ```
@@ -99,23 +99,25 @@ A Django application that creates an analytics dashboard for chat session data.
The CSV file should contain the following columns: The CSV file should contain the following columns:
- session_id: Unique identifier for the chat session | Column | Description |
- start_time: When the session started (datetime) | ------------------- | ------------------------------------------------------ |
- end_time: When the session ended (datetime) | `session_id` | Unique identifier for the chat session |
- ip_address: IP address of the user | `start_time` | When the session started (datetime) |
- country: Country of the user | `end_time` | When the session ended (datetime) |
- language: Language used in the conversation | `ip_address` | IP address of the user |
- messages_sent: Number of messages in the conversation (integer) | `country` | Country of the user |
- sentiment: Sentiment analysis of the conversation (string) | `language` | Language used in the conversation |
- escalated: Whether the conversation was escalated (boolean) | `messages_sent` | Number of messages in the conversation (integer) |
- forwarded_hr: Whether the conversation was forwarded to HR (boolean) | `sentiment` | Sentiment analysis of the conversation (string) |
- full_transcript: Full transcript of the conversation (text) | `escalated` | Whether the conversation was escalated (boolean) |
- avg_response_time: Average response time in seconds (float) | `forwarded_hr` | Whether the conversation was forwarded to HR (boolean) |
- tokens: Total number of tokens used (integer) | `full_transcript` | Full transcript of the conversation (text) |
- tokens_eur: Cost of tokens in EUR (float) | `avg_response_time` | Average response time in seconds (float) |
- category: Category of the conversation (string) | `tokens` | Total number of tokens used (integer) |
- initial_msg: First message from the user (text) | `tokens_eur` | Cost of tokens in EUR (float) |
- user_rating: User rating of the conversation (string) | `category` | Category of the conversation (string) |
| `initial_msg` | First message from the user (text) |
| `user_rating` | User rating of the conversation (string) |
## Future Enhancements ## Future Enhancements
@@ -128,4 +130,4 @@ The CSV file should contain the following columns:
## License ## License
This project is licensed under the MIT License - see the LICENSE file for details. This project is unlicensed. Usage is restricted to personal and educational purposes only. For commercial use, please contact the author.
+13
View File
@@ -0,0 +1,13 @@
# TODO List
- When I zoom into the dasboard page, the graphs don't scale/adjust to fit the window until I completely refresh the page, can we solve that?
- Add export to CSV and PDF functionality to the dashboard.
- Add a button to download the CSV file for the selected company.
- Make it possible to modify the column names in the CSV file through the admin interface.
- Add possibility to add a company logo in the admin interface.
- Add periodic download from https://proto.notso.ai/jumbo/chats possibility for the jumbo company.
- Authentication: Basic Auth
- URL: https://proto.notso.ai/jumbo/chats
- Username: jumboadmin
- Password: jumboadmin
+128 -11
View File
@@ -9,6 +9,7 @@ from django.core.paginator import Paginator
from django.db.models import Avg, Q from django.db.models import Avg, Q
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string
from django.utils import timezone from django.utils import timezone
from .forms import DashboardForm, DataSourceUploadForm from .forms import DashboardForm, DataSourceUploadForm
@@ -16,6 +17,11 @@ from .models import ChatSession, Dashboard, DataSource
from .utils import generate_dashboard_data, process_csv_file from .utils import generate_dashboard_data, process_csv_file
def is_ajax_navigation(request):
"""Check if this is an AJAX navigation request"""
return request.headers.get("X-AJAX-Navigation") == "true"
@login_required @login_required
def dashboard_view(request): def dashboard_view(request):
"""Main dashboard view""" """Main dashboard view"""
@@ -57,23 +63,27 @@ def dashboard_view(request):
# Generate dashboard data # Generate dashboard data
dashboard_data = generate_dashboard_data(selected_dashboard.data_sources.all()) dashboard_data = generate_dashboard_data(selected_dashboard.data_sources.all())
# Convert dashboard data to JSON for use in JavaScript # Convert each component of dashboard data to JSON
dashboard_data_json = json.dumps( sentiment_data_json = json.dumps(dashboard_data["sentiment_data"])
{ country_data_json = json.dumps(dashboard_data["country_data"])
"sentiment_data": dashboard_data["sentiment_data"], category_data_json = json.dumps(dashboard_data["category_data"])
"country_data": dashboard_data["country_data"], time_series_data_json = json.dumps(dashboard_data["time_series_data"])
"category_data": dashboard_data["category_data"],
"time_series_data": dashboard_data["time_series_data"],
}
)
context = { context = {
"dashboards": dashboards, "dashboards": dashboards,
"selected_dashboard": selected_dashboard, "selected_dashboard": selected_dashboard,
"dashboard_data": dashboard_data, "dashboard_data": dashboard_data,
"dashboard_data_json": dashboard_data_json, "sentiment_data_json": sentiment_data_json,
"country_data_json": country_data_json,
"category_data_json": category_data_json,
"time_series_data_json": time_series_data_json,
} }
# Check if this is an AJAX navigation request
if is_ajax_navigation(request):
html_content = render_to_string("dashboard/dashboard.html", context, request=request)
return JsonResponse({"html": html_content, "title": "Dashboard | Chat Analytics"})
return render(request, "dashboard/dashboard.html", context) return render(request, "dashboard/dashboard.html", context)
@@ -124,6 +134,11 @@ def upload_data_view(request):
"data_sources": data_sources, "data_sources": data_sources,
} }
# Check if this is an AJAX navigation request
if is_ajax_navigation(request):
html_content = render_to_string("dashboard/upload.html", context, request=request)
return JsonResponse({"html": html_content, "title": "Upload Data | Chat Analytics"})
return render(request, "dashboard/upload.html", context) return render(request, "dashboard/upload.html", context)
@@ -155,6 +170,11 @@ def data_source_detail_view(request, data_source_id):
"page_obj": page_obj, "page_obj": page_obj,
} }
# Check if this is an AJAX navigation request
if is_ajax_navigation(request):
html_content = render_to_string("dashboard/data_source_detail.html", context, request=request)
return JsonResponse({"html": html_content, "title": f"{data_source.name} | Chat Analytics"})
return render(request, "dashboard/data_source_detail.html", context) return render(request, "dashboard/data_source_detail.html", context)
@@ -177,6 +197,11 @@ def chat_session_detail_view(request, session_id):
"session": chat_session, "session": chat_session,
} }
# Check if this is an AJAX navigation request
if is_ajax_navigation(request):
html_content = render_to_string("dashboard/chat_session_detail.html", context, request=request)
return JsonResponse({"html": html_content, "title": f"Chat Session {session_id} | Chat Analytics"})
return render(request, "dashboard/chat_session_detail.html", context) return render(request, "dashboard/chat_session_detail.html", context)
@@ -209,6 +234,11 @@ def create_dashboard_view(request):
"is_create": True, "is_create": True,
} }
# Check if this is an AJAX navigation request
if is_ajax_navigation(request):
html_content = render_to_string("dashboard/dashboard_form.html", context, request=request)
return JsonResponse({"html": html_content, "title": "Create Dashboard | Chat Analytics"})
return render(request, "dashboard/dashboard_form.html", context) return render(request, "dashboard/dashboard_form.html", context)
@@ -244,6 +274,11 @@ def edit_dashboard_view(request, dashboard_id):
"is_create": False, "is_create": False,
} }
# Check if this is an AJAX navigation request
if is_ajax_navigation(request):
html_content = render_to_string("dashboard/dashboard_form.html", context, request=request)
return JsonResponse({"html": html_content, "title": f"Edit Dashboard: {dashboard.name} | Chat Analytics"})
return render(request, "dashboard/dashboard_form.html", context) return render(request, "dashboard/dashboard_form.html", context)
@@ -313,8 +348,33 @@ def dashboard_data_api(request, dashboard_id):
if not company: if not company:
return JsonResponse({"error": "User not associated with a company"}, status=403) return JsonResponse({"error": "User not associated with a company"}, status=403)
# Get time range filter if provided
time_range = request.GET.get("time_range", "all")
dashboard = get_object_or_404(Dashboard, id=dashboard_id, company=company) dashboard = get_object_or_404(Dashboard, id=dashboard_id, company=company)
dashboard_data = generate_dashboard_data(dashboard.data_sources.all())
# Get data sources for this dashboard
data_sources = dashboard.data_sources.all()
# Apply time filter if needed
filtered_data_sources = data_sources
if time_range and time_range != "all":
# This is a placeholder comment - implement time filtering in a real app
# You would filter ChatSessions based on time_range here
pass
# Generate the dashboard data
dashboard_data = generate_dashboard_data(filtered_data_sources)
# Ensure values are JSON serializable
for key in ["sentiment_data", "country_data", "category_data"]:
dashboard_data[key] = list(dashboard_data[key])
# Format time series data for proper date serialization
if "time_series_data" in dashboard_data:
for item in dashboard_data["time_series_data"]:
if "date" in item and not isinstance(item["date"], str):
item["date"] = item["date"].strftime("%Y-%m-%d")
return JsonResponse(dashboard_data) return JsonResponse(dashboard_data)
@@ -373,6 +433,34 @@ def search_chat_sessions(request):
"data_source": data_source, "data_source": data_source,
} }
# Check if this is an AJAX navigation request
if is_ajax_navigation(request):
html_content = render_to_string("dashboard/search_results.html", context, request=request)
return JsonResponse({"html": html_content, "title": "Search Chat Sessions | Chat Analytics"})
# Check if this is an AJAX pagination request
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
return JsonResponse(
{
"status": "success",
"html_data": render(request, "dashboard/partials/search_results_table.html", context).content.decode(
"utf-8"
),
"page_obj": {
"number": page_obj.number,
"has_previous": page_obj.has_previous(),
"has_next": page_obj.has_next(),
"previous_page_number": page_obj.previous_page_number() if page_obj.has_previous() else None,
"next_page_number": page_obj.next_page_number() if page_obj.has_next() else None,
"paginator": {
"num_pages": page_obj.paginator.num_pages,
"count": page_obj.paginator.count,
},
},
"query": query,
}
)
return render(request, "dashboard/search_results.html", context) return render(request, "dashboard/search_results.html", context)
@@ -449,4 +537,33 @@ def data_view(request):
"escalation_rate": escalation_rate, "escalation_rate": escalation_rate,
} }
# Check if this is an AJAX navigation request
if is_ajax_navigation(request):
html_content = render_to_string("dashboard/data_view.html", context, request=request)
return JsonResponse({"html": html_content, "title": "Data View | Chat Analytics"})
# Check if this is an AJAX pagination request
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
return JsonResponse(
{
"status": "success",
"html_data": render(request, "dashboard/partials/data_table.html", context).content.decode("utf-8"),
"page_obj": {
"number": page_obj.number,
"has_previous": page_obj.has_previous(),
"has_next": page_obj.has_next(),
"previous_page_number": page_obj.previous_page_number() if page_obj.has_previous() else None,
"next_page_number": page_obj.next_page_number() if page_obj.has_next() else None,
"paginator": {
"num_pages": page_obj.paginator.num_pages,
"count": page_obj.paginator.count,
},
},
"view": view,
"avg_response_time": avg_response_time,
"avg_messages": avg_messages,
"escalation_rate": escalation_rate,
}
)
return render(request, "dashboard/data_view.html", context) return render(request, "dashboard/data_view.html", context)
@@ -260,3 +260,21 @@
font-size: 1.5rem; font-size: 1.5rem;
} }
} }
/* --- Stat Boxes Alignment Fix (Bottom Align, No Overlap) --- */
.stats-row {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
align-items: stretch;
}
.stats-card {
flex: 1 1 0;
min-width: 200px;
display: flex;
flex-direction: column;
justify-content: flex-end; /* Push content to bottom */
align-items: flex-start;
box-sizing: border-box;
/* Remove min-height/height for natural stretch */
}
@@ -0,0 +1,273 @@
/**
* ajax-navigation.js - JavaScript for AJAX-based navigation across the entire application
*
* This script handles AJAX navigation between pages in the Chat Analytics Dashboard.
* It intercepts link clicks, loads content via AJAX, and updates the browser history.
*/
document.addEventListener("DOMContentLoaded", function () {
// Only initialize if AJAX navigation is enabled
if (typeof ENABLE_AJAX_NAVIGATION !== "undefined" && ENABLE_AJAX_NAVIGATION) {
setupAjaxNavigation();
}
// Function to set up AJAX navigation for the application
function setupAjaxNavigation() {
// Configuration
const config = {
mainContentSelector: "#main-content", // Selector for the main content area
navLinkSelector: ".ajax-nav-link", // Selector for links to handle with AJAX
loadingIndicatorId: "nav-loading-indicator", // ID of the loading indicator
excludePatterns: [
// URL patterns to exclude from AJAX navigation
/\.(pdf|xlsx?|docx?|csv|zip|png|jpe?g|gif|svg)$/i, // File downloads
/\/admin\//, // Admin pages
/\/accounts\/logout\//, // Logout page
/\/api\//, // API endpoints
],
};
// Create and insert the loading indicator
if (!document.getElementById(config.loadingIndicatorId)) {
const loadingIndicator = document.createElement("div");
loadingIndicator.id = config.loadingIndicatorId;
loadingIndicator.className = "position-fixed top-0 start-0 end-0";
loadingIndicator.innerHTML =
'<div class="progress" style="height: 3px; border-radius: 0;"><div class="progress-bar progress-bar-striped progress-bar-animated bg-primary" style="width: 100%"></div></div>';
loadingIndicator.style.display = "none";
loadingIndicator.style.zIndex = "9999";
document.body.appendChild(loadingIndicator);
}
// Get the loading indicator element
const loadingIndicator = document.getElementById(config.loadingIndicatorId);
// Get the main content container
const mainContent = document.querySelector(config.mainContentSelector);
if (!mainContent) {
console.warn("Main content container not found. AJAX navigation disabled.");
return;
}
// Function to check if a URL should be excluded from AJAX navigation
function shouldExcludeUrl(url) {
for (const pattern of config.excludePatterns) {
if (pattern.test(url)) {
return true;
}
}
return false;
}
// Function to show the loading indicator
function showLoading() {
loadingIndicator.style.display = "block";
}
// Function to hide the loading indicator
function hideLoading() {
loadingIndicator.style.display = "none";
}
// Function to handle AJAX page navigation
function handlePageNavigation(url, pushState = true) {
if (shouldExcludeUrl(url)) {
window.location.href = url;
return;
}
showLoading();
const currentScrollPos = window.scrollY;
fetch(url, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"X-AJAX-Navigation": "true",
Accept: "text/html",
},
})
.then((response) => {
if (!response.ok) throw new Error(`Network response was not ok: ${response.status}`);
return response.text();
})
.then((html) => {
// Parse the HTML and extract #main-content
const tempDiv = document.createElement("div");
tempDiv.innerHTML = html;
const newContent = tempDiv.querySelector(config.mainContentSelector);
if (!newContent) throw new Error("Could not find main content in the response");
mainContent.innerHTML = newContent.innerHTML;
// Update the page title
const titleMatch = html.match(/<title>(.*?)<\/title>/i);
if (titleMatch) document.title = titleMatch[1];
// Re-initialize dynamic content
reloadScripts(mainContent);
attachEventListeners();
initializePageScripts();
if (pushState) {
history.pushState(
{ url: url, title: document.title, scrollPos: currentScrollPos },
document.title,
url
);
window.scrollTo({ top: 0, behavior: "smooth" });
} else if (window.history.state && window.history.state.scrollPos) {
window.scrollTo({ top: window.history.state.scrollPos });
}
hideLoading();
})
.catch((error) => {
console.error("Error during AJAX navigation:", error);
hideLoading();
window.location.href = url;
});
}
// Function to reload and execute scripts in new content
function reloadScripts(container) {
const scripts = container.getElementsByTagName("script");
for (let script of scripts) {
const newScript = document.createElement("script");
// Copy all attributes
Array.from(script.attributes).forEach((attr) => {
newScript.setAttribute(attr.name, attr.value);
});
// Copy inline script content
newScript.textContent = script.textContent;
// Replace old script with new one
script.parentNode.replaceChild(newScript, script);
}
}
// Function to handle form submissions
function handleFormSubmission(form, e) {
e.preventDefault();
// Show loading indicator
showLoading();
// Get form data
const formData = new FormData(form);
const method = form.method.toLowerCase();
const url = form.action || window.location.href;
// Configure fetch options
const fetchOptions = {
method: method,
headers: {
"X-AJAX-Navigation": "true",
},
};
// Handle different HTTP methods
if (method === "get") {
const queryParams = new URLSearchParams(formData).toString();
handlePageNavigation(url + (queryParams ? "?" + queryParams : ""));
} else {
fetchOptions.body = formData;
fetch(url, fetchOptions)
.then((response) => {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return response.json();
})
.then((data) => {
if (data.redirect) {
// Handle server-side redirects
handlePageNavigation(data.redirect, true);
} else {
// Update page content
mainContent.innerHTML = data.html;
document.title = data.title || document.title;
// Re-initialize dynamic content
reloadScripts(mainContent);
attachEventListeners();
initializePageScripts();
// Update URL if needed
if (data.url) {
history.pushState({ url: data.url }, document.title, data.url);
}
}
})
.catch((error) => {
console.error("Form submission error:", error);
// Fallback to traditional form submission
form.submit();
})
.finally(() => {
hideLoading();
});
}
}
// Function to initialize scripts needed for the new page content
function initializePageScripts() {
// Re-initialize any custom scripts that might be needed
if (typeof setupAjaxPagination === "function") {
setupAjaxPagination();
}
// Initialize Bootstrap tooltips, popovers, etc.
if (typeof bootstrap !== "undefined") {
// Initialize tooltips
const tooltipTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="tooltip"]')
);
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// Initialize popovers
const popoverTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="popover"]')
);
popoverTriggerList.map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl);
});
}
}
// Function to attach event listeners to forms and links
function attachEventListeners() {
// Handle AJAX navigation links
document.querySelectorAll(config.navLinkSelector).forEach((link) => {
if (!link.dataset.ajaxNavInitialized) {
link.addEventListener("click", function (e) {
if (e.ctrlKey || e.metaKey || e.shiftKey || shouldExcludeUrl(this.href)) {
return; // Let the browser handle these cases
}
e.preventDefault();
handlePageNavigation(this.href);
});
link.dataset.ajaxNavInitialized = "true";
}
});
// Handle forms with AJAX
document
.querySelectorAll("form.ajax-form, form.search-form, form.filter-form")
.forEach((form) => {
if (!form.dataset.ajaxFormInitialized) {
form.addEventListener("submit", (e) => handleFormSubmission(form, e));
form.dataset.ajaxFormInitialized = "true";
}
});
}
// Initial attachment of event listeners
attachEventListeners();
// Handle browser back/forward buttons
window.addEventListener("popstate", function (event) {
if (event.state && event.state.url) {
handlePageNavigation(event.state.url, false);
} else {
// Fallback to current URL if no state
handlePageNavigation(window.location.href, false);
}
});
}
});
@@ -0,0 +1,106 @@
/**
* ajax-pagination.js - Common JavaScript for AJAX pagination across the application
*
* This script handles AJAX-based pagination for all pages in the Chat Analytics Dashboard.
* It intercepts pagination link clicks, loads content via AJAX, and updates the browser history.
*/
document.addEventListener("DOMContentLoaded", function () {
// Initialize AJAX pagination
setupAjaxPagination();
// Function to set up AJAX pagination for the entire application
function setupAjaxPagination() {
// Configuration - can be customized per page if needed
const config = {
contentContainerId: "ajax-content-container", // ID of the container to update
loadingSpinnerId: "ajax-loading-spinner", // ID of the loading spinner
paginationLinkClass: "pagination-link", // Class for pagination links
retryMessage: "An error occurred while loading data. Please try again.",
};
// Get container elements
const contentContainer = document.getElementById(config.contentContainerId);
const loadingSpinner = document.getElementById(config.loadingSpinnerId);
// Exit if the page doesn't have the required elements
if (!contentContainer || !loadingSpinner) return;
// Function to handle pagination clicks
function setupPaginationListeners() {
document.querySelectorAll("." + config.paginationLinkClass).forEach((link) => {
link.addEventListener("click", function (e) {
e.preventDefault();
handleAjaxNavigation(this.href);
// Get the page number if available
const page = this.getAttribute("data-page");
// Update browser URL without refreshing
const newUrl = this.href;
history.pushState({ url: newUrl, page: page }, "", newUrl);
});
});
}
// Function to handle AJAX navigation
function handleAjaxNavigation(url) {
// Show loading spinner
contentContainer.classList.add("d-none");
loadingSpinner.classList.remove("d-none");
// Fetch data via AJAX
fetch(url, {
headers: {
"X-Requested-With": "XMLHttpRequest",
},
})
.then((response) => {
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status}`);
}
return response.json();
})
.then((data) => {
if (data.status === "success") {
// Update the content
contentContainer.innerHTML = data.html_data;
// Re-attach event listeners to new pagination links
setupPaginationListeners();
// Update any summary data if present and the page provides it
if (typeof updateSummary === "function" && data.summary) {
updateSummary(data);
}
// Hide loading spinner, show content
loadingSpinner.classList.add("d-none");
contentContainer.classList.remove("d-none");
// Scroll to top of the content container
contentContainer.scrollIntoView({ behavior: "smooth", block: "start" });
}
})
.catch((error) => {
console.error("Error fetching data:", error);
loadingSpinner.classList.add("d-none");
contentContainer.classList.remove("d-none");
alert(config.retryMessage);
});
}
// Initial setup of event listeners
setupPaginationListeners();
// Handle browser back/forward buttons
window.addEventListener("popstate", function (event) {
if (event.state && event.state.url) {
handleAjaxNavigation(event.state.url);
} else {
// If no state, fetch current URL
handleAjaxNavigation(window.location.href);
}
});
}
});
+54 -29
View File
@@ -55,8 +55,14 @@ document.addEventListener("DOMContentLoaded", function () {
document.querySelector("main").appendChild(loadingOverlay); document.querySelector("main").appendChild(loadingOverlay);
fetch(`/dashboard/api/dashboard/${dashboardId}/data/?time_range=${timeRange || "all"}`) fetch(`/dashboard/api/dashboard/${dashboardId}/data/?time_range=${timeRange || "all"}`)
.then((response) => response.json()) .then((response) => {
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status}`);
}
return response.json();
})
.then((data) => { .then((data) => {
console.log("Dashboard API response:", data);
updateDashboardStats(data); updateDashboardStats(data);
updateDashboardCharts(data); updateDashboardCharts(data);
@@ -115,40 +121,59 @@ document.addEventListener("DOMContentLoaded", function () {
// Function to update dashboard charts // Function to update dashboard charts
function updateDashboardCharts(data) { function updateDashboardCharts(data) {
// Check if Plotly is available
if (!window.Plotly) {
console.error("Plotly library not loaded!");
document.querySelectorAll(".chart-container").forEach((container) => {
container.innerHTML =
'<div class="text-center py-5"><p class="text-danger">Chart library not available. Please refresh the page.</p></div>';
});
return;
}
// Update sessions over time chart // Update sessions over time chart
const timeSeriesData = data.time_series_data; const timeSeriesData = data.time_series_data;
if (timeSeriesData && timeSeriesData.length > 0 && window.Plotly) { if (timeSeriesData && timeSeriesData.length > 0) {
const timeSeriesX = timeSeriesData.map((item) => item.date); try {
const timeSeriesY = timeSeriesData.map((item) => item.count); const timeSeriesX = timeSeriesData.map((item) => item.date);
const timeSeriesY = timeSeriesData.map((item) => item.count);
Plotly.react( Plotly.react(
"sessions-time-chart", "sessions-time-chart",
[ [
{
x: timeSeriesX,
y: timeSeriesY,
type: "scatter",
mode: "lines+markers",
line: {
color: "rgb(75, 192, 192)",
width: 2,
},
marker: {
color: "rgb(75, 192, 192)",
size: 6,
},
},
],
{ {
x: timeSeriesX, margin: { t: 10, r: 10, b: 40, l: 40 },
y: timeSeriesY, xaxis: {
type: "scatter", title: "Date",
mode: "lines+markers",
line: {
color: "rgb(75, 192, 192)",
width: 2,
}, },
marker: { yaxis: {
color: "rgb(75, 192, 192)", title: "Number of Sessions",
size: 6,
}, },
}, }
], );
{ } catch (error) {
margin: { t: 10, r: 10, b: 40, l: 40 }, console.error("Error rendering time series chart:", error);
xaxis: { document.getElementById("sessions-time-chart").innerHTML =
title: "Date", '<div class="text-center py-5"><p class="text-danger">Error rendering chart.</p></div>';
}, }
yaxis: { } else {
title: "Number of Sessions", document.getElementById("sessions-time-chart").innerHTML =
}, '<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>';
}
);
} }
// Update sentiment chart // Update sentiment chart
+47 -14
View File
@@ -49,21 +49,21 @@
<ul class="navbar-nav me-auto mb-2 mb-md-0"> <ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}"
href="{% url 'dashboard' %}" href="{% url 'dashboard' %}"
>Dashboard</a >Dashboard</a
> >
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link {% if request.resolver_match.url_name == 'upload_data' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'upload_data' %}active{% endif %}"
href="{% url 'upload_data' %}" href="{% url 'upload_data' %}"
>Upload Data</a >Upload Data</a
> >
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link {% if request.resolver_match.url_name == 'search_chat_sessions' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'search_chat_sessions' %}active{% endif %}"
href="{% url 'search_chat_sessions' %}" href="{% url 'search_chat_sessions' %}"
>Search</a >Search</a
> >
@@ -86,7 +86,9 @@
{{ user.username }} {{ user.username }}
</button> </button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown"> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<li><a class="dropdown-item" href="{% url 'profile' %}">Profile</a></li> <li>
<a class="dropdown-item ajax-nav-link" href="{% url 'profile' %}">Profile</a>
</li>
{% if user.is_staff %} {% if user.is_staff %}
<li><a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a></li> <li><a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a></li>
{% endif %} {% endif %}
@@ -117,7 +119,7 @@
<ul class="nav flex-column"> <ul class="nav flex-column">
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}"
href="{% url 'dashboard' %}" href="{% url 'dashboard' %}"
> >
<i class="fas fa-tachometer-alt me-2"></i> <i class="fas fa-tachometer-alt me-2"></i>
@@ -126,7 +128,7 @@
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link {% if request.resolver_match.url_name == 'upload_data' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'upload_data' %}active{% endif %}"
href="{% url 'upload_data' %}" href="{% url 'upload_data' %}"
> >
<i class="fas fa-upload me-2"></i> <i class="fas fa-upload me-2"></i>
@@ -135,7 +137,7 @@
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link {% if request.resolver_match.url_name == 'search_chat_sessions' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'search_chat_sessions' %}active{% endif %}"
href="{% url 'search_chat_sessions' %}" href="{% url 'search_chat_sessions' %}"
> >
<i class="fas fa-search me-2"></i> <i class="fas fa-search me-2"></i>
@@ -144,7 +146,7 @@
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link {% if request.resolver_match.url_name == 'data_view' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'data_view' %}active{% endif %}"
href="{% url 'data_view' %}" href="{% url 'data_view' %}"
> >
<i class="fas fa-table me-2"></i> <i class="fas fa-table me-2"></i>
@@ -202,23 +204,54 @@
{# </div> #} {# </div> #}
{# {% endif %} #} {# {% endif %} #}
{% block content %} <div id="main-content">
{% endblock %} {% block content %}
{% endblock %}
</div>
</main> </main>
</div> </div>
</div> </div>
<footer> <footer>
<div class="container"> <div class="container">
<p>&copy; {% now "Y" %} Chat Analytics Dashboard. All rights reserved.</p> <p>&copy; {% now "Y" %} KJANAT All rights reserved. | Chat Analytics Dashboard.</p>
</div> </div>
</footer> </footer>
<!-- Bootstrap JS --> <!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/js/bootstrap.bundle.min.js"></script> <script
src="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/js/bootstrap.bundle.min.js"
crossorigin="anonymous"
></script>
<!-- jQuery (for Ajax) --> <!-- jQuery (for Ajax) -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> <script
src="https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js"
crossorigin="anonymous"
></script>
<!-- Custom JavaScript -->
<script src="{% static 'js/main.js' %}"></script>
<script src="{% static 'js/ajax-pagination.js' %}"></script>
<script src="{% static 'js/ajax-navigation.js' %}"></script>
<!-- Enable AJAX Navigation -->
<script>
// Enable AJAX navigation for the entire application
var ENABLE_AJAX_NAVIGATION = true;
</script>
<!-- Check if Plotly loaded successfully -->
<script>
if (typeof Plotly === "undefined") {
console.error("Plotly library failed to load. Will attempt to load fallback.");
// Try to load Plotly from alternative source
const script = document.createElement("script");
script.src = "https://cdn.jsdelivr.net/npm/plotly.js@latest/dist/plotly.min.js";
script.async = true;
script.crossOrigin = "anonymous";
document.head.appendChild(script);
}
</script>
{% block extra_js %} {% block extra_js %}
{{ block.super }} {{ block.super }}
@@ -147,139 +147,161 @@
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
<!-- prettier-ignore-start -->
<!-- Store the JSON data in script tags to avoid parsing issues -->
<script type="application/json" id="time-series-data">{{ time_series_data_json|safe }}</script>
<script type="application/json" id="sentiment-data">{{ sentiment_data_json|safe }}</script>
<script type="application/json" id="country-data">{{ country_data_json|safe }}</script>
<script type="application/json" id="category-data">{{ category_data_json|safe }}</script>
<!-- prettier-ignore-end -->
<script> <script>
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// Parse the dashboard data from JSON try {
const dashboardData = JSON.parse("{{ dashboard_data_json|safe }}"); // Parse the dashboard data components from script tags
const timeSeriesData = JSON.parse(document.getElementById("time-series-data").textContent);
const sentimentData = JSON.parse(document.getElementById("sentiment-data").textContent);
const countryData = JSON.parse(document.getElementById("country-data").textContent);
const categoryData = JSON.parse(document.getElementById("category-data").textContent);
// Sessions over time chart console.log("Time series data loaded:", timeSeriesData);
const timeSeriesData = dashboardData.time_series_data; console.log("Sentiment data loaded:", sentimentData);
const timeSeriesX = timeSeriesData.map((item) => item.date); console.log("Country data loaded:", countryData);
const timeSeriesY = timeSeriesData.map((item) => item.count); console.log("Category data loaded:", categoryData);
Plotly.newPlot( // Sessions over time chart
"sessions-time-chart", if (timeSeriesData && timeSeriesData.length > 0) {
[ const timeSeriesX = timeSeriesData.map((item) => item.date);
{ const timeSeriesY = timeSeriesData.map((item) => item.count);
x: timeSeriesX,
y: timeSeriesY, Plotly.newPlot(
type: "scatter", "sessions-time-chart",
mode: "lines+markers", [
line: { {
color: "rgb(75, 192, 192)", x: timeSeriesX,
width: 2, y: timeSeriesY,
}, type: "scatter",
marker: { mode: "lines+markers",
color: "rgb(75, 192, 192)", line: {
size: 6, color: "rgb(75, 192, 192)",
}, width: 2,
}, },
], marker: {
{ color: "rgb(75, 192, 192)",
margin: { t: 10, r: 10, b: 40, l: 40 }, size: 6,
xaxis: { },
title: "Date", },
}, ],
yaxis: { {
title: "Number of Sessions", margin: { t: 10, r: 10, b: 40, l: 40 },
}, xaxis: {
title: "Date",
},
yaxis: {
title: "Number of Sessions",
},
}
);
} else {
document.getElementById("sessions-time-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>';
} }
);
// Sentiment analysis chart // Sentiment analysis chart
const sentimentData = dashboardData.sentiment_data; if (sentimentData && sentimentData.length > 0) {
const sentimentLabels = sentimentData.map((item) => item.sentiment);
const sentimentValues = sentimentData.map((item) => item.count);
const sentimentColors = sentimentLabels.map((sentiment) => {
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)";
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
return "rgb(201, 203, 207)";
});
if (sentimentData.length > 0) { Plotly.newPlot(
const sentimentLabels = sentimentData.map((item) => item.sentiment); "sentiment-chart",
const sentimentValues = sentimentData.map((item) => item.count); [
const sentimentColors = sentimentLabels.map((sentiment) => { {
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)"; values: sentimentValues,
if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)"; labels: sentimentLabels,
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)"; type: "pie",
return "rgb(201, 203, 207)"; marker: {
colors: sentimentColors,
},
hole: 0.4,
textinfo: "label+percent",
insidetextorientation: "radial",
},
],
{
margin: { t: 10, r: 10, b: 10, l: 10 },
}
);
} else {
document.getElementById("sentiment-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No sentiment data available</p></div>';
}
// Country chart
if (countryData && countryData.length > 0) {
const countryLabels = countryData.map((item) => item.country);
const countryValues = countryData.map((item) => item.count);
Plotly.newPlot(
"country-chart",
[
{
x: countryValues,
y: countryLabels,
type: "bar",
orientation: "h",
marker: {
color: "rgb(54, 162, 235)",
},
},
],
{
margin: { t: 10, r: 10, b: 40, l: 100 },
xaxis: {
title: "Number of Sessions",
},
}
);
} else {
document.getElementById("country-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No country data available</p></div>';
}
// Category chart
if (categoryData && categoryData.length > 0) {
const categoryLabels = categoryData.map((item) => item.category);
const categoryValues = categoryData.map((item) => item.count);
Plotly.newPlot(
"category-chart",
[
{
labels: categoryLabels,
values: categoryValues,
type: "pie",
textinfo: "label+percent",
insidetextorientation: "radial",
},
],
{
margin: { t: 10, r: 10, b: 10, l: 10 },
}
);
} else {
document.getElementById("category-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No category data available</p></div>';
}
} catch (error) {
console.error("Error rendering charts:", error);
document.querySelectorAll(".chart-container").forEach((container) => {
container.innerHTML =
'<div class="text-center py-5"><p class="text-danger">Error loading chart data. Please refresh the page.</p></div>';
}); });
Plotly.newPlot(
"sentiment-chart",
[
{
values: sentimentValues,
labels: sentimentLabels,
type: "pie",
marker: {
colors: sentimentColors,
},
hole: 0.4,
textinfo: "label+percent",
insidetextorientation: "radial",
},
],
{
margin: { t: 10, r: 10, b: 10, l: 10 },
}
);
} else {
document.getElementById("sentiment-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No sentiment data available</p></div>';
}
// Country chart
const countryData = dashboardData.country_data;
if (countryData.length > 0) {
const countryLabels = countryData.map((item) => item.country);
const countryValues = countryData.map((item) => item.count);
Plotly.newPlot(
"country-chart",
[
{
x: countryValues,
y: countryLabels,
type: "bar",
orientation: "h",
marker: {
color: "rgb(54, 162, 235)",
},
},
],
{
margin: { t: 10, r: 10, b: 40, l: 100 },
xaxis: {
title: "Number of Sessions",
},
}
);
} else {
document.getElementById("country-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No country data available</p></div>';
}
// Category chart
const categoryData = dashboardData.category_data;
if (categoryData.length > 0) {
const categoryLabels = categoryData.map((item) => item.category);
const categoryValues = categoryData.map((item) => item.count);
Plotly.newPlot(
"category-chart",
[
{
labels: categoryLabels,
values: categoryValues,
type: "pie",
textinfo: "label+percent",
insidetextorientation: "radial",
},
],
{
margin: { t: 10, r: 10, b: 10, l: 10 },
}
);
} else {
document.getElementById("category-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No category data available</p></div>';
} }
}); });
</script> </script>
@@ -115,12 +115,18 @@
<td>{{ session.tokens }}</td> <td>{{ session.tokens }}</td>
<td>{{ session.category|default:"N/A" }}</td> <td>{{ session.category|default:"N/A" }}</td>
<td> <td>
<a {% if session.session_id %}
href="{% url 'chat_session_detail' session.session_id %}" <a
class="btn btn-sm btn-outline-primary" href="{% url 'chat_session_detail' session.session_id %}"
> class="btn btn-sm btn-outline-primary"
<i class="fas fa-eye"></i> >
</a> <i class="fas fa-eye"></i>
</a>
{% else %}
<button class="btn btn-sm btn-outline-secondary" disabled>
<i class="fas fa-eye-slash"></i>
</button>
{% endif %}
</td> </td>
</tr> </tr>
{% empty %} {% empty %}
@@ -11,13 +11,13 @@
<h1 class="h2">Data View</h1> <h1 class="h2">Data View</h1>
<div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2"> <div class="btn-group me-2">
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary"> <a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary ajax-nav-link">
<i class="fas fa-arrow-left"></i> Back to Dashboard <i class="fas fa-arrow-left"></i> Back to Dashboard
</a> </a>
{% if selected_data_source %} {% if selected_data_source %}
<a <a
href="{% url 'data_source_detail' selected_data_source.id %}" href="{% url 'data_source_detail' selected_data_source.id %}"
class="btn btn-sm btn-outline-secondary" class="btn btn-sm btn-outline-secondary ajax-nav-link"
> >
<i class="fas fa-database"></i> View Source <i class="fas fa-database"></i> View Source
</a> </a>
@@ -34,11 +34,17 @@
<i class="fas fa-filter"></i> Filter <i class="fas fa-filter"></i> Filter
</button> </button>
<ul class="dropdown-menu" aria-labelledby="dataViewDropdown"> <ul class="dropdown-menu" aria-labelledby="dataViewDropdown">
<li><a class="dropdown-item" href="?view=all">All Sessions</a></li> <li><a class="dropdown-item ajax-nav-link" href="?view=all">All Sessions</a></li>
<li><a class="dropdown-item" href="?view=recent">Recent Sessions</a></li> <li><a class="dropdown-item ajax-nav-link" href="?view=recent">Recent Sessions</a></li>
<li><a class="dropdown-item" href="?view=positive">Positive Sentiment</a></li> <li>
<li><a class="dropdown-item" href="?view=negative">Negative Sentiment</a></li> <a class="dropdown-item ajax-nav-link" href="?view=positive">Positive Sentiment</a>
<li><a class="dropdown-item" href="?view=escalated">Escalated Sessions</a></li> </li>
<li>
<a class="dropdown-item ajax-nav-link" href="?view=negative">Negative Sentiment</a>
</li>
<li>
<a class="dropdown-item ajax-nav-link" href="?view=escalated">Escalated Sessions</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -52,7 +58,7 @@
<h5 class="card-title mb-0">Data Source Selection</h5> <h5 class="card-title mb-0">Data Source Selection</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="get" class="row g-3 align-items-center"> <form method="get" class="row g-3 align-items-center filter-form">
<div class="col-md-6"> <div class="col-md-6">
<select name="data_source_id" class="form-select" aria-label="Select Data Source"> <select name="data_source_id" class="form-select" aria-label="Select Data Source">
<option value="">All Data Sources</option> <option value="">All Data Sources</option>
@@ -109,153 +115,16 @@
<span class="badge bg-primary">{{ page_obj.paginator.count }} sessions</span> <span class="badge bg-primary">{{ page_obj.paginator.count }} sessions</span>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="table-responsive"> <!-- Loading spinner shown during AJAX requests -->
<table class="table table-striped table-hover"> <div id="ajax-loading-spinner" class="text-center py-4 d-none">
<thead> <div class="spinner-border text-primary" role="status">
<tr> <span class="visually-hidden">Loading...</span>
<th>Session ID</th> </div>
<th>Start Time</th> <p class="mt-2">Loading data...</p>
<th>Country</th>
<th>Language</th>
<th>Messages</th>
<th>Sentiment</th>
<th>Response Time</th>
<th>Category</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for session in page_obj %}
<tr>
<td>{{ session.session_id|truncatechars:10 }}</td>
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
<td>{{ session.country|default:"N/A" }}</td>
<td>{{ session.language|default:"N/A" }}</td>
<td>{{ session.messages_sent }}</td>
<td>
{% if session.sentiment %}
{% if 'positive' in session.sentiment|lower %}
<span class="badge bg-success">{{ session.sentiment }}</span>
{% elif 'negative' in session.sentiment|lower %}
<span class="badge bg-danger">{{ session.sentiment }}</span>
{% elif 'neutral' in session.sentiment|lower %}
<span class="badge bg-warning">{{ session.sentiment }}</span>
{% else %}
<span class="badge bg-secondary">{{ session.sentiment }}</span>
{% endif %}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>{{ session.avg_response_time|floatformat:2 }}s</td>
<td>{{ session.category|default:"N/A" }}</td>
<td>
<a
href="{% url 'chat_session_detail' session.session_id %}"
class="btn btn-sm btn-outline-primary"
>
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center">No chat sessions found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% if page_obj.paginator.num_pages > 1 %} <!-- Data table container that will be updated via AJAX -->
<nav aria-label="Page navigation" class="mt-4"> <div id="ajax-content-container">{% include "dashboard/partials/data_table.html" %}</div>
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a
class="page-link"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page=1"
aria-label="First"
>
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.previous_page_number }}"
aria-label="Previous"
>
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="First">
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<a
class="page-link"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a
class="page-link"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a
class="page-link"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.next_page_number }}"
aria-label="Next"
>
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.paginator.num_pages }}"
aria-label="Last"
>
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Last">
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@@ -314,3 +183,33 @@
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block extra_js %}
<script>
// Function to update the summary section with new data
function updateSummary(data) {
if (document.querySelector(".stats-card h3:nth-of-type(1)")) {
document.querySelector(".stats-card h3:nth-of-type(1)").textContent =
data.page_obj.paginator.count;
}
if (document.querySelector(".stats-card h3:nth-of-type(2)")) {
document.querySelector(".stats-card h3:nth-of-type(2)").textContent =
data.avg_response_time !== null && data.avg_response_time !== undefined
? data.avg_response_time.toFixed(2) + "s"
: "0.00s";
}
if (document.querySelector(".stats-card h3:nth-of-type(3)")) {
document.querySelector(".stats-card h3:nth-of-type(3)").textContent =
data.avg_messages !== null && data.avg_messages !== undefined
? data.avg_messages.toFixed(1)
: "0.0";
}
if (document.querySelector(".stats-card h3:nth-of-type(4)")) {
document.querySelector(".stats-card h3:nth-of-type(4)").textContent =
data.escalation_rate !== null && data.escalation_rate !== undefined
? data.escalation_rate.toFixed(1) + "%"
: "0.0%";
}
}
</script>
{% endblock %}
@@ -0,0 +1,160 @@
<!-- templates/dashboard/partials/data_table.html -->
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Session ID</th>
<th>Start Time</th>
<th>Country</th>
<th>Language</th>
<th>Messages</th>
<th>Sentiment</th>
<th>Response Time</th>
<th>Category</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for session in page_obj %}
<tr>
<td>{{ session.session_id|truncatechars:10 }}</td>
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
<td>{{ session.country|default:"N/A" }}</td>
<td>{{ session.language|default:"N/A" }}</td>
<td>{{ session.messages_sent }}</td>
<td>
{% if session.sentiment %}
{% if 'positive' in session.sentiment|lower %}
<span class="badge bg-success">{{ session.sentiment }}</span>
{% elif 'negative' in session.sentiment|lower %}
<span class="badge bg-danger">{{ session.sentiment }}</span>
{% elif 'neutral' in session.sentiment|lower %}
<span class="badge bg-warning">{{ session.sentiment }}</span>
{% else %}
<span class="badge bg-secondary">{{ session.sentiment }}</span>
{% endif %}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>{{ session.avg_response_time|floatformat:2 }}s</td>
<td>{{ session.category|default:"N/A" }}</td>
<td>
{% if session.session_id %}
<a
href="{% url 'chat_session_detail' session.session_id %}"
class="btn btn-sm btn-outline-primary ajax-nav-link"
>
<i class="fas fa-eye"></i>
</a>
{% else %}
<button class="btn btn-sm btn-outline-secondary" disabled>
<i class="fas fa-eye-slash"></i>
</button>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center">No chat sessions found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if page_obj.paginator.num_pages > 1 %}
<nav aria-label="Page navigation" class="mt-4" id="pagination-container">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="1"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page=1"
aria-label="First"
>
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.previous_page_number }}"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.previous_page_number }}"
aria-label="Previous"
>
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="First">
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<a
class="page-link pagination-link"
data-page="{{ num }}"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ num }}"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.next_page_number }}"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.next_page_number }}"
aria-label="Next"
>
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.paginator.num_pages }}"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.paginator.num_pages }}"
aria-label="Last"
>
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Last">
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
@@ -0,0 +1,164 @@
<!-- templates/dashboard/partials/search_results_table.html -->
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Session ID</th>
<th>Start Time</th>
<th>Data Source</th>
<th>Country</th>
<th>Language</th>
<th>Sentiment</th>
<th>Messages</th>
<th>Category</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for session in page_obj %}
<tr>
<td>{{ session.session_id|truncatechars:10 }}</td>
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
<td>
<a href="{% url 'data_source_detail' session.data_source.id %}" class="ajax-nav-link"
>{{ session.data_source.name|truncatechars:15 }}</a
>
</td>
<td>{{ session.country }}</td>
<td>{{ session.language }}</td>
<td>
{% if session.sentiment %}
{% if 'positive' in session.sentiment|lower %}
<span class="badge bg-success">{{ session.sentiment }}</span>
{% elif 'negative' in session.sentiment|lower %}
<span class="badge bg-danger">{{ session.sentiment }}</span>
{% elif 'neutral' in session.sentiment|lower %}
<span class="badge bg-warning">{{ session.sentiment }}</span>
{% else %}
<span class="badge bg-secondary">{{ session.sentiment }}</span>
{% endif %}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>{{ session.messages_sent }}</td>
<td>{{ session.category|default:"N/A" }}</td>
<td>
{% if session.session_id %}
<a
href="{% url 'chat_session_detail' session.session_id %}"
class="btn btn-sm btn-outline-primary"
>
<i class="fas fa-eye"></i>
</a>
{% else %}
<button class="btn btn-sm btn-outline-secondary" disabled>
<i class="fas fa-eye-slash"></i>
</button>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center">No chat sessions found matching your criteria.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if page_obj.paginator.num_pages > 1 %}
<nav aria-label="Page navigation" class="mt-4" id="pagination-container">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="1"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page=1"
aria-label="First"
>
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.previous_page_number }}"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.previous_page_number }}"
aria-label="Previous"
>
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="First">
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<a
class="page-link pagination-link"
data-page="{{ num }}"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ num }}"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.next_page_number }}"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.next_page_number }}"
aria-label="Next"
>
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.paginator.num_pages }}"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.paginator.num_pages }}"
aria-label="Last"
>
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Last">
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
@@ -9,7 +9,7 @@
> >
<h1 class="h2">Search Results</h1> <h1 class="h2">Search Results</h1>
<div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary"> <a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary ajax-nav-link">
<i class="fas fa-arrow-left"></i> Back to Dashboard <i class="fas fa-arrow-left"></i> Back to Dashboard
</a> </a>
</div> </div>
@@ -22,7 +22,7 @@
<h5 class="card-title mb-0">Search Chat Sessions</h5> <h5 class="card-title mb-0">Search Chat Sessions</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="get" action="{% url 'search_chat_sessions' %}"> <form method="get" action="{% url 'search_chat_sessions' %}" class="search-form">
<div class="input-group"> <div class="input-group">
<input <input
type="text" type="text"
@@ -62,161 +62,24 @@
</h5> </h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="table-responsive"> <!-- Loading spinner shown during AJAX requests -->
<table class="table table-striped table-hover"> <div id="ajax-loading-spinner" class="text-center py-4 d-none">
<thead> <div class="spinner-border text-primary" role="status">
<tr> <span class="visually-hidden">Loading...</span>
<th>Session ID</th> </div>
<th>Start Time</th> <p class="mt-2">Loading data...</p>
<th>Data Source</th>
<th>Country</th>
<th>Language</th>
<th>Sentiment</th>
<th>Messages</th>
<th>Category</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for session in page_obj %}
<tr>
<td>{{ session.session_id|truncatechars:10 }}</td>
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
<td>
<a href="{% url 'data_source_detail' session.data_source.id %}"
>{{ session.data_source.name|truncatechars:15 }}</a
>
</td>
<td>{{ session.country }}</td>
<td>{{ session.language }}</td>
<td>
{% if session.sentiment %}
{% if 'positive' in session.sentiment|lower %}
<span class="badge bg-success">{{ session.sentiment }}</span>
{% elif 'negative' in session.sentiment|lower %}
<span class="badge bg-danger">{{ session.sentiment }}</span>
{% elif 'neutral' in session.sentiment|lower %}
<span class="badge bg-warning">{{ session.sentiment }}</span>
{% else %}
<span class="badge bg-secondary">{{ session.sentiment }}</span>
{% endif %}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>{{ session.messages_sent }}</td>
<td>{{ session.category|default:"N/A" }}</td>
<td>
<a
href="{% url 'chat_session_detail' session.session_id %}"
class="btn btn-sm btn-outline-primary"
>
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center">
No chat sessions found matching your criteria.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% if page_obj.paginator.num_pages > 1 %} <!-- Search results container that will be updated via AJAX -->
<nav aria-label="Page navigation" class="mt-4"> <div id="ajax-content-container">
<ul class="pagination justify-content-center"> {% include "dashboard/partials/search_results_table.html" %}
{% if page_obj.has_previous %} </div>
<li class="page-item">
<a
class="page-link"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page=1"
aria-label="First"
>
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.previous_page_number }}"
aria-label="Previous"
>
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="First">
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<a
class="page-link"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a
class="page-link"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a
class="page-link"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.next_page_number }}"
aria-label="Next"
>
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.paginator.num_pages }}"
aria-label="Last"
>
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Last">
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %}
<!-- No need for extra JavaScript here, using common ajax-pagination.js -->
{% endblock %}
+3 -3
View File
@@ -1,6 +1,6 @@
# docker-compose.yml # docker-compose.yml
version: '3.8' version: "3.8"
services: services:
web: web:
@@ -11,7 +11,7 @@ services:
- static_volume:/app/staticfiles - static_volume:/app/staticfiles
- media_volume:/app/media - media_volume:/app/media
ports: ports:
- '8000:8000' - "8000:8000"
environment: environment:
- DEBUG=0 - DEBUG=0
- SECRET_KEY=your_secret_key_here - SECRET_KEY=your_secret_key_here
@@ -31,7 +31,7 @@ services:
nginx: nginx:
image: nginx:latest image: nginx:latest
ports: ports:
- '80:80' - "80:80"
volumes: volumes:
- ./nginx/conf.d:/etc/nginx/conf.d - ./nginx/conf.d:/etc/nginx/conf.d
- static_volume:/app/staticfiles - static_volume:/app/staticfiles