mirror of
https://github.com/kjanat/livegraphs-django.git
synced 2026-01-16 08:42:07 +01:00
Add configuration and scripts for linting, testing, and dependency management
- Introduced .pre-commit-config.yaml for pre-commit hooks using uv-pre-commit. - Created lint.sh script to run Ruff and Black for linting and formatting. - Added test.sh script to execute tests with coverage reporting. - Configured .uv file for uv settings including lockfile management and dependency resolution. - Updated Makefile with targets for virtual environment setup, dependency installation, linting, testing, formatting, and database migrations. - Established requirements.txt with main and development dependencies for the project.
This commit is contained in:
@ -18,7 +18,8 @@ indent_size = 4
|
|||||||
|
|
||||||
# HTML and Django/Jinja2 template files
|
# HTML and Django/Jinja2 template files
|
||||||
[*.{html,htm}]
|
[*.{html,htm}]
|
||||||
indent_size = 2
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
# Allow prettier to format Django/Jinja templates properly
|
# Allow prettier to format Django/Jinja templates properly
|
||||||
# The following comment options can be used in individual files if needed:
|
# The following comment options can be used in individual files if needed:
|
||||||
# <!-- prettier-ignore -->
|
# <!-- prettier-ignore -->
|
||||||
@ -26,6 +27,7 @@ indent_size = 2
|
|||||||
|
|
||||||
# CSS, JavaScript, and JSON files
|
# CSS, JavaScript, and JSON files
|
||||||
[*.{css,scss,js,json}]
|
[*.{css,scss,js,json}]
|
||||||
|
indent_style = tab
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
||||||
# Markdown files
|
# Markdown files
|
||||||
|
|||||||
9
.gitignore
vendored
9
.gitignore
vendored
@ -407,3 +407,12 @@ pyrightconfig.json
|
|||||||
*Zone.Identifier
|
*Zone.Identifier
|
||||||
examples/
|
examples/
|
||||||
**/migrations/[0-9]**.py
|
**/migrations/[0-9]**.py
|
||||||
|
|
||||||
|
# UV specific
|
||||||
|
.uv/
|
||||||
|
.uv-configs/
|
||||||
|
|
||||||
|
# Pyright and IDE specific
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.pyright/
|
||||||
|
|||||||
92
.pre-commit-config.yaml
Normal file
92
.pre-commit-config.yaml
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
default_install_hook_types:
|
||||||
|
- pre-commit
|
||||||
|
- post-checkout
|
||||||
|
- post-merge
|
||||||
|
- post-rewrite
|
||||||
|
repos:
|
||||||
|
# uv hooks for dependency management
|
||||||
|
- repo: https://github.com/astral-sh/uv-pre-commit
|
||||||
|
rev: 0.7.5
|
||||||
|
hooks:
|
||||||
|
- id: uv-export
|
||||||
|
|
||||||
|
# Standard pre-commit hooks
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v5.0.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: check-yaml
|
||||||
|
- id: check-json
|
||||||
|
- id: check-toml
|
||||||
|
- id: check-added-large-files
|
||||||
|
args: ["--maxkb=500"]
|
||||||
|
- id: detect-private-key
|
||||||
|
- id: check-merge-conflict
|
||||||
|
- id: check-case-conflict
|
||||||
|
- id: debug-statements
|
||||||
|
- id: mixed-line-ending
|
||||||
|
args: ["--fix=lf"]
|
||||||
|
|
||||||
|
# # HTML/Django template linting
|
||||||
|
# - repo: https://github.com/rtts/djhtml
|
||||||
|
# rev: 3.0.7
|
||||||
|
# hooks:
|
||||||
|
# - id: djhtml
|
||||||
|
# entry: djhtml --tabwidth 4 --
|
||||||
|
# - id: djcss
|
||||||
|
# - id: djjs
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
|
rev: v4.0.0-alpha.8
|
||||||
|
hooks:
|
||||||
|
- id: prettier
|
||||||
|
types_or: [javascript, jsx, ts, tsx, css, scss, html, json, yaml, markdown]
|
||||||
|
additional_dependencies:
|
||||||
|
- prettier
|
||||||
|
- prettier-plugin-jinja-template
|
||||||
|
|
||||||
|
# Ruff for linting and formatting
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.11.10
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
args: [--fix]
|
||||||
|
- id: ruff-format
|
||||||
|
|
||||||
|
# Django-specific hooks
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: django-check
|
||||||
|
name: Django Check
|
||||||
|
entry: uv run python dashboard_project/manage.py check
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
types: [python]
|
||||||
|
always_run: true
|
||||||
|
|
||||||
|
- id: django-check-migrations
|
||||||
|
name: Django Check Migrations
|
||||||
|
entry: uv run python dashboard_project/manage.py makemigrations --check --dry-run
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
types: [python]
|
||||||
|
|
||||||
|
# Security checks
|
||||||
|
- repo: https://github.com/pycqa/bandit
|
||||||
|
rev: 1.8.3
|
||||||
|
hooks:
|
||||||
|
- id: bandit
|
||||||
|
args: ["-c", "pyproject.toml", "-r", "dashboard_project"]
|
||||||
|
additional_dependencies: ["bandit[toml]"]
|
||||||
|
|
||||||
|
# # Type checking
|
||||||
|
# - repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
|
# rev: v1.15.0
|
||||||
|
# hooks:
|
||||||
|
# - id: mypy
|
||||||
|
# additional_dependencies:
|
||||||
|
# - django-stubs>=5.0.2
|
||||||
|
# - types-python-dateutil
|
||||||
|
# - types-requests
|
||||||
|
# - types-PyYAML
|
||||||
@ -11,9 +11,7 @@
|
|||||||
"requirePragma": false,
|
"requirePragma": false,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"tabWidth": 2,
|
"useTabs": true,
|
||||||
"trailingComma": "es5",
|
|
||||||
"useTabs": false,
|
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": ["*.html"],
|
"files": ["*.html"],
|
||||||
|
|||||||
11
.scripts/lint.sh
Executable file
11
.scripts/lint.sh
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Run linting, formatting and type checking
|
||||||
|
|
||||||
|
echo "Running Ruff linter..."
|
||||||
|
uv run -m ruff check dashboard_project
|
||||||
|
|
||||||
|
echo "Running Ruff formatter..."
|
||||||
|
uv run -m ruff format dashboard_project
|
||||||
|
|
||||||
|
echo "Running Black formatter..."
|
||||||
|
uv run -m black dashboard_project
|
||||||
6
.scripts/test.sh
Executable file
6
.scripts/test.sh
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Run tests with coverage
|
||||||
|
|
||||||
|
echo "Running tests with coverage..."
|
||||||
|
uv run -m coverage run -m pytest
|
||||||
|
uv run -m coverage report -m
|
||||||
18
.uv
Normal file
18
.uv
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[uv]
|
||||||
|
# Keep the uv.lock file up to date
|
||||||
|
keep-lockfile = true
|
||||||
|
|
||||||
|
# Cache compiled bytecode for dependencies
|
||||||
|
compile-bytecode = true
|
||||||
|
|
||||||
|
# Use a local cache directory
|
||||||
|
local-cache = true
|
||||||
|
|
||||||
|
# Verbosity of output
|
||||||
|
verbosity = "minimal"
|
||||||
|
|
||||||
|
# Define which part of the environment to check
|
||||||
|
environment-checks = ["python", "dependencies"]
|
||||||
|
|
||||||
|
# How to resolve dependencies not specified with exact versions
|
||||||
|
dependency-resolution = "strict"
|
||||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -43,5 +43,5 @@
|
|||||||
"notebook.source.organizeImports": "explicit"
|
"notebook.source.organizeImports": "explicit"
|
||||||
},
|
},
|
||||||
"notebook.formatOnSave.enabled": true,
|
"notebook.formatOnSave.enabled": true,
|
||||||
"prettier.requireConfig": true,
|
"prettier.requireConfig": true
|
||||||
}
|
}
|
||||||
|
|||||||
16
Dockerfile
16
Dockerfile
@ -10,15 +10,25 @@ ENV DJANGO_SETTINGS_MODULE=dashboard_project.settings
|
|||||||
# Set work directory
|
# Set work directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install UV for Python package management
|
||||||
|
RUN pip install uv
|
||||||
|
|
||||||
|
# Copy project files
|
||||||
|
COPY pyproject.toml .
|
||||||
|
COPY uv.lock .
|
||||||
|
COPY . .
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY requirements.txt .
|
|
||||||
RUN uv pip install -e .
|
RUN uv pip install -e .
|
||||||
|
|
||||||
# Copy project
|
# Change to the Django project directory
|
||||||
COPY . .
|
WORKDIR /app/dashboard_project
|
||||||
|
|
||||||
# Collect static files
|
# Collect static files
|
||||||
RUN python manage.py collectstatic --noinput
|
RUN python manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
# Change back to the app directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
# Run gunicorn
|
# Run gunicorn
|
||||||
CMD ["gunicorn", "dashboard_project.wsgi:application", "--bind", "0.0.0.0:8000"]
|
CMD ["gunicorn", "dashboard_project.wsgi:application", "--bind", "0.0.0.0:8000"]
|
||||||
|
|||||||
78
Makefile
Normal file
78
Makefile
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
.PHONY: venv install install-dev lint test format clean run migrate makemigrations superuser setup-node format-js
|
||||||
|
|
||||||
|
# Create a virtual environment
|
||||||
|
venv:
|
||||||
|
uv venv -p 3.13
|
||||||
|
|
||||||
|
# Install production dependencies
|
||||||
|
install:
|
||||||
|
uv pip install -e .
|
||||||
|
|
||||||
|
# Install development dependencies
|
||||||
|
install-dev:
|
||||||
|
uv pip install -e ".[dev]"
|
||||||
|
|
||||||
|
# Run linting
|
||||||
|
lint:
|
||||||
|
uv run -m ruff check dashboard_project
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
test:
|
||||||
|
uv run -m pytest
|
||||||
|
|
||||||
|
# Format Python code
|
||||||
|
format:
|
||||||
|
uv run -m ruff format dashboard_project
|
||||||
|
uv run -m black dashboard_project
|
||||||
|
|
||||||
|
# Format JavaScript/CSS/HTML files with Prettier
|
||||||
|
format-js:
|
||||||
|
npx run format
|
||||||
|
|
||||||
|
# Setup Node.js dependencies
|
||||||
|
setup-node:
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Clean Python cache files
|
||||||
|
clean:
|
||||||
|
find . -type d -name "__pycache__" -exec rm -rf {} +
|
||||||
|
find . -type f -name "*.pyc" -delete
|
||||||
|
find . -type f -name "*.pyo" -delete
|
||||||
|
find . -type f -name "*.pyd" -delete
|
||||||
|
find . -type d -name "*.egg-info" -exec rm -rf {} +
|
||||||
|
find . -type d -name "*.egg" -exec rm -rf {} +
|
||||||
|
find . -type d -name ".pytest_cache" -exec rm -rf {} +
|
||||||
|
find . -type d -name ".coverage" -exec rm -rf {} +
|
||||||
|
find . -type d -name "htmlcov" -exec rm -rf {} +
|
||||||
|
find . -type d -name ".ruff_cache" -exec rm -rf {} +
|
||||||
|
rm -rf build/
|
||||||
|
rm -rf dist/
|
||||||
|
|
||||||
|
# Run the development server
|
||||||
|
run:
|
||||||
|
cd dashboard_project && uv run python manage.py runserver 8001
|
||||||
|
|
||||||
|
# Apply migrations
|
||||||
|
migrate:
|
||||||
|
cd dashboard_project && uv run python manage.py migrate
|
||||||
|
|
||||||
|
# Create migrations
|
||||||
|
makemigrations:
|
||||||
|
cd dashboard_project && uv run python manage.py makemigrations
|
||||||
|
|
||||||
|
# Create a superuser
|
||||||
|
superuser:
|
||||||
|
cd dashboard_project && uv run python manage.py createsuperuser
|
||||||
|
|
||||||
|
# Update uv lock file
|
||||||
|
lock:
|
||||||
|
uv pip freeze > requirements.lock
|
||||||
|
|
||||||
|
# Setup pre-commit hooks
|
||||||
|
setup-pre-commit:
|
||||||
|
uv pip install pre-commit
|
||||||
|
pre-commit install
|
||||||
|
|
||||||
|
# Run pre-commit on all files
|
||||||
|
lint-all:
|
||||||
|
pre-commit run --all-files
|
||||||
@ -1,37 +1,61 @@
|
|||||||
# Prettier for Django/Jinja Templates
|
# Prettier for Django Templates
|
||||||
|
|
||||||
This project uses Prettier with the `prettier-plugin-jinja-template` plugin to format HTML templates with Django/Jinja syntax.
|
This project uses Prettier with the `prettier-plugin-django-annotations` plugin to format HTML templates with Django template syntax.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
To use Prettier with your Django templates, you'll need to install Prettier and the Jinja template plugin:
|
The project is already configured with Prettier integration in pre-commit hooks. The configuration includes:
|
||||||
|
|
||||||
|
1. `.prettierrc` - Configuration file with Django HTML support
|
||||||
|
2. `.prettierignore` - Files to exclude from formatting
|
||||||
|
3. Pre-commit hook for automatic formatting on commits
|
||||||
|
|
||||||
|
### Manual Installation
|
||||||
|
|
||||||
|
To use Prettier locally (outside of pre-commit hooks), you'll need to install the dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Using npm
|
# Using npm
|
||||||
npm install --save-dev prettier prettier-plugin-jinja-template
|
npm install
|
||||||
|
|
||||||
# Or using yarn
|
# Or install just the required packages
|
||||||
yarn add --dev prettier prettier-plugin-jinja-template
|
npm install --save-dev prettier prettier-plugin-django-annotations
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Once installed, you can format your Django templates using:
|
### With Pre-commit
|
||||||
|
|
||||||
|
Prettier will automatically run as part of the pre-commit hooks when you commit changes.
|
||||||
|
|
||||||
|
To manually run the pre-commit hooks on all files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pre-commit run prettier --all-files
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using npm Scripts
|
||||||
|
|
||||||
|
The package.json includes npm scripts for formatting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Format all static files
|
||||||
|
npm run format
|
||||||
|
|
||||||
|
# Check formatting without modifying files
|
||||||
|
npm run format:check
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Line
|
||||||
|
|
||||||
|
You can also run Prettier directly:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Format a specific file
|
# Format a specific file
|
||||||
npx prettier --write path/to/template.html
|
npx prettier --write path/to/template.html
|
||||||
|
|
||||||
# Format all HTML files
|
# Format all HTML files
|
||||||
npx prettier --write "**/*.html"
|
npx prettier --write "dashboard_project/templates/**/*.html"
|
||||||
```
|
|
||||||
|
|
||||||
### Without install
|
|
||||||
|
|
||||||
If you don't want to install the plugin, you can use the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx prettier --plugin=prettier-plugin-jinja-template --parser=jinja-template --write **/*.html
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## VSCode Integration
|
## VSCode Integration
|
||||||
@ -40,12 +64,12 @@ For VSCode users, install the Prettier extension and add these settings to your
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"[html]": {
|
"[html]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.formatOnSave": true
|
"editor.formatOnSave": true
|
||||||
},
|
},
|
||||||
"prettier.requireConfig": true
|
"prettier.requireConfig": true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -62,3 +86,12 @@ If you need to prevent Prettier from formatting a section of your template:
|
|||||||
This works too.
|
This works too.
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Django Template Support
|
||||||
|
|
||||||
|
The `prettier-plugin-django-annotations` plugin provides special handling for Django templates, including:
|
||||||
|
|
||||||
|
- Proper formatting of Django template tags (`{% %}`)
|
||||||
|
- Support for Django template comments (`{# #}`)
|
||||||
|
- Preservation of Django template variable output (`{{ }}`)
|
||||||
|
- Special handling for Django template syntax inside HTML attributes
|
||||||
|
|||||||
91
README.md
91
README.md
@ -15,9 +15,9 @@ A Django application that creates an analytics dashboard for chat session data.
|
|||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Python 3.13+
|
- Python 3.13+
|
||||||
- Django 5.0+
|
- Django 5.2+
|
||||||
- PostgreSQL (optional, SQLite is fine for development)
|
- UV package manager (recommended)
|
||||||
- Other dependencies listed in [`pyproject.toml`](pyproject.toml)
|
- Other dependencies listed in [`pyproject.toml`](./pyproject.toml)
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
@ -27,42 +27,103 @@ A Django application that creates an analytics dashboard for chat session data.
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone <repository-url>
|
git clone <repository-url>
|
||||||
cd dashboard_project
|
cd LiveGraphsDjango
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Create a virtual environment and activate it:
|
2. Install uv if you don't have it yet:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Install using pip
|
||||||
|
pip install uv
|
||||||
|
|
||||||
|
# Or with curl (Unix/macOS)
|
||||||
|
curl -sSf https://install.pypa.io/get-uv.py | python3 -
|
||||||
|
|
||||||
|
# Or on Windows with PowerShell
|
||||||
|
irm https://install.pypa.io/get-uv.ps1 | iex
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 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
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Install dependencies:
|
4. Install dependencies:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
uv pip install -r requirements.txt
|
# Install all dependencies including dev dependencies
|
||||||
|
uv pip install -e ".[dev]"
|
||||||
|
|
||||||
|
# Or just runtime dependencies
|
||||||
|
uv pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Run migrations:
|
5. Run migrations:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
uv run python manage.py makemigrations
|
cd dashboard_project
|
||||||
uv run python manage.py migrate
|
python manage.py makemigrations
|
||||||
|
python manage.py migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Create a superuser:
|
6. Create a superuser:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
uv run python manage.py createsuperuser
|
python manage.py createsuperuser
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Run the development server:
|
7. Run the development server:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
uv run python manage.py runserver
|
python manage.py runserver
|
||||||
```
|
```
|
||||||
|
|
||||||
7. Access the application at <http://127.0.0.1:8000/>
|
8. Access the application at <http://127.0.0.1:8000/>
|
||||||
|
|
||||||
|
### Development Workflow with UV
|
||||||
|
|
||||||
|
UV offers several advantages over traditional pip, including faster dependency resolution and installation:
|
||||||
|
|
||||||
|
1. Running linting and formatting:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Using the convenience script
|
||||||
|
./.scripts/lint.sh
|
||||||
|
|
||||||
|
# Or directly
|
||||||
|
uv run -m ruff check dashboard_project
|
||||||
|
uv run -m ruff format dashboard_project
|
||||||
|
uv run -m black dashboard_project
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Running tests:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Using the convenience script
|
||||||
|
./.scripts/test.sh
|
||||||
|
|
||||||
|
# Or directly
|
||||||
|
uv run -m pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Adding new dependencies:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Add to project
|
||||||
|
uv pip install package_name
|
||||||
|
|
||||||
|
# Then update pyproject.toml manually
|
||||||
|
# And update the lockfile
|
||||||
|
uv pip freeze > requirements.lock
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Updating the lockfile:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uv pip compile pyproject.toml -o uv.lock
|
||||||
|
```
|
||||||
|
|
||||||
### Using Docker
|
### Using Docker
|
||||||
|
|
||||||
|
|||||||
42
TODO.md
42
TODO.md
@ -2,30 +2,30 @@
|
|||||||
|
|
||||||
- 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?
|
- 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 functionality to the dashboard:
|
- Add export functionality to the dashboard:
|
||||||
- File formats:
|
- File formats:
|
||||||
- CSV
|
- CSV
|
||||||
- Excel
|
- Excel
|
||||||
- JSON
|
- JSON
|
||||||
- XML
|
- XML
|
||||||
- HTML
|
- HTML
|
||||||
- PDF
|
- PDF
|
||||||
- Make the export button a dropdown with the following options:
|
- Make the export button a dropdown with the following options:
|
||||||
- Export as CSV
|
- Export as CSV
|
||||||
- Export as Excel
|
- Export as Excel
|
||||||
- Export as JSON
|
- Export as JSON
|
||||||
- Export as XML
|
- Export as XML
|
||||||
- Export as HTML
|
- Export as HTML
|
||||||
- Export as PDF
|
- Export as PDF
|
||||||
- Make the export data section folded by default and only show the export button.
|
- Make the export data section folded by default and only show the export button.
|
||||||
- Adjust the downloaded file name to include the company name, date and time of the export.
|
- Adjust the downloaded file name to include the company name, date and time of the export.
|
||||||
- Add a button to download the CSV file for the selected company.
|
- 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.
|
- 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 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.
|
- Add periodic download from <https://proto.notso.ai/XY/chats> possibility for the XY company.
|
||||||
- Authentication: Basic Auth
|
- Authentication: Basic Auth
|
||||||
- URL: https://proto.notso.ai/jumbo/chats
|
- URL: <https://proto.notso.ai/XY/chats>
|
||||||
- Username: jumboadmin
|
- Username: xxxx
|
||||||
- Password: jumboadmin
|
- Password: xxxx
|
||||||
- Reduce amount of rows in the table to fit the screen.
|
- Reduce amount of rows in the table to fit the screen.
|
||||||
- Add dark mode/theming to the dashboard.
|
- Add dark mode/theming to the dashboard.
|
||||||
- Add Notso AI branding to the dashboard.
|
- Add Notso AI branding to the dashboard.
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
"""
|
|
||||||
LiveGraphsDjango - Dashboard for analyzing chat session data.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
|
||||||
@ -18,7 +18,7 @@ User = get_user_model()
|
|||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Create sample data for testing"
|
help = "Create sample data for testing"
|
||||||
|
|
||||||
def handle(self, *args, **kwargs):
|
def handle(self, **_options):
|
||||||
self.stdout.write("Creating sample data...")
|
self.stdout.write("Creating sample data...")
|
||||||
|
|
||||||
# Create admin user if it doesn't exist
|
# Create admin user if it doesn't exist
|
||||||
@ -45,7 +45,7 @@ class Command(BaseCommand):
|
|||||||
self.stdout.write(f"Company already exists: {company.name}")
|
self.stdout.write(f"Company already exists: {company.name}")
|
||||||
|
|
||||||
# Create users for each company
|
# Create users for each company
|
||||||
for i, company in enumerate(companies):
|
for _i, company in enumerate(companies):
|
||||||
# Company admin
|
# Company admin
|
||||||
username = f"admin_{company.name.lower().replace(' ', '_')}"
|
username = f"admin_{company.name.lower().replace(' ', '_')}"
|
||||||
if not User.objects.filter(username=username).exists():
|
if not User.objects.filter(username=username).exists():
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
# dashboard/utils.py
|
# dashboard/utils.py
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -25,17 +27,13 @@ def process_csv_file(data_source):
|
|||||||
# Handle datetime fields
|
# Handle datetime fields
|
||||||
start_time = None
|
start_time = None
|
||||||
end_time = None
|
end_time = None
|
||||||
|
|
||||||
if "start_time" in row and pd.notna(row["start_time"]):
|
if "start_time" in row and pd.notna(row["start_time"]):
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
start_time = make_aware(pd.to_datetime(row["start_time"]))
|
start_time = make_aware(pd.to_datetime(row["start_time"]))
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if "end_time" in row and pd.notna(row["end_time"]):
|
if "end_time" in row and pd.notna(row["end_time"]):
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
end_time = make_aware(pd.to_datetime(row["end_time"]))
|
end_time = make_aware(pd.to_datetime(row["end_time"]))
|
||||||
except Exception:
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Convert boolean fields
|
# Convert boolean fields
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from pathlib import Path
|
|||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = "django-insecure-your-secret-key-here"
|
SECRET_KEY = "django-insecure-your-secret-key-here" # nosec: B105
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|||||||
@ -4,277 +4,311 @@
|
|||||||
|
|
||||||
/* Dashboard grid layout */
|
/* Dashboard grid layout */
|
||||||
.dashboard-grid {
|
.dashboard-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
/* Slightly larger minmax for widgets */
|
|
||||||
gap: 1.5rem;
|
/* Slightly larger minmax for widgets */
|
||||||
/* Increased gap */
|
gap: 1.5rem;
|
||||||
|
|
||||||
|
/* Increased gap */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dashboard widget cards */
|
/* Dashboard widget cards */
|
||||||
.dashboard-widget {
|
.dashboard-widget {
|
||||||
display: flex;
|
display: flex;
|
||||||
/* Allow flex for content alignment */
|
|
||||||
flex-direction: column;
|
/* Allow flex for content alignment */
|
||||||
/* Stack header, body, footer vertically */
|
flex-direction: column;
|
||||||
height: 100%;
|
|
||||||
/* Ensure widgets fill grid cell height */
|
/* Stack header, body, footer vertically */
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
/* Ensure widgets fill grid cell height */
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-widget .card-header {
|
.dashboard-widget .card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-widget .card-header .widget-title {
|
.dashboard-widget .card-header .widget-title {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
/* Slightly larger widget titles */
|
|
||||||
font-weight: 600;
|
/* Slightly larger widget titles */
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-widget .card-header .widget-actions {
|
.dashboard-widget .card-header .widget-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-widget .card-header .widget-actions .btn {
|
.dashboard-widget .card-header .widget-actions .btn {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
/* Slightly larger action buttons */
|
|
||||||
height: 32px;
|
/* Slightly larger action buttons */
|
||||||
padding: 0;
|
height: 32px;
|
||||||
display: flex;
|
padding: 0;
|
||||||
align-items: center;
|
display: flex;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
font-size: 0.85rem;
|
justify-content: center;
|
||||||
background-color: transparent;
|
font-size: 0.85rem;
|
||||||
border: 1px solid transparent;
|
background-color: transparent;
|
||||||
color: #6c757d;
|
border: 1px solid transparent;
|
||||||
|
color: #6c757d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-widget .card-header .widget-actions .btn:hover {
|
.dashboard-widget .card-header .widget-actions .btn:hover {
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
border-color: #e0e0e0;
|
border-color: #e0e0e0;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-widget .card-body {
|
.dashboard-widget .card-body {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
/* Allow card body to take available space */
|
|
||||||
padding: 1.25rem;
|
/* Allow card body to take available space */
|
||||||
/* Consistent padding */
|
padding: 1.25rem;
|
||||||
|
|
||||||
|
/* Consistent padding */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chart widgets */
|
/* Chart widgets */
|
||||||
.chart-widget .card-body {
|
.chart-widget .card-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-widget .chart-container {
|
.chart-widget .chart-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 250px;
|
min-height: 250px;
|
||||||
/* Adjusted min-height */
|
|
||||||
width: 100%;
|
/* Adjusted min-height */
|
||||||
/* Ensure it takes full width of card body */
|
width: 100%;
|
||||||
|
|
||||||
|
/* Ensure it takes full width of card body */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Stat widgets / Stat Cards */
|
/* Stat widgets / Stat Cards */
|
||||||
.stat-card {
|
.stat-card {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
/* Generous padding */
|
|
||||||
|
/* Generous padding */
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card .stat-icon {
|
.stat-card .stat-icon {
|
||||||
font-size: 2.25rem;
|
font-size: 2.25rem;
|
||||||
/* Larger icon */
|
|
||||||
margin-bottom: 1rem;
|
/* Larger icon */
|
||||||
display: inline-block;
|
margin-bottom: 1rem;
|
||||||
width: 4.5rem;
|
display: inline-block;
|
||||||
height: 4.5rem;
|
width: 4.5rem;
|
||||||
line-height: 4.5rem;
|
height: 4.5rem;
|
||||||
text-align: center;
|
line-height: 4.5rem;
|
||||||
border-radius: 50%;
|
text-align: center;
|
||||||
background-color: #e9f2ff;
|
border-radius: 50%;
|
||||||
/* Light blue background for icon */
|
background-color: #e9f2ff;
|
||||||
color: #007bff;
|
|
||||||
/* Primary color for icon */
|
/* Light blue background for icon */
|
||||||
|
color: #007bff;
|
||||||
|
|
||||||
|
/* Primary color for icon */
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card .stat-value {
|
.stat-card .stat-value {
|
||||||
font-size: 2.25rem;
|
font-size: 2.25rem;
|
||||||
/* Larger stat value */
|
|
||||||
font-weight: 700;
|
/* Larger stat value */
|
||||||
margin-bottom: 0.25rem;
|
font-weight: 700;
|
||||||
/* Reduced margin */
|
margin-bottom: 0.25rem;
|
||||||
line-height: 1.1;
|
|
||||||
color: #212529;
|
/* Reduced margin */
|
||||||
/* Darker color for value */
|
line-height: 1.1;
|
||||||
|
color: #212529;
|
||||||
|
|
||||||
|
/* Darker color for value */
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card .stat-label {
|
.stat-card .stat-label {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
/* Slightly larger label */
|
|
||||||
color: #6c757d;
|
/* Slightly larger label */
|
||||||
margin-bottom: 0;
|
color: #6c757d;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dashboard theme variations */
|
/* Dashboard theme variations */
|
||||||
.dashboard-theme-light .card {
|
.dashboard-theme-light .card {
|
||||||
background-color: #ffffff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-theme-dark {
|
.dashboard-theme-dark {
|
||||||
background-color: #212529;
|
background-color: #212529;
|
||||||
color: #f8f9fa;
|
color: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-theme-dark .card {
|
.dashboard-theme-dark .card {
|
||||||
background-color: #343a40;
|
background-color: #343a40;
|
||||||
color: #f8f9fa;
|
color: #f8f9fa;
|
||||||
border-color: #495057;
|
border-color: #495057;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-theme-dark .card-header {
|
.dashboard-theme-dark .card-header {
|
||||||
background-color: #495057;
|
background-color: #495057;
|
||||||
border-bottom-color: #6c757d;
|
border-bottom-color: #6c757d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-theme-dark .stat-card .stat-label {
|
.dashboard-theme-dark .stat-card .stat-label {
|
||||||
color: #adb5bd;
|
color: #adb5bd;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Time period selector */
|
/* Time period selector */
|
||||||
.time-period-selector {
|
.time-period-selector {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
/* Increased gap */
|
|
||||||
margin-bottom: 1.5rem;
|
/* Increased gap */
|
||||||
/* Increased margin */
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
|
/* Increased margin */
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-period-selector .btn-group {
|
.time-period-selector .btn-group {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-period-selector .btn {
|
.time-period-selector .btn {
|
||||||
padding: 0.375rem 0.75rem;
|
padding: 0.375rem 0.75rem;
|
||||||
/* Bootstrap-like padding */
|
|
||||||
font-size: 0.875rem;
|
/* Bootstrap-like padding */
|
||||||
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom metric selector */
|
/* Custom metric selector */
|
||||||
.metric-selector {
|
.metric-selector {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-selector .nav-link {
|
.metric-selector .nav-link {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-selector .nav-link.active {
|
.metric-selector .nav-link.active {
|
||||||
background-color: #007bff;
|
background-color: #007bff;
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dashboard loading states */
|
/* Dashboard loading states */
|
||||||
.widget-placeholder {
|
.widget-placeholder {
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
background: linear-gradient(90deg, #e9ecef 25%, #f8f9fa 50%, #e9ecef 75%);
|
background: linear-gradient(90deg, #e9ecef 25%, #f8f9fa 50%, #e9ecef 75%);
|
||||||
/* Lighter gradient */
|
|
||||||
background-size: 200% 100%;
|
/* Lighter gradient */
|
||||||
animation: loading 1.8s infinite ease-in-out;
|
background-size: 200% 100%;
|
||||||
/* Smoother animation */
|
animation: loading 1.8s infinite ease-in-out;
|
||||||
border-radius: 0.5rem;
|
|
||||||
/* Consistent with cards */
|
/* Smoother animation */
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
|
/* Consistent with cards */
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes loading {
|
@keyframes loading {
|
||||||
0% {
|
0% {
|
||||||
background-position: 200% 0;
|
background-position: 200% 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
background-position: -200% 0;
|
background-position: -200% 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dashboard empty states */
|
/* Dashboard empty states */
|
||||||
.empty-state {
|
.empty-state {
|
||||||
padding: 2.5rem;
|
padding: 2.5rem;
|
||||||
/* Increased padding */
|
|
||||||
text-align: center;
|
/* Increased padding */
|
||||||
color: #6c757d;
|
text-align: center;
|
||||||
background-color: #f8f9fa;
|
color: #6c757d;
|
||||||
/* Light background for empty state */
|
background-color: #f8f9fa;
|
||||||
border-radius: 0.5rem;
|
|
||||||
border: 1px dashed #ced4da;
|
/* Light background for empty state */
|
||||||
/* Dashed border */
|
border-radius: 0.5rem;
|
||||||
|
border: 1px dashed #ced4da;
|
||||||
|
|
||||||
|
/* Dashed border */
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state .empty-state-icon {
|
.empty-state .empty-state-icon {
|
||||||
font-size: 3.5rem;
|
font-size: 3.5rem;
|
||||||
/* Larger icon */
|
|
||||||
margin-bottom: 1.5rem;
|
/* Larger icon */
|
||||||
opacity: 0.4;
|
margin-bottom: 1.5rem;
|
||||||
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state .empty-state-message {
|
.empty-state .empty-state-message {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
/* Slightly larger message */
|
|
||||||
margin-bottom: 1.5rem;
|
/* Slightly larger message */
|
||||||
font-weight: 500;
|
margin-bottom: 1.5rem;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state .btn {
|
.empty-state .btn {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive adjustments */
|
/* Responsive adjustments */
|
||||||
@media (max-width: 767.98px) {
|
@media (width <=767.98px) {
|
||||||
.dashboard-grid {
|
.dashboard-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card .stat-icon {
|
.stat-card .stat-icon {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
line-height: 3rem;
|
line-height: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card .stat-value {
|
.stat-card .stat-value {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Stat Boxes Alignment Fix (Bottom Align, No Overlap) --- */
|
/* --- Stat Boxes Alignment Fix (Bottom Align, No Overlap) --- */
|
||||||
.stats-row {
|
.stats-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-card {
|
.stats-card {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-end; /* Push content to bottom */
|
justify-content: flex-end;
|
||||||
align-items: flex-start;
|
|
||||||
box-sizing: border-box;
|
/* Push content to bottom */
|
||||||
/* Remove min-height/height for natural stretch */
|
align-items: flex-start;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
/* Remove min-height/height for natural stretch */
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,325 +4,361 @@
|
|||||||
|
|
||||||
/* General Styles */
|
/* General Styles */
|
||||||
body {
|
body {
|
||||||
font-family:
|
font-family:
|
||||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif,
|
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif,
|
||||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||||
background-color: #f4f7f9;
|
background-color: #f4f7f9;
|
||||||
/* Lighter, cleaner background */
|
|
||||||
color: #333;
|
/* Lighter, cleaner background */
|
||||||
/* Darker text for better contrast */
|
color: #333;
|
||||||
line-height: 1.6;
|
|
||||||
display: flex;
|
/* Darker text for better contrast */
|
||||||
/* Added for sticky footer */
|
line-height: 1.6;
|
||||||
flex-direction: column;
|
display: flex;
|
||||||
/* Added for sticky footer */
|
|
||||||
min-height: 100vh;
|
/* Added for sticky footer */
|
||||||
/* Ensures body takes at least full viewport height */
|
flex-direction: column;
|
||||||
|
|
||||||
|
/* Added for sticky footer */
|
||||||
|
min-height: 100vh;
|
||||||
|
|
||||||
|
/* Ensures body takes at least full viewport height */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Navbar adjustments (if needed, Bootstrap usually handles this well) */
|
/* Navbar adjustments (if needed, Bootstrap usually handles this well) */
|
||||||
.navbar {
|
.navbar {
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 2px 4px rgb(0 0 0 / 5%);
|
||||||
/* Subtle shadow for depth */
|
|
||||||
|
/* Subtle shadow for depth */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Helper Classes */
|
/* Helper Classes */
|
||||||
.text-truncate-2 {
|
.text-truncate-2 {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
line-clamp: 2;
|
line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.min-w-150 {
|
.min-w-150 {
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Card styles */
|
/* Card styles */
|
||||||
.card {
|
.card {
|
||||||
border: 1px solid #e0e5e9;
|
border: 1px solid #e0e5e9;
|
||||||
/* Lighter border */
|
|
||||||
border-radius: 0.5rem;
|
/* Lighter border */
|
||||||
/* Slightly more rounded corners */
|
border-radius: 0.5rem;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
||||||
/* Softer, more modern shadow */
|
/* Slightly more rounded corners */
|
||||||
transition:
|
box-shadow: 0 4px 12px rgb(0 0 0 / 8%);
|
||||||
transform 0.2s ease-in-out,
|
|
||||||
box-shadow 0.2s ease-in-out;
|
/* Softer, more modern shadow */
|
||||||
margin-bottom: 1.5rem;
|
transition:
|
||||||
/* Consistent margin */
|
transform 0.2s ease-in-out,
|
||||||
|
box-shadow 0.2s ease-in-out;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
|
/* Consistent margin */
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-hover:hover {
|
.card-hover:hover {
|
||||||
transform: translateY(-3px);
|
transform: translateY(-3px);
|
||||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 6px 16px rgb(0 0 0 / 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
background-color: #ffffff;
|
background-color: #fff;
|
||||||
/* Clean white header */
|
|
||||||
border-bottom: 1px solid #e0e5e9;
|
/* Clean white header */
|
||||||
font-weight: 500;
|
border-bottom: 1px solid #e0e5e9;
|
||||||
/* Slightly bolder header text */
|
font-weight: 500;
|
||||||
padding: 0.75rem 1.25rem;
|
|
||||||
|
/* Slightly bolder header text */
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
/* Adjusted card title size */
|
|
||||||
font-weight: 600;
|
/* Adjusted card title size */
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sidebar enhancements */
|
/* Sidebar enhancements */
|
||||||
.sidebar {
|
.sidebar {
|
||||||
background-color: #ffffff;
|
background-color: #fff;
|
||||||
/* White sidebar for a cleaner look */
|
|
||||||
border-right: 1px solid #e0e5e9;
|
/* White sidebar for a cleaner look */
|
||||||
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.03);
|
border-right: 1px solid #e0e5e9;
|
||||||
transition: all 0.3s;
|
box-shadow: 2px 0 5px rgb(0 0 0 / 3%);
|
||||||
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-sticky {
|
.sidebar-sticky {
|
||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-link {
|
.sidebar .nav-link {
|
||||||
color: #4a5568;
|
color: #4a5568;
|
||||||
/* Softer link color */
|
|
||||||
padding: 0.65rem 1.25rem;
|
/* Softer link color */
|
||||||
/* Adjusted padding */
|
padding: 0.65rem 1.25rem;
|
||||||
border-radius: 0.375rem;
|
|
||||||
/* Bootstrap-like rounded corners for links */
|
/* Adjusted padding */
|
||||||
margin: 0.1rem 0.5rem;
|
border-radius: 0.375rem;
|
||||||
/* Margin around links */
|
|
||||||
font-weight: 500;
|
/* Bootstrap-like rounded corners for links */
|
||||||
|
margin: 0.1rem 0.5rem;
|
||||||
|
|
||||||
|
/* Margin around links */
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-link:hover {
|
.sidebar .nav-link:hover {
|
||||||
color: #007bff;
|
color: #007bff;
|
||||||
/* Primary color on hover */
|
|
||||||
background-color: #e9f2ff;
|
/* Primary color on hover */
|
||||||
/* Light blue background on hover */
|
background-color: #e9f2ff;
|
||||||
|
|
||||||
|
/* Light blue background on hover */
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-link.active {
|
.sidebar .nav-link.active {
|
||||||
color: #007bff;
|
color: #007bff;
|
||||||
background-color: #d6e4ff;
|
background-color: #d6e4ff;
|
||||||
/* Slightly darker blue for active */
|
|
||||||
font-weight: 600;
|
/* Slightly darker blue for active */
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-link i.me-2 {
|
.sidebar .nav-link i.me-2 {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
/* Ensure icons align well */
|
|
||||||
text-align: center;
|
/* Ensure icons align well */
|
||||||
margin-right: 0.75rem !important;
|
text-align: center;
|
||||||
/* Consistent icon spacing */
|
margin-right: 0.75rem !important;
|
||||||
|
|
||||||
|
/* Consistent icon spacing */
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-header {
|
.sidebar .nav-header {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
color: #718096;
|
color: #718096;
|
||||||
/* Softer header color */
|
|
||||||
padding: 0.5rem 1.25rem;
|
/* Softer header color */
|
||||||
margin-top: 1rem;
|
padding: 0.5rem 1.25rem;
|
||||||
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dashboard stats cards */
|
/* Dashboard stats cards */
|
||||||
.stats-card {
|
.stats-card {
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-card h3 {
|
.stats-card h3 {
|
||||||
font-size: 1.75rem;
|
font-size: 1.75rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-card p {
|
.stats-card p {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chart containers */
|
/* Chart containers */
|
||||||
.chart-container {
|
.chart-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loading overlay */
|
/* Loading overlay */
|
||||||
.loading-overlay {
|
.loading-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: rgba(255, 255, 255, 0.7);
|
background-color: rgb(255 255 255 / 70%);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Table enhancements */
|
/* Table enhancements */
|
||||||
.table {
|
.table {
|
||||||
border-color: #e0e5e9;
|
border-color: #e0e5e9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table th {
|
.table th {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
/* Bolder table headers */
|
|
||||||
color: #4a5568;
|
/* Bolder table headers */
|
||||||
background-color: #f8f9fc;
|
color: #4a5568;
|
||||||
/* Light background for headers */
|
background-color: #f8f9fc;
|
||||||
|
|
||||||
|
/* Light background for headers */
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-striped tbody tr:nth-of-type(odd) {
|
.table-striped tbody tr:nth-of-type(odd) {
|
||||||
background-color: rgba(0, 0, 0, 0.02);
|
background-color: rgb(0 0 0 / 2%);
|
||||||
/* Very subtle striping */
|
|
||||||
|
/* Very subtle striping */
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-hover tbody tr:hover {
|
.table-hover tbody tr:hover {
|
||||||
background-color: #e9f2ff;
|
background-color: #e9f2ff;
|
||||||
/* Consistent hover with sidebar */
|
|
||||||
|
/* Consistent hover with sidebar */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form improvements */
|
/* Form improvements */
|
||||||
.form-control,
|
.form-control,
|
||||||
.form-select {
|
.form-select {
|
||||||
border-color: #ced4da;
|
border-color: #ced4da;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
/* Consistent border radius */
|
|
||||||
padding: 0.5rem 0.75rem;
|
/* Consistent border radius */
|
||||||
/* Adjusted padding */
|
padding: 0.5rem 0.75rem;
|
||||||
|
|
||||||
|
/* Adjusted padding */
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus,
|
.form-control:focus,
|
||||||
.form-select:focus {
|
.form-select:focus {
|
||||||
border-color: #86b7fe;
|
border-color: #86b7fe;
|
||||||
/* Bootstrap focus color */
|
|
||||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
/* Bootstrap focus color */
|
||||||
/* Bootstrap focus shadow */
|
box-shadow: 0 0 0 0.25rem rgb(13 110 253 / 25%);
|
||||||
|
|
||||||
|
/* Bootstrap focus shadow */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Button styling */
|
/* Button styling */
|
||||||
.btn {
|
.btn {
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
/* Consistent border radius */
|
|
||||||
padding: 0.5rem 1rem;
|
/* Consistent border radius */
|
||||||
/* Standard button padding */
|
padding: 0.5rem 1rem;
|
||||||
font-weight: 500;
|
|
||||||
transition:
|
/* Standard button padding */
|
||||||
background-color 0.15s ease-in-out,
|
font-weight: 500;
|
||||||
border-color 0.15s ease-in-out,
|
transition:
|
||||||
box-shadow 0.15s ease-in-out;
|
background-color 0.15s ease-in-out,
|
||||||
|
border-color 0.15s ease-in-out,
|
||||||
|
box-shadow 0.15s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background-color: #007bff;
|
background-color: #007bff;
|
||||||
border-color: #007bff;
|
border-color: #007bff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
background-color: #0069d9;
|
background-color: #0069d9;
|
||||||
border-color: #0062cc;
|
border-color: #0062cc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background-color: #6c757d;
|
background-color: #6c757d;
|
||||||
border-color: #6c757d;
|
border-color: #6c757d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover {
|
.btn-secondary:hover {
|
||||||
background-color: #5a6268;
|
background-color: #5a6268;
|
||||||
border-color: #545b62;
|
border-color: #545b62;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Alert styling */
|
/* Alert styling */
|
||||||
.alert {
|
.alert {
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
padding: 0.9rem 1.25rem;
|
padding: 0.9rem 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chat transcript styling */
|
/* Chat transcript styling */
|
||||||
.chat-transcript {
|
.chat-transcript {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
border: 1px solid #e9ecef;
|
border: 1px solid #e9ecef;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-transcript pre {
|
.chat-transcript pre {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Footer styling */
|
/* Footer styling */
|
||||||
footer {
|
footer {
|
||||||
background-color: #ffffff;
|
background-color: #fff;
|
||||||
/* White footer */
|
|
||||||
border-top: 1px solid #e0e5e9;
|
/* White footer */
|
||||||
padding: 1.5rem 0;
|
border-top: 1px solid #e0e5e9;
|
||||||
color: #6c757d;
|
padding: 1.5rem 0;
|
||||||
font-size: 0.9rem;
|
color: #6c757d;
|
||||||
margin-top: auto;
|
font-size: 0.9rem;
|
||||||
/* Added for sticky footer */
|
margin-top: auto;
|
||||||
|
|
||||||
|
/* Added for sticky footer */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive adjustments */
|
/* Responsive adjustments */
|
||||||
@media (max-width: 767.98px) {
|
@media (width <=767.98px) {
|
||||||
.main-content {
|
.main-content {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-card h3 {
|
.stats-card h3 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
height: 250px;
|
height: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Print styles */
|
/* Print styles */
|
||||||
@media print {
|
@media print {
|
||||||
.sidebar,
|
.sidebar,
|
||||||
.navbar,
|
.navbar,
|
||||||
.btn,
|
.btn,
|
||||||
footer {
|
footer {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,268 +6,269 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
// Only initialize if AJAX navigation is enabled
|
// Only initialize if AJAX navigation is enabled
|
||||||
if (typeof ENABLE_AJAX_NAVIGATION !== "undefined" && ENABLE_AJAX_NAVIGATION) {
|
if (typeof ENABLE_AJAX_NAVIGATION !== "undefined" && ENABLE_AJAX_NAVIGATION) {
|
||||||
setupAjaxNavigation();
|
setupAjaxNavigation();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to set up AJAX navigation for the application
|
// Function to set up AJAX navigation for the application
|
||||||
function setupAjaxNavigation() {
|
function setupAjaxNavigation() {
|
||||||
// Configuration
|
// Configuration
|
||||||
const config = {
|
const config = {
|
||||||
mainContentSelector: "#main-content", // Selector for the main content area
|
mainContentSelector: "#main-content", // Selector for the main content area
|
||||||
navLinkSelector: ".ajax-nav-link", // Selector for links to handle with AJAX
|
navLinkSelector: ".ajax-nav-link", // Selector for links to handle with AJAX
|
||||||
loadingIndicatorId: "nav-loading-indicator", // ID of the loading indicator
|
loadingIndicatorId: "nav-loading-indicator", // ID of the loading indicator
|
||||||
excludePatterns: [
|
excludePatterns: [
|
||||||
// URL patterns to exclude from AJAX navigation
|
// URL patterns to exclude from AJAX navigation
|
||||||
/\.(pdf|xlsx?|docx?|csv|zip|png|jpe?g|gif|svg)$/i, // File downloads
|
/\.(pdf|xlsx?|docx?|csv|zip|png|jpe?g|gif|svg)$/i, // File downloads
|
||||||
/\/admin\//, // Admin pages
|
/\/admin\//, // Admin pages
|
||||||
/\/accounts\/logout\//, // Logout page
|
/\/accounts\/logout\//, // Logout page
|
||||||
/\/api\//, // API endpoints
|
/\/api\//, // API endpoints
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create and insert the loading indicator
|
// Create and insert the loading indicator
|
||||||
if (!document.getElementById(config.loadingIndicatorId)) {
|
if (!document.getElementById(config.loadingIndicatorId)) {
|
||||||
const loadingIndicator = document.createElement("div");
|
const loadingIndicator = document.createElement("div");
|
||||||
loadingIndicator.id = config.loadingIndicatorId;
|
loadingIndicator.id = config.loadingIndicatorId;
|
||||||
loadingIndicator.className = "position-fixed top-0 start-0 end-0";
|
loadingIndicator.className = "position-fixed top-0 start-0 end-0";
|
||||||
loadingIndicator.innerHTML =
|
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>';
|
'<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.display = "none";
|
||||||
loadingIndicator.style.zIndex = "9999";
|
loadingIndicator.style.zIndex = "9999";
|
||||||
document.body.appendChild(loadingIndicator);
|
document.body.appendChild(loadingIndicator);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the loading indicator element
|
// Get the loading indicator element
|
||||||
const loadingIndicator = document.getElementById(config.loadingIndicatorId);
|
const loadingIndicator = document.getElementById(config.loadingIndicatorId);
|
||||||
|
|
||||||
// Get the main content container
|
// Get the main content container
|
||||||
const mainContent = document.querySelector(config.mainContentSelector);
|
const mainContent = document.querySelector(config.mainContentSelector);
|
||||||
if (!mainContent) {
|
if (!mainContent) {
|
||||||
console.warn("Main content container not found. AJAX navigation disabled.");
|
console.warn("Main content container not found. AJAX navigation disabled.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to check if a URL should be excluded from AJAX navigation
|
// Function to check if a URL should be excluded from AJAX navigation
|
||||||
function shouldExcludeUrl(url) {
|
function shouldExcludeUrl(url) {
|
||||||
for (const pattern of config.excludePatterns) {
|
for (const pattern of config.excludePatterns) {
|
||||||
if (pattern.test(url)) {
|
if (pattern.test(url)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to show the loading indicator
|
// Function to show the loading indicator
|
||||||
function showLoading() {
|
function showLoading() {
|
||||||
loadingIndicator.style.display = "block";
|
loadingIndicator.style.display = "block";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to hide the loading indicator
|
// Function to hide the loading indicator
|
||||||
function hideLoading() {
|
function hideLoading() {
|
||||||
loadingIndicator.style.display = "none";
|
loadingIndicator.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to handle AJAX page navigation
|
// Function to handle AJAX page navigation
|
||||||
function handlePageNavigation(url, pushState = true) {
|
function handlePageNavigation(url, pushState = true) {
|
||||||
if (shouldExcludeUrl(url)) {
|
if (shouldExcludeUrl(url)) {
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoading();
|
showLoading();
|
||||||
const currentScrollPos = window.scrollY;
|
const currentScrollPos = window.scrollY;
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Requested-With": "XMLHttpRequest",
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
"X-AJAX-Navigation": "true",
|
"X-AJAX-Navigation": "true",
|
||||||
Accept: "text/html",
|
Accept: "text/html",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (!response.ok) throw new Error(`Network response was not ok: ${response.status}`);
|
if (!response.ok)
|
||||||
return response.text();
|
throw new Error(`Network response was not ok: ${response.status}`);
|
||||||
})
|
return response.text();
|
||||||
.then((html) => {
|
})
|
||||||
// Parse the HTML and extract #main-content
|
.then((html) => {
|
||||||
const tempDiv = document.createElement("div");
|
// Parse the HTML and extract #main-content
|
||||||
tempDiv.innerHTML = html;
|
const tempDiv = document.createElement("div");
|
||||||
const newContent = tempDiv.querySelector(config.mainContentSelector);
|
tempDiv.innerHTML = html;
|
||||||
if (!newContent) throw new Error("Could not find main content in the response");
|
const newContent = tempDiv.querySelector(config.mainContentSelector);
|
||||||
mainContent.innerHTML = newContent.innerHTML;
|
if (!newContent) throw new Error("Could not find main content in the response");
|
||||||
// Update the page title
|
mainContent.innerHTML = newContent.innerHTML;
|
||||||
const titleMatch = html.match(/<title>(.*?)<\/title>/i);
|
// Update the page title
|
||||||
if (titleMatch) document.title = titleMatch[1];
|
const titleMatch = html.match(/<title>(.*?)<\/title>/i);
|
||||||
// Re-initialize dynamic content
|
if (titleMatch) document.title = titleMatch[1];
|
||||||
reloadScripts(mainContent);
|
// Re-initialize dynamic content
|
||||||
attachEventListeners();
|
reloadScripts(mainContent);
|
||||||
initializePageScripts();
|
attachEventListeners();
|
||||||
if (pushState) {
|
initializePageScripts();
|
||||||
history.pushState(
|
if (pushState) {
|
||||||
{ url: url, title: document.title, scrollPos: currentScrollPos },
|
history.pushState(
|
||||||
document.title,
|
{ url: url, title: document.title, scrollPos: currentScrollPos },
|
||||||
url
|
document.title,
|
||||||
);
|
url,
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
);
|
||||||
} else if (window.history.state && window.history.state.scrollPos) {
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
window.scrollTo({ top: window.history.state.scrollPos });
|
} else if (window.history.state && window.history.state.scrollPos) {
|
||||||
}
|
window.scrollTo({ top: window.history.state.scrollPos });
|
||||||
hideLoading();
|
}
|
||||||
})
|
hideLoading();
|
||||||
.catch((error) => {
|
})
|
||||||
console.error("Error during AJAX navigation:", error);
|
.catch((error) => {
|
||||||
hideLoading();
|
console.error("Error during AJAX navigation:", error);
|
||||||
window.location.href = url;
|
hideLoading();
|
||||||
});
|
window.location.href = url;
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Function to reload and execute scripts in new content
|
// Function to reload and execute scripts in new content
|
||||||
function reloadScripts(container) {
|
function reloadScripts(container) {
|
||||||
const scripts = container.getElementsByTagName("script");
|
const scripts = container.getElementsByTagName("script");
|
||||||
for (let script of scripts) {
|
for (let script of scripts) {
|
||||||
const newScript = document.createElement("script");
|
const newScript = document.createElement("script");
|
||||||
|
|
||||||
// Copy all attributes
|
// Copy all attributes
|
||||||
Array.from(script.attributes).forEach((attr) => {
|
Array.from(script.attributes).forEach((attr) => {
|
||||||
newScript.setAttribute(attr.name, attr.value);
|
newScript.setAttribute(attr.name, attr.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Copy inline script content
|
// Copy inline script content
|
||||||
newScript.textContent = script.textContent;
|
newScript.textContent = script.textContent;
|
||||||
|
|
||||||
// Replace old script with new one
|
// Replace old script with new one
|
||||||
script.parentNode.replaceChild(newScript, script);
|
script.parentNode.replaceChild(newScript, script);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to handle form submissions
|
// Function to handle form submissions
|
||||||
function handleFormSubmission(form, e) {
|
function handleFormSubmission(form, e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// Show loading indicator
|
// Show loading indicator
|
||||||
showLoading();
|
showLoading();
|
||||||
|
|
||||||
// Get form data
|
// Get form data
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
const method = form.method.toLowerCase();
|
const method = form.method.toLowerCase();
|
||||||
const url = form.action || window.location.href;
|
const url = form.action || window.location.href;
|
||||||
|
|
||||||
// Configure fetch options
|
// Configure fetch options
|
||||||
const fetchOptions = {
|
const fetchOptions = {
|
||||||
method: method,
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
"X-AJAX-Navigation": "true",
|
"X-AJAX-Navigation": "true",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle different HTTP methods
|
// Handle different HTTP methods
|
||||||
if (method === "get") {
|
if (method === "get") {
|
||||||
const queryParams = new URLSearchParams(formData).toString();
|
const queryParams = new URLSearchParams(formData).toString();
|
||||||
handlePageNavigation(url + (queryParams ? "?" + queryParams : ""));
|
handlePageNavigation(url + (queryParams ? "?" + queryParams : ""));
|
||||||
} else {
|
} else {
|
||||||
fetchOptions.body = formData;
|
fetchOptions.body = formData;
|
||||||
|
|
||||||
fetch(url, fetchOptions)
|
fetch(url, fetchOptions)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.redirect) {
|
if (data.redirect) {
|
||||||
// Handle server-side redirects
|
// Handle server-side redirects
|
||||||
handlePageNavigation(data.redirect, true);
|
handlePageNavigation(data.redirect, true);
|
||||||
} else {
|
} else {
|
||||||
// Update page content
|
// Update page content
|
||||||
mainContent.innerHTML = data.html;
|
mainContent.innerHTML = data.html;
|
||||||
document.title = data.title || document.title;
|
document.title = data.title || document.title;
|
||||||
|
|
||||||
// Re-initialize dynamic content
|
// Re-initialize dynamic content
|
||||||
reloadScripts(mainContent);
|
reloadScripts(mainContent);
|
||||||
attachEventListeners();
|
attachEventListeners();
|
||||||
initializePageScripts();
|
initializePageScripts();
|
||||||
|
|
||||||
// Update URL if needed
|
// Update URL if needed
|
||||||
if (data.url) {
|
if (data.url) {
|
||||||
history.pushState({ url: data.url }, document.title, data.url);
|
history.pushState({ url: data.url }, document.title, data.url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Form submission error:", error);
|
console.error("Form submission error:", error);
|
||||||
// Fallback to traditional form submission
|
// Fallback to traditional form submission
|
||||||
form.submit();
|
form.submit();
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
hideLoading();
|
hideLoading();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to initialize scripts needed for the new page content
|
// Function to initialize scripts needed for the new page content
|
||||||
function initializePageScripts() {
|
function initializePageScripts() {
|
||||||
// Re-initialize any custom scripts that might be needed
|
// Re-initialize any custom scripts that might be needed
|
||||||
if (typeof setupAjaxPagination === "function") {
|
if (typeof setupAjaxPagination === "function") {
|
||||||
setupAjaxPagination();
|
setupAjaxPagination();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Bootstrap tooltips, popovers, etc.
|
// Initialize Bootstrap tooltips, popovers, etc.
|
||||||
if (typeof bootstrap !== "undefined") {
|
if (typeof bootstrap !== "undefined") {
|
||||||
// Initialize tooltips
|
// Initialize tooltips
|
||||||
const tooltipTriggerList = [].slice.call(
|
const tooltipTriggerList = [].slice.call(
|
||||||
document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
document.querySelectorAll('[data-bs-toggle="tooltip"]'),
|
||||||
);
|
);
|
||||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize popovers
|
// Initialize popovers
|
||||||
const popoverTriggerList = [].slice.call(
|
const popoverTriggerList = [].slice.call(
|
||||||
document.querySelectorAll('[data-bs-toggle="popover"]')
|
document.querySelectorAll('[data-bs-toggle="popover"]'),
|
||||||
);
|
);
|
||||||
popoverTriggerList.map(function (popoverTriggerEl) {
|
popoverTriggerList.map(function (popoverTriggerEl) {
|
||||||
return new bootstrap.Popover(popoverTriggerEl);
|
return new bootstrap.Popover(popoverTriggerEl);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to attach event listeners to forms and links
|
// Function to attach event listeners to forms and links
|
||||||
function attachEventListeners() {
|
function attachEventListeners() {
|
||||||
// Handle AJAX navigation links
|
// Handle AJAX navigation links
|
||||||
document.querySelectorAll(config.navLinkSelector).forEach((link) => {
|
document.querySelectorAll(config.navLinkSelector).forEach((link) => {
|
||||||
if (!link.dataset.ajaxNavInitialized) {
|
if (!link.dataset.ajaxNavInitialized) {
|
||||||
link.addEventListener("click", function (e) {
|
link.addEventListener("click", function (e) {
|
||||||
if (e.ctrlKey || e.metaKey || e.shiftKey || shouldExcludeUrl(this.href)) {
|
if (e.ctrlKey || e.metaKey || e.shiftKey || shouldExcludeUrl(this.href)) {
|
||||||
return; // Let the browser handle these cases
|
return; // Let the browser handle these cases
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handlePageNavigation(this.href);
|
handlePageNavigation(this.href);
|
||||||
});
|
});
|
||||||
link.dataset.ajaxNavInitialized = "true";
|
link.dataset.ajaxNavInitialized = "true";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle forms with AJAX
|
// Handle forms with AJAX
|
||||||
document
|
document
|
||||||
.querySelectorAll("form.ajax-form, form.search-form, form.filter-form")
|
.querySelectorAll("form.ajax-form, form.search-form, form.filter-form")
|
||||||
.forEach((form) => {
|
.forEach((form) => {
|
||||||
if (!form.dataset.ajaxFormInitialized) {
|
if (!form.dataset.ajaxFormInitialized) {
|
||||||
form.addEventListener("submit", (e) => handleFormSubmission(form, e));
|
form.addEventListener("submit", (e) => handleFormSubmission(form, e));
|
||||||
form.dataset.ajaxFormInitialized = "true";
|
form.dataset.ajaxFormInitialized = "true";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial attachment of event listeners
|
// Initial attachment of event listeners
|
||||||
attachEventListeners();
|
attachEventListeners();
|
||||||
|
|
||||||
// Handle browser back/forward buttons
|
// Handle browser back/forward buttons
|
||||||
window.addEventListener("popstate", function (event) {
|
window.addEventListener("popstate", function (event) {
|
||||||
if (event.state && event.state.url) {
|
if (event.state && event.state.url) {
|
||||||
handlePageNavigation(event.state.url, false);
|
handlePageNavigation(event.state.url, false);
|
||||||
} else {
|
} else {
|
||||||
// Fallback to current URL if no state
|
// Fallback to current URL if no state
|
||||||
handlePageNavigation(window.location.href, false);
|
handlePageNavigation(window.location.href, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,101 +6,101 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
// Initialize AJAX pagination
|
// Initialize AJAX pagination
|
||||||
setupAjaxPagination();
|
setupAjaxPagination();
|
||||||
|
|
||||||
// Function to set up AJAX pagination for the entire application
|
// Function to set up AJAX pagination for the entire application
|
||||||
function setupAjaxPagination() {
|
function setupAjaxPagination() {
|
||||||
// Configuration - can be customized per page if needed
|
// Configuration - can be customized per page if needed
|
||||||
const config = {
|
const config = {
|
||||||
contentContainerId: "ajax-content-container", // ID of the container to update
|
contentContainerId: "ajax-content-container", // ID of the container to update
|
||||||
loadingSpinnerId: "ajax-loading-spinner", // ID of the loading spinner
|
loadingSpinnerId: "ajax-loading-spinner", // ID of the loading spinner
|
||||||
paginationLinkClass: "pagination-link", // Class for pagination links
|
paginationLinkClass: "pagination-link", // Class for pagination links
|
||||||
retryMessage: "An error occurred while loading data. Please try again.",
|
retryMessage: "An error occurred while loading data. Please try again.",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get container elements
|
// Get container elements
|
||||||
const contentContainer = document.getElementById(config.contentContainerId);
|
const contentContainer = document.getElementById(config.contentContainerId);
|
||||||
const loadingSpinner = document.getElementById(config.loadingSpinnerId);
|
const loadingSpinner = document.getElementById(config.loadingSpinnerId);
|
||||||
|
|
||||||
// Exit if the page doesn't have the required elements
|
// Exit if the page doesn't have the required elements
|
||||||
if (!contentContainer || !loadingSpinner) return;
|
if (!contentContainer || !loadingSpinner) return;
|
||||||
|
|
||||||
// Function to handle pagination clicks
|
// Function to handle pagination clicks
|
||||||
function setupPaginationListeners() {
|
function setupPaginationListeners() {
|
||||||
document.querySelectorAll("." + config.paginationLinkClass).forEach((link) => {
|
document.querySelectorAll("." + config.paginationLinkClass).forEach((link) => {
|
||||||
link.addEventListener("click", function (e) {
|
link.addEventListener("click", function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleAjaxNavigation(this.href);
|
handleAjaxNavigation(this.href);
|
||||||
|
|
||||||
// Get the page number if available
|
// Get the page number if available
|
||||||
const page = this.getAttribute("data-page");
|
const page = this.getAttribute("data-page");
|
||||||
|
|
||||||
// Update browser URL without refreshing
|
// Update browser URL without refreshing
|
||||||
const newUrl = this.href;
|
const newUrl = this.href;
|
||||||
history.pushState({ url: newUrl, page: page }, "", newUrl);
|
history.pushState({ url: newUrl, page: page }, "", newUrl);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to handle AJAX navigation
|
// Function to handle AJAX navigation
|
||||||
function handleAjaxNavigation(url) {
|
function handleAjaxNavigation(url) {
|
||||||
// Show loading spinner
|
// Show loading spinner
|
||||||
contentContainer.classList.add("d-none");
|
contentContainer.classList.add("d-none");
|
||||||
loadingSpinner.classList.remove("d-none");
|
loadingSpinner.classList.remove("d-none");
|
||||||
|
|
||||||
// Fetch data via AJAX
|
// Fetch data via AJAX
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Requested-With": "XMLHttpRequest",
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Network response was not ok: ${response.status}`);
|
throw new Error(`Network response was not ok: ${response.status}`);
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.status === "success") {
|
if (data.status === "success") {
|
||||||
// Update the content
|
// Update the content
|
||||||
contentContainer.innerHTML = data.html_data;
|
contentContainer.innerHTML = data.html_data;
|
||||||
|
|
||||||
// Re-attach event listeners to new pagination links
|
// Re-attach event listeners to new pagination links
|
||||||
setupPaginationListeners();
|
setupPaginationListeners();
|
||||||
|
|
||||||
// Update any summary data if present and the page provides it
|
// Update any summary data if present and the page provides it
|
||||||
if (typeof updateSummary === "function" && data.summary) {
|
if (typeof updateSummary === "function" && data.summary) {
|
||||||
updateSummary(data);
|
updateSummary(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide loading spinner, show content
|
// Hide loading spinner, show content
|
||||||
loadingSpinner.classList.add("d-none");
|
loadingSpinner.classList.add("d-none");
|
||||||
contentContainer.classList.remove("d-none");
|
contentContainer.classList.remove("d-none");
|
||||||
|
|
||||||
// Scroll to top of the content container
|
// Scroll to top of the content container
|
||||||
contentContainer.scrollIntoView({ behavior: "smooth", block: "start" });
|
contentContainer.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Error fetching data:", error);
|
console.error("Error fetching data:", error);
|
||||||
loadingSpinner.classList.add("d-none");
|
loadingSpinner.classList.add("d-none");
|
||||||
contentContainer.classList.remove("d-none");
|
contentContainer.classList.remove("d-none");
|
||||||
alert(config.retryMessage);
|
alert(config.retryMessage);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial setup of event listeners
|
// Initial setup of event listeners
|
||||||
setupPaginationListeners();
|
setupPaginationListeners();
|
||||||
|
|
||||||
// Handle browser back/forward buttons
|
// Handle browser back/forward buttons
|
||||||
window.addEventListener("popstate", function (event) {
|
window.addEventListener("popstate", function (event) {
|
||||||
if (event.state && event.state.url) {
|
if (event.state && event.state.url) {
|
||||||
handleAjaxNavigation(event.state.url);
|
handleAjaxNavigation(event.state.url);
|
||||||
} else {
|
} else {
|
||||||
// If no state, fetch current URL
|
// If no state, fetch current URL
|
||||||
handleAjaxNavigation(window.location.href);
|
handleAjaxNavigation(window.location.href);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,272 +7,272 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
// Chart responsiveness
|
// Chart responsiveness
|
||||||
function resizeCharts() {
|
function resizeCharts() {
|
||||||
const charts = document.querySelectorAll(".chart-container");
|
const charts = document.querySelectorAll(".chart-container");
|
||||||
charts.forEach((chart) => {
|
charts.forEach((chart) => {
|
||||||
if (chart.id && window.Plotly) {
|
if (chart.id && window.Plotly) {
|
||||||
Plotly.relayout(chart.id, {
|
Plotly.relayout(chart.id, {
|
||||||
"xaxis.automargin": true,
|
"xaxis.automargin": true,
|
||||||
"yaxis.automargin": true,
|
"yaxis.automargin": true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle window resize
|
// Handle window resize
|
||||||
window.addEventListener("resize", function () {
|
window.addEventListener("resize", function () {
|
||||||
if (window.Plotly) {
|
if (window.Plotly) {
|
||||||
resizeCharts();
|
resizeCharts();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Time range filtering
|
// Time range filtering
|
||||||
const timeRangeDropdown = document.getElementById("timeRangeDropdown");
|
const timeRangeDropdown = document.getElementById("timeRangeDropdown");
|
||||||
if (timeRangeDropdown) {
|
if (timeRangeDropdown) {
|
||||||
const timeRangeLinks = timeRangeDropdown.querySelectorAll(".dropdown-item");
|
const timeRangeLinks = timeRangeDropdown.querySelectorAll(".dropdown-item");
|
||||||
timeRangeLinks.forEach((link) => {
|
timeRangeLinks.forEach((link) => {
|
||||||
link.addEventListener("click", function (e) {
|
link.addEventListener("click", function (e) {
|
||||||
const url = new URL(this.href);
|
const url = new URL(this.href);
|
||||||
const dashboardId = url.searchParams.get("dashboard_id");
|
const dashboardId = url.searchParams.get("dashboard_id");
|
||||||
const timeRange = url.searchParams.get("time_range");
|
const timeRange = url.searchParams.get("time_range");
|
||||||
|
|
||||||
// Fetch updated data via AJAX
|
// Fetch updated data via AJAX
|
||||||
if (dashboardId) {
|
if (dashboardId) {
|
||||||
fetchDashboardData(dashboardId, timeRange);
|
fetchDashboardData(dashboardId, timeRange);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to fetch dashboard data
|
// Function to fetch dashboard data
|
||||||
function fetchDashboardData(dashboardId, timeRange) {
|
function fetchDashboardData(dashboardId, timeRange) {
|
||||||
const loadingOverlay = document.createElement("div");
|
const loadingOverlay = document.createElement("div");
|
||||||
loadingOverlay.className = "loading-overlay";
|
loadingOverlay.className = "loading-overlay";
|
||||||
loadingOverlay.innerHTML =
|
loadingOverlay.innerHTML =
|
||||||
'<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>';
|
'<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>';
|
||||||
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) => {
|
.then((response) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Network response was not ok: ${response.status}`);
|
throw new Error(`Network response was not ok: ${response.status}`);
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log("Dashboard API response:", data);
|
console.log("Dashboard API response:", data);
|
||||||
updateDashboardStats(data);
|
updateDashboardStats(data);
|
||||||
updateDashboardCharts(data);
|
updateDashboardCharts(data);
|
||||||
|
|
||||||
// Update URL without page reload
|
// Update URL without page reload
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
url.searchParams.set("dashboard_id", dashboardId);
|
url.searchParams.set("dashboard_id", dashboardId);
|
||||||
if (timeRange) {
|
if (timeRange) {
|
||||||
url.searchParams.set("time_range", timeRange);
|
url.searchParams.set("time_range", timeRange);
|
||||||
}
|
}
|
||||||
window.history.pushState({}, "", url);
|
window.history.pushState({}, "", url);
|
||||||
|
|
||||||
document.querySelector(".loading-overlay").remove();
|
document.querySelector(".loading-overlay").remove();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Error fetching dashboard data:", error);
|
console.error("Error fetching dashboard data:", error);
|
||||||
document.querySelector(".loading-overlay").remove();
|
document.querySelector(".loading-overlay").remove();
|
||||||
|
|
||||||
// Show error message
|
// Show error message
|
||||||
const alertElement = document.createElement("div");
|
const alertElement = document.createElement("div");
|
||||||
alertElement.className = "alert alert-danger alert-dismissible fade show";
|
alertElement.className = "alert alert-danger alert-dismissible fade show";
|
||||||
alertElement.setAttribute("role", "alert");
|
alertElement.setAttribute("role", "alert");
|
||||||
alertElement.innerHTML = `
|
alertElement.innerHTML = `
|
||||||
Error loading dashboard data. Please try again.
|
Error loading dashboard data. Please try again.
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
`;
|
`;
|
||||||
document.querySelector("main").prepend(alertElement);
|
document.querySelector("main").prepend(alertElement);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to update dashboard statistics
|
// Function to update dashboard statistics
|
||||||
function updateDashboardStats(data) {
|
function updateDashboardStats(data) {
|
||||||
// Update total sessions
|
// Update total sessions
|
||||||
const totalSessionsElement = document.querySelector(".stats-card:nth-child(1) h3");
|
const totalSessionsElement = document.querySelector(".stats-card:nth-child(1) h3");
|
||||||
if (totalSessionsElement) {
|
if (totalSessionsElement) {
|
||||||
totalSessionsElement.textContent = data.total_sessions;
|
totalSessionsElement.textContent = data.total_sessions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update average response time
|
// Update average response time
|
||||||
const avgResponseTimeElement = document.querySelector(".stats-card:nth-child(2) h3");
|
const avgResponseTimeElement = document.querySelector(".stats-card:nth-child(2) h3");
|
||||||
if (avgResponseTimeElement) {
|
if (avgResponseTimeElement) {
|
||||||
avgResponseTimeElement.textContent = data.avg_response_time + "s";
|
avgResponseTimeElement.textContent = data.avg_response_time + "s";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update total tokens
|
// Update total tokens
|
||||||
const totalTokensElement = document.querySelector(".stats-card:nth-child(3) h3");
|
const totalTokensElement = document.querySelector(".stats-card:nth-child(3) h3");
|
||||||
if (totalTokensElement) {
|
if (totalTokensElement) {
|
||||||
totalTokensElement.textContent = data.total_tokens;
|
totalTokensElement.textContent = data.total_tokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update total cost
|
// Update total cost
|
||||||
const totalCostElement = document.querySelector(".stats-card:nth-child(4) h3");
|
const totalCostElement = document.querySelector(".stats-card:nth-child(4) h3");
|
||||||
if (totalCostElement) {
|
if (totalCostElement) {
|
||||||
totalCostElement.textContent = "€" + data.total_cost;
|
totalCostElement.textContent = "€" + data.total_cost;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to update dashboard charts
|
// Function to update dashboard charts
|
||||||
function updateDashboardCharts(data) {
|
function updateDashboardCharts(data) {
|
||||||
// Check if Plotly is available
|
// Check if Plotly is available
|
||||||
if (!window.Plotly) {
|
if (!window.Plotly) {
|
||||||
console.error("Plotly library not loaded!");
|
console.error("Plotly library not loaded!");
|
||||||
document.querySelectorAll(".chart-container").forEach((container) => {
|
document.querySelectorAll(".chart-container").forEach((container) => {
|
||||||
container.innerHTML =
|
container.innerHTML =
|
||||||
'<div class="text-center py-5"><p class="text-danger">Chart library not available. Please refresh the page.</p></div>';
|
'<div class="text-center py-5"><p class="text-danger">Chart library not available. Please refresh the page.</p></div>';
|
||||||
});
|
});
|
||||||
return;
|
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) {
|
if (timeSeriesData && timeSeriesData.length > 0) {
|
||||||
try {
|
try {
|
||||||
const timeSeriesX = timeSeriesData.map((item) => item.date);
|
const timeSeriesX = timeSeriesData.map((item) => item.date);
|
||||||
const timeSeriesY = timeSeriesData.map((item) => item.count);
|
const timeSeriesY = timeSeriesData.map((item) => item.count);
|
||||||
|
|
||||||
Plotly.react(
|
Plotly.react(
|
||||||
"sessions-time-chart",
|
"sessions-time-chart",
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
x: timeSeriesX,
|
x: timeSeriesX,
|
||||||
y: timeSeriesY,
|
y: timeSeriesY,
|
||||||
type: "scatter",
|
type: "scatter",
|
||||||
mode: "lines+markers",
|
mode: "lines+markers",
|
||||||
line: {
|
line: {
|
||||||
color: "rgb(75, 192, 192)",
|
color: "rgb(75, 192, 192)",
|
||||||
width: 2,
|
width: 2,
|
||||||
},
|
},
|
||||||
marker: {
|
marker: {
|
||||||
color: "rgb(75, 192, 192)",
|
color: "rgb(75, 192, 192)",
|
||||||
size: 6,
|
size: 6,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
margin: { t: 10, r: 10, b: 40, l: 40 },
|
margin: { t: 10, r: 10, b: 40, l: 40 },
|
||||||
xaxis: {
|
xaxis: {
|
||||||
title: "Date",
|
title: "Date",
|
||||||
},
|
},
|
||||||
yaxis: {
|
yaxis: {
|
||||||
title: "Number of Sessions",
|
title: "Number of Sessions",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error rendering time series chart:", error);
|
console.error("Error rendering time series chart:", error);
|
||||||
document.getElementById("sessions-time-chart").innerHTML =
|
document.getElementById("sessions-time-chart").innerHTML =
|
||||||
'<div class="text-center py-5"><p class="text-danger">Error rendering chart.</p></div>';
|
'<div class="text-center py-5"><p class="text-danger">Error rendering chart.</p></div>';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
document.getElementById("sessions-time-chart").innerHTML =
|
document.getElementById("sessions-time-chart").innerHTML =
|
||||||
'<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>';
|
'<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update sentiment chart
|
// Update sentiment chart
|
||||||
const sentimentData = data.sentiment_data;
|
const sentimentData = data.sentiment_data;
|
||||||
if (sentimentData && sentimentData.length > 0 && window.Plotly) {
|
if (sentimentData && sentimentData.length > 0 && window.Plotly) {
|
||||||
const sentimentLabels = sentimentData.map((item) => item.sentiment);
|
const sentimentLabels = sentimentData.map((item) => item.sentiment);
|
||||||
const sentimentValues = sentimentData.map((item) => item.count);
|
const sentimentValues = sentimentData.map((item) => item.count);
|
||||||
const sentimentColors = sentimentLabels.map((sentiment) => {
|
const sentimentColors = sentimentLabels.map((sentiment) => {
|
||||||
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
|
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
|
||||||
if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)";
|
if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)";
|
||||||
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
|
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
|
||||||
return "rgb(201, 203, 207)";
|
return "rgb(201, 203, 207)";
|
||||||
});
|
});
|
||||||
|
|
||||||
Plotly.react(
|
Plotly.react(
|
||||||
"sentiment-chart",
|
"sentiment-chart",
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
values: sentimentValues,
|
values: sentimentValues,
|
||||||
labels: sentimentLabels,
|
labels: sentimentLabels,
|
||||||
type: "pie",
|
type: "pie",
|
||||||
marker: {
|
marker: {
|
||||||
colors: sentimentColors,
|
colors: sentimentColors,
|
||||||
},
|
},
|
||||||
hole: 0.4,
|
hole: 0.4,
|
||||||
textinfo: "label+percent",
|
textinfo: "label+percent",
|
||||||
insidetextorientation: "radial",
|
insidetextorientation: "radial",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
margin: { t: 10, r: 10, b: 10, l: 10 },
|
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update country chart
|
// Update country chart
|
||||||
const countryData = data.country_data;
|
const countryData = data.country_data;
|
||||||
if (countryData && countryData.length > 0 && window.Plotly) {
|
if (countryData && countryData.length > 0 && window.Plotly) {
|
||||||
const countryLabels = countryData.map((item) => item.country);
|
const countryLabels = countryData.map((item) => item.country);
|
||||||
const countryValues = countryData.map((item) => item.count);
|
const countryValues = countryData.map((item) => item.count);
|
||||||
|
|
||||||
Plotly.react(
|
Plotly.react(
|
||||||
"country-chart",
|
"country-chart",
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
x: countryValues,
|
x: countryValues,
|
||||||
y: countryLabels,
|
y: countryLabels,
|
||||||
type: "bar",
|
type: "bar",
|
||||||
orientation: "h",
|
orientation: "h",
|
||||||
marker: {
|
marker: {
|
||||||
color: "rgb(54, 162, 235)",
|
color: "rgb(54, 162, 235)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
margin: { t: 10, r: 10, b: 40, l: 100 },
|
margin: { t: 10, r: 10, b: 40, l: 100 },
|
||||||
xaxis: {
|
xaxis: {
|
||||||
title: "Number of Sessions",
|
title: "Number of Sessions",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update category chart
|
// Update category chart
|
||||||
const categoryData = data.category_data;
|
const categoryData = data.category_data;
|
||||||
if (categoryData && categoryData.length > 0 && window.Plotly) {
|
if (categoryData && categoryData.length > 0 && window.Plotly) {
|
||||||
const categoryLabels = categoryData.map((item) => item.category);
|
const categoryLabels = categoryData.map((item) => item.category);
|
||||||
const categoryValues = categoryData.map((item) => item.count);
|
const categoryValues = categoryData.map((item) => item.count);
|
||||||
|
|
||||||
Plotly.react(
|
Plotly.react(
|
||||||
"category-chart",
|
"category-chart",
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
labels: categoryLabels,
|
labels: categoryLabels,
|
||||||
values: categoryValues,
|
values: categoryValues,
|
||||||
type: "pie",
|
type: "pie",
|
||||||
textinfo: "label+percent",
|
textinfo: "label+percent",
|
||||||
insidetextorientation: "radial",
|
insidetextorientation: "radial",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
margin: { t: 10, r: 10, b: 10, l: 10 },
|
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dashboard selector
|
// Dashboard selector
|
||||||
const dashboardSelector = document.querySelectorAll('a[href^="?dashboard_id="]');
|
const dashboardSelector = document.querySelectorAll('a[href^="?dashboard_id="]');
|
||||||
dashboardSelector.forEach((link) => {
|
dashboardSelector.forEach((link) => {
|
||||||
link.addEventListener("click", function (e) {
|
link.addEventListener("click", function (e) {
|
||||||
const url = new URL(this.href);
|
const url = new URL(this.href);
|
||||||
const dashboardId = url.searchParams.get("dashboard_id");
|
const dashboardId = url.searchParams.get("dashboard_id");
|
||||||
|
|
||||||
// Fetch updated data via AJAX
|
// Fetch updated data via AJAX
|
||||||
if (dashboardId) {
|
if (dashboardId) {
|
||||||
fetchDashboardData(dashboardId);
|
fetchDashboardData(dashboardId);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,147 +6,147 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
// Initialize tooltips
|
// Initialize tooltips
|
||||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize popovers
|
// Initialize popovers
|
||||||
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
|
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
|
||||||
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
|
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
|
||||||
return new bootstrap.Popover(popoverTriggerEl);
|
return new bootstrap.Popover(popoverTriggerEl);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggle sidebar on mobile
|
// Toggle sidebar on mobile
|
||||||
const sidebarToggle = document.querySelector("#sidebarToggle");
|
const sidebarToggle = document.querySelector("#sidebarToggle");
|
||||||
if (sidebarToggle) {
|
if (sidebarToggle) {
|
||||||
sidebarToggle.addEventListener("click", function () {
|
sidebarToggle.addEventListener("click", function () {
|
||||||
document.querySelector(".sidebar").classList.toggle("show");
|
document.querySelector(".sidebar").classList.toggle("show");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-dismiss alerts after 5 seconds
|
// Auto-dismiss alerts after 5 seconds
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
var alerts = document.querySelectorAll(".alert:not(.alert-important)");
|
var alerts = document.querySelectorAll(".alert:not(.alert-important)");
|
||||||
alerts.forEach(function (alert) {
|
alerts.forEach(function (alert) {
|
||||||
if (alert && bootstrap.Alert.getInstance(alert)) {
|
if (alert && bootstrap.Alert.getInstance(alert)) {
|
||||||
bootstrap.Alert.getInstance(alert).close();
|
bootstrap.Alert.getInstance(alert).close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
// Form validation
|
// Form validation
|
||||||
const forms = document.querySelectorAll(".needs-validation");
|
const forms = document.querySelectorAll(".needs-validation");
|
||||||
forms.forEach(function (form) {
|
forms.forEach(function (form) {
|
||||||
form.addEventListener(
|
form.addEventListener(
|
||||||
"submit",
|
"submit",
|
||||||
function (event) {
|
function (event) {
|
||||||
if (!form.checkValidity()) {
|
if (!form.checkValidity()) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
form.classList.add("was-validated");
|
form.classList.add("was-validated");
|
||||||
},
|
},
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Confirm dialogs
|
// Confirm dialogs
|
||||||
const confirmButtons = document.querySelectorAll("[data-confirm]");
|
const confirmButtons = document.querySelectorAll("[data-confirm]");
|
||||||
confirmButtons.forEach(function (button) {
|
confirmButtons.forEach(function (button) {
|
||||||
button.addEventListener("click", function (event) {
|
button.addEventListener("click", function (event) {
|
||||||
if (!confirm(this.dataset.confirm || "Are you sure?")) {
|
if (!confirm(this.dataset.confirm || "Are you sure?")) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Back button
|
// Back button
|
||||||
const backButtons = document.querySelectorAll(".btn-back");
|
const backButtons = document.querySelectorAll(".btn-back");
|
||||||
backButtons.forEach(function (button) {
|
backButtons.forEach(function (button) {
|
||||||
button.addEventListener("click", function (event) {
|
button.addEventListener("click", function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
window.history.back();
|
window.history.back();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// File input customization
|
// File input customization
|
||||||
const fileInputs = document.querySelectorAll(".custom-file-input");
|
const fileInputs = document.querySelectorAll(".custom-file-input");
|
||||||
fileInputs.forEach(function (input) {
|
fileInputs.forEach(function (input) {
|
||||||
input.addEventListener("change", function (e) {
|
input.addEventListener("change", function (e) {
|
||||||
const fileName = this.files[0]?.name || "Choose file";
|
const fileName = this.files[0]?.name || "Choose file";
|
||||||
const nextSibling = this.nextElementSibling;
|
const nextSibling = this.nextElementSibling;
|
||||||
if (nextSibling) {
|
if (nextSibling) {
|
||||||
nextSibling.innerText = fileName;
|
nextSibling.innerText = fileName;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Search form submit on enter
|
// Search form submit on enter
|
||||||
const searchInputs = document.querySelectorAll(".search-input");
|
const searchInputs = document.querySelectorAll(".search-input");
|
||||||
searchInputs.forEach(function (input) {
|
searchInputs.forEach(function (input) {
|
||||||
input.addEventListener("keypress", function (e) {
|
input.addEventListener("keypress", function (e) {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.closest("form").submit();
|
this.closest("form").submit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggle password visibility
|
// Toggle password visibility
|
||||||
const togglePasswordButtons = document.querySelectorAll(".toggle-password");
|
const togglePasswordButtons = document.querySelectorAll(".toggle-password");
|
||||||
togglePasswordButtons.forEach(function (button) {
|
togglePasswordButtons.forEach(function (button) {
|
||||||
button.addEventListener("click", function () {
|
button.addEventListener("click", function () {
|
||||||
const target = document.querySelector(this.dataset.target);
|
const target = document.querySelector(this.dataset.target);
|
||||||
if (target) {
|
if (target) {
|
||||||
const type = target.getAttribute("type") === "password" ? "text" : "password";
|
const type = target.getAttribute("type") === "password" ? "text" : "password";
|
||||||
target.setAttribute("type", type);
|
target.setAttribute("type", type);
|
||||||
this.querySelector("i").classList.toggle("fa-eye");
|
this.querySelector("i").classList.toggle("fa-eye");
|
||||||
this.querySelector("i").classList.toggle("fa-eye-slash");
|
this.querySelector("i").classList.toggle("fa-eye-slash");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dropdown menu positioning
|
// Dropdown menu positioning
|
||||||
const dropdowns = document.querySelectorAll(".dropdown-menu");
|
const dropdowns = document.querySelectorAll(".dropdown-menu");
|
||||||
dropdowns.forEach(function (dropdown) {
|
dropdowns.forEach(function (dropdown) {
|
||||||
dropdown.addEventListener("click", function (e) {
|
dropdown.addEventListener("click", function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Responsive table handling
|
// Responsive table handling
|
||||||
const tables = document.querySelectorAll(".table-responsive");
|
const tables = document.querySelectorAll(".table-responsive");
|
||||||
if (window.innerWidth < 768) {
|
if (window.innerWidth < 768) {
|
||||||
tables.forEach(function (table) {
|
tables.forEach(function (table) {
|
||||||
table.classList.add("table-responsive-force");
|
table.classList.add("table-responsive-force");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle special links (printable views, exports)
|
// Handle special links (printable views, exports)
|
||||||
const printLinks = document.querySelectorAll(".print-link");
|
const printLinks = document.querySelectorAll(".print-link");
|
||||||
printLinks.forEach(function (link) {
|
printLinks.forEach(function (link) {
|
||||||
link.addEventListener("click", function (e) {
|
link.addEventListener("click", function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.print();
|
window.print();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const exportLinks = document.querySelectorAll("[data-export]");
|
const exportLinks = document.querySelectorAll("[data-export]");
|
||||||
exportLinks.forEach(function (link) {
|
exportLinks.forEach(function (link) {
|
||||||
link.addEventListener("click", function (e) {
|
link.addEventListener("click", function (e) {
|
||||||
// Handle export functionality if needed
|
// Handle export functionality if needed
|
||||||
console.log("Export requested:", this.dataset.export);
|
console.log("Export requested:", this.dataset.export);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle sidebar collapse on small screens
|
// Handle sidebar collapse on small screens
|
||||||
function handleSidebarOnResize() {
|
function handleSidebarOnResize() {
|
||||||
if (window.innerWidth < 768) {
|
if (window.innerWidth < 768) {
|
||||||
document.querySelector(".sidebar")?.classList.remove("show");
|
document.querySelector(".sidebar")?.classList.remove("show");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("resize", handleSidebarOnResize);
|
window.addEventListener("resize", handleSidebarOnResize);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,25 +5,27 @@
|
|||||||
{% block title %}Login | Chat Analytics{% endblock %}
|
{% block title %}Login | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card mt-4">
|
<div class="card mt-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h4 class="card-title mb-0">Login</h4>
|
<h4 class="card-title mb-0">Login</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form|crispy }}
|
{{ form|crispy }}
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<button type="submit" class="btn btn-primary">Login</button>
|
<button type="submit" class="btn btn-primary">Login</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer text-center">
|
<div class="card-footer text-center">
|
||||||
<p class="mb-0">Don't have an account? <a href="{% url 'register' %}">Register</a></p>
|
<p class="mb-0">
|
||||||
</div>
|
Don't have an account? <a href="{% url 'register' %}">Register</a>
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -5,33 +5,33 @@
|
|||||||
{% block title %}Change Password | Chat Analytics{% endblock %}
|
{% block title %}Change Password | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div
|
<div
|
||||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
>
|
>
|
||||||
<h1 class="h2">Change Password</h1>
|
<h1 class="h2">Change Password</h1>
|
||||||
<div class="btn-toolbar mb-2 mb-md-0">
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
<a href="{% url 'profile' %}" class="btn btn-sm btn-outline-secondary">
|
<a href="{% url 'profile' %}" class="btn btn-sm btn-outline-secondary">
|
||||||
<i class="fas fa-arrow-left"></i> Back to Profile
|
<i class="fas fa-arrow-left"></i> Back to Profile
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">Change Your Password</h5>
|
<h5 class="card-title mb-0">Change Your Password</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form|crispy }}
|
{{ form|crispy }}
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<button type="submit" class="btn btn-primary">Change Password</button>
|
<button type="submit" class="btn btn-primary">Change Password</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -4,38 +4,41 @@
|
|||||||
{% block title %}Password Changed | Chat Analytics{% endblock %}
|
{% block title %}Password Changed | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div
|
<div
|
||||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
>
|
>
|
||||||
<h1 class="h2">Password Changed</h1>
|
<h1 class="h2">Password Changed</h1>
|
||||||
<div class="btn-toolbar mb-2 mb-md-0">
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
<a href="{% url 'profile' %}" class="btn btn-sm btn-outline-secondary">
|
<a href="{% url 'profile' %}" class="btn btn-sm btn-outline-secondary">
|
||||||
<i class="fas fa-arrow-left"></i> Back to Profile
|
<i class="fas fa-arrow-left"></i> Back to Profile
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header bg-success text-white">
|
<div class="card-header bg-success text-white">
|
||||||
<h5 class="card-title mb-0">Password Changed Successfully</h5>
|
<h5 class="card-title mb-0">Password Changed Successfully</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<i class="fas fa-check-circle fa-4x text-success mb-3"></i>
|
<i class="fas fa-check-circle fa-4x text-success mb-3"></i>
|
||||||
<h4>Your password has been changed successfully!</h4>
|
<h4>Your password has been changed successfully!</h4>
|
||||||
<p>Your new password is now active. You can use it the next time you log in.</p>
|
<p>
|
||||||
</div>
|
Your new password is now active. You can use it the next time you log
|
||||||
|
in.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<a href="{% url 'profile' %}" class="btn btn-primary">Return to Profile</a>
|
<a href="{% url 'profile' %}" class="btn btn-primary">Return to Profile</a>
|
||||||
<a href="{% url 'dashboard' %}" class="btn btn-outline-secondary ms-2"
|
<a href="{% url 'dashboard' %}" class="btn btn-outline-secondary ms-2"
|
||||||
>Go to Dashboard</a
|
>Go to Dashboard</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -4,186 +4,210 @@
|
|||||||
{% block title %}My Profile | Chat Analytics{% endblock %}
|
{% block title %}My Profile | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div
|
<div
|
||||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
>
|
>
|
||||||
<h1 class="h2">My Profile</h1>
|
<h1 class="h2">My Profile</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">
|
||||||
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">Account Information</h5>
|
<h5 class="card-title mb-0">Account Information</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-4 fw-bold">Username:</div>
|
<div class="col-md-4 fw-bold">Username:</div>
|
||||||
<div class="col-md-8">{{ user.username }}</div>
|
<div class="col-md-8">{{ user.username }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-4 fw-bold">Email:</div>
|
<div class="col-md-4 fw-bold">Email:</div>
|
||||||
<div class="col-md-8">{{ user.email }}</div>
|
<div class="col-md-8">{{ user.email }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-4 fw-bold">Company:</div>
|
<div class="col-md-4 fw-bold">Company:</div>
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
{% if user.company %}
|
{% if user.company %}
|
||||||
{{ user.company.name }}
|
{{ user.company.name }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">Not assigned to a company</span>
|
<span class="text-muted">Not assigned to a company</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-4 fw-bold">Role:</div>
|
<div class="col-md-4 fw-bold">Role:</div>
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff %}
|
||||||
<span class="badge bg-danger">Admin</span>
|
<span class="badge bg-danger">Admin</span>
|
||||||
{% elif user.is_company_admin %}
|
{% elif user.is_company_admin %}
|
||||||
<span class="badge bg-primary">Company Admin</span>
|
<span class="badge bg-primary">Company Admin</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-secondary">User</span>
|
<span class="badge bg-secondary">User</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-4 fw-bold">Last Login:</div>
|
<div class="col-md-4 fw-bold">Last Login:</div>
|
||||||
<div class="col-md-8">{{ user.last_login|date:"F d, Y H:i" }}</div>
|
<div class="col-md-8">{{ user.last_login|date:"F d, Y H:i" }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-4 fw-bold">Date Joined:</div>
|
<div class="col-md-4 fw-bold">Date Joined:</div>
|
||||||
<div class="col-md-8">{{ user.date_joined|date:"F d, Y H:i" }}</div>
|
<div class="col-md-8">{{ user.date_joined|date:"F d, Y H:i" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<a href="{% url 'password_change' %}" class="btn btn-primary">Change Password</a>
|
<a href="{% url 'password_change' %}" class="btn btn-primary"
|
||||||
</div>
|
>Change Password</a
|
||||||
</div>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if user.company %}
|
{% if user.company %}
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">Company Information</h5>
|
<h5 class="card-title mb-0">Company Information</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-4 fw-bold">Company Name:</div>
|
<div class="col-md-4 fw-bold">Company Name:</div>
|
||||||
<div class="col-md-8">{{ user.company.name }}</div>
|
<div class="col-md-8">{{ user.company.name }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-4 fw-bold">Description:</div>
|
<div class="col-md-4 fw-bold">Description:</div>
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
{{ user.company.description|default:"No description available." }}
|
{{ user.company.description|default:"No description available." }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-4 fw-bold">Created:</div>
|
<div class="col-md-4 fw-bold">Created:</div>
|
||||||
<div class="col-md-8">{{ user.company.created_at|date:"F d, Y" }}</div>
|
<div class="col-md-8">{{ user.company.created_at|date:"F d, Y" }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-4 fw-bold">Total Employees:</div>
|
<div class="col-md-4 fw-bold">Total Employees:</div>
|
||||||
<div class="col-md-8">{{ user.company.employees.count }}</div>
|
<div class="col-md-8">{{ user.company.employees.count }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-4 fw-bold">Data Sources:</div>
|
<div class="col-md-4 fw-bold">Data Sources:</div>
|
||||||
<div class="col-md-8">{{ user.company.data_sources.count }}</div>
|
<div class="col-md-8">{{ user.company.data_sources.count }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if user.is_company_admin or user.is_staff %}
|
{% if user.is_company_admin or user.is_staff %}
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">Admin Actions</h5>
|
<h5 class="card-title mb-0">Admin Actions</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff %}
|
||||||
<div class="col-md-4 mb-3">
|
<div class="col-md-4 mb-3">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<h5 class="card-title">Manage Users</h5>
|
<h5 class="card-title">Manage Users</h5>
|
||||||
<p class="card-text">Manage users and assign them to companies.</p>
|
<p class="card-text">
|
||||||
<a
|
Manage users and assign them to companies.
|
||||||
href="{% url 'admin:accounts_customuser_changelist' %}"
|
</p>
|
||||||
class="btn btn-primary"
|
<a
|
||||||
>Manage Users</a
|
href="{% url 'admin:accounts_customuser_changelist' %}"
|
||||||
>
|
class="btn btn-primary"
|
||||||
</div>
|
>Manage Users</a
|
||||||
</div>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 mb-3">
|
</div>
|
||||||
<div class="card h-100">
|
</div>
|
||||||
<div class="card-body text-center">
|
<div class="col-md-4 mb-3">
|
||||||
<h5 class="card-title">Manage Companies</h5>
|
<div class="card h-100">
|
||||||
<p class="card-text">Create and edit companies in the system.</p>
|
<div class="card-body text-center">
|
||||||
<a
|
<h5 class="card-title">Manage Companies</h5>
|
||||||
href="{% url 'admin:accounts_company_changelist' %}"
|
<p class="card-text">
|
||||||
class="btn btn-primary"
|
Create and edit companies in the system.
|
||||||
>Manage Companies</a
|
</p>
|
||||||
>
|
<a
|
||||||
</div>
|
href="{% url 'admin:accounts_company_changelist' %}"
|
||||||
</div>
|
class="btn btn-primary"
|
||||||
</div>
|
>Manage Companies</a
|
||||||
<div class="col-md-4 mb-3">
|
>
|
||||||
<div class="card h-100">
|
</div>
|
||||||
<div class="card-body text-center">
|
</div>
|
||||||
<h5 class="card-title">Admin Dashboard</h5>
|
</div>
|
||||||
<p class="card-text">Go to the full admin dashboard.</p>
|
<div class="col-md-4 mb-3">
|
||||||
<a href="{% url 'admin:index' %}" class="btn btn-primary">Admin Dashboard</a>
|
<div class="card h-100">
|
||||||
</div>
|
<div class="card-body text-center">
|
||||||
</div>
|
<h5 class="card-title">Admin Dashboard</h5>
|
||||||
</div>
|
<p class="card-text">Go to the full admin dashboard.</p>
|
||||||
{% elif user.is_company_admin %}
|
<a
|
||||||
<div class="col-md-4 mb-3">
|
href="{% url 'admin:index' %}"
|
||||||
<div class="card h-100">
|
class="btn btn-primary"
|
||||||
<div class="card-body text-center">
|
>Admin Dashboard</a
|
||||||
<h5 class="card-title">Manage Dashboards</h5>
|
>
|
||||||
<p class="card-text">Create and edit dashboards for your company.</p>
|
</div>
|
||||||
<a href="{% url 'create_dashboard' %}" class="btn btn-primary"
|
</div>
|
||||||
>Manage Dashboards</a
|
</div>
|
||||||
>
|
{% elif user.is_company_admin %}
|
||||||
</div>
|
<div class="col-md-4 mb-3">
|
||||||
</div>
|
<div class="card h-100">
|
||||||
</div>
|
<div class="card-body text-center">
|
||||||
<div class="col-md-4 mb-3">
|
<h5 class="card-title">Manage Dashboards</h5>
|
||||||
<div class="card h-100">
|
<p class="card-text">
|
||||||
<div class="card-body text-center">
|
Create and edit dashboards for your company.
|
||||||
<h5 class="card-title">Upload Data</h5>
|
</p>
|
||||||
<p class="card-text">Upload and manage data sources for analysis.</p>
|
<a
|
||||||
<a href="{% url 'upload_data' %}" class="btn btn-primary">Upload Data</a>
|
href="{% url 'create_dashboard' %}"
|
||||||
</div>
|
class="btn btn-primary"
|
||||||
</div>
|
>Manage Dashboards</a
|
||||||
</div>
|
>
|
||||||
<div class="col-md-4 mb-3">
|
</div>
|
||||||
<div class="card h-100">
|
</div>
|
||||||
<div class="card-body text-center">
|
</div>
|
||||||
<h5 class="card-title">Search Sessions</h5>
|
<div class="col-md-4 mb-3">
|
||||||
<p class="card-text">Search and analyze chat sessions.</p>
|
<div class="card h-100">
|
||||||
<a href="{% url 'search_chat_sessions' %}" class="btn btn-primary"
|
<div class="card-body text-center">
|
||||||
>Search Sessions</a
|
<h5 class="card-title">Upload Data</h5>
|
||||||
>
|
<p class="card-text">
|
||||||
</div>
|
Upload and manage data sources for analysis.
|
||||||
</div>
|
</p>
|
||||||
</div>
|
<a
|
||||||
{% endif %}
|
href="{% url 'upload_data' %}"
|
||||||
</div>
|
class="btn btn-primary"
|
||||||
</div>
|
>Upload Data</a
|
||||||
</div>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title">Search Sessions</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
Search and analyze chat sessions.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="{% url 'search_chat_sessions' %}"
|
||||||
|
class="btn btn-primary"
|
||||||
|
>Search Sessions</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -5,25 +5,27 @@
|
|||||||
{% block title %}Register | Chat Analytics{% endblock %}
|
{% block title %}Register | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card mt-4">
|
<div class="card mt-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h4 class="card-title mb-0">Register</h4>
|
<h4 class="card-title mb-0">Register</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form|crispy }}
|
{{ form|crispy }}
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<button type="submit" class="btn btn-primary">Register</button>
|
<button type="submit" class="btn btn-primary">Register</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer text-center">
|
<div class="card-footer text-center">
|
||||||
<p class="mb-0">Already have an account? <a href="{% url 'login' %}">Login</a></p>
|
<p class="mb-0">
|
||||||
</div>
|
Already have an account? <a href="{% url 'login' %}">Login</a>
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -2,314 +2,339 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>{% block title %}Chat Analytics Dashboard{% endblock %}</title>
|
<title>{% block title %}Chat Analytics Dashboard{% endblock %}</title>
|
||||||
|
|
||||||
<!-- Bootstrap CSS -->
|
<!-- Bootstrap CSS -->
|
||||||
<link
|
<link
|
||||||
href="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/css/bootstrap.min.css"
|
href="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/css/bootstrap.min.css"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Font Awesome -->
|
<!-- Font Awesome -->
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@latest/css/all.min.css"
|
href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@latest/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Plotly.js -->
|
<!-- Plotly.js -->
|
||||||
<script
|
<script
|
||||||
src="https://cdn.jsdelivr.net/npm/plotly.js@latest/dist/plotly.min.js"
|
src="https://cdn.jsdelivr.net/npm/plotly.js@latest/dist/plotly.min.js"
|
||||||
charset="utf-8"
|
charset="utf-8"
|
||||||
></script>
|
></script>
|
||||||
|
|
||||||
<!-- Custom CSS -->
|
<!-- Custom CSS -->
|
||||||
<link rel="stylesheet" href="{% static 'css/style.css' %}" />
|
<link rel="stylesheet" href="{% static 'css/style.css' %}" />
|
||||||
<link rel="stylesheet" href="{% static 'css/dashboard.css' %}" />
|
<link rel="stylesheet" href="{% static 'css/dashboard.css' %}" />
|
||||||
|
|
||||||
{% block extra_css %}{% endblock %}
|
{% block extra_css %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<!-- Navbar -->
|
<!-- Navbar -->
|
||||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark absolute-top">
|
<nav class="navbar navbar-expand-md navbar-dark bg-dark absolute-top">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="{% url 'dashboard' %}">Chat Analytics</a>
|
<a class="navbar-brand" href="{% url 'dashboard' %}">Chat Analytics</a>
|
||||||
<button
|
<button
|
||||||
class="navbar-toggler"
|
class="navbar-toggler"
|
||||||
type="button"
|
type="button"
|
||||||
data-bs-toggle="collapse"
|
data-bs-toggle="collapse"
|
||||||
data-bs-target="#navbarCollapse"
|
data-bs-target="#navbarCollapse"
|
||||||
>
|
>
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||||
<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 ajax-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 ajax-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 ajax-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
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-light dropdown-toggle"
|
class="btn btn-outline-light dropdown-toggle"
|
||||||
type="button"
|
type="button"
|
||||||
id="userDropdown"
|
id="userDropdown"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
>
|
>
|
||||||
{% if user.company %}
|
{% if user.company %}
|
||||||
<span class="badge bg-info me-1">{{ user.company.name }}</span>
|
<span class="badge bg-info me-1"
|
||||||
{% endif %}
|
>{{ user.company.name }}</span
|
||||||
{{ user.username }}
|
>
|
||||||
</button>
|
{% endif %}
|
||||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
|
{{ user.username }}
|
||||||
<li>
|
</button>
|
||||||
<a class="dropdown-item ajax-nav-link" href="{% url 'profile' %}">Profile</a>
|
<ul
|
||||||
</li>
|
class="dropdown-menu dropdown-menu-end"
|
||||||
{% if user.is_staff %}
|
aria-labelledby="userDropdown"
|
||||||
<li><a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a></li>
|
>
|
||||||
{% endif %}
|
<li>
|
||||||
<li>
|
<a
|
||||||
<hr class="dropdown-divider" />
|
class="dropdown-item ajax-nav-link"
|
||||||
</li>
|
href="{% url 'profile' %}"
|
||||||
<li><a class="dropdown-item" href="{% url 'logout' %}">Logout</a></li>
|
>Profile</a
|
||||||
</ul>
|
>
|
||||||
</div>
|
</li>
|
||||||
{% else %}
|
{% if user.is_staff %}
|
||||||
<a href="{% url 'login' %}" class="btn btn-outline-light me-2">Login</a>
|
<li>
|
||||||
<a href="{% url 'register' %}" class="btn btn-light">Register</a>
|
<a class="dropdown-item" href="{% url 'admin:index' %}"
|
||||||
{% endif %}
|
>Admin</a
|
||||||
</div>
|
>
|
||||||
</div>
|
</li>
|
||||||
</div>
|
{% endif %}
|
||||||
</nav>
|
<li>
|
||||||
|
<hr class="dropdown-divider" />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{% url 'logout' %}"
|
||||||
|
>Logout</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'login' %}" class="btn btn-outline-light me-2">Login</a>
|
||||||
|
<a href="{% url 'register' %}" class="btn btn-light">Register</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<nav
|
<nav
|
||||||
id="sidebarMenu"
|
id="sidebarMenu"
|
||||||
class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse sticky-top h-100 p-0"
|
class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse sticky-top h-100 p-0"
|
||||||
>
|
>
|
||||||
<div class="sidebar-sticky pt-3">
|
<div class="sidebar-sticky pt-3">
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a
|
<a
|
||||||
class="nav-link ajax-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>
|
||||||
Dashboard
|
Dashboard
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a
|
<a
|
||||||
class="nav-link ajax-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>
|
||||||
Upload Data
|
Upload Data
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a
|
<a
|
||||||
class="nav-link ajax-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>
|
||||||
Search
|
Search
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a
|
<a
|
||||||
class="nav-link ajax-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>
|
||||||
Data View
|
Data View
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% if user.is_authenticated and user.company %}
|
{% if user.is_authenticated and user.company %}
|
||||||
{% if dashboards %}
|
{% if dashboards %}
|
||||||
<li class="nav-header"><strong>Dashboards</strong></li>
|
<li class="nav-header"><strong>Dashboards</strong></li>
|
||||||
{% for dashboard in dashboards %}
|
{% for dashboard in dashboards %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a
|
<a
|
||||||
class="nav-link {% if selected_dashboard.id == dashboard.id %}active{% endif %}"
|
class="nav-link {% if selected_dashboard.id == dashboard.id %}active{% endif %}"
|
||||||
href="{% url 'dashboard' %}?dashboard_id={{ dashboard.id }}"
|
href="{% url 'dashboard' %}?dashboard_id={{ dashboard.id }}"
|
||||||
>
|
>
|
||||||
<i class="fas fa-chart-line me-2"></i>
|
<i class="fas fa-chart-line me-2"></i>
|
||||||
{{ dashboard.name }}
|
{{ dashboard.name }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'create_dashboard' %}">
|
<a class="nav-link" href="{% url 'create_dashboard' %}">
|
||||||
<i class="fas fa-plus-circle me-2"></i>
|
<i class="fas fa-plus-circle me-2"></i>
|
||||||
New Dashboard
|
New Dashboard
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if data_sources %}
|
{% if data_sources %}
|
||||||
<li class="nav-header"><strong>Data Sources</strong></li>
|
<li class="nav-header"><strong>Data Sources</strong></li>
|
||||||
{% for data_source in data_sources %}
|
{% for data_source in data_sources %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'data_source_detail' data_source.id %}">
|
<a
|
||||||
<i class="fas fa-database me-2"></i>
|
class="nav-link"
|
||||||
{{ data_source.name }}
|
href="{% url 'data_source_detail' data_source.id %}"
|
||||||
</a>
|
>
|
||||||
</li>
|
<i class="fas fa-database me-2"></i>
|
||||||
{% endfor %}
|
{{ data_source.name }}
|
||||||
{% endif %}
|
</a>
|
||||||
{% endif %}
|
</li>
|
||||||
</ul>
|
{% endfor %}
|
||||||
{% endblock %}
|
{% endif %}
|
||||||
</div>
|
{% endif %}
|
||||||
</nav>
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 main-content">
|
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 main-content">
|
||||||
{# {% if messages %} #}
|
{# {% if messages %} #}
|
||||||
{# <div class="messages mt-3"> #}
|
{# <div class="messages mt-3"> #}
|
||||||
{# {% for message in messages %} #}
|
{# {% for message in messages %} #}
|
||||||
{# <div class="alert {% if message.tags == 'error' %}alert-danger{% elif message.tags == 'success' %}alert-success{% elif message.tags == 'warning' %}alert-warning{% else %}alert-info{% endif %} alert-dismissible fade show" role="alert"> #}
|
{# <div class="alert {% if message.tags == 'error' %}alert-danger{% elif message.tags == 'success' %}alert-success{% elif message.tags == 'warning' %}alert-warning{% else %}alert-info{% endif %} alert-dismissible fade show" role="alert"> #}
|
||||||
{# {{ message }} #}
|
{# {{ message }} #}
|
||||||
{# <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> #}
|
{# <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> #}
|
||||||
{# </div> #}
|
{# </div> #}
|
||||||
{# {% endfor %} #}
|
{# {% endfor %} #}
|
||||||
{# </div> #}
|
{# </div> #}
|
||||||
{# {% endif %} #}
|
{# {% endif %} #}
|
||||||
|
|
||||||
<div id="main-content">
|
<div id="main-content">
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<p>© {% now "Y" %} KJANAT All rights reserved. | Chat Analytics Dashboard.</p>
|
<p>© {% now "Y" %} KJANAT All rights reserved. | Chat Analytics Dashboard.</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- Bootstrap JS -->
|
<!-- Bootstrap JS -->
|
||||||
<script
|
<script
|
||||||
src="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/js/bootstrap.bundle.min.js"
|
src="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/js/bootstrap.bundle.min.js"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
></script>
|
></script>
|
||||||
<!-- jQuery (for Ajax) -->
|
<!-- jQuery (for Ajax) -->
|
||||||
<script
|
<script
|
||||||
src="https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js"
|
src="https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
></script>
|
></script>
|
||||||
|
|
||||||
<!-- Custom JavaScript -->
|
<!-- Custom JavaScript -->
|
||||||
<script src="{% static 'js/main.js' %}"></script>
|
<script src="{% static 'js/main.js' %}"></script>
|
||||||
<script src="{% static 'js/ajax-pagination.js' %}"></script>
|
<script src="{% static 'js/ajax-pagination.js' %}"></script>
|
||||||
<script src="{% static 'js/ajax-navigation.js' %}"></script>
|
<script src="{% static 'js/ajax-navigation.js' %}"></script>
|
||||||
|
|
||||||
<!-- Enable AJAX Navigation -->
|
<!-- Enable AJAX Navigation -->
|
||||||
<script>
|
<script>
|
||||||
// Enable AJAX navigation for the entire application
|
// Enable AJAX navigation for the entire application
|
||||||
var ENABLE_AJAX_NAVIGATION = true;
|
var ENABLE_AJAX_NAVIGATION = true;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Check if Plotly loaded successfully -->
|
<!-- Check if Plotly loaded successfully -->
|
||||||
<script>
|
<script>
|
||||||
if (typeof Plotly === "undefined") {
|
if (typeof Plotly === "undefined") {
|
||||||
console.error("Plotly library failed to load. Will attempt to load fallback.");
|
console.error("Plotly library failed to load. Will attempt to load fallback.");
|
||||||
// Try to load Plotly from alternative source
|
// Try to load Plotly from alternative source
|
||||||
const script = document.createElement("script");
|
const script = document.createElement("script");
|
||||||
script.src = "https://cdn.jsdelivr.net/npm/plotly.js@latest/dist/plotly.min.js";
|
script.src = "https://cdn.jsdelivr.net/npm/plotly.js@latest/dist/plotly.min.js";
|
||||||
script.async = true;
|
script.async = true;
|
||||||
script.crossOrigin = "anonymous";
|
script.crossOrigin = "anonymous";
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1100;">
|
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1100;">
|
||||||
<!-- Toasts will be appended here -->
|
<!-- Toasts will be appended here -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<!-- Pre-render message data that will be used by JavaScript -->
|
<!-- Pre-render message data that will be used by JavaScript -->
|
||||||
<script type="application/json" id="message-data-{{ forloop.counter }}">
|
<script type="application/json" id="message-data-{{ forloop.counter }}">
|
||||||
{
|
{
|
||||||
"message": "{{ message|escapejs }}",
|
"message": "{{ message|escapejs }}",
|
||||||
"tags": "{{ message.tags|default:'' }}"
|
"tags": "{{ message.tags|default:'' }}"
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
const toastContainer = document.querySelector(".toast-container");
|
const toastContainer = document.querySelector(".toast-container");
|
||||||
if (!toastContainer) return;
|
if (!toastContainer) return;
|
||||||
|
|
||||||
// Find all message data elements
|
// Find all message data elements
|
||||||
const messageDataElements = document.querySelectorAll('script[id^="message-data-"]');
|
const messageDataElements = document.querySelectorAll(
|
||||||
messageDataElements.forEach(function (dataElement) {
|
'script[id^="message-data-"]',
|
||||||
try {
|
);
|
||||||
const messageData = JSON.parse(dataElement.textContent);
|
messageDataElements.forEach(function (dataElement) {
|
||||||
createToast(messageData.message, messageData.tags);
|
try {
|
||||||
} catch (e) {
|
const messageData = JSON.parse(dataElement.textContent);
|
||||||
console.error("Error parsing message data:", e);
|
createToast(messageData.message, messageData.tags);
|
||||||
}
|
} catch (e) {
|
||||||
});
|
console.error("Error parsing message data:", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function createToast(messageText, messageTags) {
|
function createToast(messageText, messageTags) {
|
||||||
let toastClass = "";
|
let toastClass = "";
|
||||||
let autohide = true;
|
let autohide = true;
|
||||||
let delay = 5000;
|
let delay = 5000;
|
||||||
|
|
||||||
if (messageTags.includes("debug")) {
|
if (messageTags.includes("debug")) {
|
||||||
toastClass = "bg-secondary text-white";
|
toastClass = "bg-secondary text-white";
|
||||||
} else if (messageTags.includes("info")) {
|
} else if (messageTags.includes("info")) {
|
||||||
toastClass = "bg-info text-dark";
|
toastClass = "bg-info text-dark";
|
||||||
} else if (messageTags.includes("success")) {
|
} else if (messageTags.includes("success")) {
|
||||||
toastClass = "bg-success text-white";
|
toastClass = "bg-success text-white";
|
||||||
} else if (messageTags.includes("warning")) {
|
} else if (messageTags.includes("warning")) {
|
||||||
toastClass = "bg-warning text-dark";
|
toastClass = "bg-warning text-dark";
|
||||||
autohide = false;
|
autohide = false;
|
||||||
} else if (messageTags.includes("error")) {
|
} else if (messageTags.includes("error")) {
|
||||||
toastClass = "bg-danger text-white";
|
toastClass = "bg-danger text-white";
|
||||||
autohide = false;
|
autohide = false;
|
||||||
} else {
|
} else {
|
||||||
toastClass = "bg-light text-dark";
|
toastClass = "bg-light text-dark";
|
||||||
}
|
}
|
||||||
|
|
||||||
const toastId =
|
const toastId =
|
||||||
"toast-" + Date.now() + "-" + Math.random().toString(36).substring(2, 11);
|
"toast-" +
|
||||||
const toastHtml = `
|
Date.now() +
|
||||||
|
"-" +
|
||||||
|
Math.random().toString(36).substring(2, 11);
|
||||||
|
const toastHtml = `
|
||||||
<div id="${toastId}" class="toast ${toastClass}" role="alert" aria-live="assertive" aria-atomic="true">
|
<div id="${toastId}" class="toast ${toastClass}" role="alert" aria-live="assertive" aria-atomic="true">
|
||||||
<div class="toast-header">
|
<div class="toast-header">
|
||||||
<strong class="me-auto">Notification</strong>
|
<strong class="me-auto">Notification</strong>
|
||||||
@ -321,25 +346,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
toastContainer.insertAdjacentHTML("beforeend", toastHtml);
|
toastContainer.insertAdjacentHTML("beforeend", toastHtml);
|
||||||
|
|
||||||
const toastElement = document.getElementById(toastId);
|
const toastElement = document.getElementById(toastId);
|
||||||
if (toastElement) {
|
if (toastElement) {
|
||||||
const toast = new bootstrap.Toast(toastElement, {
|
const toast = new bootstrap.Toast(toastElement, {
|
||||||
autohide: autohide,
|
autohide: autohide,
|
||||||
delay: autohide ? delay : undefined,
|
delay: autohide ? delay : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
toastElement.addEventListener("hidden.bs.toast", function () {
|
toastElement.addEventListener("hidden.bs.toast", function () {
|
||||||
toastElement.remove();
|
toastElement.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.show();
|
toast.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -3,128 +3,143 @@
|
|||||||
{% block title %}Chat Session {{ session.session_id }} | Chat Analytics{% endblock %}
|
{% block title %}Chat Session {{ session.session_id }} | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div
|
<div
|
||||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
>
|
>
|
||||||
<h1 class="h2">Chat Session: {{ session.session_id }}</h1>
|
<h1 class="h2">Chat Session: {{ session.session_id }}</h1>
|
||||||
<div class="btn-toolbar mb-2 mb-md-0">
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
<a
|
<a
|
||||||
href="{% url 'data_source_detail' session.data_source.id %}"
|
href="{% url 'data_source_detail' session.data_source.id %}"
|
||||||
class="btn btn-sm btn-outline-secondary"
|
class="btn btn-sm btn-outline-secondary"
|
||||||
>
|
>
|
||||||
<i class="fas fa-arrow-left"></i> Back to Data Source
|
<i class="fas fa-arrow-left"></i> Back to Data Source
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">Session Information</h5>
|
<h5 class="card-title mb-0">Session Information</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<p><strong>Session ID:</strong> {{ session.session_id }}</p>
|
<p><strong>Session ID:</strong> {{ session.session_id }}</p>
|
||||||
<p><strong>Start Time:</strong> {{ session.start_time|date:"F d, Y H:i" }}</p>
|
<p>
|
||||||
<p><strong>End Time:</strong> {{ session.end_time|date:"F d, Y H:i" }}</p>
|
<strong>Start Time:</strong>
|
||||||
<p><strong>IP Address:</strong> {{ session.ip_address|default:"N/A" }}</p>
|
{{ session.start_time|date:"F d, Y H:i" }}
|
||||||
<p><strong>Country:</strong> {{ session.country|default:"N/A" }}</p>
|
</p>
|
||||||
<p><strong>Language:</strong> {{ session.language|default:"N/A" }}</p>
|
<p>
|
||||||
</div>
|
<strong>End Time:</strong> {{ session.end_time|date:"F d, Y H:i" }}
|
||||||
<div class="col-md-6">
|
</p>
|
||||||
<p><strong>Messages Sent:</strong> {{ session.messages_sent }}</p>
|
<p>
|
||||||
<p>
|
<strong>IP Address:</strong> {{ session.ip_address|default:"N/A" }}
|
||||||
<strong>Average Response Time:</strong>
|
</p>
|
||||||
{{ session.avg_response_time|floatformat:2 }}s
|
<p><strong>Country:</strong> {{ session.country|default:"N/A" }}</p>
|
||||||
</p>
|
<p><strong>Language:</strong> {{ session.language|default:"N/A" }}</p>
|
||||||
<p><strong>Tokens:</strong> {{ session.tokens }}</p>
|
</div>
|
||||||
<p><strong>Token Cost:</strong> €{{ session.tokens_eur|floatformat:2 }}</p>
|
<div class="col-md-6">
|
||||||
<p><strong>Category:</strong> {{ session.category|default:"N/A" }}</p>
|
<p><strong>Messages Sent:</strong> {{ session.messages_sent }}</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Sentiment:</strong>
|
<strong>Average Response Time:</strong>
|
||||||
{% if session.sentiment %}
|
{{ session.avg_response_time|floatformat:2 }}s
|
||||||
{% if 'positive' in session.sentiment|lower %}
|
</p>
|
||||||
<span class="badge bg-success">{{ session.sentiment }}</span>
|
<p><strong>Tokens:</strong> {{ session.tokens }}</p>
|
||||||
{% elif 'negative' in session.sentiment|lower %}
|
<p>
|
||||||
<span class="badge bg-danger">{{ session.sentiment }}</span>
|
<strong>Token Cost:</strong> €{{ session.tokens_eur|floatformat:2 }}
|
||||||
{% elif 'neutral' in session.sentiment|lower %}
|
</p>
|
||||||
<span class="badge bg-warning">{{ session.sentiment }}</span>
|
<p><strong>Category:</strong> {{ session.category|default:"N/A" }}</p>
|
||||||
{% else %}
|
<p>
|
||||||
<span class="badge bg-secondary">{{ session.sentiment }}</span>
|
<strong>Sentiment:</strong>
|
||||||
{% endif %}
|
{% if session.sentiment %}
|
||||||
{% else %}
|
{% if 'positive' in session.sentiment|lower %}
|
||||||
<span class="text-muted">N/A</span>
|
<span class="badge bg-success"
|
||||||
{% endif %}
|
>{{ session.sentiment }}</span
|
||||||
</p>
|
>
|
||||||
</div>
|
{% elif 'negative' in session.sentiment|lower %}
|
||||||
</div>
|
<span class="badge bg-danger">{{ session.sentiment }}</span>
|
||||||
<div class="row mt-3">
|
{% elif 'neutral' in session.sentiment|lower %}
|
||||||
<div class="col-12">
|
<span class="badge bg-warning"
|
||||||
<p class="mb-2"><strong>Initial Message:</strong></p>
|
>{{ session.sentiment }}</span
|
||||||
<div class="card bg-light">
|
>
|
||||||
<div class="card-body">
|
{% else %}
|
||||||
<p class="mb-0">{{ session.initial_msg|default:"N/A" }}</p>
|
<span class="badge bg-secondary"
|
||||||
</div>
|
>{{ session.sentiment }}</span
|
||||||
</div>
|
>
|
||||||
</div>
|
{% endif %}
|
||||||
</div>
|
{% else %}
|
||||||
</div>
|
<span class="text-muted">N/A</span>
|
||||||
</div>
|
{% endif %}
|
||||||
</div>
|
</p>
|
||||||
<div class="col-md-4">
|
</div>
|
||||||
<div class="card">
|
</div>
|
||||||
<div class="card-header">
|
<div class="row mt-3">
|
||||||
<h5 class="card-title mb-0">Additional Info</h5>
|
<div class="col-12">
|
||||||
</div>
|
<p class="mb-2"><strong>Initial Message:</strong></p>
|
||||||
<div class="card-body">
|
<div class="card bg-light">
|
||||||
<p>
|
<div class="card-body">
|
||||||
<strong>Escalated:</strong> {% if session.escalated %}
|
<p class="mb-0">{{ session.initial_msg|default:"N/A" }}</p>
|
||||||
<span class="badge bg-danger">Yes</span>
|
</div>
|
||||||
{% else %}
|
</div>
|
||||||
<span class="badge bg-success">No</span>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
</p>
|
</div>
|
||||||
<p>
|
</div>
|
||||||
<strong>Forwarded to HR:</strong> {% if session.forwarded_hr %}
|
</div>
|
||||||
<span class="badge bg-danger">Yes</span>
|
<div class="col-md-4">
|
||||||
{% else %}
|
<div class="card">
|
||||||
<span class="badge bg-success">No</span>
|
<div class="card-header">
|
||||||
{% endif %}
|
<h5 class="card-title mb-0">Additional Info</h5>
|
||||||
</p>
|
</div>
|
||||||
<p><strong>User Rating:</strong> {{ session.user_rating|default:"N/A" }}</p>
|
<div class="card-body">
|
||||||
<hr />
|
<p>
|
||||||
<p>
|
<strong>Escalated:</strong> {% if session.escalated %}
|
||||||
<strong>Data Source:</strong>
|
<span class="badge bg-danger">Yes</span>
|
||||||
<a href="{% url 'data_source_detail' session.data_source.id %}"
|
{% else %}
|
||||||
>{{ session.data_source.name }}</a
|
<span class="badge bg-success">No</span>
|
||||||
>
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
<p><strong>Company:</strong> {{ session.data_source.company.name }}</p>
|
<p>
|
||||||
</div>
|
<strong>Forwarded to HR:</strong> {% if session.forwarded_hr %}
|
||||||
</div>
|
<span class="badge bg-danger">Yes</span>
|
||||||
</div>
|
{% else %}
|
||||||
</div>
|
<span class="badge bg-success">No</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<p><strong>User Rating:</strong> {{ session.user_rating|default:"N/A" }}</p>
|
||||||
|
<hr />
|
||||||
|
<p>
|
||||||
|
<strong>Data Source:</strong>
|
||||||
|
<a href="{% url 'data_source_detail' session.data_source.id %}"
|
||||||
|
>{{ session.data_source.name }}</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p><strong>Company:</strong> {{ session.data_source.company.name }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">Full Transcript</h5>
|
<h5 class="card-title mb-0">Full Transcript</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if session.full_transcript %}
|
{% if session.full_transcript %}
|
||||||
<div class="chat-transcript" style="max-height: 500px; overflow-y: auto;">
|
<div class="chat-transcript" style="max-height: 500px; overflow-y: auto;">
|
||||||
<pre style="white-space: pre-wrap; font-family: inherit;">
|
<pre style="white-space: pre-wrap; font-family: inherit;">
|
||||||
{{ session.full_transcript }}</pre
|
{{ session.full_transcript }}</pre
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="text-center text-muted">No transcript available.</p>
|
<p class="text-center text-muted">No transcript available.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -5,155 +5,163 @@
|
|||||||
{% block title %}Dashboard | Chat Analytics{% endblock %}
|
{% block title %}Dashboard | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div
|
<div
|
||||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
>
|
>
|
||||||
<h1 class="h2">{{ selected_dashboard.name }}</h1>
|
<h1 class="h2">{{ selected_dashboard.name }}</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
|
<a
|
||||||
href="{% url 'edit_dashboard' selected_dashboard.id %}"
|
href="{% url 'edit_dashboard' selected_dashboard.id %}"
|
||||||
class="btn btn-sm btn-outline-secondary"
|
class="btn btn-sm btn-outline-secondary"
|
||||||
>
|
>
|
||||||
<i class="fas fa-edit"></i> Edit
|
<i class="fas fa-edit"></i> Edit
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="{% url 'delete_dashboard' selected_dashboard.id %}"
|
href="{% url 'delete_dashboard' selected_dashboard.id %}"
|
||||||
class="btn btn-sm btn-outline-danger"
|
class="btn btn-sm btn-outline-danger"
|
||||||
>
|
>
|
||||||
<i class="fas fa-trash"></i> Delete
|
<i class="fas fa-trash"></i> Delete
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="{% url 'export_chats_csv' %}?dashboard_id={{ selected_dashboard.id }}"
|
href="{% url 'export_chats_csv' %}?dashboard_id={{ selected_dashboard.id }}"
|
||||||
class="btn btn-sm btn-outline-success"
|
class="btn btn-sm btn-outline-success"
|
||||||
>
|
>
|
||||||
<i class="fas fa-file-csv"></i> Export CSV
|
<i class="fas fa-file-csv"></i> Export CSV
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-outline-primary dropdown-toggle"
|
class="btn btn-sm btn-outline-primary dropdown-toggle"
|
||||||
type="button"
|
type="button"
|
||||||
id="timeRangeDropdown"
|
id="timeRangeDropdown"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
>
|
>
|
||||||
<i class="fas fa-calendar"></i> Time Range
|
<i class="fas fa-calendar"></i> Time Range
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu" aria-labelledby="timeRangeDropdown">
|
<ul class="dropdown-menu" aria-labelledby="timeRangeDropdown">
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="?dashboard_id={{ selected_dashboard.id }}&time_range=7"
|
<a
|
||||||
>Last 7 days</a
|
class="dropdown-item"
|
||||||
>
|
href="?dashboard_id={{ selected_dashboard.id }}&time_range=7"
|
||||||
</li>
|
>Last 7 days</a
|
||||||
<li>
|
>
|
||||||
<a class="dropdown-item" href="?dashboard_id={{ selected_dashboard.id }}&time_range=30"
|
</li>
|
||||||
>Last 30 days</a
|
<li>
|
||||||
>
|
<a
|
||||||
</li>
|
class="dropdown-item"
|
||||||
<li>
|
href="?dashboard_id={{ selected_dashboard.id }}&time_range=30"
|
||||||
<a class="dropdown-item" href="?dashboard_id={{ selected_dashboard.id }}&time_range=90"
|
>Last 30 days</a
|
||||||
>Last 90 days</a
|
>
|
||||||
>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<a
|
||||||
<a class="dropdown-item" href="?dashboard_id={{ selected_dashboard.id }}&time_range=all"
|
class="dropdown-item"
|
||||||
>All time</a
|
href="?dashboard_id={{ selected_dashboard.id }}&time_range=90"
|
||||||
>
|
>Last 90 days</a
|
||||||
</li>
|
>
|
||||||
</ul>
|
</li>
|
||||||
</div>
|
<li>
|
||||||
</div>
|
<a
|
||||||
</div>
|
class="dropdown-item"
|
||||||
|
href="?dashboard_id={{ selected_dashboard.id }}&time_range=all"
|
||||||
|
>All time</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="card stats-card bg-primary text-white">
|
<div class="card stats-card bg-primary text-white">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h6 class="card-title">Total Sessions</h6>
|
<h6 class="card-title">Total Sessions</h6>
|
||||||
<h3>{{ dashboard_data.total_sessions }}</h3>
|
<h3>{{ dashboard_data.total_sessions }}</h3>
|
||||||
<p>Chat conversations</p>
|
<p>Chat conversations</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="card stats-card bg-success text-white">
|
<div class="card stats-card bg-success text-white">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h6 class="card-title">Avg Response Time</h6>
|
<h6 class="card-title">Avg Response Time</h6>
|
||||||
<h3>{{ dashboard_data.avg_response_time }}s</h3>
|
<h3>{{ dashboard_data.avg_response_time }}s</h3>
|
||||||
<p>Average response</p>
|
<p>Average response</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="card stats-card bg-info text-white">
|
<div class="card stats-card bg-info text-white">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h6 class="card-title">Total Tokens</h6>
|
<h6 class="card-title">Total Tokens</h6>
|
||||||
<h3>{{ dashboard_data.total_tokens }}</h3>
|
<h3>{{ dashboard_data.total_tokens }}</h3>
|
||||||
<p>Total usage</p>
|
<p>Total usage</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="card stats-card bg-warning text-white">
|
<div class="card stats-card bg-warning text-white">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h6 class="card-title">Total Cost</h6>
|
<h6 class="card-title">Total Cost</h6>
|
||||||
<h3>€{{ dashboard_data.total_cost }}</h3>
|
<h3>€{{ dashboard_data.total_cost }}</h3>
|
||||||
<p>Token cost</p>
|
<p>Token cost</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">Sessions Over Time</h5>
|
<h5 class="card-title mb-0">Sessions Over Time</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div id="sessions-time-chart" class="chart-container"></div>
|
<div id="sessions-time-chart" class="chart-container"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">Sentiment Analysis</h5>
|
<h5 class="card-title mb-0">Sentiment Analysis</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div id="sentiment-chart" class="chart-container"></div>
|
<div id="sentiment-chart" class="chart-container"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">Top Countries</h5>
|
<h5 class="card-title mb-0">Top Countries</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div id="country-chart" class="chart-container"></div>
|
<div id="country-chart" class="chart-container"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">Categories</h5>
|
<h5 class="card-title mb-0">Categories</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div id="category-chart" class="chart-container"></div>
|
<div id="category-chart" class="chart-container"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<!-- prettier-ignore-start -->
|
<!-- prettier-ignore-start -->
|
||||||
<!-- Store the JSON data in script tags to avoid parsing issues -->
|
<!-- 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="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="sentiment-data">{{ sentiment_data_json|safe }}</script>
|
||||||
@ -161,154 +169,161 @@
|
|||||||
<script type="application/json" id="category-data">{{ category_data_json|safe }}</script>
|
<script type="application/json" id="category-data">{{ category_data_json|safe }}</script>
|
||||||
<!-- prettier-ignore-end -->
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
try {
|
try {
|
||||||
// Parse the dashboard data components from script tags
|
// Parse the dashboard data components from script tags
|
||||||
const timeSeriesData = JSON.parse(document.getElementById("time-series-data").textContent);
|
const timeSeriesData = JSON.parse(
|
||||||
const sentimentData = JSON.parse(document.getElementById("sentiment-data").textContent);
|
document.getElementById("time-series-data").textContent,
|
||||||
const countryData = JSON.parse(document.getElementById("country-data").textContent);
|
);
|
||||||
const categoryData = JSON.parse(document.getElementById("category-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,
|
||||||
|
);
|
||||||
|
|
||||||
console.log("Time series data loaded:", timeSeriesData);
|
console.log("Time series data loaded:", timeSeriesData);
|
||||||
console.log("Sentiment data loaded:", sentimentData);
|
console.log("Sentiment data loaded:", sentimentData);
|
||||||
console.log("Country data loaded:", countryData);
|
console.log("Country data loaded:", countryData);
|
||||||
console.log("Category data loaded:", categoryData);
|
console.log("Category data loaded:", categoryData);
|
||||||
|
|
||||||
// Sessions over time chart
|
// Sessions over time chart
|
||||||
if (timeSeriesData && timeSeriesData.length > 0) {
|
if (timeSeriesData && timeSeriesData.length > 0) {
|
||||||
const timeSeriesX = timeSeriesData.map((item) => item.date);
|
const timeSeriesX = timeSeriesData.map((item) => item.date);
|
||||||
const timeSeriesY = timeSeriesData.map((item) => item.count);
|
const timeSeriesY = timeSeriesData.map((item) => item.count);
|
||||||
|
|
||||||
Plotly.newPlot(
|
Plotly.newPlot(
|
||||||
"sessions-time-chart",
|
"sessions-time-chart",
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
x: timeSeriesX,
|
x: timeSeriesX,
|
||||||
y: timeSeriesY,
|
y: timeSeriesY,
|
||||||
type: "scatter",
|
type: "scatter",
|
||||||
mode: "lines+markers",
|
mode: "lines+markers",
|
||||||
line: {
|
line: {
|
||||||
color: "rgb(75, 192, 192)",
|
color: "rgb(75, 192, 192)",
|
||||||
width: 2,
|
width: 2,
|
||||||
},
|
},
|
||||||
marker: {
|
marker: {
|
||||||
color: "rgb(75, 192, 192)",
|
color: "rgb(75, 192, 192)",
|
||||||
size: 6,
|
size: 6,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
margin: { t: 10, r: 10, b: 40, l: 40 },
|
margin: { t: 10, r: 10, b: 40, l: 40 },
|
||||||
xaxis: {
|
xaxis: {
|
||||||
title: "Date",
|
title: "Date",
|
||||||
},
|
},
|
||||||
yaxis: {
|
yaxis: {
|
||||||
title: "Number of Sessions",
|
title: "Number of Sessions",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
document.getElementById("sessions-time-chart").innerHTML =
|
document.getElementById("sessions-time-chart").innerHTML =
|
||||||
'<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>';
|
'<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sentiment analysis chart
|
// Sentiment analysis chart
|
||||||
if (sentimentData && sentimentData.length > 0) {
|
if (sentimentData && sentimentData.length > 0) {
|
||||||
const sentimentLabels = sentimentData.map((item) => item.sentiment);
|
const sentimentLabels = sentimentData.map((item) => item.sentiment);
|
||||||
const sentimentValues = sentimentData.map((item) => item.count);
|
const sentimentValues = sentimentData.map((item) => item.count);
|
||||||
const sentimentColors = sentimentLabels.map((sentiment) => {
|
const sentimentColors = sentimentLabels.map((sentiment) => {
|
||||||
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
|
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
|
||||||
if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)";
|
if (sentiment.toLowerCase().includes("negative"))
|
||||||
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
|
return "rgb(255, 99, 132)";
|
||||||
return "rgb(201, 203, 207)";
|
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
|
||||||
});
|
return "rgb(201, 203, 207)";
|
||||||
|
});
|
||||||
|
|
||||||
Plotly.newPlot(
|
Plotly.newPlot(
|
||||||
"sentiment-chart",
|
"sentiment-chart",
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
values: sentimentValues,
|
values: sentimentValues,
|
||||||
labels: sentimentLabels,
|
labels: sentimentLabels,
|
||||||
type: "pie",
|
type: "pie",
|
||||||
marker: {
|
marker: {
|
||||||
colors: sentimentColors,
|
colors: sentimentColors,
|
||||||
},
|
},
|
||||||
hole: 0.4,
|
hole: 0.4,
|
||||||
textinfo: "label+percent",
|
textinfo: "label+percent",
|
||||||
insidetextorientation: "radial",
|
insidetextorientation: "radial",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
margin: { t: 10, r: 10, b: 10, l: 10 },
|
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
document.getElementById("sentiment-chart").innerHTML =
|
document.getElementById("sentiment-chart").innerHTML =
|
||||||
'<div class="text-center py-5"><p class="text-muted">No sentiment data available</p></div>';
|
'<div class="text-center py-5"><p class="text-muted">No sentiment data available</p></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Country chart
|
// Country chart
|
||||||
if (countryData && countryData.length > 0) {
|
if (countryData && countryData.length > 0) {
|
||||||
const countryLabels = countryData.map((item) => item.country);
|
const countryLabels = countryData.map((item) => item.country);
|
||||||
const countryValues = countryData.map((item) => item.count);
|
const countryValues = countryData.map((item) => item.count);
|
||||||
|
|
||||||
Plotly.newPlot(
|
Plotly.newPlot(
|
||||||
"country-chart",
|
"country-chart",
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
x: countryValues,
|
x: countryValues,
|
||||||
y: countryLabels,
|
y: countryLabels,
|
||||||
type: "bar",
|
type: "bar",
|
||||||
orientation: "h",
|
orientation: "h",
|
||||||
marker: {
|
marker: {
|
||||||
color: "rgb(54, 162, 235)",
|
color: "rgb(54, 162, 235)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
margin: { t: 10, r: 10, b: 40, l: 100 },
|
margin: { t: 10, r: 10, b: 40, l: 100 },
|
||||||
xaxis: {
|
xaxis: {
|
||||||
title: "Number of Sessions",
|
title: "Number of Sessions",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
document.getElementById("country-chart").innerHTML =
|
document.getElementById("country-chart").innerHTML =
|
||||||
'<div class="text-center py-5"><p class="text-muted">No country data available</p></div>';
|
'<div class="text-center py-5"><p class="text-muted">No country data available</p></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category chart
|
// Category chart
|
||||||
if (categoryData && categoryData.length > 0) {
|
if (categoryData && categoryData.length > 0) {
|
||||||
const categoryLabels = categoryData.map((item) => item.category);
|
const categoryLabels = categoryData.map((item) => item.category);
|
||||||
const categoryValues = categoryData.map((item) => item.count);
|
const categoryValues = categoryData.map((item) => item.count);
|
||||||
|
|
||||||
Plotly.newPlot(
|
Plotly.newPlot(
|
||||||
"category-chart",
|
"category-chart",
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
labels: categoryLabels,
|
labels: categoryLabels,
|
||||||
values: categoryValues,
|
values: categoryValues,
|
||||||
type: "pie",
|
type: "pie",
|
||||||
textinfo: "label+percent",
|
textinfo: "label+percent",
|
||||||
insidetextorientation: "radial",
|
insidetextorientation: "radial",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
margin: { t: 10, r: 10, b: 10, l: 10 },
|
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
document.getElementById("category-chart").innerHTML =
|
document.getElementById("category-chart").innerHTML =
|
||||||
'<div class="text-center py-5"><p class="text-muted">No category data available</p></div>';
|
'<div class="text-center py-5"><p class="text-muted">No category data available</p></div>';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error rendering charts:", error);
|
console.error("Error rendering charts:", error);
|
||||||
document.querySelectorAll(".chart-container").forEach((container) => {
|
document.querySelectorAll(".chart-container").forEach((container) => {
|
||||||
container.innerHTML =
|
container.innerHTML =
|
||||||
'<div class="text-center py-5"><p class="text-danger">Error loading chart data. Please refresh the page.</p></div>';
|
'<div class="text-center py-5"><p class="text-danger">Error loading chart data. Please refresh the page.</p></div>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -3,41 +3,42 @@
|
|||||||
{% block title %}Delete Dashboard | Chat Analytics{% endblock %}
|
{% block title %}Delete Dashboard | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div
|
<div
|
||||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
>
|
>
|
||||||
<h1 class="h2">Delete Dashboard</h1>
|
<h1 class="h2">Delete Dashboard</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">
|
||||||
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card border-danger">
|
<div class="card border-danger">
|
||||||
<div class="card-header bg-danger text-white">
|
<div class="card-header bg-danger text-white">
|
||||||
<h5 class="card-title mb-0">Confirm Deletion</h5>
|
<h5 class="card-title mb-0">Confirm Deletion</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
Are you sure you want to delete the dashboard "<strong>{{ dashboard.name }}</strong>"?
|
Are you sure you want to delete the dashboard
|
||||||
</p>
|
"<strong>{{ dashboard.name }}</strong>"?
|
||||||
<p>
|
</p>
|
||||||
This action cannot be undone. The dashboard will be permanently deleted, but the
|
<p>
|
||||||
underlying data sources will remain intact.
|
This action cannot be undone. The dashboard will be permanently deleted, but
|
||||||
</p>
|
the underlying data sources will remain intact.
|
||||||
|
</p>
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="d-flex justify-content-between mt-4">
|
<div class="d-flex justify-content-between mt-4">
|
||||||
<a href="{% url 'dashboard' %}" class="btn btn-secondary">Cancel</a>
|
<a href="{% url 'dashboard' %}" class="btn btn-secondary">Cancel</a>
|
||||||
<button type="submit" class="btn btn-danger">Delete Dashboard</button>
|
<button type="submit" class="btn btn-danger">Delete Dashboard</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -2,42 +2,42 @@
|
|||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %}
|
{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %}
|
||||||
| Chat Analytics
|
| Chat Analytics
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div
|
<div
|
||||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
>
|
>
|
||||||
<h1 class="h2">{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %}</h1>
|
<h1 class="h2">{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %}</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">
|
||||||
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">
|
<h5 class="card-title mb-0">
|
||||||
{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %}
|
{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form|crispy }}
|
{{ form|crispy }}
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
{% if is_create %}Create Dashboard{% else %}Update Dashboard{% endif %}
|
{% if is_create %}Create Dashboard{% else %}Update Dashboard{% endif %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -4,47 +4,50 @@
|
|||||||
{% block title %}Delete Data Source | Chat Analytics{% endblock %}
|
{% block title %}Delete Data Source | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div
|
<div
|
||||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
>
|
>
|
||||||
<h1 class="h2">Delete Data Source</h1>
|
<h1 class="h2">Delete Data Source</h1>
|
||||||
<div class="btn-toolbar mb-2 mb-md-0">
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
<a
|
<a
|
||||||
href="{% url 'data_source_detail' data_source.id %}"
|
href="{% url 'data_source_detail' data_source.id %}"
|
||||||
class="btn btn-sm btn-outline-secondary"
|
class="btn btn-sm btn-outline-secondary"
|
||||||
>
|
>
|
||||||
<i class="fas fa-arrow-left"></i> Back to Data Source
|
<i class="fas fa-arrow-left"></i> Back to Data Source
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card border-danger">
|
<div class="card border-danger">
|
||||||
<div class="card-header bg-danger text-white">
|
<div class="card-header bg-danger text-white">
|
||||||
<h5 class="card-title mb-0">Confirm Deletion</h5>
|
<h5 class="card-title mb-0">Confirm Deletion</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
Are you sure you want to delete the data source
|
Are you sure you want to delete the data source
|
||||||
"<strong>{{ data_source.name }}</strong>"?
|
"<strong>{{ data_source.name }}</strong>"?
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
This action cannot be undone. The data source and all associated chat sessions
|
This action cannot be undone. The data source and all associated chat
|
||||||
({{ data_source.chat_sessions.count }} sessions) will be permanently deleted.
|
sessions ({{ data_source.chat_sessions.count }} sessions) will be
|
||||||
</p>
|
permanently deleted.
|
||||||
|
</p>
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="d-flex justify-content-between mt-4">
|
<div class="d-flex justify-content-between mt-4">
|
||||||
<a href="{% url 'data_source_detail' data_source.id %}" class="btn btn-secondary"
|
<a
|
||||||
>Cancel</a
|
href="{% url 'data_source_detail' data_source.id %}"
|
||||||
>
|
class="btn btn-secondary"
|
||||||
<button type="submit" class="btn btn-danger">Delete Data Source</button>
|
>Cancel</a
|
||||||
</div>
|
>
|
||||||
</form>
|
<button type="submit" class="btn btn-danger">Delete Data Source</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -5,224 +5,251 @@
|
|||||||
{% block title %}{{ data_source.name }} | Chat Analytics{% endblock %}
|
{% block title %}{{ data_source.name }} | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div
|
<div
|
||||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
>
|
>
|
||||||
<h1 class="h2">{{ data_source.name }}</h1>
|
<h1 class="h2">{{ data_source.name }}</h1>
|
||||||
<div class="btn-toolbar mb-2 mb-md-0">
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
<a href="{% url 'upload_data' %}" class="btn btn-sm btn-outline-secondary me-2">
|
<a href="{% url 'upload_data' %}" class="btn btn-sm btn-outline-secondary me-2">
|
||||||
<i class="fas fa-arrow-left"></i> Back to Data Sources
|
<i class="fas fa-arrow-left"></i> Back to Data Sources
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="{% url 'export_chats_csv' %}?data_source_id={{ data_source.id }}"
|
href="{% url 'export_chats_csv' %}?data_source_id={{ data_source.id }}"
|
||||||
class="btn btn-sm btn-outline-success me-2"
|
class="btn btn-sm btn-outline-success me-2"
|
||||||
>
|
>
|
||||||
<i class="fas fa-file-csv"></i> Export CSV
|
<i class="fas fa-file-csv"></i> Export CSV
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'delete_data_source' data_source.id %}" class="btn btn-sm btn-outline-danger">
|
<a
|
||||||
<i class="fas fa-trash"></i> Delete
|
href="{% url 'delete_data_source' data_source.id %}"
|
||||||
</a>
|
class="btn btn-sm btn-outline-danger"
|
||||||
</div>
|
>
|
||||||
</div>
|
<i class="fas fa-trash"></i> Delete
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">Data Source Details</h5>
|
<h5 class="card-title mb-0">Data Source Details</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<p><strong>Name:</strong> {{ data_source.name }}</p>
|
<p><strong>Name:</strong> {{ data_source.name }}</p>
|
||||||
<p><strong>Uploaded At:</strong> {{ data_source.uploaded_at|date:"F d, Y H:i" }}</p>
|
<p>
|
||||||
<p><strong>File:</strong> {{ data_source.file.name|split:"/"|last }}</p>
|
<strong>Uploaded At:</strong>
|
||||||
</div>
|
{{ data_source.uploaded_at|date:"F d, Y H:i" }}
|
||||||
<div class="col-md-6">
|
</p>
|
||||||
<p><strong>Company:</strong> {{ data_source.company.name }}</p>
|
<p><strong>File:</strong> {{ data_source.file.name|split:"/"|last }}</p>
|
||||||
<p><strong>Total Sessions:</strong> {{ page_obj.paginator.count }}</p>
|
</div>
|
||||||
<p><strong>Description:</strong> {{ data_source.description }}</p>
|
<div class="col-md-6">
|
||||||
</div>
|
<p><strong>Company:</strong> {{ data_source.company.name }}</p>
|
||||||
</div>
|
<p><strong>Total Sessions:</strong> {{ page_obj.paginator.count }}</p>
|
||||||
</div>
|
<p><strong>Description:</strong> {{ data_source.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
</div>
|
||||||
<div class="card">
|
</div>
|
||||||
<div class="card-header">
|
</div>
|
||||||
<h5 class="card-title mb-0">Filter Sessions</h5>
|
<div class="col-md-4">
|
||||||
</div>
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-header">
|
||||||
<form method="get" action="{% url 'search_chat_sessions' %}">
|
<h5 class="card-title mb-0">Filter Sessions</h5>
|
||||||
<div class="input-group mb-3">
|
</div>
|
||||||
<input
|
<div class="card-body">
|
||||||
type="text"
|
<form method="get" action="{% url 'search_chat_sessions' %}">
|
||||||
name="q"
|
<div class="input-group mb-3">
|
||||||
class="form-control"
|
<input
|
||||||
placeholder="Search sessions..."
|
type="text"
|
||||||
aria-label="Search sessions"
|
name="q"
|
||||||
/>
|
class="form-control"
|
||||||
<input type="hidden" name="data_source_id" value="{{ data_source.id }}" />
|
placeholder="Search sessions..."
|
||||||
<button class="btn btn-outline-primary" type="submit">
|
aria-label="Search sessions"
|
||||||
<i class="fas fa-search"></i>
|
/>
|
||||||
</button>
|
<input
|
||||||
</div>
|
type="hidden"
|
||||||
</form>
|
name="data_source_id"
|
||||||
</div>
|
value="{{ data_source.id }}"
|
||||||
</div>
|
/>
|
||||||
</div>
|
<button class="btn btn-outline-primary" type="submit">
|
||||||
</div>
|
<i class="fas fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">Chat Sessions ({{ page_obj.paginator.count }})</h5>
|
<h5 class="card-title mb-0">Chat Sessions ({{ page_obj.paginator.count }})</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-hover">
|
<table class="table table-striped table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Session ID</th>
|
<th>Session ID</th>
|
||||||
<th>Start Time</th>
|
<th>Start Time</th>
|
||||||
<th>Country</th>
|
<th>Country</th>
|
||||||
<th>Language</th>
|
<th>Language</th>
|
||||||
<th>Sentiment</th>
|
<th>Sentiment</th>
|
||||||
<th>Messages</th>
|
<th>Messages</th>
|
||||||
<th>Tokens</th>
|
<th>Tokens</th>
|
||||||
<th>Category</th>
|
<th>Category</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for session in page_obj %}
|
{% for session in page_obj %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ session.session_id|truncatechars:10 }}</td>
|
<td>{{ session.session_id|truncatechars:10 }}</td>
|
||||||
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
|
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
|
||||||
<td>{{ session.country }}</td>
|
<td>{{ session.country }}</td>
|
||||||
<td>{{ session.language }}</td>
|
<td>{{ session.language }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if session.sentiment %}
|
{% if session.sentiment %}
|
||||||
{% if 'positive' in session.sentiment|lower %}
|
{% if 'positive' in session.sentiment|lower %}
|
||||||
<span class="badge bg-success">{{ session.sentiment }}</span>
|
<span class="badge bg-success"
|
||||||
{% elif 'negative' in session.sentiment|lower %}
|
>{{ session.sentiment }}</span
|
||||||
<span class="badge bg-danger">{{ session.sentiment }}</span>
|
>
|
||||||
{% elif 'neutral' in session.sentiment|lower %}
|
{% elif 'negative' in session.sentiment|lower %}
|
||||||
<span class="badge bg-warning">{{ session.sentiment }}</span>
|
<span class="badge bg-danger"
|
||||||
{% else %}
|
>{{ session.sentiment }}</span
|
||||||
<span class="badge bg-secondary">{{ session.sentiment }}</span>
|
>
|
||||||
{% endif %}
|
{% elif 'neutral' in session.sentiment|lower %}
|
||||||
{% else %}
|
<span class="badge bg-warning"
|
||||||
<span class="text-muted">N/A</span>
|
>{{ session.sentiment }}</span
|
||||||
{% endif %}
|
>
|
||||||
</td>
|
{% else %}
|
||||||
<td>{{ session.messages_sent }}</td>
|
<span class="badge bg-secondary"
|
||||||
<td>{{ session.tokens }}</td>
|
>{{ session.sentiment }}</span
|
||||||
<td>{{ session.category|default:"N/A" }}</td>
|
>
|
||||||
<td>
|
{% endif %}
|
||||||
{% if session.session_id %}
|
{% else %}
|
||||||
<a
|
<span class="text-muted">N/A</span>
|
||||||
href="{% url 'chat_session_detail' session.session_id %}"
|
{% endif %}
|
||||||
class="btn btn-sm btn-outline-primary"
|
</td>
|
||||||
>
|
<td>{{ session.messages_sent }}</td>
|
||||||
<i class="fas fa-eye"></i>
|
<td>{{ session.tokens }}</td>
|
||||||
</a>
|
<td>{{ session.category|default:"N/A" }}</td>
|
||||||
{% else %}
|
<td>
|
||||||
<button class="btn btn-sm btn-outline-secondary" disabled>
|
{% if session.session_id %}
|
||||||
<i class="fas fa-eye-slash"></i>
|
<a
|
||||||
</button>
|
href="{% url 'chat_session_detail' session.session_id %}"
|
||||||
{% endif %}
|
class="btn btn-sm btn-outline-primary"
|
||||||
</td>
|
>
|
||||||
</tr>
|
<i class="fas fa-eye"></i>
|
||||||
{% empty %}
|
</a>
|
||||||
<tr>
|
{% else %}
|
||||||
<td colspan="9" class="text-center">No chat sessions found.</td>
|
<button
|
||||||
</tr>
|
class="btn btn-sm btn-outline-secondary"
|
||||||
{% endfor %}
|
disabled
|
||||||
</tbody>
|
>
|
||||||
</table>
|
<i class="fas fa-eye-slash"></i>
|
||||||
</div>
|
</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 %}
|
{% if page_obj.paginator.num_pages > 1 %}
|
||||||
<nav aria-label="Page navigation" class="mt-4">
|
<nav aria-label="Page navigation" class="mt-4">
|
||||||
<ul class="pagination justify-content-center">
|
<ul class="pagination justify-content-center">
|
||||||
{% if page_obj.has_previous %}
|
{% if page_obj.has_previous %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="?page=1" aria-label="First">
|
<a class="page-link" href="?page=1" aria-label="First">
|
||||||
<span aria-hidden="true">««</span>
|
<span aria-hidden="true">««</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a
|
<a
|
||||||
class="page-link"
|
class="page-link"
|
||||||
href="?page={{ page_obj.previous_page_number }}"
|
href="?page={{ page_obj.previous_page_number }}"
|
||||||
aria-label="Previous"
|
aria-label="Previous"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">«</span>
|
<span aria-hidden="true">«</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<a class="page-link" href="#" aria-label="First">
|
<a class="page-link" href="#" aria-label="First">
|
||||||
<span aria-hidden="true">««</span>
|
<span aria-hidden="true">««</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<a class="page-link" href="#" aria-label="Previous">
|
<a class="page-link" href="#" aria-label="Previous">
|
||||||
<span aria-hidden="true">«</span>
|
<span aria-hidden="true">«</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% for num in page_obj.paginator.page_range %}
|
{% for num in page_obj.paginator.page_range %}
|
||||||
{% if page_obj.number == num %}
|
{% if page_obj.number == num %}
|
||||||
<li class="page-item active">
|
<li class="page-item active">
|
||||||
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
|
<a class="page-link" href="?page={{ num }}"
|
||||||
</li>
|
>{{ num }}</a
|
||||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
>
|
||||||
<li class="page-item">
|
</li>
|
||||||
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
|
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||||
</li>
|
<li class="page-item">
|
||||||
{% endif %}
|
<a class="page-link" href="?page={{ num }}"
|
||||||
{% endfor %}
|
>{{ num }}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
{% if page_obj.has_next %}
|
{% if page_obj.has_next %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a
|
<a
|
||||||
class="page-link"
|
class="page-link"
|
||||||
href="?page={{ page_obj.next_page_number }}"
|
href="?page={{ page_obj.next_page_number }}"
|
||||||
aria-label="Next"
|
aria-label="Next"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">»</span>
|
<span aria-hidden="true">»</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a
|
<a
|
||||||
class="page-link"
|
class="page-link"
|
||||||
href="?page={{ page_obj.paginator.num_pages }}"
|
href="?page={{ page_obj.paginator.num_pages }}"
|
||||||
aria-label="Last"
|
aria-label="Last"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">»»</span>
|
<span aria-hidden="true">»»</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<a class="page-link" href="#" aria-label="Next">
|
<a class="page-link" href="#" aria-label="Next">
|
||||||
<span aria-hidden="true">»</span>
|
<span aria-hidden="true">»</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<a class="page-link" href="#" aria-label="Last">
|
<a class="page-link" href="#" aria-label="Last">
|
||||||
<span aria-hidden="true">»»</span>
|
<span aria-hidden="true">»»</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -5,270 +5,316 @@
|
|||||||
{% block title %}Data View | Chat Analytics{% endblock %}
|
{% block title %}Data View | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div
|
<div
|
||||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
>
|
>
|
||||||
<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 ajax-nav-link">
|
<a
|
||||||
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
href="{% url 'dashboard' %}"
|
||||||
</a>
|
class="btn btn-sm btn-outline-secondary ajax-nav-link"
|
||||||
{% if selected_data_source %}
|
>
|
||||||
<a
|
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||||
href="{% url 'data_source_detail' selected_data_source.id %}"
|
</a>
|
||||||
class="btn btn-sm btn-outline-secondary ajax-nav-link"
|
{% if selected_data_source %}
|
||||||
>
|
<a
|
||||||
<i class="fas fa-database"></i> View Source
|
href="{% url 'data_source_detail' selected_data_source.id %}"
|
||||||
</a>
|
class="btn btn-sm btn-outline-secondary ajax-nav-link"
|
||||||
{% endif %}
|
>
|
||||||
</div>
|
<i class="fas fa-database"></i> View Source
|
||||||
<div class="dropdown">
|
</a>
|
||||||
<button
|
{% endif %}
|
||||||
class="btn btn-sm btn-outline-primary dropdown-toggle"
|
</div>
|
||||||
type="button"
|
<div class="dropdown">
|
||||||
id="dataViewDropdown"
|
<button
|
||||||
data-bs-toggle="dropdown"
|
class="btn btn-sm btn-outline-primary dropdown-toggle"
|
||||||
aria-expanded="false"
|
type="button"
|
||||||
>
|
id="dataViewDropdown"
|
||||||
<i class="fas fa-filter"></i> Filter
|
data-bs-toggle="dropdown"
|
||||||
</button>
|
aria-expanded="false"
|
||||||
<ul class="dropdown-menu" aria-labelledby="dataViewDropdown">
|
>
|
||||||
<li><a class="dropdown-item ajax-nav-link" href="?view=all">All Sessions</a></li>
|
<i class="fas fa-filter"></i> Filter
|
||||||
<li><a class="dropdown-item ajax-nav-link" href="?view=recent">Recent Sessions</a></li>
|
</button>
|
||||||
<li>
|
<ul class="dropdown-menu" aria-labelledby="dataViewDropdown">
|
||||||
<a class="dropdown-item ajax-nav-link" href="?view=positive">Positive Sentiment</a>
|
<li>
|
||||||
</li>
|
<a class="dropdown-item ajax-nav-link" href="?view=all">All Sessions</a>
|
||||||
<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=recent"
|
||||||
<li>
|
>Recent Sessions</a
|
||||||
<a class="dropdown-item ajax-nav-link" href="?view=escalated">Escalated Sessions</a>
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
<li>
|
||||||
</div>
|
<a class="dropdown-item ajax-nav-link" href="?view=positive"
|
||||||
</div>
|
>Positive Sentiment</a
|
||||||
</div>
|
>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Data Source Selection -->
|
<!-- Data Source Selection -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<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 filter-form">
|
<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
|
||||||
<option value="">All Data Sources</option>
|
name="data_source_id"
|
||||||
{% for ds in data_sources %}
|
class="form-select"
|
||||||
<option
|
aria-label="Select Data Source"
|
||||||
value="{{ ds.id }}"
|
>
|
||||||
{% if selected_data_source.id == ds.id %}selected{% endif %}
|
<option value="">All Data Sources</option>
|
||||||
>
|
{% for ds in data_sources %}
|
||||||
{{ ds.name }}
|
<option
|
||||||
</option>
|
value="{{ ds.id }}"
|
||||||
{% endfor %}
|
{% if selected_data_source.id == ds.id %}selected{% endif %}
|
||||||
</select>
|
>
|
||||||
</div>
|
{{ ds.name }}
|
||||||
<div class="col-md-4">
|
</option>
|
||||||
<select name="view" class="form-select" aria-label="Select View">
|
{% endfor %}
|
||||||
<option value="all" {% if view == 'all' %}selected{% endif %}>All Sessions</option>
|
</select>
|
||||||
<option value="recent" {% if view == 'recent' %}selected{% endif %}>
|
</div>
|
||||||
Recent Sessions
|
<div class="col-md-4">
|
||||||
</option>
|
<select name="view" class="form-select" aria-label="Select View">
|
||||||
<option value="positive" {% if view == 'positive' %}selected{% endif %}>
|
<option value="all" {% if view == 'all' %}selected{% endif %}>
|
||||||
Positive Sentiment
|
All Sessions
|
||||||
</option>
|
</option>
|
||||||
<option value="negative" {% if view == 'negative' %}selected{% endif %}>
|
<option value="recent" {% if view == 'recent' %}selected{% endif %}>
|
||||||
Negative Sentiment
|
Recent Sessions
|
||||||
</option>
|
</option>
|
||||||
<option value="escalated" {% if view == 'escalated' %}selected{% endif %}>
|
<option
|
||||||
Escalated Sessions
|
value="positive"
|
||||||
</option>
|
{% if view == 'positive' %}selected{% endif %}
|
||||||
</select>
|
>
|
||||||
</div>
|
Positive Sentiment
|
||||||
<div class="col-md-2">
|
</option>
|
||||||
<button type="submit" class="btn btn-primary w-100">Apply</button>
|
<option
|
||||||
</div>
|
value="negative"
|
||||||
</form>
|
{% if view == 'negative' %}selected{% endif %}
|
||||||
</div>
|
>
|
||||||
</div>
|
Negative Sentiment
|
||||||
</div>
|
</option>
|
||||||
</div>
|
<option
|
||||||
|
value="escalated"
|
||||||
|
{% if view == 'escalated' %}selected{% endif %}
|
||||||
|
>
|
||||||
|
Escalated Sessions
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Apply</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Export to CSV -->
|
<!-- Export to CSV -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">Export Data</h5>
|
<h5 class="card-title mb-0">Export Data</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="export-form" method="get" action="{% url 'export_chats_csv' %}" class="row g-3">
|
<form
|
||||||
<!-- Pass current filters to export -->
|
id="export-form"
|
||||||
<input type="hidden" name="data_source_id" value="{{ selected_data_source.id }}" />
|
method="get"
|
||||||
<input type="hidden" name="view" value="{{ view }}" />
|
action="{% url 'export_chats_csv' %}"
|
||||||
|
class="row g-3"
|
||||||
|
>
|
||||||
|
<!-- Pass current filters to export -->
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="data_source_id"
|
||||||
|
value="{{ selected_data_source.id }}"
|
||||||
|
/>
|
||||||
|
<input type="hidden" name="view" value="{{ view }}" />
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label for="start_date" class="form-label">Start Date</label>
|
<label for="start_date" class="form-label">Start Date</label>
|
||||||
<input type="date" name="start_date" id="start_date" class="form-control" />
|
<input
|
||||||
</div>
|
type="date"
|
||||||
<div class="col-md-3">
|
name="start_date"
|
||||||
<label for="end_date" class="form-label">End Date</label>
|
id="start_date"
|
||||||
<input type="date" name="end_date" id="end_date" class="form-control" />
|
class="form-control"
|
||||||
</div>
|
/>
|
||||||
<div class="col-md-3">
|
</div>
|
||||||
<label for="country" class="form-label">Country</label>
|
<div class="col-md-3">
|
||||||
<input
|
<label for="end_date" class="form-label">End Date</label>
|
||||||
type="text"
|
<input type="date" name="end_date" id="end_date" class="form-control" />
|
||||||
name="country"
|
</div>
|
||||||
id="country"
|
<div class="col-md-3">
|
||||||
class="form-control"
|
<label for="country" class="form-label">Country</label>
|
||||||
placeholder="Country"
|
<input
|
||||||
/>
|
type="text"
|
||||||
</div>
|
name="country"
|
||||||
<div class="col-md-3">
|
id="country"
|
||||||
<label for="sentiment" class="form-label">Sentiment</label>
|
class="form-control"
|
||||||
<select name="sentiment" id="sentiment" class="form-select">
|
placeholder="Country"
|
||||||
<option value="">All</option>
|
/>
|
||||||
<option value="positive">Positive</option>
|
</div>
|
||||||
<option value="negative">Negative</option>
|
<div class="col-md-3">
|
||||||
<option value="neutral">Neutral</option>
|
<label for="sentiment" class="form-label">Sentiment</label>
|
||||||
</select>
|
<select name="sentiment" id="sentiment" class="form-select">
|
||||||
</div>
|
<option value="">All</option>
|
||||||
<div class="col-md-3">
|
<option value="positive">Positive</option>
|
||||||
<label for="escalated" class="form-label">Escalated</label>
|
<option value="negative">Negative</option>
|
||||||
<select name="escalated" id="escalated" class="form-select">
|
<option value="neutral">Neutral</option>
|
||||||
<option value="">All</option>
|
</select>
|
||||||
<option value="true">Yes</option>
|
</div>
|
||||||
<option value="false">No</option>
|
<div class="col-md-3">
|
||||||
</select>
|
<label for="escalated" class="form-label">Escalated</label>
|
||||||
</div>
|
<select name="escalated" id="escalated" class="form-select">
|
||||||
<div class="col-md-3 d-flex align-items-end">
|
<option value="">All</option>
|
||||||
<button type="submit" class="btn btn-success w-100">
|
<option value="true">Yes</option>
|
||||||
<i class="fas fa-file-csv me-1"></i> Export to CSV
|
<option value="false">No</option>
|
||||||
</button>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<div class="col-md-3 d-flex align-items-end">
|
||||||
</div>
|
<button type="submit" class="btn btn-success w-100">
|
||||||
</div>
|
<i class="fas fa-file-csv me-1"></i> Export to CSV
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Data Table -->
|
<!-- Data Table -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h5 class="card-title mb-0">
|
<h5 class="card-title mb-0">
|
||||||
Chat Sessions
|
Chat Sessions
|
||||||
{% if selected_data_source %}
|
{% if selected_data_source %}
|
||||||
for {{ selected_data_source.name }}
|
for {{ selected_data_source.name }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if view != 'all' %}
|
{% if view != 'all' %}
|
||||||
({{ view|title }})
|
({{ view|title }})
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h5>
|
</h5>
|
||||||
<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">
|
||||||
<!-- Loading spinner shown during AJAX requests -->
|
<!-- Loading spinner shown during AJAX requests -->
|
||||||
<div id="ajax-loading-spinner" class="text-center py-4 d-none">
|
<div id="ajax-loading-spinner" class="text-center py-4 d-none">
|
||||||
<div class="spinner-border text-primary" role="status">
|
<div class="spinner-border text-primary" role="status">
|
||||||
<span class="visually-hidden">Loading...</span>
|
<span class="visually-hidden">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2">Loading data...</p>
|
<p class="mt-2">Loading data...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Data table container that will be updated via AJAX -->
|
<!-- Data table container that will be updated via AJAX -->
|
||||||
<div id="ajax-content-container">{% include "dashboard/partials/data_table.html" %}</div>
|
<div id="ajax-content-container">
|
||||||
</div>
|
{% include "dashboard/partials/data_table.html" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Data Summary -->
|
<!-- Data Summary -->
|
||||||
{% if page_obj %}
|
{% if page_obj %}
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">Summary</h5>
|
<h5 class="card-title mb-0">Summary</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="card stats-card bg-light">
|
<div class="card stats-card bg-light">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h6 class="card-title">Total Sessions</h6>
|
<h6 class="card-title">Total Sessions</h6>
|
||||||
<h3>{{ page_obj.paginator.count }}</h3>
|
<h3>{{ page_obj.paginator.count }}</h3>
|
||||||
<p>Chat conversations</p>
|
<p>Chat conversations</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="card stats-card bg-light">
|
<div class="card stats-card bg-light">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h6 class="card-title">Avg Response Time</h6>
|
<h6 class="card-title">Avg Response Time</h6>
|
||||||
<h3>{{ avg_response_time|floatformat:2 }}s</h3>
|
<h3>{{ avg_response_time|floatformat:2 }}s</h3>
|
||||||
<p>Average response</p>
|
<p>Average response</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="card stats-card bg-light">
|
<div class="card stats-card bg-light">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h6 class="card-title">Avg Messages</h6>
|
<h6 class="card-title">Avg Messages</h6>
|
||||||
<h3>{{ avg_messages|floatformat:1 }}</h3>
|
<h3>{{ avg_messages|floatformat:1 }}</h3>
|
||||||
<p>Per conversation</p>
|
<p>Per conversation</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="card stats-card bg-light">
|
<div class="card stats-card bg-light">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h6 class="card-title">Escalation Rate</h6>
|
<h6 class="card-title">Escalation Rate</h6>
|
||||||
<h3>{{ escalation_rate|floatformat:1 }}%</h3>
|
<h3>{{ escalation_rate|floatformat:1 }}%</h3>
|
||||||
<p>Escalated sessions</p>
|
<p>Escalated sessions</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
// Function to update the summary section with new data
|
// Function to update the summary section with new data
|
||||||
function updateSummary(data) {
|
function updateSummary(data) {
|
||||||
if (document.querySelector(".stats-card h3:nth-of-type(1)")) {
|
if (document.querySelector(".stats-card h3:nth-of-type(1)")) {
|
||||||
document.querySelector(".stats-card h3:nth-of-type(1)").textContent =
|
document.querySelector(".stats-card h3:nth-of-type(1)").textContent =
|
||||||
data.page_obj.paginator.count;
|
data.page_obj.paginator.count;
|
||||||
}
|
}
|
||||||
if (document.querySelector(".stats-card h3:nth-of-type(2)")) {
|
if (document.querySelector(".stats-card h3:nth-of-type(2)")) {
|
||||||
document.querySelector(".stats-card h3:nth-of-type(2)").textContent =
|
document.querySelector(".stats-card h3:nth-of-type(2)").textContent =
|
||||||
data.avg_response_time !== null && data.avg_response_time !== undefined
|
data.avg_response_time !== null && data.avg_response_time !== undefined
|
||||||
? data.avg_response_time.toFixed(2) + "s"
|
? data.avg_response_time.toFixed(2) + "s"
|
||||||
: "0.00s";
|
: "0.00s";
|
||||||
}
|
}
|
||||||
if (document.querySelector(".stats-card h3:nth-of-type(3)")) {
|
if (document.querySelector(".stats-card h3:nth-of-type(3)")) {
|
||||||
document.querySelector(".stats-card h3:nth-of-type(3)").textContent =
|
document.querySelector(".stats-card h3:nth-of-type(3)").textContent =
|
||||||
data.avg_messages !== null && data.avg_messages !== undefined
|
data.avg_messages !== null && data.avg_messages !== undefined
|
||||||
? data.avg_messages.toFixed(1)
|
? data.avg_messages.toFixed(1)
|
||||||
: "0.0";
|
: "0.0";
|
||||||
}
|
}
|
||||||
if (document.querySelector(".stats-card h3:nth-of-type(4)")) {
|
if (document.querySelector(".stats-card h3:nth-of-type(4)")) {
|
||||||
document.querySelector(".stats-card h3:nth-of-type(4)").textContent =
|
document.querySelector(".stats-card h3:nth-of-type(4)").textContent =
|
||||||
data.escalation_rate !== null && data.escalation_rate !== undefined
|
data.escalation_rate !== null && data.escalation_rate !== undefined
|
||||||
? data.escalation_rate.toFixed(1) + "%"
|
? data.escalation_rate.toFixed(1) + "%"
|
||||||
: "0.0%";
|
: "0.0%";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -4,37 +4,41 @@
|
|||||||
{% block title %}No Company | Chat Analytics{% endblock %}
|
{% block title %}No Company | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div
|
<div
|
||||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
>
|
>
|
||||||
<h1 class="h2">No Company Association</h1>
|
<h1 class="h2">No Company Association</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header bg-warning text-dark">
|
<div class="card-header bg-warning text-dark">
|
||||||
<h5 class="card-title mb-0">Account Not Associated with a Company</h5>
|
<h5 class="card-title mb-0">Account Not Associated with a Company</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<i class="fas fa-building fa-4x text-warning mb-3"></i>
|
<i class="fas fa-building fa-4x text-warning mb-3"></i>
|
||||||
<h4>You are not currently associated with any company</h4>
|
<h4>You are not currently associated with any company</h4>
|
||||||
<p class="lead">You need to be associated with a company to access the dashboard.</p>
|
<p class="lead">
|
||||||
</div>
|
You need to be associated with a company to access the dashboard.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Please contact an administrator to have your account assigned to a company. Once your
|
Please contact an administrator to have your account assigned to a company.
|
||||||
account is associated with a company, you'll be able to access the dashboard and its
|
Once your account is associated with a company, you'll be able to access the
|
||||||
features.
|
dashboard and its features.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<a href="{% url 'profile' %}" class="btn btn-primary">View Your Profile</a>
|
<a href="{% url 'profile' %}" class="btn btn-primary">View Your Profile</a>
|
||||||
<a href="{% url 'logout' %}" class="btn btn-outline-secondary ms-2">Logout</a>
|
<a href="{% url 'logout' %}" class="btn btn-outline-secondary ms-2"
|
||||||
</div>
|
>Logout</a
|
||||||
</div>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -1,160 +1,160 @@
|
|||||||
<!-- templates/dashboard/partials/data_table.html -->
|
<!-- templates/dashboard/partials/data_table.html -->
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-hover">
|
<table class="table table-striped table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Session ID</th>
|
<th>Session ID</th>
|
||||||
<th>Start Time</th>
|
<th>Start Time</th>
|
||||||
<th>Country</th>
|
<th>Country</th>
|
||||||
<th>Language</th>
|
<th>Language</th>
|
||||||
<th>Messages</th>
|
<th>Messages</th>
|
||||||
<th>Sentiment</th>
|
<th>Sentiment</th>
|
||||||
<th>Response Time</th>
|
<th>Response Time</th>
|
||||||
<th>Category</th>
|
<th>Category</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for session in page_obj %}
|
{% for session in page_obj %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ session.session_id|truncatechars:10 }}</td>
|
<td>{{ session.session_id|truncatechars:10 }}</td>
|
||||||
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
|
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
|
||||||
<td>{{ session.country|default:"N/A" }}</td>
|
<td>{{ session.country|default:"N/A" }}</td>
|
||||||
<td>{{ session.language|default:"N/A" }}</td>
|
<td>{{ session.language|default:"N/A" }}</td>
|
||||||
<td>{{ session.messages_sent }}</td>
|
<td>{{ session.messages_sent }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if session.sentiment %}
|
{% if session.sentiment %}
|
||||||
{% if 'positive' in session.sentiment|lower %}
|
{% if 'positive' in session.sentiment|lower %}
|
||||||
<span class="badge bg-success">{{ session.sentiment }}</span>
|
<span class="badge bg-success">{{ session.sentiment }}</span>
|
||||||
{% elif 'negative' in session.sentiment|lower %}
|
{% elif 'negative' in session.sentiment|lower %}
|
||||||
<span class="badge bg-danger">{{ session.sentiment }}</span>
|
<span class="badge bg-danger">{{ session.sentiment }}</span>
|
||||||
{% elif 'neutral' in session.sentiment|lower %}
|
{% elif 'neutral' in session.sentiment|lower %}
|
||||||
<span class="badge bg-warning">{{ session.sentiment }}</span>
|
<span class="badge bg-warning">{{ session.sentiment }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-secondary">{{ session.sentiment }}</span>
|
<span class="badge bg-secondary">{{ session.sentiment }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">N/A</span>
|
<span class="text-muted">N/A</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ session.avg_response_time|floatformat:2 }}s</td>
|
<td>{{ session.avg_response_time|floatformat:2 }}s</td>
|
||||||
<td>{{ session.category|default:"N/A" }}</td>
|
<td>{{ session.category|default:"N/A" }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if session.session_id %}
|
{% if session.session_id %}
|
||||||
<a
|
<a
|
||||||
href="{% url 'chat_session_detail' session.session_id %}"
|
href="{% url 'chat_session_detail' session.session_id %}"
|
||||||
class="btn btn-sm btn-outline-primary ajax-nav-link"
|
class="btn btn-sm btn-outline-primary ajax-nav-link"
|
||||||
>
|
>
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button class="btn btn-sm btn-outline-secondary" disabled>
|
<button class="btn btn-sm btn-outline-secondary" disabled>
|
||||||
<i class="fas fa-eye-slash"></i>
|
<i class="fas fa-eye-slash"></i>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="9" class="text-center">No chat sessions found.</td>
|
<td colspan="9" class="text-center">No chat sessions found.</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if page_obj.paginator.num_pages > 1 %}
|
{% if page_obj.paginator.num_pages > 1 %}
|
||||||
<nav aria-label="Page navigation" class="mt-4" id="pagination-container">
|
<nav aria-label="Page navigation" class="mt-4" id="pagination-container">
|
||||||
<ul class="pagination justify-content-center">
|
<ul class="pagination justify-content-center">
|
||||||
{% if page_obj.has_previous %}
|
{% if page_obj.has_previous %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a
|
<a
|
||||||
class="page-link pagination-link"
|
class="page-link pagination-link"
|
||||||
data-page="1"
|
data-page="1"
|
||||||
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}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"
|
aria-label="First"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">««</span>
|
<span aria-hidden="true">««</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a
|
<a
|
||||||
class="page-link pagination-link"
|
class="page-link pagination-link"
|
||||||
data-page="{{ page_obj.previous_page_number }}"
|
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 }}"
|
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"
|
aria-label="Previous"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">«</span>
|
<span aria-hidden="true">«</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<a class="page-link" href="#" aria-label="First">
|
<a class="page-link" href="#" aria-label="First">
|
||||||
<span aria-hidden="true">««</span>
|
<span aria-hidden="true">««</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<a class="page-link" href="#" aria-label="Previous">
|
<a class="page-link" href="#" aria-label="Previous">
|
||||||
<span aria-hidden="true">«</span>
|
<span aria-hidden="true">«</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% for num in page_obj.paginator.page_range %}
|
{% for num in page_obj.paginator.page_range %}
|
||||||
{% if page_obj.number == num %}
|
{% if page_obj.number == num %}
|
||||||
<li class="page-item active">
|
<li class="page-item active">
|
||||||
<a
|
<a
|
||||||
class="page-link pagination-link"
|
class="page-link pagination-link"
|
||||||
data-page="{{ num }}"
|
data-page="{{ num }}"
|
||||||
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ num }}"
|
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ num }}"
|
||||||
>{{ num }}</a
|
>{{ num }}</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a
|
<a
|
||||||
class="page-link pagination-link"
|
class="page-link pagination-link"
|
||||||
data-page="{{ num }}"
|
data-page="{{ num }}"
|
||||||
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ num }}"
|
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ num }}"
|
||||||
>{{ num }}</a
|
>{{ num }}</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if page_obj.has_next %}
|
{% if page_obj.has_next %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a
|
<a
|
||||||
class="page-link pagination-link"
|
class="page-link pagination-link"
|
||||||
data-page="{{ page_obj.next_page_number }}"
|
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 }}"
|
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"
|
aria-label="Next"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">»</span>
|
<span aria-hidden="true">»</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a
|
<a
|
||||||
class="page-link pagination-link"
|
class="page-link pagination-link"
|
||||||
data-page="{{ page_obj.paginator.num_pages }}"
|
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 }}"
|
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"
|
aria-label="Last"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">»»</span>
|
<span aria-hidden="true">»»</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<a class="page-link" href="#" aria-label="Next">
|
<a class="page-link" href="#" aria-label="Next">
|
||||||
<span aria-hidden="true">»</span>
|
<span aria-hidden="true">»</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<a class="page-link" href="#" aria-label="Last">
|
<a class="page-link" href="#" aria-label="Last">
|
||||||
<span aria-hidden="true">»»</span>
|
<span aria-hidden="true">»»</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@ -1,164 +1,168 @@
|
|||||||
<!-- templates/dashboard/partials/search_results_table.html -->
|
<!-- templates/dashboard/partials/search_results_table.html -->
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-hover">
|
<table class="table table-striped table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Session ID</th>
|
<th>Session ID</th>
|
||||||
<th>Start Time</th>
|
<th>Start Time</th>
|
||||||
<th>Data Source</th>
|
<th>Data Source</th>
|
||||||
<th>Country</th>
|
<th>Country</th>
|
||||||
<th>Language</th>
|
<th>Language</th>
|
||||||
<th>Sentiment</th>
|
<th>Sentiment</th>
|
||||||
<th>Messages</th>
|
<th>Messages</th>
|
||||||
<th>Category</th>
|
<th>Category</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for session in page_obj %}
|
{% for session in page_obj %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ session.session_id|truncatechars:10 }}</td>
|
<td>{{ session.session_id|truncatechars:10 }}</td>
|
||||||
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
|
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'data_source_detail' session.data_source.id %}" class="ajax-nav-link"
|
<a
|
||||||
>{{ session.data_source.name|truncatechars:15 }}</a
|
href="{% url 'data_source_detail' session.data_source.id %}"
|
||||||
>
|
class="ajax-nav-link"
|
||||||
</td>
|
>{{ session.data_source.name|truncatechars:15 }}</a
|
||||||
<td>{{ session.country }}</td>
|
>
|
||||||
<td>{{ session.language }}</td>
|
</td>
|
||||||
<td>
|
<td>{{ session.country }}</td>
|
||||||
{% if session.sentiment %}
|
<td>{{ session.language }}</td>
|
||||||
{% if 'positive' in session.sentiment|lower %}
|
<td>
|
||||||
<span class="badge bg-success">{{ session.sentiment }}</span>
|
{% if session.sentiment %}
|
||||||
{% elif 'negative' in session.sentiment|lower %}
|
{% if 'positive' in session.sentiment|lower %}
|
||||||
<span class="badge bg-danger">{{ session.sentiment }}</span>
|
<span class="badge bg-success">{{ session.sentiment }}</span>
|
||||||
{% elif 'neutral' in session.sentiment|lower %}
|
{% elif 'negative' in session.sentiment|lower %}
|
||||||
<span class="badge bg-warning">{{ session.sentiment }}</span>
|
<span class="badge bg-danger">{{ session.sentiment }}</span>
|
||||||
{% else %}
|
{% elif 'neutral' in session.sentiment|lower %}
|
||||||
<span class="badge bg-secondary">{{ session.sentiment }}</span>
|
<span class="badge bg-warning">{{ session.sentiment }}</span>
|
||||||
{% endif %}
|
{% else %}
|
||||||
{% else %}
|
<span class="badge bg-secondary">{{ session.sentiment }}</span>
|
||||||
<span class="text-muted">N/A</span>
|
{% endif %}
|
||||||
{% endif %}
|
{% else %}
|
||||||
</td>
|
<span class="text-muted">N/A</span>
|
||||||
<td>{{ session.messages_sent }}</td>
|
{% endif %}
|
||||||
<td>{{ session.category|default:"N/A" }}</td>
|
</td>
|
||||||
<td>
|
<td>{{ session.messages_sent }}</td>
|
||||||
{% if session.session_id %}
|
<td>{{ session.category|default:"N/A" }}</td>
|
||||||
<a
|
<td>
|
||||||
href="{% url 'chat_session_detail' session.session_id %}"
|
{% if session.session_id %}
|
||||||
class="btn btn-sm btn-outline-primary"
|
<a
|
||||||
>
|
href="{% url 'chat_session_detail' session.session_id %}"
|
||||||
<i class="fas fa-eye"></i>
|
class="btn btn-sm btn-outline-primary"
|
||||||
</a>
|
>
|
||||||
{% else %}
|
<i class="fas fa-eye"></i>
|
||||||
<button class="btn btn-sm btn-outline-secondary" disabled>
|
</a>
|
||||||
<i class="fas fa-eye-slash"></i>
|
{% else %}
|
||||||
</button>
|
<button class="btn btn-sm btn-outline-secondary" disabled>
|
||||||
{% endif %}
|
<i class="fas fa-eye-slash"></i>
|
||||||
</td>
|
</button>
|
||||||
</tr>
|
{% endif %}
|
||||||
{% empty %}
|
</td>
|
||||||
<tr>
|
</tr>
|
||||||
<td colspan="9" class="text-center">No chat sessions found matching your criteria.</td>
|
{% empty %}
|
||||||
</tr>
|
<tr>
|
||||||
{% endfor %}
|
<td colspan="9" class="text-center">
|
||||||
</tbody>
|
No chat sessions found matching your criteria.
|
||||||
</table>
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if page_obj.paginator.num_pages > 1 %}
|
{% if page_obj.paginator.num_pages > 1 %}
|
||||||
<nav aria-label="Page navigation" class="mt-4" id="pagination-container">
|
<nav aria-label="Page navigation" class="mt-4" id="pagination-container">
|
||||||
<ul class="pagination justify-content-center">
|
<ul class="pagination justify-content-center">
|
||||||
{% if page_obj.has_previous %}
|
{% if page_obj.has_previous %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a
|
<a
|
||||||
class="page-link pagination-link"
|
class="page-link pagination-link"
|
||||||
data-page="1"
|
data-page="1"
|
||||||
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page=1"
|
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page=1"
|
||||||
aria-label="First"
|
aria-label="First"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">««</span>
|
<span aria-hidden="true">««</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a
|
<a
|
||||||
class="page-link pagination-link"
|
class="page-link pagination-link"
|
||||||
data-page="{{ page_obj.previous_page_number }}"
|
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 }}"
|
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"
|
aria-label="Previous"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">«</span>
|
<span aria-hidden="true">«</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<a class="page-link" href="#" aria-label="First">
|
<a class="page-link" href="#" aria-label="First">
|
||||||
<span aria-hidden="true">««</span>
|
<span aria-hidden="true">««</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<a class="page-link" href="#" aria-label="Previous">
|
<a class="page-link" href="#" aria-label="Previous">
|
||||||
<span aria-hidden="true">«</span>
|
<span aria-hidden="true">«</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% for num in page_obj.paginator.page_range %}
|
{% for num in page_obj.paginator.page_range %}
|
||||||
{% if page_obj.number == num %}
|
{% if page_obj.number == num %}
|
||||||
<li class="page-item active">
|
<li class="page-item active">
|
||||||
<a
|
<a
|
||||||
class="page-link pagination-link"
|
class="page-link pagination-link"
|
||||||
data-page="{{ num }}"
|
data-page="{{ num }}"
|
||||||
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ num }}"
|
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ num }}"
|
||||||
>{{ num }}</a
|
>{{ num }}</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a
|
<a
|
||||||
class="page-link pagination-link"
|
class="page-link pagination-link"
|
||||||
data-page="{{ num }}"
|
data-page="{{ num }}"
|
||||||
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ num }}"
|
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ num }}"
|
||||||
>{{ num }}</a
|
>{{ num }}</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if page_obj.has_next %}
|
{% if page_obj.has_next %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a
|
<a
|
||||||
class="page-link pagination-link"
|
class="page-link pagination-link"
|
||||||
data-page="{{ page_obj.next_page_number }}"
|
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 }}"
|
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"
|
aria-label="Next"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">»</span>
|
<span aria-hidden="true">»</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a
|
<a
|
||||||
class="page-link pagination-link"
|
class="page-link pagination-link"
|
||||||
data-page="{{ page_obj.paginator.num_pages }}"
|
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 }}"
|
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"
|
aria-label="Last"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">»»</span>
|
<span aria-hidden="true">»»</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<a class="page-link" href="#" aria-label="Next">
|
<a class="page-link" href="#" aria-label="Next">
|
||||||
<span aria-hidden="true">»</span>
|
<span aria-hidden="true">»</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<a class="page-link" href="#" aria-label="Last">
|
<a class="page-link" href="#" aria-label="Last">
|
||||||
<span aria-hidden="true">»»</span>
|
<span aria-hidden="true">»»</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@ -4,82 +4,90 @@
|
|||||||
{% block title %}Search Results | Chat Analytics{% endblock %}
|
{% block title %}Search Results | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div
|
<div
|
||||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
>
|
>
|
||||||
<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 ajax-nav-link">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<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' %}" class="search-form">
|
<form
|
||||||
<div class="input-group">
|
method="get"
|
||||||
<input
|
action="{% url 'search_chat_sessions' %}"
|
||||||
type="text"
|
class="search-form"
|
||||||
name="q"
|
>
|
||||||
class="form-control"
|
<div class="input-group">
|
||||||
placeholder="Search sessions..."
|
<input
|
||||||
value="{{ query }}"
|
type="text"
|
||||||
aria-label="Search sessions"
|
name="q"
|
||||||
/>
|
class="form-control"
|
||||||
{% if data_source %}
|
placeholder="Search sessions..."
|
||||||
<input type="hidden" name="data_source_id" value="{{ data_source.id }}" />
|
value="{{ query }}"
|
||||||
{% endif %}
|
aria-label="Search sessions"
|
||||||
<button class="btn btn-outline-primary" type="submit">
|
/>
|
||||||
<i class="fas fa-search"></i> Search
|
{% if data_source %}
|
||||||
</button>
|
<input
|
||||||
</div>
|
type="hidden"
|
||||||
<div class="mt-2 text-muted">
|
name="data_source_id"
|
||||||
<small
|
value="{{ data_source.id }}"
|
||||||
>Search by session ID, country, language, sentiment, category, or message
|
/>
|
||||||
content.</small
|
{% endif %}
|
||||||
>
|
<button class="btn btn-outline-primary" type="submit">
|
||||||
</div>
|
<i class="fas fa-search"></i> Search
|
||||||
</form>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="mt-2 text-muted">
|
||||||
</div>
|
<small
|
||||||
</div>
|
>Search by session ID, country, language, sentiment, category, or
|
||||||
|
message content.</small
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">
|
<h5 class="card-title mb-0">
|
||||||
Results {% if query %}for "{{ query }}"{% endif %}
|
Results {% if query %}for "{{ query }}"{% endif %}
|
||||||
{% if data_source %}in {{ data_source.name }}{% endif %}
|
{% if data_source %}in {{ data_source.name }}{% endif %}
|
||||||
({{ page_obj.paginator.count }})
|
({{ page_obj.paginator.count }})
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<!-- Loading spinner shown during AJAX requests -->
|
<!-- Loading spinner shown during AJAX requests -->
|
||||||
<div id="ajax-loading-spinner" class="text-center py-4 d-none">
|
<div id="ajax-loading-spinner" class="text-center py-4 d-none">
|
||||||
<div class="spinner-border text-primary" role="status">
|
<div class="spinner-border text-primary" role="status">
|
||||||
<span class="visually-hidden">Loading...</span>
|
<span class="visually-hidden">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2">Loading data...</p>
|
<p class="mt-2">Loading data...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Search results container that will be updated via AJAX -->
|
<!-- Search results container that will be updated via AJAX -->
|
||||||
<div id="ajax-content-container">
|
<div id="ajax-content-container">
|
||||||
{% include "dashboard/partials/search_results_table.html" %}
|
{% include "dashboard/partials/search_results_table.html" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<!-- No need for extra JavaScript here, using common ajax-pagination.js -->
|
<!-- No need for extra JavaScript here, using common ajax-pagination.js -->
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -6,192 +6,192 @@
|
|||||||
{% block title %}Upload Data | Chat Analytics{% endblock %}
|
{% block title %}Upload Data | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div
|
<div
|
||||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
>
|
>
|
||||||
<h1 class="h2">Upload Data</h1>
|
<h1 class="h2">Upload Data</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">
|
||||||
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">Upload CSV File</h5>
|
<h5 class="card-title mb-0">Upload CSV File</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
{% csrf_token %} {{ form|crispy }}
|
{% csrf_token %} {{ form|crispy }}
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<button type="submit" class="btn btn-primary">Upload</button>
|
<button type="submit" class="btn btn-primary">Upload</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">CSV File Format</h5>
|
<h5 class="card-title mb-0">CSV File Format</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>The CSV file should contain the following columns:</p>
|
<p>The CSV file should contain the following columns:</p>
|
||||||
<table class="table table-sm">
|
<table class="table table-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Column</th>
|
<th>Column</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>session_id</td>
|
<td>session_id</td>
|
||||||
<td>Unique identifier for the chat session</td>
|
<td>Unique identifier for the chat session</td>
|
||||||
<td>String</td>
|
<td>String</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>start_time</td>
|
<td>start_time</td>
|
||||||
<td>When the session started</td>
|
<td>When the session started</td>
|
||||||
<td>Datetime</td>
|
<td>Datetime</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>end_time</td>
|
<td>end_time</td>
|
||||||
<td>When the session ended</td>
|
<td>When the session ended</td>
|
||||||
<td>Datetime</td>
|
<td>Datetime</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>ip_address</td>
|
<td>ip_address</td>
|
||||||
<td>IP address of the user</td>
|
<td>IP address of the user</td>
|
||||||
<td>String</td>
|
<td>String</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>country</td>
|
<td>country</td>
|
||||||
<td>Country of the user</td>
|
<td>Country of the user</td>
|
||||||
<td>String</td>
|
<td>String</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>language</td>
|
<td>language</td>
|
||||||
<td>Language used in the conversation</td>
|
<td>Language used in the conversation</td>
|
||||||
<td>String</td>
|
<td>String</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>messages_sent</td>
|
<td>messages_sent</td>
|
||||||
<td>Number of messages in the conversation</td>
|
<td>Number of messages in the conversation</td>
|
||||||
<td>Integer</td>
|
<td>Integer</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>sentiment</td>
|
<td>sentiment</td>
|
||||||
<td>Sentiment analysis of the conversation</td>
|
<td>Sentiment analysis of the conversation</td>
|
||||||
<td>String</td>
|
<td>String</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>escalated</td>
|
<td>escalated</td>
|
||||||
<td>Whether the conversation was escalated</td>
|
<td>Whether the conversation was escalated</td>
|
||||||
<td>Boolean</td>
|
<td>Boolean</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>forwarded_hr</td>
|
<td>forwarded_hr</td>
|
||||||
<td>Whether the conversation was forwarded to HR</td>
|
<td>Whether the conversation was forwarded to HR</td>
|
||||||
<td>Boolean</td>
|
<td>Boolean</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>full_transcript</td>
|
<td>full_transcript</td>
|
||||||
<td>Full transcript of the conversation</td>
|
<td>Full transcript of the conversation</td>
|
||||||
<td>Text</td>
|
<td>Text</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>avg_response_time</td>
|
<td>avg_response_time</td>
|
||||||
<td>Average response time in seconds</td>
|
<td>Average response time in seconds</td>
|
||||||
<td>Float</td>
|
<td>Float</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>tokens</td>
|
<td>tokens</td>
|
||||||
<td>Total number of tokens used</td>
|
<td>Total number of tokens used</td>
|
||||||
<td>Integer</td>
|
<td>Integer</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>tokens_eur</td>
|
<td>tokens_eur</td>
|
||||||
<td>Cost of tokens in EUR</td>
|
<td>Cost of tokens in EUR</td>
|
||||||
<td>Float</td>
|
<td>Float</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>category</td>
|
<td>category</td>
|
||||||
<td>Category of the conversation</td>
|
<td>Category of the conversation</td>
|
||||||
<td>String</td>
|
<td>String</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>initial_msg</td>
|
<td>initial_msg</td>
|
||||||
<td>First message from the user</td>
|
<td>First message from the user</td>
|
||||||
<td>Text</td>
|
<td>Text</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>user_rating</td>
|
<td>user_rating</td>
|
||||||
<td>User rating of the conversation</td>
|
<td>User rating of the conversation</td>
|
||||||
<td>String</td>
|
<td>String</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if data_sources %}
|
{% if data_sources %}
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">Uploaded Data Sources</h5>
|
<h5 class="card-title mb-0">Uploaded Data Sources</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-hover">
|
<table class="table table-striped table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Uploaded</th>
|
<th>Uploaded</th>
|
||||||
<th>File</th>
|
<th>File</th>
|
||||||
<th>Sessions</th>
|
<th>Sessions</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for data_source in data_sources %}
|
{% for data_source in data_sources %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ data_source.name }}</td>
|
<td>{{ data_source.name }}</td>
|
||||||
<td>{{ data_source.description|truncatechars:50 }}</td>
|
<td>{{ data_source.description|truncatechars:50 }}</td>
|
||||||
<td>{{ data_source.uploaded_at|date:"M d, Y H:i" }}</td>
|
<td>{{ data_source.uploaded_at|date:"M d, Y H:i" }}</td>
|
||||||
<td>{{ data_source.file.name|split:"/"|last }}</td>
|
<td>{{ data_source.file.name|split:"/"|last }}</td>
|
||||||
<td>{{ data_source.chat_sessions.count }}</td>
|
<td>{{ data_source.chat_sessions.count }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a
|
<a
|
||||||
href="{% url 'data_source_detail' data_source.id %}"
|
href="{% url 'data_source_detail' data_source.id %}"
|
||||||
class="btn btn-sm btn-outline-primary"
|
class="btn btn-sm btn-outline-primary"
|
||||||
>
|
>
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="{% url 'delete_data_source' data_source.id %}"
|
href="{% url 'delete_data_source' data_source.id %}"
|
||||||
class="btn btn-sm btn-outline-danger"
|
class="btn btn-sm btn-outline-danger"
|
||||||
>
|
>
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ services:
|
|||||||
- DEBUG=0
|
- DEBUG=0
|
||||||
- SECRET_KEY=your_secret_key_here
|
- SECRET_KEY=your_secret_key_here
|
||||||
- ALLOWED_HOSTS=localhost,127.0.0.1
|
- ALLOWED_HOSTS=localhost,127.0.0.1
|
||||||
|
- DJANGO_SETTINGS_MODULE=dashboard_project.settings
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
|
||||||
@ -27,6 +28,8 @@ services:
|
|||||||
- POSTGRES_USER=postgres
|
- POSTGRES_USER=postgres
|
||||||
- POSTGRES_PASSWORD=postgres
|
- POSTGRES_PASSWORD=postgres
|
||||||
- POSTGRES_DB=dashboard_db
|
- POSTGRES_DB=dashboard_db
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:latest
|
image: nginx:latest
|
||||||
|
|||||||
39
package-lock.json
generated
39
package-lock.json
generated
@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "LiveGraphsDjango",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"devDependencies": {
|
|
||||||
"prettier": "^3.5.3",
|
|
||||||
"prettier-plugin-jinja-template": "^2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prettier": {
|
|
||||||
"version": "3.5.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
|
||||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"prettier": "bin/prettier.cjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prettier-plugin-jinja-template": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/prettier-plugin-jinja-template/-/prettier-plugin-jinja-template-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-mzoCp2Oy9BDSug80fw3B3J4n4KQj1hRvoQOL1akqcDKBb5nvYxrik9zUEDs4AEJ6nK7QDTGoH0y9rx7AlnQ78Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"prettier": "^3.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
19
package.json
19
package.json
@ -1,6 +1,17 @@
|
|||||||
{
|
{
|
||||||
"devDependencies": {
|
"name": "livegraphsdjango",
|
||||||
"prettier": "^3.5.3",
|
"version": "0.1.0",
|
||||||
"prettier-plugin-jinja-template": "^2.1.0"
|
"description": "Live Graphs Django Dashboard",
|
||||||
}
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint dashboard_project/static/js/",
|
||||||
|
"format": "prettier --write \"dashboard_project/static/**/*.{js,css,html}\"",
|
||||||
|
"format:check": "prettier --check \"dashboard_project/static/**/*.{js,css,html}\"",
|
||||||
|
"stylelint": "stylelint \"dashboard_project/static/css/**/*.css\" --fix",
|
||||||
|
"stylelint:check": "stylelint \"dashboard_project/static/css/**/*.css\""
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"prettier-plugin-jinja-template": "^2.1.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
101
pyproject.toml
101
pyproject.toml
@ -1,9 +1,21 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "livegraphsdjango"
|
name = "livegraphsdjango"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Add your description here"
|
description = "Live Graphs Django Dashboard"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
authors = [{ name = "LiveGraphs Team" }]
|
||||||
|
license = { text = "MIT" }
|
||||||
|
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
|
"Framework :: Django",
|
||||||
|
"Framework :: Django :: 5.2",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
]
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crispy-bootstrap5>=2025.4",
|
"crispy-bootstrap5>=2025.4",
|
||||||
"django>=5.2.1",
|
"django>=5.2.1",
|
||||||
@ -17,16 +29,29 @@ dependencies = [
|
|||||||
"whitenoise>=6.9.0",
|
"whitenoise>=6.9.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[dependency-groups]
|
||||||
# Django management commands
|
dev = [
|
||||||
"manage" = "dashboard_project.manage:main"
|
"bandit>=1.8.3",
|
||||||
"runserver" = "dashboard_project.manage:main"
|
"black>=25.1.0",
|
||||||
"migrate" = "dashboard_project.manage:main"
|
"coverage>=7.8.0",
|
||||||
"makemigrations" = "dashboard_project.manage:main"
|
"django-debug-toolbar>=5.2.0",
|
||||||
"collectstatic" = "dashboard_project.manage:main"
|
"django-stubs>=5.2.0",
|
||||||
"createsuperuser" = "dashboard_project.manage:main"
|
"mypy>=1.15.0",
|
||||||
"shell" = "dashboard_project.manage:main"
|
"pre-commit>=4.2.0",
|
||||||
"test" = "dashboard_project.manage:main"
|
"pytest>=8.3.5",
|
||||||
|
"pytest-django>=4.11.1",
|
||||||
|
"ruff>=0.11.10",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=69.0.0", "wheel>=0.42.0"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
packages = ["dashboard_project"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
"dashboard_project" = ["static/**/*", "templates/**/*", "media/**/*"]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
# Exclude a variety of commonly ignored directories.
|
# Exclude a variety of commonly ignored directories.
|
||||||
@ -67,7 +92,7 @@ indent-width = 4
|
|||||||
target-version = "py313"
|
target-version = "py313"
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select = ["E", "F", "I"]
|
select = ["E", "F", "I", "B", "C4", "ARG", "SIM", "PERF"]
|
||||||
ignore = ["E501"]
|
ignore = ["E501"]
|
||||||
fixable = ["ALL"]
|
fixable = ["ALL"]
|
||||||
unfixable = []
|
unfixable = []
|
||||||
@ -76,3 +101,55 @@ unfixable = []
|
|||||||
quote-style = "double"
|
quote-style = "double"
|
||||||
indent-style = "space"
|
indent-style = "space"
|
||||||
line-ending = "lf"
|
line-ending = "lf"
|
||||||
|
|
||||||
|
[tool.bandit]
|
||||||
|
exclude_dirs = ["tests", "venv", ".venv", ".git", "__pycache__", "migrations", "**/create_sample_data.py"]
|
||||||
|
skips = ["B101"]
|
||||||
|
targets = ["dashboard_project"]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = "3.13"
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unused_configs = true
|
||||||
|
disallow_untyped_defs = false
|
||||||
|
disallow_incomplete_defs = false
|
||||||
|
plugins = ["mypy_django_plugin.main"]
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = ["django.*", "rest_framework.*"]
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
[tool.django-stubs]
|
||||||
|
django_settings_module = "dashboard_project.settings"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
DJANGO_SETTINGS_MODULE = "dashboard_project.settings"
|
||||||
|
python_files = "test_*.py"
|
||||||
|
testpaths = ["dashboard_project"]
|
||||||
|
filterwarnings = [
|
||||||
|
"ignore::DeprecationWarning",
|
||||||
|
"ignore::PendingDeprecationWarning",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
source = ["dashboard_project"]
|
||||||
|
omit = [
|
||||||
|
"dashboard_project/manage.py",
|
||||||
|
"dashboard_project/*/migrations/*",
|
||||||
|
"dashboard_project/*/tests/*",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
exclude_lines = [
|
||||||
|
"pragma: no cover",
|
||||||
|
"def __repr__",
|
||||||
|
"raise NotImplementedError",
|
||||||
|
"if __name__ == .__main__.:",
|
||||||
|
"pass",
|
||||||
|
"raise ImportError",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
"Documentation" = "https://github.com/kjanat/livegraphsdjango#readme"
|
||||||
|
"Source" = "https://github.com/kjanat/livegraphsdjango"
|
||||||
|
"Bug Tracker" = "https://github.com/kjanat/livegraphsdjango/issues"
|
||||||
|
|||||||
320
requirements.txt
Normal file
320
requirements.txt
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
# This file was autogenerated by uv via the following command:
|
||||||
|
# uv export --frozen --output-file=requirements.txt
|
||||||
|
-e .
|
||||||
|
asgiref==3.8.1 \
|
||||||
|
--hash=sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47 \
|
||||||
|
--hash=sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590
|
||||||
|
# via
|
||||||
|
# django
|
||||||
|
# django-allauth
|
||||||
|
# django-stubs
|
||||||
|
bandit==1.8.3 \
|
||||||
|
--hash=sha256:28f04dc0d258e1dd0f99dee8eefa13d1cb5e3fde1a5ab0c523971f97b289bcd8 \
|
||||||
|
--hash=sha256:f5847beb654d309422985c36644649924e0ea4425c76dec2e89110b87506193a
|
||||||
|
black==25.1.0 \
|
||||||
|
--hash=sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171 \
|
||||||
|
--hash=sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666 \
|
||||||
|
--hash=sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f \
|
||||||
|
--hash=sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717 \
|
||||||
|
--hash=sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18 \
|
||||||
|
--hash=sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3
|
||||||
|
cfgv==3.4.0 \
|
||||||
|
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
|
||||||
|
--hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
|
||||||
|
# via pre-commit
|
||||||
|
click==8.2.0 \
|
||||||
|
--hash=sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c \
|
||||||
|
--hash=sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d
|
||||||
|
# via black
|
||||||
|
colorama==0.4.6 ; sys_platform == 'win32' \
|
||||||
|
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
|
||||||
|
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
|
||||||
|
# via
|
||||||
|
# bandit
|
||||||
|
# click
|
||||||
|
# pytest
|
||||||
|
coverage==7.8.0 \
|
||||||
|
--hash=sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3 \
|
||||||
|
--hash=sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25 \
|
||||||
|
--hash=sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257 \
|
||||||
|
--hash=sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada \
|
||||||
|
--hash=sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64 \
|
||||||
|
--hash=sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067 \
|
||||||
|
--hash=sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733 \
|
||||||
|
--hash=sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008 \
|
||||||
|
--hash=sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd \
|
||||||
|
--hash=sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00 \
|
||||||
|
--hash=sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501 \
|
||||||
|
--hash=sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42 \
|
||||||
|
--hash=sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487 \
|
||||||
|
--hash=sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4 \
|
||||||
|
--hash=sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73 \
|
||||||
|
--hash=sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a \
|
||||||
|
--hash=sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1 \
|
||||||
|
--hash=sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7 \
|
||||||
|
--hash=sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d \
|
||||||
|
--hash=sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502 \
|
||||||
|
--hash=sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323 \
|
||||||
|
--hash=sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883
|
||||||
|
crispy-bootstrap5==2025.4 \
|
||||||
|
--hash=sha256:51efa19c7d40e339774a6fe23407e83b95b7634cad6de70fd1f1093131bea1d9 \
|
||||||
|
--hash=sha256:d675ea7e245048905077dfe16bf1fa1ee16842f52fe88164ccc8a5e2d11119b3
|
||||||
|
# via livegraphsdjango
|
||||||
|
distlib==0.3.9 \
|
||||||
|
--hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \
|
||||||
|
--hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403
|
||||||
|
# via virtualenv
|
||||||
|
django==5.2.1 \
|
||||||
|
--hash=sha256:57fe1f1b59462caed092c80b3dd324fd92161b620d59a9ba9181c34746c97284 \
|
||||||
|
--hash=sha256:a9b680e84f9a0e71da83e399f1e922e1ab37b2173ced046b541c72e1589a5961
|
||||||
|
# via
|
||||||
|
# crispy-bootstrap5
|
||||||
|
# django-allauth
|
||||||
|
# django-crispy-forms
|
||||||
|
# django-debug-toolbar
|
||||||
|
# django-stubs
|
||||||
|
# django-stubs-ext
|
||||||
|
# livegraphsdjango
|
||||||
|
django-allauth==65.8.0 \
|
||||||
|
--hash=sha256:9da589d99d412740629333a01865a90c95c97e0fae0cde789aa45a8fda90e83b
|
||||||
|
# via livegraphsdjango
|
||||||
|
django-crispy-forms==2.4 \
|
||||||
|
--hash=sha256:5a4b99876cfb1bdd3e47727731b6d4197c51c0da502befbfbec6a93010b02030 \
|
||||||
|
--hash=sha256:915e1ffdeb2987d78b33fabfeff8e5203c8776aa910a3a659a2c514ca125f3bd
|
||||||
|
# via
|
||||||
|
# crispy-bootstrap5
|
||||||
|
# livegraphsdjango
|
||||||
|
django-debug-toolbar==5.2.0 \
|
||||||
|
--hash=sha256:15627f4c2836a9099d795e271e38e8cf5204ccd79d5dbcd748f8a6c284dcd195 \
|
||||||
|
--hash=sha256:9e7f0145e1a1b7d78fcc3b53798686170a5b472d9cf085d88121ff823e900821
|
||||||
|
django-stubs==5.2.0 \
|
||||||
|
--hash=sha256:07e25c2d3cbff5be540227ff37719cc89f215dfaaaa5eb038a75b01bbfbb2722 \
|
||||||
|
--hash=sha256:cd52da033489afc1357d6245f49e3cc57bf49015877253fb8efc6722ea3d2d2b
|
||||||
|
django-stubs-ext==5.2.0 \
|
||||||
|
--hash=sha256:00c4ae307b538f5643af761a914c3f8e4e3f25f4e7c6d7098f1906c0d8f2aac9 \
|
||||||
|
--hash=sha256:b27ae0aab970af4894ba4e9b3fcd3e03421dc8731516669659ee56122d148b23
|
||||||
|
# via django-stubs
|
||||||
|
filelock==3.18.0 \
|
||||||
|
--hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \
|
||||||
|
--hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de
|
||||||
|
# via virtualenv
|
||||||
|
gunicorn==23.0.0 \
|
||||||
|
--hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \
|
||||||
|
--hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec
|
||||||
|
# via livegraphsdjango
|
||||||
|
identify==2.6.10 \
|
||||||
|
--hash=sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8 \
|
||||||
|
--hash=sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25
|
||||||
|
# via pre-commit
|
||||||
|
iniconfig==2.1.0 \
|
||||||
|
--hash=sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7 \
|
||||||
|
--hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760
|
||||||
|
# via pytest
|
||||||
|
markdown-it-py==3.0.0 \
|
||||||
|
--hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \
|
||||||
|
--hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb
|
||||||
|
# via rich
|
||||||
|
mdurl==0.1.2 \
|
||||||
|
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
|
||||||
|
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
|
||||||
|
# via markdown-it-py
|
||||||
|
mypy==1.15.0 \
|
||||||
|
--hash=sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43 \
|
||||||
|
--hash=sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e \
|
||||||
|
--hash=sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d \
|
||||||
|
--hash=sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445 \
|
||||||
|
--hash=sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5 \
|
||||||
|
--hash=sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf \
|
||||||
|
--hash=sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357 \
|
||||||
|
--hash=sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036
|
||||||
|
mypy-extensions==1.1.0 \
|
||||||
|
--hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
|
||||||
|
--hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558
|
||||||
|
# via
|
||||||
|
# black
|
||||||
|
# mypy
|
||||||
|
narwhals==1.39.1 \
|
||||||
|
--hash=sha256:68d0f29c760f1a9419ada537f35f21ff202b0be1419e6d22135a0352c6d96deb \
|
||||||
|
--hash=sha256:cf15389e6f8c5321e8cd0ca8b5bace3b1aea5f5622fa59dfd64821998741d836
|
||||||
|
# via plotly
|
||||||
|
nodeenv==1.9.1 \
|
||||||
|
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||||
|
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
||||||
|
# via pre-commit
|
||||||
|
numpy==2.2.5 \
|
||||||
|
--hash=sha256:02f226baeefa68f7d579e213d0f3493496397d8f1cff5e2b222af274c86a552a \
|
||||||
|
--hash=sha256:059b51b658f4414fff78c6d7b1b4e18283ab5fa56d270ff212d5ba0c561846f4 \
|
||||||
|
--hash=sha256:1a161c2c79ab30fe4501d5a2bbfe8b162490757cf90b7f05be8b80bc02f7bb8e \
|
||||||
|
--hash=sha256:261a1ef047751bb02f29dfe337230b5882b54521ca121fc7f62668133cb119c9 \
|
||||||
|
--hash=sha256:2ba321813a00e508d5421104464510cc962a6f791aa2fca1c97b1e65027da80d \
|
||||||
|
--hash=sha256:352d330048c055ea6db701130abc48a21bec690a8d38f8284e00fab256dc1376 \
|
||||||
|
--hash=sha256:3d14b17b9be5f9c9301f43d2e2a4886a33b53f4e6fdf9ca2f4cc60aeeee76372 \
|
||||||
|
--hash=sha256:4520caa3807c1ceb005d125a75e715567806fed67e315cea619d5ec6e75a4191 \
|
||||||
|
--hash=sha256:47f9ed103af0bc63182609044b0490747e03bd20a67e391192dde119bf43d52f \
|
||||||
|
--hash=sha256:54088a5a147ab71a8e7fdfd8c3601972751ded0739c6b696ad9cb0343e21ab73 \
|
||||||
|
--hash=sha256:55f09e00d4dccd76b179c0f18a44f041e5332fd0e022886ba1c0bbf3ea4a18d0 \
|
||||||
|
--hash=sha256:8b4c0773b6ada798f51f0f8e30c054d32304ccc6e9c5d93d46cb26f3d385ab19 \
|
||||||
|
--hash=sha256:8dfa94b6a4374e7851bbb6f35e6ded2120b752b063e6acdd3157e4d2bb922eba \
|
||||||
|
--hash=sha256:97c8425d4e26437e65e1d189d22dff4a079b747ff9c2788057bfb8114ce1e133 \
|
||||||
|
--hash=sha256:a4cbdef3ddf777423060c6f81b5694bad2dc9675f110c4b2a60dc0181543fac7 \
|
||||||
|
--hash=sha256:a9c0d994680cd991b1cb772e8b297340085466a6fe964bc9d4e80f5e2f43c291 \
|
||||||
|
--hash=sha256:c26843fd58f65da9491165072da2cccc372530681de481ef670dcc8e27cfb066 \
|
||||||
|
--hash=sha256:c8b82a55ef86a2d8e81b63da85e55f5537d2157165be1cb2ce7cfa57b6aef38b \
|
||||||
|
--hash=sha256:d403c84991b5ad291d3809bace5e85f4bbf44a04bdc9a88ed2bb1807b3360bb8 \
|
||||||
|
--hash=sha256:d8882a829fd779f0f43998e931c466802a77ca1ee0fe25a3abe50278616b1471 \
|
||||||
|
--hash=sha256:e8b025c351b9f0e8b5436cf28a07fa4ac0204d67b38f01433ac7f9b870fa38c6
|
||||||
|
# via
|
||||||
|
# livegraphsdjango
|
||||||
|
# pandas
|
||||||
|
packaging==25.0 \
|
||||||
|
--hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \
|
||||||
|
--hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f
|
||||||
|
# via
|
||||||
|
# black
|
||||||
|
# gunicorn
|
||||||
|
# plotly
|
||||||
|
# pytest
|
||||||
|
pandas==2.2.3 \
|
||||||
|
--hash=sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d \
|
||||||
|
--hash=sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4 \
|
||||||
|
--hash=sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0 \
|
||||||
|
--hash=sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28 \
|
||||||
|
--hash=sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18 \
|
||||||
|
--hash=sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468 \
|
||||||
|
--hash=sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667 \
|
||||||
|
--hash=sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d \
|
||||||
|
--hash=sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb \
|
||||||
|
--hash=sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659 \
|
||||||
|
--hash=sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a \
|
||||||
|
--hash=sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2 \
|
||||||
|
--hash=sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015 \
|
||||||
|
--hash=sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24
|
||||||
|
# via livegraphsdjango
|
||||||
|
pathspec==0.12.1 \
|
||||||
|
--hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \
|
||||||
|
--hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712
|
||||||
|
# via black
|
||||||
|
pbr==6.1.1 \
|
||||||
|
--hash=sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76 \
|
||||||
|
--hash=sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b
|
||||||
|
# via stevedore
|
||||||
|
platformdirs==4.3.8 \
|
||||||
|
--hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \
|
||||||
|
--hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4
|
||||||
|
# via
|
||||||
|
# black
|
||||||
|
# virtualenv
|
||||||
|
plotly==6.1.0 \
|
||||||
|
--hash=sha256:a29d3ed523c9d7960095693af1ee52689830df0f9c6bae3e5e92c20c4f5684c3 \
|
||||||
|
--hash=sha256:f13f497ccc2d97f06f771a30b27fab0cbd220f2975865f4ecbc75057135521de
|
||||||
|
# via livegraphsdjango
|
||||||
|
pluggy==1.6.0 \
|
||||||
|
--hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
|
||||||
|
--hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
|
||||||
|
# via pytest
|
||||||
|
pre-commit==4.2.0 \
|
||||||
|
--hash=sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146 \
|
||||||
|
--hash=sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd
|
||||||
|
pygments==2.19.1 \
|
||||||
|
--hash=sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f \
|
||||||
|
--hash=sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c
|
||||||
|
# via rich
|
||||||
|
pytest==8.3.5 \
|
||||||
|
--hash=sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820 \
|
||||||
|
--hash=sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845
|
||||||
|
# via pytest-django
|
||||||
|
pytest-django==4.11.1 \
|
||||||
|
--hash=sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10 \
|
||||||
|
--hash=sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991
|
||||||
|
python-dateutil==2.9.0.post0 \
|
||||||
|
--hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
|
||||||
|
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
|
||||||
|
# via pandas
|
||||||
|
python-dotenv==1.1.0 \
|
||||||
|
--hash=sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5 \
|
||||||
|
--hash=sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d
|
||||||
|
# via livegraphsdjango
|
||||||
|
pytz==2025.2 \
|
||||||
|
--hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \
|
||||||
|
--hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00
|
||||||
|
# via pandas
|
||||||
|
pyyaml==6.0.2 \
|
||||||
|
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
|
||||||
|
--hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
|
||||||
|
--hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
|
||||||
|
--hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
|
||||||
|
--hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
|
||||||
|
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
|
||||||
|
--hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
|
||||||
|
--hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
|
||||||
|
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
|
||||||
|
--hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba
|
||||||
|
# via
|
||||||
|
# bandit
|
||||||
|
# pre-commit
|
||||||
|
rich==14.0.0 \
|
||||||
|
--hash=sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0 \
|
||||||
|
--hash=sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725
|
||||||
|
# via bandit
|
||||||
|
ruff==0.11.10 \
|
||||||
|
--hash=sha256:1067245bad978e7aa7b22f67113ecc6eb241dca0d9b696144256c3a879663bca \
|
||||||
|
--hash=sha256:2f071b0deed7e9245d5820dac235cbdd4ef99d7b12ff04c330a241ad3534319f \
|
||||||
|
--hash=sha256:3afead355f1d16d95630df28d4ba17fb2cb9c8dfac8d21ced14984121f639bad \
|
||||||
|
--hash=sha256:4a60e3a0a617eafba1f2e4186d827759d65348fa53708ca547e384db28406a0b \
|
||||||
|
--hash=sha256:5a94acf798a82db188f6f36575d80609072b032105d114b0f98661e1679c9125 \
|
||||||
|
--hash=sha256:5b6a9cc5b62c03cc1fea0044ed8576379dbaf751d5503d718c973d5418483641 \
|
||||||
|
--hash=sha256:5cc725fbb4d25b0f185cb42df07ab6b76c4489b4bfb740a175f3a59c70e8a224 \
|
||||||
|
--hash=sha256:607ecbb6f03e44c9e0a93aedacb17b4eb4f3563d00e8b474298a201622677947 \
|
||||||
|
--hash=sha256:7b3a522fa389402cd2137df9ddefe848f727250535c70dafa840badffb56b7a4 \
|
||||||
|
--hash=sha256:859a7bfa7bc8888abbea31ef8a2b411714e6a80f0d173c2a82f9041ed6b50f58 \
|
||||||
|
--hash=sha256:8b4564e9f99168c0f9195a0fd5fa5928004b33b377137f978055e40008a082c5 \
|
||||||
|
--hash=sha256:968220a57e09ea5e4fd48ed1c646419961a0570727c7e069842edd018ee8afed \
|
||||||
|
--hash=sha256:d522fb204b4959909ecac47da02830daec102eeb100fb50ea9554818d47a5fa6 \
|
||||||
|
--hash=sha256:da8ec977eaa4b7bf75470fb575bea2cb41a0e07c7ea9d5a0a97d13dbca697bf2 \
|
||||||
|
--hash=sha256:dc061a98d32a97211af7e7f3fa1d4ca2fcf919fb96c28f39551f35fc55bdbc19 \
|
||||||
|
--hash=sha256:ddf8967e08227d1bd95cc0851ef80d2ad9c7c0c5aab1eba31db49cf0a7b99523 \
|
||||||
|
--hash=sha256:ef69637b35fb8b210743926778d0e45e1bffa850a7c61e428c6b971549b5f5d1 \
|
||||||
|
--hash=sha256:f4854fd09c7aed5b1590e996a81aeff0c9ff51378b084eb5a0b9cd9518e6cff2
|
||||||
|
setuptools==80.7.1 \
|
||||||
|
--hash=sha256:ca5cc1069b85dc23070a6628e6bcecb3292acac802399c7f8edc0100619f9009 \
|
||||||
|
--hash=sha256:f6ffc5f0142b1bd8d0ca94ee91b30c0ca862ffd50826da1ea85258a06fd94552
|
||||||
|
# via pbr
|
||||||
|
six==1.17.0 \
|
||||||
|
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
|
||||||
|
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
|
||||||
|
# via python-dateutil
|
||||||
|
sqlparse==0.5.3 \
|
||||||
|
--hash=sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272 \
|
||||||
|
--hash=sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca
|
||||||
|
# via
|
||||||
|
# django
|
||||||
|
# django-debug-toolbar
|
||||||
|
stevedore==5.4.1 \
|
||||||
|
--hash=sha256:3135b5ae50fe12816ef291baff420acb727fcd356106e3e9cbfa9e5985cd6f4b \
|
||||||
|
--hash=sha256:d10a31c7b86cba16c1f6e8d15416955fc797052351a56af15e608ad20811fcfe
|
||||||
|
# via bandit
|
||||||
|
types-pyyaml==6.0.12.20250516 \
|
||||||
|
--hash=sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530 \
|
||||||
|
--hash=sha256:9f21a70216fc0fa1b216a8176db5f9e0af6eb35d2f2932acb87689d03a5bf6ba
|
||||||
|
# via django-stubs
|
||||||
|
typing-extensions==4.13.2 \
|
||||||
|
--hash=sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c \
|
||||||
|
--hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef
|
||||||
|
# via
|
||||||
|
# django-stubs
|
||||||
|
# django-stubs-ext
|
||||||
|
# mypy
|
||||||
|
tzdata==2025.2 \
|
||||||
|
--hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \
|
||||||
|
--hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9
|
||||||
|
# via
|
||||||
|
# django
|
||||||
|
# pandas
|
||||||
|
virtualenv==20.31.2 \
|
||||||
|
--hash=sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11 \
|
||||||
|
--hash=sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af
|
||||||
|
# via pre-commit
|
||||||
|
whitenoise==6.9.0 \
|
||||||
|
--hash=sha256:8c4a7c9d384694990c26f3047e118c691557481d624f069b7f7752a2f735d609 \
|
||||||
|
--hash=sha256:c8a489049b7ee9889617bb4c274a153f3d979e8f51d2efd0f5b403caf41c57df
|
||||||
|
# via livegraphsdjango
|
||||||
19
setup.py
19
setup.py
@ -1,19 +0,0 @@
|
|||||||
from setuptools import setup
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name="livegraphsdjango",
|
|
||||||
version="0.1.0",
|
|
||||||
packages=["dashboard_project"],
|
|
||||||
entry_points={
|
|
||||||
"console_scripts": [
|
|
||||||
"manage=dashboard_project.manage:main",
|
|
||||||
"runserver=dashboard_project.__main__:main",
|
|
||||||
"migrate=dashboard_project.__main__:main",
|
|
||||||
"makemigrations=dashboard_project.__main__:main",
|
|
||||||
"collectstatic=dashboard_project.__main__:main",
|
|
||||||
"createsuperuser=dashboard_project.__main__:main",
|
|
||||||
"shell=dashboard_project.__main__:main",
|
|
||||||
"test=dashboard_project.__main__:main",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
459
uv.lock
generated
459
uv.lock
generated
@ -11,6 +11,100 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828, upload-time = "2024-03-22T14:39:34.521Z" },
|
{ url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828, upload-time = "2024-03-22T14:39:34.521Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bandit"
|
||||||
|
version = "1.8.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
{ name = "rich" },
|
||||||
|
{ name = "stevedore" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/1a/a5/144a45f8e67df9d66c3bc3f7e69a39537db8bff1189ab7cff4e9459215da/bandit-1.8.3.tar.gz", hash = "sha256:f5847beb654d309422985c36644649924e0ea4425c76dec2e89110b87506193a", size = 4232005, upload-time = "2025-02-17T05:24:57.031Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/85/db74b9233e0aa27ec96891045c5e920a64dd5cbccd50f8e64e9460f48d35/bandit-1.8.3-py3-none-any.whl", hash = "sha256:28f04dc0d258e1dd0f99dee8eefa13d1cb5e3fde1a5ab0c523971f97b289bcd8", size = 129078, upload-time = "2025-02-17T05:24:54.068Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "black"
|
||||||
|
version = "25.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "mypy-extensions" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pathspec" },
|
||||||
|
{ name = "platformdirs" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfgv"
|
||||||
|
version = "3.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/cd/0f/62ca20172d4f87d93cf89665fbaedcd560ac48b465bd1d92bfc7ea6b0a41/click-8.2.0.tar.gz", hash = "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d", size = 235857, upload-time = "2025-05-10T22:21:03.111Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/58/1f37bf81e3c689cc74ffa42102fa8915b59085f54a6e4a80bc6265c0f6bf/click-8.2.0-py3-none-any.whl", hash = "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c", size = 102156, upload-time = "2025-05-10T22:21:01.352Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "coverage"
|
||||||
|
version = "7.8.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872, upload-time = "2025-03-30T20:36:45.376Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708, upload-time = "2025-03-30T20:35:47.417Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981, upload-time = "2025-03-30T20:35:49.002Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495, upload-time = "2025-03-30T20:35:51.073Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538, upload-time = "2025-03-30T20:35:52.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561, upload-time = "2025-03-30T20:35:54.658Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633, upload-time = "2025-03-30T20:35:56.221Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712, upload-time = "2025-03-30T20:35:57.801Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000, upload-time = "2025-03-30T20:35:59.378Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195, upload-time = "2025-03-30T20:36:01.005Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998, upload-time = "2025-03-30T20:36:03.006Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541, upload-time = "2025-03-30T20:36:04.638Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767, upload-time = "2025-03-30T20:36:06.503Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997, upload-time = "2025-03-30T20:36:08.137Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708, upload-time = "2025-03-30T20:36:09.781Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046, upload-time = "2025-03-30T20:36:11.409Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139, upload-time = "2025-03-30T20:36:13.86Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307, upload-time = "2025-03-30T20:36:16.074Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116, upload-time = "2025-03-30T20:36:18.033Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909, upload-time = "2025-03-30T20:36:19.644Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068, upload-time = "2025-03-30T20:36:21.282Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435, upload-time = "2025-03-30T20:36:43.61Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crispy-bootstrap5"
|
name = "crispy-bootstrap5"
|
||||||
version = "2025.4"
|
version = "2025.4"
|
||||||
@ -24,6 +118,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b8/9a/4f1166cc82c9f777cf9a5bc2a75171d63301ac317c5de8f59bd44bfe2b7a/crispy_bootstrap5-2025.4-py3-none-any.whl", hash = "sha256:51efa19c7d40e339774a6fe23407e83b95b7634cad6de70fd1f1093131bea1d9", size = 24772, upload-time = "2025-04-02T12:33:14.904Z" },
|
{ url = "https://files.pythonhosted.org/packages/b8/9a/4f1166cc82c9f777cf9a5bc2a75171d63301ac317c5de8f59bd44bfe2b7a/crispy_bootstrap5-2025.4-py3-none-any.whl", hash = "sha256:51efa19c7d40e339774a6fe23407e83b95b7634cad6de70fd1f1093131bea1d9", size = 24772, upload-time = "2025-04-02T12:33:14.904Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "distlib"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django"
|
name = "django"
|
||||||
version = "5.2.1"
|
version = "5.2.1"
|
||||||
@ -60,6 +163,57 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/1d/ec/a25f81e56a674e63cf6c3dd8e36b1b3fecc238fecd6098504adc0cc61402/django_crispy_forms-2.4-py3-none-any.whl", hash = "sha256:5a4b99876cfb1bdd3e47727731b6d4197c51c0da502befbfbec6a93010b02030", size = 31446, upload-time = "2025-04-13T07:24:58.516Z" },
|
{ url = "https://files.pythonhosted.org/packages/1d/ec/a25f81e56a674e63cf6c3dd8e36b1b3fecc238fecd6098504adc0cc61402/django_crispy_forms-2.4-py3-none-any.whl", hash = "sha256:5a4b99876cfb1bdd3e47727731b6d4197c51c0da502befbfbec6a93010b02030", size = 31446, upload-time = "2025-04-13T07:24:58.516Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-debug-toolbar"
|
||||||
|
version = "5.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "django" },
|
||||||
|
{ name = "sqlparse" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2a/9f/97ba2648f66fa208fc7f19d6895586d08bc5f0ab930a1f41032e60f31a41/django_debug_toolbar-5.2.0.tar.gz", hash = "sha256:9e7f0145e1a1b7d78fcc3b53798686170a5b472d9cf085d88121ff823e900821", size = 297901, upload-time = "2025-04-29T05:23:57.533Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/c2/ed3cb815002664349e9e50799b8c00ef15941f4cad797247cadbdeebab02/django_debug_toolbar-5.2.0-py3-none-any.whl", hash = "sha256:15627f4c2836a9099d795e271e38e8cf5204ccd79d5dbcd748f8a6c284dcd195", size = 262834, upload-time = "2025-04-29T05:23:55.472Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-stubs"
|
||||||
|
version = "5.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "asgiref" },
|
||||||
|
{ name = "django" },
|
||||||
|
{ name = "django-stubs-ext" },
|
||||||
|
{ name = "types-pyyaml" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/87/79/7e7ad8b4bac545c8e608fa8db0bd061977f93035a112be78a7f3ffc6ff66/django_stubs-5.2.0.tar.gz", hash = "sha256:07e25c2d3cbff5be540227ff37719cc89f215dfaaaa5eb038a75b01bbfbb2722", size = 276297, upload-time = "2025-04-26T10:49:04.974Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/01/5913ba5514337f3896c7bcbff6075808184dd303cd0fc3ecc289ec7e0c96/django_stubs-5.2.0-py3-none-any.whl", hash = "sha256:cd52da033489afc1357d6245f49e3cc57bf49015877253fb8efc6722ea3d2d2b", size = 481836, upload-time = "2025-04-26T10:49:02.97Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-stubs-ext"
|
||||||
|
version = "5.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "django" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7c/7a/84338605817960942c1ea9d852685923ccccd0d91ba0d49532605973491f/django_stubs_ext-5.2.0.tar.gz", hash = "sha256:00c4ae307b538f5643af761a914c3f8e4e3f25f4e7c6d7098f1906c0d8f2aac9", size = 9618, upload-time = "2025-04-26T10:48:38.05Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/65/9f5ca467d84a67c0c547f10b0ece9fd9c26c5efc818a01bf6a3d306c2a0c/django_stubs_ext-5.2.0-py3-none-any.whl", hash = "sha256:b27ae0aab970af4894ba4e9b3fcd3e03421dc8731516669659ee56122d148b23", size = 9066, upload-time = "2025-04-26T10:48:36.032Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filelock"
|
||||||
|
version = "3.18.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gunicorn"
|
name = "gunicorn"
|
||||||
version = "23.0.0"
|
version = "23.0.0"
|
||||||
@ -72,10 +226,28 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
|
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "identify"
|
||||||
|
version = "2.6.10"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/0c/83/b6ea0334e2e7327084a46aaaf71f2146fc061a192d6518c0d020120cd0aa/identify-2.6.10.tar.gz", hash = "sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8", size = 99201, upload-time = "2025-04-19T15:10:38.32Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl", hash = "sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25", size = 99101, upload-time = "2025-04-19T15:10:36.701Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "livegraphsdjango"
|
name = "livegraphsdjango"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "crispy-bootstrap5" },
|
{ name = "crispy-bootstrap5" },
|
||||||
{ name = "django" },
|
{ name = "django" },
|
||||||
@ -89,6 +261,20 @@ dependencies = [
|
|||||||
{ name = "whitenoise" },
|
{ name = "whitenoise" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "bandit" },
|
||||||
|
{ name = "black" },
|
||||||
|
{ name = "coverage" },
|
||||||
|
{ name = "django-debug-toolbar" },
|
||||||
|
{ name = "django-stubs" },
|
||||||
|
{ name = "mypy" },
|
||||||
|
{ name = "pre-commit" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-django" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "crispy-bootstrap5", specifier = ">=2025.4" },
|
{ name = "crispy-bootstrap5", specifier = ">=2025.4" },
|
||||||
@ -103,6 +289,69 @@ requires-dist = [
|
|||||||
{ name = "whitenoise", specifier = ">=6.9.0" },
|
{ name = "whitenoise", specifier = ">=6.9.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [
|
||||||
|
{ name = "bandit", specifier = ">=1.8.3" },
|
||||||
|
{ name = "black", specifier = ">=25.1.0" },
|
||||||
|
{ name = "coverage", specifier = ">=7.8.0" },
|
||||||
|
{ name = "django-debug-toolbar", specifier = ">=5.2.0" },
|
||||||
|
{ name = "django-stubs", specifier = ">=5.2.0" },
|
||||||
|
{ name = "mypy", specifier = ">=1.15.0" },
|
||||||
|
{ name = "pre-commit", specifier = ">=4.2.0" },
|
||||||
|
{ name = "pytest", specifier = ">=8.3.5" },
|
||||||
|
{ name = "pytest-django", specifier = ">=4.11.1" },
|
||||||
|
{ name = "ruff", specifier = ">=0.11.10" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markdown-it-py"
|
||||||
|
version = "3.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "mdurl" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mdurl"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "mypy-extensions" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717, upload-time = "2025-02-05T03:50:34.655Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592, upload-time = "2025-02-05T03:48:55.789Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611, upload-time = "2025-02-05T03:48:44.581Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443, upload-time = "2025-02-05T03:49:25.514Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541, upload-time = "2025-02-05T03:49:57.623Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348, upload-time = "2025-02-05T03:48:52.361Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648, upload-time = "2025-02-05T03:49:11.395Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777, upload-time = "2025-02-05T03:50:08.348Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-extensions"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "narwhals"
|
name = "narwhals"
|
||||||
version = "1.39.1"
|
version = "1.39.1"
|
||||||
@ -112,6 +361,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/70/c4/b83520ecc27840a4d58bd585ae0ec0f8a4f2b0c5a965b66749254a54de0e/narwhals-1.39.1-py3-none-any.whl", hash = "sha256:68d0f29c760f1a9419ada537f35f21ff202b0be1419e6d22135a0352c6d96deb", size = 355009, upload-time = "2025-05-15T17:45:07.954Z" },
|
{ url = "https://files.pythonhosted.org/packages/70/c4/b83520ecc27840a4d58bd585ae0ec0f8a4f2b0c5a965b66749254a54de0e/narwhals-1.39.1-py3-none-any.whl", hash = "sha256:68d0f29c760f1a9419ada537f35f21ff202b0be1419e6d22135a0352c6d96deb", size = 355009, upload-time = "2025-05-15T17:45:07.954Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nodeenv"
|
||||||
|
version = "1.9.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "numpy"
|
name = "numpy"
|
||||||
version = "2.2.5"
|
version = "2.2.5"
|
||||||
@ -176,6 +434,36 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436, upload-time = "2024-09-20T13:09:48.112Z" },
|
{ url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436, upload-time = "2024-09-20T13:09:48.112Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathspec"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pbr"
|
||||||
|
version = "6.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "setuptools" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/01/d2/510cc0d218e753ba62a1bc1434651db3cd797a9716a0a66cc714cb4f0935/pbr-6.1.1.tar.gz", hash = "sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b", size = 125702, upload-time = "2025-02-04T14:28:06.514Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/ac/684d71315abc7b1214d59304e23a982472967f6bf4bde5a98f1503f648dc/pbr-6.1.1-py2.py3-none-any.whl", hash = "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76", size = 108997, upload-time = "2025-02-04T14:28:03.168Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "4.3.8"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "plotly"
|
name = "plotly"
|
||||||
version = "6.1.0"
|
version = "6.1.0"
|
||||||
@ -189,6 +477,67 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ee/11/83ae52318353f9da4a88cc23e7f9dbc3d449b3f0fd6158fba15eb3c3b816/plotly-6.1.0-py3-none-any.whl", hash = "sha256:a29d3ed523c9d7960095693af1ee52689830df0f9c6bae3e5e92c20c4f5684c3", size = 16118476, upload-time = "2025-05-15T16:04:30.81Z" },
|
{ url = "https://files.pythonhosted.org/packages/ee/11/83ae52318353f9da4a88cc23e7f9dbc3d449b3f0fd6158fba15eb3c3b816/plotly-6.1.0-py3-none-any.whl", hash = "sha256:a29d3ed523c9d7960095693af1ee52689830df0f9c6bae3e5e92c20c4f5684c3", size = 16118476, upload-time = "2025-05-15T16:04:30.81Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pre-commit"
|
||||||
|
version = "4.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cfgv" },
|
||||||
|
{ name = "identify" },
|
||||||
|
{ name = "nodeenv" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
{ name = "virtualenv" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.19.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "8.3.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-django"
|
||||||
|
version = "4.11.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b1/fb/55d580352db26eb3d59ad50c64321ddfe228d3d8ac107db05387a2fadf3a/pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991", size = 86202, upload-time = "2025-04-03T18:56:09.338Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/ac/bd0608d229ec808e51a21044f3f2f27b9a37e7a0ebaca7247882e67876af/pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10", size = 25281, upload-time = "2025-04-03T18:56:07.678Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dateutil"
|
name = "python-dateutil"
|
||||||
version = "2.9.0.post0"
|
version = "2.9.0.post0"
|
||||||
@ -219,6 +568,70 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
|
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rich"
|
||||||
|
version = "14.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markdown-it-py" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff"
|
||||||
|
version = "0.11.10"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e8/4c/4a3c5a97faaae6b428b336dcca81d03ad04779f8072c267ad2bd860126bf/ruff-0.11.10.tar.gz", hash = "sha256:d522fb204b4959909ecac47da02830daec102eeb100fb50ea9554818d47a5fa6", size = 4165632, upload-time = "2025-05-15T14:08:56.76Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/9f/596c628f8824a2ce4cd12b0f0b4c0629a62dfffc5d0f742c19a1d71be108/ruff-0.11.10-py3-none-linux_armv6l.whl", hash = "sha256:859a7bfa7bc8888abbea31ef8a2b411714e6a80f0d173c2a82f9041ed6b50f58", size = 10316243, upload-time = "2025-05-15T14:08:12.884Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/38/c1e0b77ab58b426f8c332c1d1d3432d9fc9a9ea622806e208220cb133c9e/ruff-0.11.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:968220a57e09ea5e4fd48ed1c646419961a0570727c7e069842edd018ee8afed", size = 11083636, upload-time = "2025-05-15T14:08:16.551Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/41/b75e15961d6047d7fe1b13886e56e8413be8467a4e1be0a07f3b303cd65a/ruff-0.11.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1067245bad978e7aa7b22f67113ecc6eb241dca0d9b696144256c3a879663bca", size = 10441624, upload-time = "2025-05-15T14:08:19.032Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/2c/e396b6703f131406db1811ea3d746f29d91b41bbd43ad572fea30da1435d/ruff-0.11.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4854fd09c7aed5b1590e996a81aeff0c9ff51378b084eb5a0b9cd9518e6cff2", size = 10624358, upload-time = "2025-05-15T14:08:21.542Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/8c/ee6cca8bdaf0f9a3704796022851a33cd37d1340bceaf4f6e991eb164e2e/ruff-0.11.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b4564e9f99168c0f9195a0fd5fa5928004b33b377137f978055e40008a082c5", size = 10176850, upload-time = "2025-05-15T14:08:23.682Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/ce/4e27e131a434321b3b7c66512c3ee7505b446eb1c8a80777c023f7e876e6/ruff-0.11.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b6a9cc5b62c03cc1fea0044ed8576379dbaf751d5503d718c973d5418483641", size = 11759787, upload-time = "2025-05-15T14:08:25.733Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/de/1e2e77fc72adc7cf5b5123fd04a59ed329651d3eab9825674a9e640b100b/ruff-0.11.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:607ecbb6f03e44c9e0a93aedacb17b4eb4f3563d00e8b474298a201622677947", size = 12430479, upload-time = "2025-05-15T14:08:28.013Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/ed/af0f2340f33b70d50121628ef175523cc4c37619e98d98748c85764c8d88/ruff-0.11.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b3a522fa389402cd2137df9ddefe848f727250535c70dafa840badffb56b7a4", size = 11919760, upload-time = "2025-05-15T14:08:30.956Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/09/d7b3d3226d535cb89234390f418d10e00a157b6c4a06dfbe723e9322cb7d/ruff-0.11.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f071b0deed7e9245d5820dac235cbdd4ef99d7b12ff04c330a241ad3534319f", size = 14041747, upload-time = "2025-05-15T14:08:33.297Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/b3/a63b4e91850e3f47f78795e6630ee9266cb6963de8f0191600289c2bb8f4/ruff-0.11.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a60e3a0a617eafba1f2e4186d827759d65348fa53708ca547e384db28406a0b", size = 11550657, upload-time = "2025-05-15T14:08:35.639Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/63/a4f95c241d79402ccdbdb1d823d156c89fbb36ebfc4289dce092e6c0aa8f/ruff-0.11.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:da8ec977eaa4b7bf75470fb575bea2cb41a0e07c7ea9d5a0a97d13dbca697bf2", size = 10489671, upload-time = "2025-05-15T14:08:38.437Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/9b/c2238bfebf1e473495659c523d50b1685258b6345d5ab0b418ca3f010cd7/ruff-0.11.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ddf8967e08227d1bd95cc0851ef80d2ad9c7c0c5aab1eba31db49cf0a7b99523", size = 10160135, upload-time = "2025-05-15T14:08:41.247Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/ef/ba7251dd15206688dbfba7d413c0312e94df3b31b08f5d695580b755a899/ruff-0.11.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5a94acf798a82db188f6f36575d80609072b032105d114b0f98661e1679c9125", size = 11170179, upload-time = "2025-05-15T14:08:43.762Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/9f/5c336717293203ba275dbfa2ea16e49b29a9fd9a0ea8b6febfc17e133577/ruff-0.11.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3afead355f1d16d95630df28d4ba17fb2cb9c8dfac8d21ced14984121f639bad", size = 11626021, upload-time = "2025-05-15T14:08:46.451Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/2b/162fa86d2639076667c9aa59196c020dc6d7023ac8f342416c2f5ec4bda0/ruff-0.11.10-py3-none-win32.whl", hash = "sha256:dc061a98d32a97211af7e7f3fa1d4ca2fcf919fb96c28f39551f35fc55bdbc19", size = 10494958, upload-time = "2025-05-15T14:08:49.601Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/f3/66643d8f32f50a4b0d09a4832b7d919145ee2b944d43e604fbd7c144d175/ruff-0.11.10-py3-none-win_amd64.whl", hash = "sha256:5cc725fbb4d25b0f185cb42df07ab6b76c4489b4bfb740a175f3a59c70e8a224", size = 11650285, upload-time = "2025-05-15T14:08:52.392Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/3a/2e8704d19f376c799748ff9cb041225c1d59f3e7711bc5596c8cfdc24925/ruff-0.11.10-py3-none-win_arm64.whl", hash = "sha256:ef69637b35fb8b210743926778d0e45e1bffa850a7c61e428c6b971549b5f5d1", size = 10765278, upload-time = "2025-05-15T14:08:54.56Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "setuptools"
|
||||||
|
version = "80.7.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9e/8b/dc1773e8e5d07fd27c1632c45c1de856ac3dbf09c0147f782ca6d990cf15/setuptools-80.7.1.tar.gz", hash = "sha256:f6ffc5f0142b1bd8d0ca94ee91b30c0ca862ffd50826da1ea85258a06fd94552", size = 1319188, upload-time = "2025-05-15T02:41:00.955Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/18/0e835c3a557dc5faffc8f91092f62fc337c1dab1066715842e7a4b318ec4/setuptools-80.7.1-py3-none-any.whl", hash = "sha256:ca5cc1069b85dc23070a6628e6bcecb3292acac802399c7f8edc0100619f9009", size = 1200776, upload-time = "2025-05-15T02:40:58.887Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "six"
|
name = "six"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
@ -237,6 +650,36 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" },
|
{ url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stevedore"
|
||||||
|
version = "5.4.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pbr" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/28/3f/13cacea96900bbd31bb05c6b74135f85d15564fc583802be56976c940470/stevedore-5.4.1.tar.gz", hash = "sha256:3135b5ae50fe12816ef291baff420acb727fcd356106e3e9cbfa9e5985cd6f4b", size = 513858, upload-time = "2025-02-20T14:03:57.285Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/45/8c4ebc0c460e6ec38e62ab245ad3c7fc10b210116cea7c16d61602aa9558/stevedore-5.4.1-py3-none-any.whl", hash = "sha256:d10a31c7b86cba16c1f6e8d15416955fc797052351a56af15e608ad20811fcfe", size = 49533, upload-time = "2025-02-20T14:03:55.849Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-pyyaml"
|
||||||
|
version = "6.0.12.20250516"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/4e/22/59e2aeb48ceeee1f7cd4537db9568df80d62bdb44a7f9e743502ea8aab9c/types_pyyaml-6.0.12.20250516.tar.gz", hash = "sha256:9f21a70216fc0fa1b216a8176db5f9e0af6eb35d2f2932acb87689d03a5bf6ba", size = 17378, upload-time = "2025-05-16T03:08:04.897Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/5f/e0af6f7f6a260d9af67e1db4f54d732abad514252a7a378a6c4d17dd1036/types_pyyaml-6.0.12.20250516-py3-none-any.whl", hash = "sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530", size = 20312, upload-time = "2025-05-16T03:08:04.019Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.13.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tzdata"
|
name = "tzdata"
|
||||||
version = "2025.2"
|
version = "2025.2"
|
||||||
@ -246,6 +689,20 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
|
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "virtualenv"
|
||||||
|
version = "20.31.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "distlib" },
|
||||||
|
{ name = "filelock" },
|
||||||
|
{ name = "platformdirs" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "whitenoise"
|
name = "whitenoise"
|
||||||
version = "6.9.0"
|
version = "6.9.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user