Compare commits
1
Commits
master
...
3ef49d1d15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ef49d1d15 |
No files matched your search
@@ -0,0 +1,37 @@
|
|||||||
|
# Generated by Django 6.0 on 2026-10-17
|
||||||
|
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("chat_backend", "0033_agentrun_agentstep"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="WaitlistEntry",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created", models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
(
|
||||||
|
"last_modified",
|
||||||
|
models.DateTimeField(default=django.utils.timezone.now),
|
||||||
|
),
|
||||||
|
("email", models.EmailField(max_length=254, unique=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["-created"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -799,3 +799,15 @@ class StoredFile(TimeInfoBase):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class WaitlistEntry(TimeInfoBase):
|
||||||
|
"""Email captured from the public site when self-serve registration is off."""
|
||||||
|
|
||||||
|
email = models.EmailField(unique=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["-created"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.email
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from chat_backend.models import Document, WaitlistEntry
|
||||||
|
from chat_backend.tests.factories import make_company, make_document, make_user, make_workspace
|
||||||
|
|
||||||
|
|
||||||
|
class PublicUsageViewTests(APITestCase):
|
||||||
|
def test_counts_active_accounts_and_documents_without_personal_data(self):
|
||||||
|
company = make_company()
|
||||||
|
make_user(email="active@example.com", company=company)
|
||||||
|
inactive = make_user(email="inactive@example.com")
|
||||||
|
inactive.is_active = False
|
||||||
|
inactive.save(update_fields=["is_active"])
|
||||||
|
deleted = make_user(email="deleted@example.com")
|
||||||
|
deleted.deleted = True
|
||||||
|
deleted.save(update_fields=["deleted"])
|
||||||
|
make_document(make_workspace(company=company))
|
||||||
|
|
||||||
|
response = self.client.get(reverse("public_usage"))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(
|
||||||
|
set(response.data.keys()),
|
||||||
|
{"account_count", "document_count", "as_of"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.data["account_count"], 1)
|
||||||
|
self.assertEqual(response.data["document_count"], Document.objects.count())
|
||||||
|
self.assertIn("T", response.data["as_of"])
|
||||||
|
body = response.content.decode()
|
||||||
|
self.assertNotIn("active@example.com", body)
|
||||||
|
self.assertNotIn("avatar", body)
|
||||||
|
|
||||||
|
def test_does_not_require_authentication(self):
|
||||||
|
response = self.client.get(reverse("public_usage"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.data["account_count"], 0)
|
||||||
|
|
||||||
|
|
||||||
|
class PublicWaitlistViewTests(APITestCase):
|
||||||
|
def test_stores_an_email_once(self):
|
||||||
|
url = reverse("public_waitlist")
|
||||||
|
first = self.client.post(url, {"email": " Person@Example.com "}, format="json")
|
||||||
|
second = self.client.post(url, {"email": "person@example.com"}, format="json")
|
||||||
|
|
||||||
|
self.assertEqual(first.status_code, 200)
|
||||||
|
self.assertEqual(second.status_code, 200)
|
||||||
|
self.assertEqual(first.data["detail"], "You're on the list.")
|
||||||
|
self.assertEqual(second.data["detail"], "You're on the list.")
|
||||||
|
self.assertEqual(WaitlistEntry.objects.count(), 1)
|
||||||
|
self.assertEqual(WaitlistEntry.objects.get().email, "person@example.com")
|
||||||
|
self.assertEqual(get_user_model().objects.count(), 0)
|
||||||
|
|
||||||
|
def test_rejects_a_bad_email(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("public_waitlist"), {"email": "not-an-email"}, format="json"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(WaitlistEntry.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_rejects_a_missing_email(self):
|
||||||
|
response = self.client.post(reverse("public_waitlist"), {}, format="json")
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(WaitlistEntry.objects.count(), 0)
|
||||||
@@ -9,6 +9,8 @@ from .views import (
|
|||||||
LogoutAndBlacklistRefreshTokenForUserView,
|
LogoutAndBlacklistRefreshTokenForUserView,
|
||||||
CustomUserGet,
|
CustomUserGet,
|
||||||
PublicSettingsView,
|
PublicSettingsView,
|
||||||
|
PublicUsageView,
|
||||||
|
PublicWaitlistView,
|
||||||
is_authenticated,
|
is_authenticated,
|
||||||
AnnouncmentView,
|
AnnouncmentView,
|
||||||
FeedbackView,
|
FeedbackView,
|
||||||
@@ -46,6 +48,8 @@ urlpatterns = [
|
|||||||
path("token/refresh/", jwt_views.TokenRefreshView.as_view(), name="token_refresh"),
|
path("token/refresh/", jwt_views.TokenRefreshView.as_view(), name="token_refresh"),
|
||||||
path("user/create/", CustomUserCreate.as_view(), name="create_user"),
|
path("user/create/", CustomUserCreate.as_view(), name="create_user"),
|
||||||
path("public/settings/", PublicSettingsView.as_view(), name="public_settings"),
|
path("public/settings/", PublicSettingsView.as_view(), name="public_settings"),
|
||||||
|
path("public/usage/", PublicUsageView.as_view(), name="public_usage"),
|
||||||
|
path("public/waitlist/", PublicWaitlistView.as_view(), name="public_waitlist"),
|
||||||
path(
|
path(
|
||||||
"auth/oauth/<str:provider>/start/",
|
"auth/oauth/<str:provider>/start/",
|
||||||
OAuthStartView.as_view(),
|
OAuthStartView.as_view(),
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from .models import (
|
|||||||
DocumentWorkspace,
|
DocumentWorkspace,
|
||||||
Document,
|
Document,
|
||||||
UserAuthEvent,
|
UserAuthEvent,
|
||||||
|
WaitlistEntry,
|
||||||
)
|
)
|
||||||
from django.views.decorators.cache import never_cache
|
from django.views.decorators.cache import never_cache
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
@@ -127,6 +128,55 @@ class PublicSettingsView(APIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PublicUsageView(APIView):
|
||||||
|
"""Aggregate counts for the public site. No emails, names, or avatars."""
|
||||||
|
|
||||||
|
permission_classes = (permissions.AllowAny,)
|
||||||
|
authentication_classes = ()
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"account_count": CustomUser.objects.filter(
|
||||||
|
deleted=False, is_active=True
|
||||||
|
).count(),
|
||||||
|
"document_count": Document.objects.count(),
|
||||||
|
"as_of": timezone.now().isoformat(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PublicWaitlistView(APIView):
|
||||||
|
"""Store a waitlist email. Same response for new and existing addresses."""
|
||||||
|
|
||||||
|
permission_classes = (permissions.AllowAny,)
|
||||||
|
authentication_classes = ()
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import validate_email
|
||||||
|
|
||||||
|
raw = request.data.get("email") if isinstance(request.data, dict) else None
|
||||||
|
email = str(raw or "").strip().lower()
|
||||||
|
if len(email) > 254:
|
||||||
|
return Response(
|
||||||
|
{"detail": "Enter a valid email."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
validate_email(email)
|
||||||
|
except ValidationError:
|
||||||
|
return Response(
|
||||||
|
{"detail": "Enter a valid email."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
WaitlistEntry.objects.get_or_create(email=email)
|
||||||
|
return Response({"detail": "You're on the list."}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class CustomUserCreate(APIView):
|
class CustomUserCreate(APIView):
|
||||||
"""Self-serve registration. Gated by ENABLE_ACCOUNT_REGISTRATION (default off)."""
|
"""Self-serve registration. Gated by ENABLE_ACCOUNT_REGISTRATION (default off)."""
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user