mirror of
https://github.com/kjanat/livegraphs-django.git
synced 2026-02-13 14:15:42 +01:00
feat: Add uv Docker, Postgres, and company linking
Introduces uv-based Docker workflow with non-root runtime, cached installs, and uv-run for web and Celery. Updates docker-compose to Postgres + Redis, loads .env, and removes source bind mount for reproducible builds. Switches settings to use Postgres when env is present with SQLite fallback; broadens allowed hosts for containerized development. Adds psycopg2-binary and updates sample env for Redis in Docker. Adds company scoping to external data models and links sessions during ingestion; provides management commands to seed a Jumbo company/users and sync external chat data into the dashboard. Includes .dockerignore, TypeScript config and typings, and minor template/docs tweaks. Requires database migration.
This commit is contained in:
@@ -27,7 +27,21 @@ SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", get_random_secret_key())
|
||||
|
||||
DEBUG = os.environ.get("DJANGO_DEBUG", "True") == "True"
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
# Allow localhost, Docker IPs, and entire private network ranges
|
||||
ALLOWED_HOSTS = [
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
"0.0.0.0", # nosec B104
|
||||
".localhost",
|
||||
# Allow all 192.168.x.x addresses (private network)
|
||||
"192.168.*.*",
|
||||
# Allow all 10.x.x.x addresses (Docker default)
|
||||
"10.*.*.*",
|
||||
# Allow all 172.16-31.x.x addresses (Docker)
|
||||
"172.*.*.*",
|
||||
# Wildcard for any other IPs (development only)
|
||||
"*",
|
||||
]
|
||||
|
||||
# Application definition
|
||||
|
||||
@@ -85,13 +99,28 @@ TEMPLATES = [
|
||||
WSGI_APPLICATION = "dashboard_project.wsgi.application"
|
||||
|
||||
# Database
|
||||
# Use PostgreSQL when DATABASE_URL is set (Docker), otherwise SQLite (local dev)
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
if os.environ.get("DATABASE_URL"):
|
||||
# PostgreSQL configuration for Docker
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": os.environ.get("POSTGRES_DB", "dashboard_db"),
|
||||
"USER": os.environ.get("POSTGRES_USER", "postgres"),
|
||||
"PASSWORD": os.environ.get("POSTGRES_PASSWORD", "postgres"),
|
||||
"HOST": os.environ.get("POSTGRES_HOST", "db"),
|
||||
"PORT": os.environ.get("POSTGRES_PORT", "5432"),
|
||||
}
|
||||
}
|
||||
else:
|
||||
# SQLite configuration for local development
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Password validation
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
Management command to set up Jumbo company, users, and link existing data.
|
||||
"""
|
||||
|
||||
from accounts.models import Company, CustomUser
|
||||
from data_integration.models import ChatSession, ExternalDataSource
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Set up Jumbo company, create users, and link existing external data"
|
||||
|
||||
def handle(self, *_args, **_options):
|
||||
self.stdout.write("Setting up Jumbo company and data...")
|
||||
|
||||
# 1. Create Jumbo company
|
||||
jumbo_company, created = Company.objects.get_or_create(
|
||||
name="Jumbo", defaults={"description": "Jumbo Supermarkets - External API Data"}
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(self.style.SUCCESS("✓ Created Jumbo company"))
|
||||
else:
|
||||
self.stdout.write(" Jumbo company already exists")
|
||||
|
||||
# 2. Create admin user for Jumbo
|
||||
admin_created = False
|
||||
if not CustomUser.objects.filter(username="jumbo_admin").exists():
|
||||
CustomUser.objects.create_user( # nosec B106
|
||||
username="jumbo_admin",
|
||||
email="admin@jumbo.nl",
|
||||
password="jumbo123",
|
||||
company=jumbo_company,
|
||||
is_company_admin=True,
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS("✓ Created Jumbo admin: jumbo_admin / jumbo123"))
|
||||
admin_created = True
|
||||
else:
|
||||
self.stdout.write(" Jumbo admin already exists")
|
||||
|
||||
# 3. Create regular users for Jumbo
|
||||
jumbo_users = [
|
||||
{
|
||||
"username": "jumbo_analyst",
|
||||
"email": "analyst@jumbo.nl",
|
||||
"password": "jumbo123",
|
||||
"is_company_admin": False,
|
||||
},
|
||||
{
|
||||
"username": "jumbo_manager",
|
||||
"email": "manager@jumbo.nl",
|
||||
"password": "jumbo123",
|
||||
"is_company_admin": False,
|
||||
},
|
||||
]
|
||||
|
||||
users_created = 0
|
||||
for user_data in jumbo_users:
|
||||
if not CustomUser.objects.filter(username=user_data["username"]).exists():
|
||||
CustomUser.objects.create_user(
|
||||
username=user_data["username"],
|
||||
email=user_data["email"],
|
||||
password=user_data["password"],
|
||||
company=jumbo_company,
|
||||
is_company_admin=user_data["is_company_admin"],
|
||||
)
|
||||
users_created += 1
|
||||
|
||||
if users_created:
|
||||
self.stdout.write(self.style.SUCCESS(f"✓ Created {users_created} Jumbo users"))
|
||||
else:
|
||||
self.stdout.write(" Jumbo users already exist")
|
||||
|
||||
# 4. Link External Data Source to Jumbo company
|
||||
try:
|
||||
jumbo_ext_source = ExternalDataSource.objects.get(name="Jumbo API")
|
||||
if not jumbo_ext_source.company:
|
||||
jumbo_ext_source.company = jumbo_company
|
||||
jumbo_ext_source.save()
|
||||
self.stdout.write(self.style.SUCCESS("✓ Linked Jumbo API data source to company"))
|
||||
else:
|
||||
self.stdout.write(" Jumbo API data source already linked")
|
||||
except ExternalDataSource.DoesNotExist:
|
||||
self.stdout.write(
|
||||
self.style.WARNING("⚠ Jumbo API external data source not found. Create it in admin first.")
|
||||
)
|
||||
|
||||
# 5. Link existing chat sessions to Jumbo company
|
||||
unlinked_sessions = ChatSession.objects.filter(company__isnull=True)
|
||||
if unlinked_sessions.exists():
|
||||
count = unlinked_sessions.update(company=jumbo_company)
|
||||
self.stdout.write(self.style.SUCCESS(f"✓ Linked {count} existing chat sessions to Jumbo company"))
|
||||
else:
|
||||
self.stdout.write(" All chat sessions already linked to companies")
|
||||
|
||||
# 6. Summary
|
||||
total_sessions = ChatSession.objects.filter(company=jumbo_company).count()
|
||||
total_users = CustomUser.objects.filter(company=jumbo_company).count()
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"\n✓ Setup complete!"
|
||||
f"\n Company: {jumbo_company.name}"
|
||||
f"\n Users: {total_users} (including {1 if admin_created or CustomUser.objects.filter(username='jumbo_admin').exists() else 0} admin)"
|
||||
f"\n Chat sessions: {total_sessions}"
|
||||
)
|
||||
)
|
||||
self.stdout.write("\nLogin as jumbo_admin/jumbo123 to view the dashboard with Jumbo data.")
|
||||
@@ -0,0 +1,106 @@
|
||||
"""
|
||||
Management command to sync Jumbo API data to dashboard app with proper company linking.
|
||||
"""
|
||||
|
||||
from accounts.models import Company, CustomUser
|
||||
from dashboard.models import ChatSession, DataSource
|
||||
from data_integration.models import ChatSession as ExtChatSession
|
||||
from data_integration.models import ExternalDataSource
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Sync Jumbo API data to dashboard app with company linking"
|
||||
|
||||
def handle(self, *_args, **_options):
|
||||
self.stdout.write("Starting Jumbo data sync to dashboard...")
|
||||
|
||||
# 1. Get or create Jumbo company
|
||||
jumbo_company, created = Company.objects.get_or_create(
|
||||
name="Jumbo", defaults={"description": "Jumbo Supermarkets - External API Data"}
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(self.style.SUCCESS("✓ Created Jumbo company"))
|
||||
else:
|
||||
self.stdout.write(" Jumbo company already exists")
|
||||
|
||||
# 2. Get Jumbo external data source
|
||||
try:
|
||||
jumbo_ext_source = ExternalDataSource.objects.get(name="Jumbo API")
|
||||
except ExternalDataSource.DoesNotExist:
|
||||
self.stdout.write(
|
||||
self.style.ERROR("✗ Jumbo API external data source not found. Please create it in admin first.")
|
||||
)
|
||||
return
|
||||
|
||||
# 3. Get or create DataSource linked to Jumbo company
|
||||
jumbo_datasource, created = DataSource.objects.get_or_create(
|
||||
name="Jumbo API Data",
|
||||
company=jumbo_company,
|
||||
defaults={
|
||||
"description": "Chat sessions from Jumbo external API",
|
||||
"external_source": jumbo_ext_source,
|
||||
},
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(self.style.SUCCESS("✓ Created Jumbo DataSource"))
|
||||
else:
|
||||
self.stdout.write(" Jumbo DataSource already exists")
|
||||
|
||||
# 4. Sync chat sessions from data_integration to dashboard
|
||||
ext_sessions = ExtChatSession.objects.all()
|
||||
synced_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
for ext_session in ext_sessions:
|
||||
# Check if already synced
|
||||
if ChatSession.objects.filter(data_source=jumbo_datasource, session_id=ext_session.session_id).exists():
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Create dashboard ChatSession
|
||||
ChatSession.objects.create(
|
||||
data_source=jumbo_datasource,
|
||||
session_id=ext_session.session_id,
|
||||
start_time=ext_session.start_time,
|
||||
end_time=ext_session.end_time,
|
||||
ip_address=ext_session.ip_address,
|
||||
country=ext_session.country or "",
|
||||
language=ext_session.language or "",
|
||||
messages_sent=ext_session.messages_sent or 0,
|
||||
sentiment=ext_session.sentiment or "",
|
||||
escalated=ext_session.escalated or False,
|
||||
forwarded_hr=ext_session.forwarded_hr or False,
|
||||
full_transcript=ext_session.full_transcript_url or "",
|
||||
avg_response_time=ext_session.avg_response_time,
|
||||
tokens=ext_session.tokens or 0,
|
||||
tokens_eur=ext_session.tokens_eur,
|
||||
category=ext_session.category or "",
|
||||
initial_msg=ext_session.initial_msg or "",
|
||||
user_rating=str(ext_session.user_rating) if ext_session.user_rating else "",
|
||||
)
|
||||
synced_count += 1
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"✓ Synced {synced_count} chat sessions (skipped {skipped_count} existing)")
|
||||
)
|
||||
|
||||
# 5. Create admin user for Jumbo company if needed
|
||||
if not CustomUser.objects.filter(company=jumbo_company, is_company_admin=True).exists():
|
||||
CustomUser.objects.create_user( # nosec B106
|
||||
username="jumbo_admin",
|
||||
email="admin@jumbo.nl",
|
||||
password="jumbo123",
|
||||
company=jumbo_company,
|
||||
is_company_admin=True,
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS("✓ Created Jumbo admin user: jumbo_admin / jumbo123"))
|
||||
else:
|
||||
self.stdout.write(" Jumbo admin user already exists")
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"\n✓ Sync complete! Jumbo company now has {ChatSession.objects.filter(data_source__company=jumbo_company).count()} chat sessions"
|
||||
)
|
||||
)
|
||||
self.stdout.write("\nLogin as jumbo_admin to view the dashboard with Jumbo data.")
|
||||
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-05 18:20
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("accounts", "0001_initial"),
|
||||
("data_integration", "0002_externaldatasource_error_count_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="chatsession",
|
||||
name="company",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Company this session belongs to",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="external_chat_sessions",
|
||||
to="accounts.company",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="externaldatasource",
|
||||
name="company",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Company this data source belongs to",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="external_data_sources",
|
||||
to="accounts.company",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,10 +1,19 @@
|
||||
import os
|
||||
|
||||
from accounts.models import Company
|
||||
from django.db import models
|
||||
|
||||
|
||||
class ChatSession(models.Model):
|
||||
session_id = models.CharField(max_length=255, unique=True)
|
||||
company = models.ForeignKey(
|
||||
Company,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="external_chat_sessions",
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Company this session belongs to",
|
||||
)
|
||||
start_time = models.DateTimeField()
|
||||
end_time = models.DateTimeField()
|
||||
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
||||
@@ -39,7 +48,15 @@ class ChatMessage(models.Model):
|
||||
|
||||
class ExternalDataSource(models.Model):
|
||||
name = models.CharField(max_length=255, default="External API")
|
||||
api_url = models.URLField(default="<https://proto.notso.ai/jumbo/chats>")
|
||||
company = models.ForeignKey(
|
||||
Company,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="external_data_sources",
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Company this data source belongs to",
|
||||
)
|
||||
api_url = models.URLField(default="https://proto.notso.ai/jumbo/chats")
|
||||
auth_username = models.CharField(max_length=255, blank=True, null=True)
|
||||
auth_password = models.CharField(
|
||||
max_length=255, blank=True, null=True
|
||||
|
||||
@@ -144,6 +144,7 @@ def fetch_and_store_chat_data(source_id=None):
|
||||
session, created = ChatSession.objects.update_or_create(
|
||||
session_id=data["session_id"],
|
||||
defaults={
|
||||
"company": source.company, # Link to the company from the data source
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"ip_address": data.get("ip_address"),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% load crispy_forms_filters %}
|
||||
<!-- templates/accounts/login.html -->
|
||||
{% extends 'base.html' %} {% load crispy_forms_tags %}
|
||||
{% block title %}
|
||||
|
||||
Reference in New Issue
Block a user