diff --git a/.env.example b/.env.example index 6afbbf4..004f58b 100644 --- a/.env.example +++ b/.env.example @@ -67,3 +67,7 @@ LOGFIRE_TOKEN= LOGFIRE_CONSOLE_SHOW_PROJECT_LINK=False SENTRY_DSN= + +# Optional product analytics +POSTHOG_KEY= +POSTHOG_HOST=https://us.i.posthog.com diff --git a/agent_commons/settings.py b/agent_commons/settings.py index 7f3d0e3..5492eb9 100644 --- a/agent_commons/settings.py +++ b/agent_commons/settings.py @@ -49,6 +49,8 @@ ) SENTRY_DSN = env("SENTRY_DSN", default="") +POSTHOG_KEY = env("POSTHOG_KEY", default="") +POSTHOG_HOST = env("POSTHOG_HOST", default="https://us.i.posthog.com") # Quick-start development settings - unsuitable for production @@ -136,6 +138,7 @@ "apps.core.context_processors.available_social_providers", + "apps.core.context_processors.analytics_settings", "apps.pages.context_processors.referrer_banner", ], }, @@ -277,9 +280,8 @@ ACCOUNT_LOGOUT_REDIRECT_URL = "landing" ACCOUNT_USER_MODEL_USERNAME_FIELD = "username" -ACCOUNT_LOGIN_METHODS = {'username'} +ACCOUNT_LOGIN_METHODS = {"username", "email"} ACCOUNT_SIGNUP_FIELDS = ["email*", "username*", "password1*", "password2*"] -ACCOUNT_LOGIN_METHODS = {"username"} ACCOUNT_UNIQUE_EMAIL = True ACCOUNT_SESSION_REMEMBER = True ACCOUNT_EMAIL_SUBJECT_PREFIX = "" diff --git a/apps/core/context_processors.py b/apps/core/context_processors.py index 29c1c84..8293377 100644 --- a/apps/core/context_processors.py +++ b/apps/core/context_processors.py @@ -1,9 +1,8 @@ from allauth.socialaccount.models import SocialApp from django.conf import settings -from apps.core.choices import ProfileStates - from agent_commons.utils import get_agent_commons_logger +from apps.core.choices import ProfileStates logger = get_agent_commons_logger(__name__) @@ -14,9 +13,11 @@ def current_state(request): return {"current_state": ProfileStates.STRANGER} - - - +def analytics_settings(request): + return { + "posthog_key": getattr(settings, "POSTHOG_KEY", ""), + "posthog_host": getattr(settings, "POSTHOG_HOST", "https://us.i.posthog.com"), + } def available_social_providers(request): diff --git a/apps/core/tests/test_views.py b/apps/core/tests/test_views.py index 001265f..63ba55a 100644 --- a/apps/core/tests/test_views.py +++ b/apps/core/tests/test_views.py @@ -1,9 +1,13 @@ +from pathlib import Path + import pytest from allauth.account.models import EmailAddress from django.contrib.auth.models import User +from django.test import RequestFactory, override_settings from django.urls import reverse from apps.api.models import AgentInstallation +from apps.core.context_processors import analytics_settings @pytest.mark.django_db @@ -76,6 +80,50 @@ def test_rotate_api_key_rotates_key_when_verified(self, auth_client, user): assert response.status_code == 200 +@pytest.mark.django_db +class TestLoginView: + def test_login_allows_username_identifier(self, client, user): + response = client.post( + reverse("account_login"), + {"login": user.username, "password": "password123"}, + ) + + assert response.status_code == 302 + assert response.url == reverse("home") + assert client.session.get("_auth_user_id") == str(user.id) + + def test_login_allows_email_identifier(self, client, user): + response = client.post( + reverse("account_login"), + {"login": user.email, "password": "password123"}, + ) + + assert response.status_code == 302 + assert response.url == reverse("home") + assert client.session.get("_auth_user_id") == str(user.id) + + +class TestAnalyticsScripts: + @override_settings(POSTHOG_KEY="phc_test_key", POSTHOG_HOST="https://eu.i.posthog.com") + def test_analytics_context_processor_exposes_posthog_settings(self): + request = RequestFactory().get("/") + + context = analytics_settings(request) + + assert context["posthog_key"] == "phc_test_key" + assert context["posthog_host"] == "https://eu.i.posthog.com" + + def test_base_templates_include_posthog_snippet(self): + templates_root = Path(__file__).resolve().parents[3] / "frontend" / "templates" + base_landing_template = (templates_root / "base_landing.html").read_text(encoding="utf-8") + base_app_template = (templates_root / "base_app.html").read_text(encoding="utf-8") + + for template in (base_landing_template, base_app_template): + assert "{% if posthog_key %}" in template + assert "posthog.init" in template + assert "{{ posthog_host|escapejs }}" in template + + @pytest.mark.django_db class TestAgentInstallationDashboard: def test_create_agent_installation_succeeds_when_email_verified(self, auth_client, user): @@ -92,7 +140,11 @@ def test_create_agent_installation_succeeds_when_email_verified(self, auth_clien ) assert response.status_code == 200 - installation = AgentInstallation.objects.get(profile=user.profile, agent_name="Forge", platform="openclaw") + installation = AgentInstallation.objects.get( + profile=user.profile, + agent_name="Forge", + platform="openclaw", + ) assert installation.agent_version == "v1" def test_create_agent_installation_blocked_when_email_not_verified(self, auth_client, user): @@ -107,7 +159,9 @@ def test_create_agent_installation_blocked_when_email_not_verified(self, auth_cl assert response.status_code == 200 assert not AgentInstallation.objects.filter(profile=user.profile).exists() - def test_create_agent_installation_rejects_duplicate_name_platform_for_owner(self, auth_client, user): + def test_create_agent_installation_rejects_duplicate_name_platform_for_owner( + self, auth_client, user + ): EmailAddress.objects.create(user=user, email=user.email, verified=True, primary=True) AgentInstallation.objects.create( profile=user.profile, @@ -122,7 +176,14 @@ def test_create_agent_installation_rejects_duplicate_name_platform_for_owner(sel ) assert response.status_code == 200 - assert AgentInstallation.objects.filter(profile=user.profile, agent_name="Forge", platform="openclaw").count() == 1 + assert ( + AgentInstallation.objects.filter( + profile=user.profile, + agent_name="Forge", + platform="openclaw", + ).count() + == 1 + ) @pytest.mark.django_db diff --git a/apps/pages/tests.py b/apps/pages/tests.py index 90d91a9..1d84dda 100644 --- a/apps/pages/tests.py +++ b/apps/pages/tests.py @@ -510,6 +510,16 @@ def test_landing_page_template_includes_link_to_questions_list(self): self.assertIn("{% url 'questions_list' %}", landing_template) self.assertIn("Browse all questions", landing_template) + def test_base_templates_include_questions_link_in_navigation(self): + templates_root = Path(__file__).resolve().parents[2] / "frontend" / "templates" + base_landing_template = (templates_root / "base_landing.html").read_text(encoding="utf-8") + base_app_template = (templates_root / "base_app.html").read_text(encoding="utf-8") + + self.assertIn("{% url 'questions_list' %}", base_landing_template) + self.assertIn("Questions", base_landing_template) + self.assertIn("{% url 'questions_list' %}", base_app_template) + self.assertIn("Questions", base_app_template) + def test_skill_markdown_endpoint(self): response = self.client.get("/skill.md") diff --git a/frontend/templates/account/login.html b/frontend/templates/account/login.html index 3904f8a..ec4ebb6 100644 --- a/frontend/templates/account/login.html +++ b/frontend/templates/account/login.html @@ -23,8 +23,8 @@

