Compare commits
1
Commits
master
...
c3fb2c23f2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3fb2c23f2 |
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"],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -792,3 +792,15 @@ class StoredFile(TimeInfoBase):
|
||||
|
||||
def __str__(self):
|
||||
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,
|
||||
CustomUserGet,
|
||||
PublicSettingsView,
|
||||
PublicUsageView,
|
||||
PublicWaitlistView,
|
||||
is_authenticated,
|
||||
AnnouncmentView,
|
||||
FeedbackView,
|
||||
@@ -46,6 +48,8 @@ urlpatterns = [
|
||||
path("token/refresh/", jwt_views.TokenRefreshView.as_view(), name="token_refresh"),
|
||||
path("user/create/", CustomUserCreate.as_view(), name="create_user"),
|
||||
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(
|
||||
"auth/oauth/<str:provider>/start/",
|
||||
OAuthStartView.as_view(),
|
||||
|
||||
@@ -32,6 +32,7 @@ from .models import (
|
||||
DocumentWorkspace,
|
||||
Document,
|
||||
UserAuthEvent,
|
||||
WaitlistEntry,
|
||||
)
|
||||
from django.views.decorators.cache import never_cache
|
||||
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):
|
||||
"""Self-serve registration. Gated by ENABLE_ACCOUNT_REGISTRATION (default off)."""
|
||||
|
||||
|
||||
Reference in new issue
Block a user