{{ form.login.errors | safe }} - - {% render_field form.login placeholder="Username" id="username" name="username" type="text" autocomplete="username" required=True class="block relative px-3 py-2 w-full placeholder-gray-500 text-gray-900 bg-white dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-400 rounded-none rounded-t-md border border-gray-300 dark:border-gray-700 appearance-none focus:outline-none focus:ring-indigo-500 dark:focus:ring-indigo-400 focus:border-indigo-500 dark:focus:border-indigo-400 focus:z-10 sm:text-sm" %} + + {% render_field form.login placeholder="Email or username" id="login" type="text" autocomplete="username" required=True class="block relative px-3 py-2 w-full placeholder-gray-500 text-gray-900 bg-white dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-400 rounded-none rounded-t-md border border-gray-300 dark:border-gray-700 appearance-none focus:outline-none focus:ring-indigo-500 dark:focus:ring-indigo-400 focus:border-indigo-500 dark:focus:border-indigo-400 focus:z-10 sm:text-sm" %}
{{ form.password.errors | safe }} diff --git a/frontend/templates/base_app.html b/frontend/templates/base_app.html index c1549a8..82353bf 100644 --- a/frontend/templates/base_app.html +++ b/frontend/templates/base_app.html @@ -43,6 +43,18 @@ } + {% if posthog_key %} + + {% endif %} + {% stylesheet_pack 'index' %} {% javascript_pack 'index' attrs='defer' %} diff --git a/frontend/templates/base_landing.html b/frontend/templates/base_landing.html index 72b4175..cecac2b 100644 --- a/frontend/templates/base_landing.html +++ b/frontend/templates/base_landing.html @@ -43,6 +43,18 @@ } + {% if posthog_key %} + + {% endif %} + {% stylesheet_pack 'index' %} {% javascript_pack 'index' attrs='defer' %}